refactor get_facets as RecipeFacets class

This commit is contained in:
smilerz 2022-01-12 12:21:28 -06:00
parent f9b04a3f1e
commit 20d61160ba
4 changed files with 412 additions and 177 deletions

View File

@ -3,8 +3,8 @@ from datetime import timedelta
from django.contrib.postgres.search import SearchQuery, SearchRank, TrigramSimilarity from django.contrib.postgres.search import SearchQuery, SearchRank, TrigramSimilarity
from django.core.cache import caches from django.core.cache import caches
from django.db.models import Avg, Case, Count, Func, Max, Q, Subquery, Value, When from django.db.models import Avg, Case, Count, Func, Max, OuterRef, Q, Subquery, Value, When
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce, Substr
from django.utils import timezone, translation from django.utils import timezone, translation
from cookbook.filters import RecipeFilter from cookbook.filters import RecipeFilter
@ -145,6 +145,8 @@ def search_recipes(request, queryset, params):
# TODO creating setting to include descendants of keywords a setting # TODO creating setting to include descendants of keywords a setting
# for kw in Keyword.objects.filter(pk__in=search_keywords): # for kw in Keyword.objects.filter(pk__in=search_keywords):
# search_keywords += list(kw.get_descendants().values_list('pk', flat=True)) # 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) queryset = queryset.filter(keywords__id__in=search_keywords)
else: else:
# when performing an 'and' search returned recipes should include a parent OR any of its descedants # when performing an 'and' search returned recipes should include a parent OR any of its descedants
@ -155,6 +157,8 @@ def search_recipes(request, queryset, params):
if len(search_foods) > 0: if len(search_foods) > 0:
if search_foods_or: if search_foods_or:
# TODO creating setting to include descendants of food a setting # 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) queryset = queryset.filter(steps__ingredients__food__id__in=search_foods)
else: else:
# when performing an 'and' search returned recipes should include a parent OR any of its descedants # when performing an 'and' search returned recipes should include a parent OR any of its descedants
@ -170,6 +174,7 @@ def search_recipes(request, queryset, params):
queryset = queryset.filter(recipebookentry__book__id=k) queryset = queryset.filter(recipebookentry__book__id=k)
if search_rating: 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))))) queryset = queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=request.user, then='cooklog__rating'), default=Value(0)))))
if search_rating == -1: if search_rating == -1:
queryset = queryset.filter(rating=0) queryset = queryset.filter(rating=0)
@ -196,189 +201,375 @@ def search_recipes(request, queryset, params):
return queryset return queryset
# TODO: This might be faster https://github.com/django-treebeard/django-treebeard/issues/115 class CacheEmpty(Exception):
def get_facet(qs=None, request=None, use_cache=True, hash_key=None): pass
"""
Gets an annotated list from a queryset.
:param qs:
recipe queryset to build facets from
:param request:
the web request that contains the necessary query parameters
:param use_cache:
will find results in cache, if any, and return them or empty list.
will save the list of recipes IDs in the cache for future processing
:param hash_key:
the cache key of the recipe list to process
only evaluated if the use_cache parameter is false
"""
facets = {}
recipe_list = []
cache_timeout = 600
if use_cache:
qs_hash = hash(frozenset(qs.values_list('pk')))
facets['cache_key'] = str(qs_hash)
SEARCH_CACHE_KEY = f"recipes_filter_{qs_hash}"
if c := caches['default'].get(SEARCH_CACHE_KEY, None):
facets['Keywords'] = c['Keywords'] or []
facets['Foods'] = c['Foods'] or []
facets['Books'] = c['Books'] or []
facets['Ratings'] = c['Ratings'] or []
facets['Recent'] = c['Recent'] or []
else:
facets['Keywords'] = []
facets['Foods'] = []
facets['Books'] = []
rating_qs = qs.annotate(rating=Round(Avg(Case(When(cooklog__created_by=request.user, then='cooklog__rating'), default=Value(0)))))
facets['Ratings'] = dict(Counter(r.rating for r in rating_qs))
facets['Recent'] = ViewLog.objects.filter(
created_by=request.user, space=request.space,
created_at__gte=timezone.now() - timedelta(days=14) # TODO make days of recent recipe a setting
).values_list('recipe__pk', flat=True)
cached_search = { class RecipeFacet():
'recipe_list': list(qs.values_list('id', flat=True)), def __init__(self, request, queryset=None, hash_key=None, cache_timeout=600):
'keyword_list': request.query_params.getlist('keywords', []), if hash_key is None and queryset is None:
'food_list': request.query_params.getlist('foods', []), raise ValueError(_("One of queryset or hash_key must be provided"))
'book_list': request.query_params.getlist('book', []),
'search_keywords_or': str2bool(request.query_params.get('keywords_or', True)), self._request = request
'search_foods_or': str2bool(request.query_params.get('foods_or', True)), self._queryset = queryset
'search_books_or': str2bool(request.query_params.get('books_or', True)), self.hash_key = hash_key or str(hash(frozenset(self._queryset.values_list('pk'))))
'space': request.space, self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}"
'Ratings': facets['Ratings'], self._cache_timeout = cache_timeout
'Recent': facets['Recent'], self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, None)
'Keywords': facets['Keywords'], if self._cache is None and self._queryset is None:
'Foods': facets['Foods'], raise CacheEmpty("No queryset provided and cache empty")
'Books': facets['Books']
self.Keywords = getattr(self._cache, 'Keywords', None)
self.Foods = getattr(self._cache, 'Foods', None)
self.Books = getattr(self._cache, 'Books', None)
self.Ratings = getattr(self._cache, 'Ratings', None)
self.Recent = getattr(self._cache, 'Recent', None)
if self._queryset:
self._recipe_list = list(self._queryset.values_list('id', flat=True))
self._search_params = {
'keyword_list': self._request.query_params.getlist('keywords', []),
'food_list': self._request.query_params.getlist('foods', []),
'book_list': self._request.query_params.getlist('book', []),
'search_keywords_or': str2bool(self._request.query_params.get('keywords_or', True)),
'search_foods_or': str2bool(self._request.query_params.get('foods_or', True)),
'search_books_or': str2bool(self._request.query_params.get('books_or', True)),
'space': self._request.space,
}
elif self.hash_key:
self._recipe_list = self._cache.get('recipe_list', None)
self._search_params = {
'keyword_list': self._cache.get('keyword_list', None),
'food_list': self._cache.get('food_list', None),
'book_list': self._cache.get('book_list', None),
'search_keywords_or': self._cache.get('search_keywords_or', None),
'search_foods_or': self._cache.get('search_foods_or', None),
'search_books_or': self._cache.get('search_books_or', None),
'space': self._cache.get('space', None),
}
self._cache = {
**self._search_params,
'recipe_list': self._recipe_list,
'Ratings': self.Ratings,
'Recent': self.Recent,
'Keywords': self.Keywords,
'Foods': self.Foods,
'Books': self.Books
} }
caches['default'].set(SEARCH_CACHE_KEY, cached_search, cache_timeout) caches['default'].set(self._SEARCH_CACHE_KEY, self._cache, self._cache_timeout)
return facets
SEARCH_CACHE_KEY = f'recipes_filter_{hash_key}' def get_facets(self):
if c := caches['default'].get(SEARCH_CACHE_KEY, None): if self._cache is None:
recipe_list = c['recipe_list'] pass
keyword_list = c['keyword_list'] return {
food_list = c['food_list'] 'cache_key': self.hash_key,
book_list = c['book_list'] 'Ratings': self.get_ratings(),
search_keywords_or = c['search_keywords_or'] 'Recent': self.get_recent(),
search_foods_or = c['search_foods_or'] 'Keywords': self.get_keywords(),
search_books_or = c['search_books_or'] 'Foods': self.get_foods(),
else: 'Books': self.get_books()
return {} }
# if using an OR search, will annotate all keywords, otherwise, just those that appear in results def set_cache(self, key, value):
if search_keywords_or: self._cache = {**self._cache, key: value}
keywords = Keyword.objects.filter(space=request.space).annotate(recipe_count=Count('recipe')) caches['default'].set(
else: self._SEARCH_CACHE_KEY,
keywords = Keyword.objects.filter(recipe__in=recipe_list, space=request.space).annotate(recipe_count=Count('recipe')) self._cache,
# custom django-tree function annotates a queryset to make building a tree easier. self._cache_timeout
# see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details )
kw_a = annotated_qs(keywords, root=True, fill=True)
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results def get_books(self):
if search_foods_or: if self.Books is None:
foods = Food.objects.filter(space=request.space).annotate(recipe_count=Count('ingredient')) self.Books = []
else: return self.Books
foods = Food.objects.filter(ingredient__step__recipe__in=recipe_list, space=request.space).annotate(recipe_count=Count('ingredient'))
food_a = annotated_qs(foods, root=True, fill=True)
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list) def get_keywords(self):
facets['Foods'] = fill_annotated_parents(food_a, food_list) if self.Keywords is None:
# TODO add book facet if self._search_params['search_keywords_or']:
facets['Books'] = [] keywords = Keyword.objects.filter(space=self._request.space).distinct()
c['Keywords'] = facets['Keywords'] else:
c['Foods'] = facets['Foods'] keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
c['Books'] = facets['Books']
caches['default'].set(SEARCH_CACHE_KEY, c, cache_timeout) # Subquery that counts recipes for keyword including children
return facets kw_recipe_count = Recipe.objects.filter(**{'keywords__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space
).values(kw=Substr('keywords__path', 1, Keyword.steplen)
).annotate(count=Count('pk', distinct=True)).values('count')
# set keywords to root objects only
keywords = keywords.annotate(count=Coalesce(Subquery(kw_recipe_count), 0)
).filter(depth=1, count__gt=0
).values('id', 'name', 'count', 'numchild').order_by('name')
self.Keywords = list(keywords)
self.set_cache('Keywords', self.Keywords)
return self.Keywords
def get_foods(self):
if self.Foods is None:
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
if self._search_params['search_foods_or']:
foods = Food.objects.filter(space=self._request.space).distinct()
else:
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
food_recipe_count = Recipe.objects.filter(**{'steps__ingredients__food__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space
).values(kw=Substr('steps__ingredients__food__path', 1, Food.steplen)
).annotate(count=Count('pk', distinct=True)).values('count')
# set keywords to root objects only
foods = foods.annotate(count=Coalesce(Subquery(food_recipe_count), 0)
).filter(depth=1, count__gt=0
).values('id', 'name', 'count', 'numchild'
).order_by('name')
self.Foods = list(foods)
self.set_cache('Foods', self.Foods)
return self.Foods
def get_books(self):
if self.Books is None:
self.Books = []
return self.Books
def get_ratings(self):
if self.Ratings is None:
if self._queryset is None:
self._queryset = Recipe.objects.filter(id__in=self._recipe_list)
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
self.Ratings = dict(Counter(r.rating for r in rating_qs))
self.set_cache('Ratings', self.Ratings)
return self.Ratings
def get_recent(self):
if self.Recent is None:
# TODO make days of recent recipe a setting
recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space, created_at__gte=timezone.now() - timedelta(days=14)
).values_list('recipe__pk', flat=True)
self.Recent = list(recent_recipes)
self.set_cache('Recent', self.Recent)
return self.Recent
def fill_annotated_parents(annotation, filters): # # TODO: This might be faster https://github.com/django-treebeard/django-treebeard/issues/115
tree_list = [] # def get_facet(qs=None, request=None, use_cache=True, hash_key=None, food=None, keyword=None):
parent = [] # """
i = 0 # Gets an annotated list from a queryset.
level = -1 # :param qs:
for r in annotation: # recipe queryset to build facets from
expand = False # :param request:
# the web request that contains the necessary query parameters
# :param use_cache:
# will find results in cache, if any, and return them or empty list.
# will save the list of recipes IDs in the cache for future processing
# :param hash_key:
# the cache key of the recipe list to process
# only evaluated if the use_cache parameter is false
# :param food:
# return children facets of food
# only evaluated if the use_cache parameter is false
# :param keyword:
# return children facets of keyword
# only evaluated if the use_cache parameter is false
# """
# facets = {}
# recipe_list = []
# cache_timeout = 600
annotation[i][1]['id'] = r[0].id # # return cached values
annotation[i][1]['name'] = r[0].name # if use_cache:
annotation[i][1]['count'] = getattr(r[0], 'recipe_count', 0) # qs_hash = hash(frozenset(qs.values_list('pk')))
annotation[i][1]['isDefaultExpanded'] = False # facets['cache_key'] = str(qs_hash)
# SEARCH_CACHE_KEY = f"recipes_filter_{qs_hash}"
# if c := caches['default'].get(SEARCH_CACHE_KEY, None):
# facets['Keywords'] = c['Keywords'] or []
# facets['Foods'] = c['Foods'] or []
# facets['Books'] = c['Books'] or []
# facets['Ratings'] = c['Ratings'] or []
# facets['Recent'] = c['Recent'] or []
# else:
# facets['Keywords'] = []
# facets['Foods'] = []
# facets['Books'] = []
# # TODO make ratings a settings user-only vs all-users
# rating_qs = qs.annotate(rating=Round(Avg(Case(When(cooklog__created_by=request.user, then='cooklog__rating'), default=Value(0)))))
# facets['Ratings'] = dict(Counter(r.rating for r in rating_qs))
# facets['Recent'] = ViewLog.objects.filter(
# created_by=request.user, space=request.space,
# created_at__gte=timezone.now() - timedelta(days=14) # TODO make days of recent recipe a setting
# ).values_list('recipe__pk', flat=True)
if str(r[0].id) in filters: # cached_search = {
expand = True # 'recipe_list': list(qs.values_list('id', flat=True)),
if r[1]['level'] < level: # 'keyword_list': request.query_params.getlist('keywords', []),
parent = parent[:r[1]['level'] - level] # 'food_list': request.query_params.getlist('foods', []),
parent[-1] = i # 'book_list': request.query_params.getlist('book', []),
level = r[1]['level'] # 'search_keywords_or': str2bool(request.query_params.get('keywords_or', True)),
elif r[1]['level'] > level: # 'search_foods_or': str2bool(request.query_params.get('foods_or', True)),
parent.extend([i]) # 'search_books_or': str2bool(request.query_params.get('books_or', True)),
level = r[1]['level'] # 'space': request.space,
else: # 'Ratings': facets['Ratings'],
parent[-1] = i # 'Recent': facets['Recent'],
j = 0 # 'Keywords': facets['Keywords'],
# 'Foods': facets['Foods'],
# 'Books': facets['Books']
# }
# caches['default'].set(SEARCH_CACHE_KEY, cached_search, cache_timeout)
# return facets
while j < level: # # construct and cache new values by retrieving search parameters from the cache
# this causes some double counting when a recipe has both a child and an ancestor # SEARCH_CACHE_KEY = f'recipes_filter_{hash_key}'
annotation[parent[j]][1]['count'] += getattr(r[0], 'recipe_count', 0) # if c := caches['default'].get(SEARCH_CACHE_KEY, None):
if expand: # recipe_list = c['recipe_list']
annotation[parent[j]][1]['isDefaultExpanded'] = True # keyword_list = c['keyword_list']
j += 1 # food_list = c['food_list']
if level == 0: # book_list = c['book_list']
tree_list.append(annotation[i][1]) # search_keywords_or = c['search_keywords_or']
elif level > 0: # search_foods_or = c['search_foods_or']
annotation[parent[level - 1]][1].setdefault('children', []).append(annotation[i][1]) # search_books_or = c['search_books_or']
i += 1 # else:
return tree_list # return {}
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
# if search_keywords_or:
# keywords = Keyword.objects.filter(space=request.space).distinct()
# else:
# keywords = Keyword.objects.filter(Q(recipe__in=recipe_list) | Q(depth=1)).filter(space=request.space).distinct()
# # Subquery that counts recipes for keyword including children
# kw_recipe_count = Recipe.objects.filter(**{'keywords__path__startswith': OuterRef('path')}, id__in=recipe_list, space=request.space
# ).values(kw=Substr('keywords__path', 1, Keyword.steplen)
# ).annotate(count=Count('pk', distinct=True)).values('count')
# # set keywords to root objects only
# keywords = keywords.annotate(count=Coalesce(Subquery(kw_recipe_count), 0)
# ).filter(depth=1, count__gt=0
# ).values('id', 'name', 'count', 'numchild'
# ).order_by('name')
# if keyword:
# facets['Keywords'] = list(keywords)
# return facets
# # custom django-tree function annotates a queryset to make building a tree easier.
# # see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details
# # kw_a = annotated_qs(keywords, root=True, fill=True)
# # # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
# if search_foods_or:
# foods = Food.objects.filter(space=request.space).distinct()
# else:
# foods = Food.objects.filter(Q(ingredient__step__recipe__in=recipe_list) | Q(depth=1)).filter(space=request.space).distinct()
# food_recipe_count = Recipe.objects.filter(**{'steps__ingredients__food__path__startswith': OuterRef('path')}, id__in=recipe_list, space=request.space
# ).values(kw=Substr('steps__ingredients__food__path', 1, Food.steplen * (1+getattr(food, 'depth', 0)))
# ).annotate(count=Count('pk', distinct=True)).values('count')
# # set keywords to root objects only
# foods = foods.annotate(count=Coalesce(Subquery(food_recipe_count), 0)
# ).filter(depth=(1+getattr(food, 'depth', 0)), count__gt=0
# ).values('id', 'name', 'count', 'numchild'
# ).order_by('name')
# if food:
# facets['Foods'] = list(foods)
# return facets
# # food_a = annotated_qs(foods, root=True, fill=True)
# # c['Keywords'] = facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
# c['Keywords'] = facets['Keywords'] = list(keywords)
# # c['Foods'] = facets['Foods'] = fill_annotated_parents(food_a, food_list)
# c['Foods'] = facets['Foods'] = list(foods)
# # TODO add book facet
# c['Books'] = facets['Books'] = []
# caches['default'].set(SEARCH_CACHE_KEY, c, cache_timeout)
# return facets
def annotated_qs(qs, root=False, fill=False): # def fill_annotated_parents(annotation, filters):
""" # tree_list = []
Gets an annotated list from a queryset. # parent = []
:param root: # i = 0
# level = -1
# for r in annotation:
# expand = False
Will backfill in annotation to include all parents to root node. # annotation[i][1]['id'] = r[0].id
# annotation[i][1]['name'] = r[0].name
# annotation[i][1]['count'] = getattr(r[0], 'recipe_count', 0)
# annotation[i][1]['isDefaultExpanded'] = False
:param fill: # if str(r[0].id) in filters:
Will fill in gaps in annotation where nodes between children # expand = True
and ancestors are not included in the queryset. # if r[1]['level'] < level:
""" # parent = parent[:r[1]['level'] - level]
# parent[-1] = i
# level = r[1]['level']
# elif r[1]['level'] > level:
# parent.extend([i])
# level = r[1]['level']
# else:
# parent[-1] = i
# j = 0
result, info = [], {} # while j < level:
start_depth, prev_depth = (None, None) # # this causes some double counting when a recipe has both a child and an ancestor
nodes_list = list(qs.values_list('pk', flat=True)) # annotation[parent[j]][1]['count'] += getattr(r[0], 'recipe_count', 0)
for node in qs.order_by('path'): # if expand:
node_queue = [node] # annotation[parent[j]][1]['isDefaultExpanded'] = True
while len(node_queue) > 0: # j += 1
dirty = False # if level == 0:
current_node = node_queue[-1] # tree_list.append(annotation[i][1])
depth = current_node.get_depth() # elif level > 0:
parent_id = current_node.parent # annotation[parent[level - 1]][1].setdefault('children', []).append(annotation[i][1])
if root and depth > 1 and parent_id not in nodes_list: # i += 1
parent_id = current_node.parent # return tree_list
nodes_list.append(parent_id)
node_queue.append(current_node.__class__.objects.get(pk=parent_id))
dirty = True
if fill and depth > 1 and prev_depth and depth > prev_depth and parent_id not in nodes_list:
nodes_list.append(parent_id)
node_queue.append(current_node.__class__.objects.get(pk=parent_id))
dirty = True
if not dirty: # def annotated_qs(qs, root=False, fill=False):
working_node = node_queue.pop() # """
if start_depth is None: # Gets an annotated list from a queryset.
start_depth = depth # :param root:
open = (depth and (prev_depth is None or depth > prev_depth))
if prev_depth is not None and depth < prev_depth: # Will backfill in annotation to include all parents to root node.
info['close'] = list(range(0, prev_depth - depth))
info = {'open': open, 'close': [], 'level': depth - start_depth} # :param fill:
result.append((working_node, info,)) # Will fill in gaps in annotation where nodes between children
prev_depth = depth # and ancestors are not included in the queryset.
if start_depth and start_depth > 0: # """
info['close'] = list(range(0, prev_depth - start_depth + 1))
return result # result, info = [], {}
# start_depth, prev_depth = (None, None)
# nodes_list = list(qs.values_list('pk', flat=True))
# for node in qs.order_by('path'):
# node_queue = [node]
# while len(node_queue) > 0:
# dirty = False
# current_node = node_queue[-1]
# depth = current_node.get_depth()
# parent_id = current_node.parent
# if root and depth > 1 and parent_id not in nodes_list:
# parent_id = current_node.parent
# nodes_list.append(parent_id)
# node_queue.append(current_node.__class__.objects.get(pk=parent_id))
# dirty = True
# if fill and depth > 1 and prev_depth and depth > prev_depth and parent_id not in nodes_list:
# nodes_list.append(parent_id)
# node_queue.append(current_node.__class__.objects.get(pk=parent_id))
# dirty = True
# if not dirty:
# working_node = node_queue.pop()
# if start_depth is None:
# start_depth = depth
# open = (depth and (prev_depth is None or depth > prev_depth))
# if prev_depth is not None and depth < prev_depth:
# info['close'] = list(range(0, prev_depth - depth))
# info = {'open': open, 'close': [], 'level': depth - start_depth}
# result.append((working_node, info,))
# prev_depth = depth
# if start_depth and start_depth > 0:
# info['close'] = list(range(0, prev_depth - start_depth + 1))
# return result
def old_search(request): def old_search(request):

View File

@ -38,7 +38,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, Cus
CustomIsShare, CustomIsShared, CustomIsUser, CustomIsShare, CustomIsShared, CustomIsUser,
group_required) group_required)
from cookbook.helper.recipe_html_import import get_recipe_from_source from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_search import get_facet, old_search, search_recipes from cookbook.helper.recipe_search import RecipeFacet, old_search, search_recipes
from cookbook.helper.recipe_url_import import get_from_scraper from cookbook.helper.recipe_url_import import get_from_scraper
from cookbook.helper.shopping_helper import list_from_recipe, shopping_helper from cookbook.helper.shopping_helper import list_from_recipe, shopping_helper
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodInheritField, from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodInheritField,
@ -604,7 +604,7 @@ class RecipePagination(PageNumberPagination):
max_page_size = 100 max_page_size = 100
def paginate_queryset(self, queryset, request, view=None): def paginate_queryset(self, queryset, request, view=None):
self.facets = get_facet(qs=queryset, request=request) self.facets = RecipeFacet(request, queryset=queryset)
return super().paginate_queryset(queryset, request, view) return super().paginate_queryset(queryset, request, view)
def get_paginated_response(self, data): def get_paginated_response(self, data):
@ -613,7 +613,7 @@ class RecipePagination(PageNumberPagination):
('next', self.get_next_link()), ('next', self.get_next_link()),
('previous', self.get_previous_link()), ('previous', self.get_previous_link()),
('results', data), ('results', data),
('facets', self.facets) ('facets', self.facets.get_facets())
])) ]))
@ -651,8 +651,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
self.queryset = self.queryset.filter(space=self.request.space) self.queryset = self.queryset.filter(space=self.request.space)
self.queryset = search_recipes(self.request, self.queryset, self.request.GET) self.queryset = search_recipes(self.request, self.queryset, self.request.GET)
return super().get_queryset().prefetch_related('cooklog_set')
return super().get_queryset()
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
if self.request.GET.get('debug', False): if self.request.GET.get('debug', False):
@ -1132,10 +1131,13 @@ def ingredient_from_string(request):
@group_required('user') @group_required('user')
def get_facets(request): def get_facets(request):
key = request.GET.get('hash', None) key = request.GET.get('hash', None)
food = request.GET.get('food', None)
keyword = request.GET.get('keyword', None)
facets = RecipeFacet(request, hash_key=key)
return JsonResponse( return JsonResponse(
{ {
'facets': get_facet(request=request, use_cache=False, hash_key=key), 'facets': facets.get_facets(),
}, },
status=200 status=200
) )

