refactor recipe search

This commit is contained in:
smilerz
2022-01-17 08:26:34 -06:00
parent 6d9a90c6ba
commit 37971acb48
9 changed files with 1474 additions and 1196 deletions

View File

@@ -3,7 +3,7 @@ from datetime import timedelta
from django.contrib.postgres.search import SearchQuery, SearchRank, TrigramSimilarity
from django.core.cache import caches
from django.db.models import Avg, Case, Count, Func, Max, OuterRef, Q, Subquery, Value, When
from django.db.models import Avg, Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When
from django.db.models.functions import Coalesce, Substr
from django.utils import timezone, translation
from django.utils.translation import gettext as _
@@ -12,194 +12,446 @@ from cookbook.filters import RecipeFilter
from cookbook.helper.HelperFunctions import Round, str2bool
from cookbook.helper.permission_helper import has_group_permission
from cookbook.managers import DICTIONARY
from cookbook.models import Food, Keyword, Recipe, SearchPreference, ViewLog
from cookbook.models import CookLog, Food, Keyword, Recipe, SearchPreference, ViewLog
from recipes import settings
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
def search_recipes(request, queryset, params):
if request.user.is_authenticated:
search_prefs = request.user.searchpreference
else:
search_prefs = SearchPreference()
search_string = params.get('query', '').strip()
search_rating = int(params.get('rating', 0))
search_keywords = params.getlist('keywords', [])
search_foods = params.getlist('foods', [])
search_books = params.getlist('books', [])
search_steps = params.getlist('steps', [])
search_units = params.get('units', None)
class RecipeSearch():
_postgres = settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
search_keywords_or = str2bool(params.get('keywords_or', True))
search_foods_or = str2bool(params.get('foods_or', True))
search_books_or = str2bool(params.get('books_or', True))
search_internal = str2bool(params.get('internal', False))
search_random = str2bool(params.get('random', False))
search_new = str2bool(params.get('new', False))
search_last_viewed = int(params.get('last_viewed', 0)) # not included in schema currently?
orderby = []
# only sort by recent not otherwise filtering/sorting
if search_last_viewed > 0:
last_viewed_recipes = ViewLog.objects.filter(
created_by=request.user, space=request.space,
created_at__gte=timezone.now() - timedelta(days=14) # TODO make recent days a setting
).order_by('-pk').values_list('recipe__pk', flat=True)
last_viewed_recipes = list(dict.fromkeys(last_viewed_recipes))[:search_last_viewed] # removes duplicates from list prior to slicing
# return queryset.annotate(last_view=Max('viewlog__pk')).annotate(new=Case(When(pk__in=last_viewed_recipes, then=('last_view')), default=Value(0))).filter(new__gt=0).order_by('-new')
# queryset that only annotates most recent view (higher pk = lastest view)
queryset = queryset.annotate(recent=Coalesce(Max(Case(When(viewlog__created_by=request.user, then='viewlog__pk'))), Value(0)))
orderby += ['-recent']
# TODO create setting for default ordering - most cooked, rating,
# TODO create options for live sorting
# TODO make days of new recipe a setting
if search_new:
queryset = (
queryset.annotate(new_recipe=Case(
When(created_at__gte=(timezone.now() - timedelta(days=7)), then=('pk')), default=Value(0), ))
)
# only sort by new recipes if not otherwise filtering/sorting
orderby += ['-new_recipe']
search_type = search_prefs.search or 'plain'
if len(search_string) > 0:
unaccent_include = search_prefs.unaccent.values_list('field', flat=True)
icontains_include = [x + '__unaccent' if x in unaccent_include else x for x in search_prefs.icontains.values_list('field', flat=True)]
istartswith_include = [x + '__unaccent' if x in unaccent_include else x for x in search_prefs.istartswith.values_list('field', flat=True)]
trigram_include = [x + '__unaccent' if x in unaccent_include else x for x in search_prefs.trigram.values_list('field', flat=True)]
fulltext_include = search_prefs.fulltext.values_list('field', flat=True) # fulltext doesn't use field name directly
# if no filters are configured use name__icontains as default
if len(icontains_include) + len(istartswith_include) + len(trigram_include) + len(fulltext_include) == 0:
filters = [Q(**{"name__icontains": search_string})]
def __init__(self, request, **params):
self._request = request
self._queryset = None
self._params = {**params}
if self._request.user.is_authenticated:
self._search_prefs = request.user.searchpreference
else:
filters = []
self._search_prefs = SearchPreference()
self._string = params.get('query').strip() if params.get('query', None) else None
self._rating = self._params.get('rating', None)
self._keywords = self._params.get('keywords', None)
self._foods = self._params.get('foods', None)
self._books = self._params.get('books', None)
self._steps = self._params.get('steps', None)
self._units = self._params.get('units', None)
# TODO add created by
# TODO add created before/after
# TODO image exists
self._sort_order = self._params.get('sort_order', None)
# TODO add save
# dynamically build array of filters that will be applied
for f in icontains_include:
filters += [Q(**{"%s__icontains" % f: search_string})]
self._keywords_or = str2bool(self._params.get('keywords_or', True))
self._foods_or = str2bool(self._params.get('foods_or', True))
self._books_or = str2bool(self._params.get('books_or', True))
for f in istartswith_include:
filters += [Q(**{"%s__istartswith" % f: search_string})]
self._internal = str2bool(self._params.get('internal', False))
self._random = str2bool(self._params.get('random', False))
self._new = str2bool(self._params.get('new', False))
self._last_viewed = int(self._params.get('last_viewed', 0))
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
language = DICTIONARY.get(translation.get_language(), 'simple')
# django full text search https://docs.djangoproject.com/en/3.2/ref/contrib/postgres/search/#searchquery
# TODO can options install this extension to further enhance search query language https://github.com/caub/pg-tsquery
# trigram breaks full text search 'websearch' and 'raw' capabilities and will be ignored if those methods are chosen
if search_type in ['websearch', 'raw']:
search_trigram = False
else:
search_trigram = True
search_query = SearchQuery(
search_string,
search_type=search_type,
config=language,
self._search_type = self._search_prefs.search or 'plain'
if self._string:
unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
self._icontains_include = [x + '__unaccent' if x in unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
self._istartswith_include = [x + '__unaccent' if x in unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
self._trigram_include = None
self._fulltext_include = None
self._trigram = False
if self._postgres and self._string:
self._language = DICTIONARY.get(translation.get_language(), 'simple')
self._trigram_include = [x + '__unaccent' if x in unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
self._fulltext_include = self._search_prefs.fulltext.values_list('field', flat=True)
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
self._trigram = True
self.search_query = SearchQuery(
self._string,
search_type=self._search_type,
config=self._language,
)
self.search_rank = (
SearchRank('name_search_vector', self.search_query, cover_density=True)
+ SearchRank('desc_search_vector', self.search_query, cover_density=True)
+ SearchRank('steps__search_vector', self.search_query, cover_density=True)
)
self.orderby = []
self._default_sort = ['-favorite'] # TODO add user setting
self._filters = None
self._fuzzy_match = None
# iterate through fields to use in trigrams generating a single trigram
if search_trigram and len(trigram_include) > 0:
trigram = None
for f in trigram_include:
if trigram:
trigram += TrigramSimilarity(f, search_string)
else:
trigram = TrigramSimilarity(f, search_string)
queryset = queryset.annotate(similarity=trigram)
filters += [Q(similarity__gt=search_prefs.trigram_threshold)]
def get_queryset(self, queryset):
self._queryset = queryset
self.recently_viewed_recipes(self._last_viewed)
self._favorite_recipes()
# self._last_viewed()
# self._last_cooked()
self.keyword_filters(keywords=self._keywords, operator=self._keywords_or)
self.food_filters(foods=self._foods, operator=self._foods_or)
self.book_filters(books=self._books, operator=self._books_or)
self.rating_filter(rating=self._rating)
self.internal_filter()
self.step_filters(steps=self._steps)
self.unit_filters(units=self._units)
self.string_filters(string=self._string)
# self._queryset = self._queryset.distinct() # TODO 2x check. maybe add filter of recipe__in after orderby
self._apply_order_by()
return self._queryset.filter(space=self._request.space)
if 'name' in fulltext_include:
filters += [Q(name_search_vector=search_query)]
if 'description' in fulltext_include:
filters += [Q(desc_search_vector=search_query)]
if 'instructions' in fulltext_include:
filters += [Q(steps__search_vector=search_query)]
if 'keywords' in fulltext_include:
filters += [Q(keywords__in=Subquery(Keyword.objects.filter(name__search=search_query).values_list('id', flat=True)))]
if 'foods' in fulltext_include:
filters += [Q(steps__ingredients__food__in=Subquery(Food.objects.filter(name__search=search_query).values_list('id', flat=True)))]
def _apply_order_by(self):
if self._random:
self._queryset = self._queryset.order_by("?")
else:
if self._sort_order:
self._queryset.order_by(*self._sort_order)
return
order = [] # TODO add user preferences here: name, date cooked, rating, times cooked, date created, date viewed, random
if '-recent' in self.orderby and self._last_viewed:
order += ['-recent']
if '-rank' in self.orderby and '-simularity' in self.orderby:
self._queryset = self._queryset.annotate(score=Sum(F('rank')+F('simularity')))
order += ['-score']
elif '-rank' in self.orderby:
self._queryset = self._queryset.annotate(score=F('rank'))
order += ['-score']
elif '-simularity' in self.orderby:
self._queryset = self._queryset.annotate(score=F('simularity'))
order += ['-score']
for x in list(set(self.orderby)-set([*order, '-rank', '-simularity'])):
order += [x]
self._queryset = self._queryset.order_by(*order)
def string_filters(self, string=None):
if not string:
return
self.build_text_filters(self._string)
if self._postgres:
self.build_fulltext_filters(self._string)
self.build_trigram(self._string)
if self._filters:
query_filter = None
for f in filters:
for f in self._filters:
if query_filter:
query_filter |= f
else:
query_filter = f
self._queryset = self._queryset.filter(query_filter).distinct()
# TODO add annotation for simularity
if self._fulltext_include:
self._queryset = self._queryset.annotate(rank=self.search_rank)
self.orderby += ['-rank']
# TODO add order by user settings - only do search rank and annotation if rank order is configured
search_rank = (
SearchRank('name_search_vector', search_query, cover_density=True)
+ SearchRank('desc_search_vector', search_query, cover_density=True)
+ SearchRank('steps__search_vector', search_query, cover_density=True)
)
queryset = queryset.filter(query_filter).annotate(rank=search_rank)
orderby += ['-rank']
if self._fuzzy_match is not None: # this annotation is full text, not trigram
simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity')
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
self.orderby += ['-simularity']
else:
queryset = queryset.filter(name__icontains=search_string)
self._queryset = self._queryset.filter(name__icontains=self._string)
if len(search_keywords) > 0:
if search_keywords_or:
def recently_viewed_recipes(self, last_viewed=None):
if not last_viewed:
return
last_viewed_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space).values(
'recipe').annotate(recent=Max('created_at')).order_by('-recent')
last_viewed_recipes = last_viewed_recipes[:last_viewed]
self.orderby += ['-recent']
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=last_viewed_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
def _favorite_recipes(self):
self.orderby += ['-favorite'] # default sort?
favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk')
).values('recipe').annotate(count=Count('pk', distinct=True)).values('count')
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), 0))
def keyword_filters(self, keywords=None, operator=True):
if not keywords:
return
if operator == True:
# TODO creating setting to include descendants of keywords a setting
# for kw in Keyword.objects.filter(pk__in=search_keywords):
# search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
for kw in Keyword.objects.filter(pk__in=search_keywords):
search_keywords = [*search_keywords, *list(kw.get_descendants_and_self().values_list('pk', flat=True))]
queryset = queryset.filter(keywords__id__in=search_keywords)
self._queryset = self._queryset.filter(keywords__in=Keyword.include_descendants(Keyword.objects.filter(pk__in=keywords)))
else:
# when performing an 'and' search returned recipes should include a parent OR any of its descedants
# AND other keywords selected so filters are appended using keyword__id__in the list of keywords and descendants
for kw in Keyword.objects.filter(pk__in=search_keywords):
queryset = queryset.filter(keywords__id__in=list(kw.get_descendants_and_self().values_list('pk', flat=True)))
for kw in Keyword.objects.filter(pk__in=keywords):
self._queryset = self._queryset.filter(keywords__in=list(kw.get_descendants_and_self()))
if len(search_foods) > 0:
if search_foods_or:
def food_filters(self, foods=None, operator=True):
if not foods:
return
if operator == True:
# TODO creating setting to include descendants of food a setting
for fd in Food.objects.filter(pk__in=search_foods):
search_foods = [*search_foods, *list(fd.get_descendants_and_self().values_list('pk', flat=True))]
queryset = queryset.filter(steps__ingredients__food__id__in=search_foods)
self._queryset = self._queryset.filter(steps__ingredients__food__in=Food.include_descendants(Food.objects.filter(pk__in=foods)))
else:
# when performing an 'and' search returned recipes should include a parent OR any of its descedants
# AND other foods selected so filters are appended using steps__ingredients__food__id__in the list of foods and descendants
for fd in Food.objects.filter(pk__in=search_foods):
queryset = queryset.filter(steps__ingredients__food__id__in=list(fd.get_descendants_and_self().values_list('pk', flat=True)))
for fd in Food.objects.filter(pk__in=foods):
self._queryset = self._queryset.filter(steps__ingredients__food__in=list(fd.get_descendants_and_self()))
if len(search_books) > 0:
if search_books_or:
queryset = queryset.filter(recipebookentry__book__id__in=search_books)
else:
for k in search_books:
queryset = queryset.filter(recipebookentry__book__id=k)
def unit_filters(self, units=None, operator=True):
if operator != True:
raise NotImplementedError
if not units:
return
self._queryset = self._queryset.filter(steps__ingredients__unit__id=units)
if search_rating:
def rating_filter(self, rating=None):
if rating is None:
return
rating = int(rating)
# TODO make ratings a settings user-only vs all-users
queryset = queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=request.user, then='cooklog__rating'), default=Value(0)))))
if search_rating == -1:
queryset = queryset.filter(rating=0)
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
if rating == 0:
self._queryset = self._queryset.filter(rating=0)
else:
queryset = queryset.filter(rating__gte=search_rating)
self._queryset = self._queryset.filter(rating__gte=rating)
# probably only useful in Unit list view, so keeping it simple
if search_units:
queryset = queryset.filter(steps__ingredients__unit__id=search_units)
def internal_filter(self):
self._queryset = self._queryset.filter(internal=True)
# probably only useful in Unit list view, so keeping it simple
if search_steps:
queryset = queryset.filter(steps__id__in=search_steps)
def book_filters(self, books=None, operator=True):
if not books:
return
if operator == True:
self._queryset = self._queryset.filter(recipebookentry__book__id__in=books)
else:
for k in books:
self._queryset = self._queryset.filter(recipebookentry__book__id=k)
if search_internal:
queryset = queryset.filter(internal=True)
def step_filters(self, steps=None, operator=True):
if operator != True:
raise NotImplementedError
if not steps:
return
self._queryset = self._queryset.filter(steps__id__in=steps)
queryset = queryset.distinct()
def build_fulltext_filters(self, string=None):
if not string:
return
if self._fulltext_include:
if not self._filters:
self._filters = []
if 'name' in self._fulltext_include:
self._filters += [Q(name_search_vector=self.search_query)]
if 'description' in self._fulltext_include:
self._filters += [Q(desc_search_vector=self.search_query)]
if 'steps__instruction' in self._fulltext_include:
self._filters += [Q(steps__search_vector=self.search_query)]
if 'keywords__name' in self._fulltext_include:
self._filters += [Q(keywords__in=Keyword.objects.filter(name__search=self.search_query))]
if 'steps__ingredients__food__name' in self._fulltext_include:
self._filters += [Q(steps__ingredients__food__in=Food.objects.filter(name__search=self.search_query))]
if search_random:
queryset = queryset.order_by("?")
else:
queryset = queryset.order_by(*orderby)
return queryset
def build_text_filters(self, string=None):
if not string:
return
if not self._filters:
self._filters = []
# dynamically build array of filters that will be applied
for f in self._icontains_include:
self._filters += [Q(**{"%s__icontains" % f: self._string})]
for f in self._istartswith_include:
self._filters += [Q(**{"%s__istartswith" % f: self._string})]
def build_trigram(self, string=None):
if not string:
return
if self._trigram:
trigram = None
for f in self._trigram_include:
if trigram:
trigram += TrigramSimilarity(f, self._string)
else:
trigram = TrigramSimilarity(f, self._string)
self._fuzzy_match = Recipe.objects.annotate(trigram=trigram).distinct(
).annotate(simularity=Max('trigram')).values('id', 'simularity').filter(simularity__gt=self._search_prefs.trigram_threshold)
self._filters += [Q(pk__in=self._fuzzy_match.values('pk'))]
# def search_recipes(request, queryset, params):
# if request.user.is_authenticated:
# search_prefs = request.user.searchpreference
# else:
# search_prefs = SearchPreference()
# search_string = params.get('query', '').strip()
# search_rating = int(params.get('rating', 0))
# search_keywords = params.getlist('keywords', [])
# search_foods = params.getlist('foods', [])
# search_books = params.getlist('books', [])
# search_steps = params.getlist('steps', [])
# search_units = params.get('units', None)
# search_keywords_or = str2bool(params.get('keywords_or', True))
# search_foods_or = str2bool(params.get('foods_or', True))
# search_books_or = str2bool(params.get('books_or', True))
# search_internal = str2bool(params.get('internal', False))
# search_random = str2bool(params.get('random', False))
# search_new = str2bool(params.get('new', False))
# search_last_viewed = int(params.get('last_viewed', 0)) # not included in schema currently?
# orderby = []
# # only sort by recent not otherwise filtering/sorting
# if search_last_viewed > 0:
# last_viewed_recipes = ViewLog.objects.filter(created_by=request.user, space=request.space).values('recipe').annotate(recent=Max('created_at')).order_by('-recent')[:search_last_viewed]
# queryset = queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=last_viewed_recipes.values('recipe'), then='viewlog__pk'))), Value(0))).order_by('-recent')
# orderby += ['-recent']
# # TODO add sort by favorite
# favorite_recipes = CookLog.objects.filter(created_by=request.user, space=request.space, recipe=OuterRef('pk')).values('recipe').annotate(count=Count('pk', distinct=True)).values('count')
# # TODO add to serialization and RecipeCard and RecipeView
# queryset = queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), 0))
# # TODO create setting for default ordering - most cooked, rating,
# # TODO create options for live sorting
# # TODO make days of new recipe a setting
# if search_new:
# queryset = (
# queryset.annotate(new_recipe=Case(
# When(created_at__gte=(timezone.now() - timedelta(days=7)), then=('pk')), default=Value(0), ))
# )
# # TODO create setting for 'new' recipes
# # only sort by new recipes if not otherwise filtering/sorting
# orderby += ['-new_recipe']
# orderby += ['-favorite']
# search_type = search_prefs.search or 'plain'
# if len(search_string) > 0:
# unaccent_include = search_prefs.unaccent.values_list('field', flat=True)
# icontains_include = [x + '__unaccent' if x in unaccent_include else x for x in search_prefs.icontains.values_list('field', flat=True)]
# istartswith_include = [x + '__unaccent' if x in unaccent_include else x for x in search_prefs.istartswith.values_list('field', flat=True)]
# trigram_include = [x + '__unaccent' if x in unaccent_include else x for x in search_prefs.trigram.values_list('field', flat=True)]
# fulltext_include = search_prefs.fulltext.values_list('field', flat=True) # fulltext doesn't use field name directly
# # if no filters are configured use name__icontains as default
# if icontains_include or istartswith_include or trigram_include or fulltext_include:
# filters = [Q(**{"name__icontains": search_string})]
# else:
# filters = []
# # dynamically build array of filters that will be applied
# for f in icontains_include:
# filters += [Q(**{"%s__icontains" % f: search_string})]
# for f in istartswith_include:
# filters += [Q(**{"%s__istartswith" % f: search_string})]
# if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
# language = DICTIONARY.get(translation.get_language(), 'simple')
# # django full text search https://docs.djangoproject.com/en/3.2/ref/contrib/postgres/search/#searchquery
# # TODO can options install this extension to further enhance search query language https://github.com/caub/pg-tsquery
# # trigram breaks full text search 'websearch' and 'raw' capabilities and will be ignored if those methods are chosen
# if search_type in ['websearch', 'raw']:
# search_trigram = False
# else:
# search_trigram = True
# search_query = SearchQuery(
# search_string,
# search_type=search_type,
# config=language,
# )
# # iterate through fields to use in trigrams generating a single trigram
# if search_trigram and len(trigram_include) > 0:
# trigram = None
# for f in trigram_include:
# if trigram:
# trigram += TrigramSimilarity(f, search_string)
# else:
# trigram = TrigramSimilarity(f, search_string)
# queryset = queryset.annotate(similarity=trigram)
# filters += [Q(similarity__gt=search_prefs.trigram_threshold)]
# if 'name' in fulltext_include:
# filters += [Q(name_search_vector=search_query)]
# if 'description' in fulltext_include:
# filters += [Q(desc_search_vector=search_query)]
# if 'instructions' in fulltext_include:
# filters += [Q(steps__search_vector=search_query)]
# if 'keywords' in fulltext_include:
# filters += [Q(keywords__in=Subquery(Keyword.objects.filter(name__search=search_query).values_list('id', flat=True)))]
# if 'foods' in fulltext_include:
# filters += [Q(steps__ingredients__food__in=Subquery(Food.objects.filter(name__search=search_query).values_list('id', flat=True)))]
# query_filter = None
# for f in filters:
# if query_filter:
# query_filter |= f
# else:
# query_filter = f
# # TODO add order by user settings - only do search rank and annotation if rank order is configured
# search_rank = (
# SearchRank('name_search_vector', search_query, cover_density=True)
# + SearchRank('desc_search_vector', search_query, cover_density=True)
# + SearchRank('steps__search_vector', search_query, cover_density=True)
# )
# queryset = queryset.filter(query_filter).annotate(rank=search_rank)
# orderby += ['-rank']
# else:
# queryset = queryset.filter(name__icontains=search_string)
# if len(search_keywords) > 0:
# if search_keywords_or:
# # TODO creating setting to include descendants of keywords a setting
# # for kw in Keyword.objects.filter(pk__in=search_keywords):
# # search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
# queryset = queryset.filter(keywords__in=Keyword.include_descendants(Keyword.objects.filter(pk__in=search_keywords)))
# else:
# # when performing an 'and' search returned recipes should include a parent OR any of its descedants
# # AND other keywords selected so filters are appended using keyword__id__in the list of keywords and descendants
# for kw in Keyword.objects.filter(pk__in=search_keywords):
# queryset = queryset.filter(keywords__in=list(kw.get_descendants_and_self()))
# if len(search_foods) > 0:
# if search_foods_or:
# # TODO creating setting to include descendants of food a setting
# queryset = queryset.filter(steps__ingredients__food__in=Food.include_descendants(Food.objects.filter(pk__in=search_foods)))
# else:
# # when performing an 'and' search returned recipes should include a parent OR any of its descedants
# # AND other foods selected so filters are appended using steps__ingredients__food__id__in the list of foods and descendants
# for fd in Food.objects.filter(pk__in=search_foods):
# queryset = queryset.filter(steps__ingredients__food__in=list(fd.get_descendants_and_self()))
# if len(search_books) > 0:
# if search_books_or:
# queryset = queryset.filter(recipebookentry__book__id__in=search_books)
# else:
# for k in search_books:
# queryset = queryset.filter(recipebookentry__book__id=k)
# if search_rating:
# # TODO make ratings a settings user-only vs all-users
# queryset = queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=request.user, then='cooklog__rating'), default=Value(0)))))
# if search_rating == -1:
# queryset = queryset.filter(rating=0)
# else:
# queryset = queryset.filter(rating__gte=search_rating)
# # probably only useful in Unit list view, so keeping it simple
# if search_units:
# queryset = queryset.filter(steps__ingredients__unit__id=search_units)
# # probably only useful in Unit list view, so keeping it simple
# if search_steps:
# queryset = queryset.filter(steps__id__in=search_steps)
# if search_internal:
# queryset = queryset.filter(internal=True)
# queryset = queryset.distinct()
# if search_random:
# queryset = queryset.order_by("?")
# else:
# queryset = queryset.order_by(*orderby)
# return queryset
class RecipeFacet():
@@ -223,6 +475,7 @@ class RecipeFacet():
self.Foods = self._cache.get('Foods', None)
self.Books = self._cache.get('Books', None)
self.Ratings = self._cache.get('Ratings', None)
# TODO Move Recent to recipe annotation/serializer: requrires change in RecipeSearch(), RecipeSearchView.vue and serializer
self.Recent = self._cache.get('Recent', None)
if self._queryset is not None:
@@ -666,4 +919,10 @@ def old_search(request):
# other[name] = [*other.get(name, []), x.name]
# if x.hidden:
# hidden[name] = [*hidden.get(name, []), x.name]
# print('---', x.name, ' - ', x.db_type, x.remote_name)
# print('---', x.name, ' - ', x.db_type)
# for field_type in [(char, 'char'), (number, 'number'), (other, 'other'), (date, 'date'), (image, 'image'), (one_to_many, 'one_to_many'), (many_to_one, 'many_to_one'), (many_to_many, 'many_to_many')]:
# print(f"{field_type[1]}:")
# for model in field_type[0]:
# print(f"--{model}")
# for field in field_type[0][model]:
# print(f" --{field}")