complex keyword filters

This commit is contained in:
Chris Scoggins
2022-01-20 13:34:02 -06:00
parent bb226a221e
commit 28d3d8a1e0
6 changed files with 274 additions and 107 deletions

View File

@@ -31,7 +31,13 @@ class RecipeSearch():
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._keywords = {
'or': self._params.get('keywords_or', None),
'and': self._params.get('keywords_and', None),
'or_not': self._params.get('keywords_or_not', None),
'and_not': self._params.get('keywords_and_not', None)
}
self._foods = self._params.get('foods', None)
self._books = self._params.get('books', None)
self._steps = self._params.get('steps', None)
@@ -42,7 +48,6 @@ class RecipeSearch():
self._sort_order = self._params.get('sort_order', None)
# TODO add save
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))
@@ -88,7 +93,7 @@ class RecipeSearch():
self._new_recipes()
# self._last_viewed()
# self._last_cooked()
self.keyword_filters(keywords=self._keywords, operator=self._keywords_or)
self.keyword_filters(**self._keywords)
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)
@@ -182,19 +187,31 @@ class RecipeSearch():
).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:
def keyword_filters(self, **kwargs):
if all([kwargs[x] is None for x in kwargs]):
return
if not isinstance(keywords, list):
keywords = [keywords]
if operator == True:
# TODO creating setting to include descendants of keywords a setting
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=keywords):
self._queryset = self._queryset.filter(keywords__in=list(kw.get_descendants_and_self()))
for kw_filter in kwargs:
if not kwargs[kw_filter]:
continue
if not isinstance(kwargs[kw_filter], list):
kwargs[kw_filter] = [kwargs[kw_filter]]
keywords = Keyword.objects.filter(pk__in=kwargs[kw_filter])
if 'or' in kw_filter:
f = Q(keywords__in=Keyword.include_descendants(keywords))
if 'not' in kw_filter:
self._queryset = self._queryset.exclude(f)
else:
self._queryset = self._queryset.filter(f)
elif 'and' in kw_filter:
recipes = Recipe.objects.all()
for kw in keywords:
if 'not' in kw_filter:
recipes = recipes.filter(keywords__in=kw.get_descendants_and_self())
else:
self._queryset = self._queryset.filter(keywords__in=kw.get_descendants_and_self())
if 'not' in kw_filter:
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
def food_filters(self, foods=None, operator=True):
if not foods:
@@ -229,7 +246,7 @@ class RecipeSearch():
if rating == 0:
self._queryset = self._queryset.filter(rating=0)
elif lessthan:
self._queryset = self._queryset.filter(rating__lte=int(rating[1:]))
self._queryset = self._queryset.filter(rating__lte=int(rating[1:])).exclude(rating=0)
else:
self._queryset = self._queryset.filter(rating__gte=int(rating))

View File

@@ -630,15 +630,18 @@ class RecipeViewSet(viewsets.ModelViewSet):
# TODO split read and write permission for meal plan guest
permission_classes = [CustomIsShare | CustomIsGuest]
pagination_class = RecipePagination
# TODO the boolean params below (keywords_or through new) should be updated to boolean types with front end refactored accordingly
query_params = [
QueryParam(name='query', description=_('Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
QueryParam(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter.'), qtype='int'),
QueryParam(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'), qtype='int'),
QueryParam(name='keywords_or', description=_('Keyword IDs, repeat for multiple Return recipes with any of the keywords'), qtype='int'),
QueryParam(name='keywords_and', description=_('Keyword IDs, repeat for multiple Return recipes with all of the keywords.'), qtype='int'),
QueryParam(name='keywords_or_not', description=_('Keyword IDs, repeat for multiple Exclude recipes with any of the keywords.'), qtype='int'),
QueryParam(name='keywords_and_not', description=_('Keyword IDs, repeat for multiple Exclude recipes with all of the keywords.'), qtype='int'),
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'), qtype='int'),
QueryParam(name='units', description=_('ID of unit a recipe should have.'), qtype='int'),
QueryParam(name='rating', description=_('Rating a recipe should have. [0 - 5]'), qtype='int'),
QueryParam(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.')),
QueryParam(name='keywords_or', description=_('If recipe should have all (AND=''false'') or any (OR=''<b>true</b>'') of the provided keywords.')),
QueryParam(name='foods_or', description=_('If recipe should have all (AND=''false'') or any (OR=''<b>true</b>'') of the provided foods.')),
QueryParam(name='books_or', description=_('If recipe should be in all (AND=''false'') or any (OR=''<b>true</b>'') of the provided books.')),
QueryParam(name='internal', description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),