View File

@ -1,3 +1,4 @@
import time
from os import getenv from os import getenv
from django.conf import settings from django.conf import settings

View File

@ -99,7 +99,7 @@
:options="facets.Keywords" :options="facets.Keywords"
:flat="true" :flat="true"
searchNested searchNested
multiple :multiple="true"
:placeholder="$t('Keywords')" :placeholder="$t('Keywords')"
:normalizer="normalizer" :normalizer="normalizer"
@input="refreshData(false)" @input="refreshData(false)"
@ -123,10 +123,11 @@
<b-input-group class="mt-2"> <b-input-group class="mt-2">
<treeselect <treeselect
v-model="settings.search_foods" v-model="settings.search_foods"
:options="facets.Foods" :options="foodFacet"
:load-options="loadFoodChildren"
:flat="true" :flat="true"
searchNested searchNested
multiple :multiple="true"
:placeholder="$t('Ingredients')" :placeholder="$t('Ingredients')"
:normalizer="normalizer" :normalizer="normalizer"
@input="refreshData(false)" @input="refreshData(false)"
@ -243,7 +244,7 @@ import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprec
import RecipeCard from "@/components/RecipeCard" import RecipeCard from "@/components/RecipeCard"
import GenericMultiselect from "@/components/GenericMultiselect" import GenericMultiselect from "@/components/GenericMultiselect"
import Treeselect from "@riophae/vue-treeselect" import { Treeselect, LOAD_CHILDREN_OPTIONS } from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css" import "@riophae/vue-treeselect/dist/vue-treeselect.css"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher" import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
@ -290,6 +291,16 @@ export default {
} }
}, },
computed: { computed: {
foodFacet: function () {
console.log("test", this.facets)
return this.facets?.Foods?.map((x) => {
if (x?.numchild > 0) {
return { ...x, children: null }
} else {
return x
}
})
},
ratingOptions: function () { ratingOptions: function () {
return [ return [
{ id: 5, label: "⭐⭐⭐⭐⭐" + " (" + (this.facets.Ratings?.["5.0"] ?? 0) + ")" }, { id: 5, label: "⭐⭐⭐⭐⭐" + " (" + (this.facets.Ratings?.["5.0"] ?? 0) + ")" },
@ -403,6 +414,7 @@ export default {
this.pagination_count = result.data.count this.pagination_count = result.data.count
this.facets = result.data.facets this.facets = result.data.facets
console.log(this.facets)
if (this.facets?.cache_key) { if (this.facets?.cache_key) {
this.getFacets(this.facets.cache_key) this.getFacets(this.facets.cache_key)
} }
@ -480,7 +492,7 @@ export default {
} }
}, },
getFacets: function (hash) { getFacets: function (hash) {
this.genericGetAPI("api_get_facets", { hash: hash }).then((response) => { return this.genericGetAPI("api_get_facets", { hash: hash }).then((response) => {
this.facets = { ...this.facets, ...response.data.facets } this.facets = { ...this.facets, ...response.data.facets }
}) })
}, },
@ -512,6 +524,35 @@ export default {
console.log(result.data) console.log(result.data)
}) })
}, },
loadFoodChildren({ action, parentNode, callback }) {
// Typically, do the AJAX stuff here.
// Once the server has responded,
// assign children options to the parent node & call the callback.
if (action === LOAD_CHILDREN_OPTIONS) {
switch (parentNode.id) {
case "success": {
console.log(parentNode)
break
}
// case "no-children": {
// simulateAsyncOperation(() => {
// parentNode.children = []
// callback()
// })
// break
// }
// case "failure": {
// simulateAsyncOperation(() => {
// callback(new Error("Failed to load options: network error."))
// })
// break
// }
default: /* empty */
}
}
callback()
},
}, },
} }
</script> </script>