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),
} }
caches['default'].set(SEARCH_CACHE_KEY, cached_search, cache_timeout)
return facets
SEARCH_CACHE_KEY = f'recipes_filter_{hash_key}' self._cache = {
if c := caches['default'].get(SEARCH_CACHE_KEY, None): **self._search_params,
recipe_list = c['recipe_list'] 'recipe_list': self._recipe_list,
keyword_list = c['keyword_list'] 'Ratings': self.Ratings,
food_list = c['food_list'] 'Recent': self.Recent,
book_list = c['book_list'] 'Keywords': self.Keywords,
search_keywords_or = c['search_keywords_or'] 'Foods': self.Foods,
search_foods_or = c['search_foods_or'] 'Books': self.Books
search_books_or = c['search_books_or']
else:
return {}
# if using an OR search, will annotate all keywords, otherwise, just those that appear in results }
if search_keywords_or: caches['default'].set(self._SEARCH_CACHE_KEY, self._cache, self._cache_timeout)
keywords = Keyword.objects.filter(space=request.space).annotate(recipe_count=Count('recipe'))
def get_facets(self):
if self._cache is None:
pass
return {
'cache_key': self.hash_key,
'Ratings': self.get_ratings(),
'Recent': self.get_recent(),
'Keywords': self.get_keywords(),
'Foods': self.get_foods(),
'Books': self.get_books()
}
def set_cache(self, key, value):
self._cache = {**self._cache, key: value}
caches['default'].set(
self._SEARCH_CACHE_KEY,
self._cache,
self._cache_timeout
)
def get_books(self):
if self.Books is None:
self.Books = []
return self.Books
def get_keywords(self):
if self.Keywords is None:
if self._search_params['search_keywords_or']:
keywords = Keyword.objects.filter(space=self._request.space).distinct()
else: else:
keywords = Keyword.objects.filter(recipe__in=recipe_list, space=request.space).annotate(recipe_count=Count('recipe')) keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
# 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 # Subquery that counts recipes for keyword including children
kw_a = annotated_qs(keywords, root=True, fill=True) 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
# # TODO: This might be faster https://github.com/django-treebeard/django-treebeard/issues/115
# def get_facet(qs=None, request=None, use_cache=True, hash_key=None, food=None, keyword=None):
# """
# 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
# :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
# # return cached values
# 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'] = []
# # 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)
# cached_search = {
# 'recipe_list': list(qs.values_list('id', flat=True)),
# 'keyword_list': request.query_params.getlist('keywords', []),
# 'food_list': request.query_params.getlist('foods', []),
# 'book_list': request.query_params.getlist('book', []),
# 'search_keywords_or': str2bool(request.query_params.get('keywords_or', True)),
# 'search_foods_or': str2bool(request.query_params.get('foods_or', True)),
# 'search_books_or': str2bool(request.query_params.get('books_or', True)),
# 'space': request.space,
# 'Ratings': facets['Ratings'],
# 'Recent': facets['Recent'],
# 'Keywords': facets['Keywords'],
# 'Foods': facets['Foods'],
# 'Books': facets['Books']
# }
# caches['default'].set(SEARCH_CACHE_KEY, cached_search, cache_timeout)
# return facets
# # construct and cache new values by retrieving search parameters from the cache
# SEARCH_CACHE_KEY = f'recipes_filter_{hash_key}'
# if c := caches['default'].get(SEARCH_CACHE_KEY, None):
# recipe_list = c['recipe_list']
# keyword_list = c['keyword_list']
# food_list = c['food_list']
# book_list = c['book_list']
# search_keywords_or = c['search_keywords_or']
# search_foods_or = c['search_foods_or']
# search_books_or = c['search_books_or']
# else:
# return {}
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results # # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
if search_foods_or: # if search_keywords_or:
foods = Food.objects.filter(space=request.space).annotate(recipe_count=Count('ingredient')) # keywords = Keyword.objects.filter(space=request.space).distinct()
else: # else:
foods = Food.objects.filter(ingredient__step__recipe__in=recipe_list, space=request.space).annotate(recipe_count=Count('ingredient')) # keywords = Keyword.objects.filter(Q(recipe__in=recipe_list) | Q(depth=1)).filter(space=request.space).distinct()
food_a = annotated_qs(foods, root=True, fill=True)
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list) # # Subquery that counts recipes for keyword including children
facets['Foods'] = fill_annotated_parents(food_a, food_list) # kw_recipe_count = Recipe.objects.filter(**{'keywords__path__startswith': OuterRef('path')}, id__in=recipe_list, space=request.space
# TODO add book facet # ).values(kw=Substr('keywords__path', 1, Keyword.steplen)
facets['Books'] = [] # ).annotate(count=Count('pk', distinct=True)).values('count')
c['Keywords'] = facets['Keywords']
c['Foods'] = facets['Foods'] # # set keywords to root objects only
c['Books'] = facets['Books'] # keywords = keywords.annotate(count=Coalesce(Subquery(kw_recipe_count), 0)
caches['default'].set(SEARCH_CACHE_KEY, c, cache_timeout) # ).filter(depth=1, count__gt=0
return facets # ).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 fill_annotated_parents(annotation, filters): # def fill_annotated_parents(annotation, filters):
tree_list = [] # tree_list = []
parent = [] # parent = []
i = 0 # i = 0
level = -1 # level = -1
for r in annotation: # for r in annotation:
expand = False # expand = False
annotation[i][1]['id'] = r[0].id # annotation[i][1]['id'] = r[0].id
annotation[i][1]['name'] = r[0].name # annotation[i][1]['name'] = r[0].name
annotation[i][1]['count'] = getattr(r[0], 'recipe_count', 0) # annotation[i][1]['count'] = getattr(r[0], 'recipe_count', 0)
annotation[i][1]['isDefaultExpanded'] = False # annotation[i][1]['isDefaultExpanded'] = False
if str(r[0].id) in filters: # if str(r[0].id) in filters:
expand = True # expand = True
if r[1]['level'] < level: # if r[1]['level'] < level:
parent = parent[:r[1]['level'] - level] # parent = parent[:r[1]['level'] - level]
parent[-1] = i # parent[-1] = i
level = r[1]['level'] # level = r[1]['level']
elif r[1]['level'] > level: # elif r[1]['level'] > level:
parent.extend([i]) # parent.extend([i])
level = r[1]['level'] # level = r[1]['level']
else: # else:
parent[-1] = i # parent[-1] = i
j = 0 # j = 0
while j < level: # while j < level:
# this causes some double counting when a recipe has both a child and an ancestor # # this causes some double counting when a recipe has both a child and an ancestor
annotation[parent[j]][1]['count'] += getattr(r[0], 'recipe_count', 0) # annotation[parent[j]][1]['count'] += getattr(r[0], 'recipe_count', 0)
if expand: # if expand:
annotation[parent[j]][1]['isDefaultExpanded'] = True # annotation[parent[j]][1]['isDefaultExpanded'] = True
j += 1 # j += 1
if level == 0: # if level == 0:
tree_list.append(annotation[i][1]) # tree_list.append(annotation[i][1])
elif level > 0: # elif level > 0:
annotation[parent[level - 1]][1].setdefault('children', []).append(annotation[i][1]) # annotation[parent[level - 1]][1].setdefault('children', []).append(annotation[i][1])
i += 1 # i += 1
return tree_list # return tree_list
def annotated_qs(qs, root=False, fill=False): # def annotated_qs(qs, root=False, fill=False):
""" # """
Gets an annotated list from a queryset. # Gets an annotated list from a queryset.
:param root: # :param root:
Will backfill in annotation to include all parents to root node. # Will backfill in annotation to include all parents to root node.
:param fill: # :param fill:
Will fill in gaps in annotation where nodes between children # Will fill in gaps in annotation where nodes between children
and ancestors are not included in the queryset. # and ancestors are not included in the queryset.
""" # """
result, info = [], {} # result, info = [], {}
start_depth, prev_depth = (None, None) # start_depth, prev_depth = (None, None)
nodes_list = list(qs.values_list('pk', flat=True)) # nodes_list = list(qs.values_list('pk', flat=True))
for node in qs.order_by('path'): # for node in qs.order_by('path'):
node_queue = [node] # node_queue = [node]
while len(node_queue) > 0: # while len(node_queue) > 0:
dirty = False # dirty = False
current_node = node_queue[-1] # current_node = node_queue[-1]
depth = current_node.get_depth() # depth = current_node.get_depth()
parent_id = current_node.parent # parent_id = current_node.parent
if root and depth > 1 and parent_id not in nodes_list: # if root and depth > 1 and parent_id not in nodes_list:
parent_id = current_node.parent # parent_id = current_node.parent
nodes_list.append(parent_id) # nodes_list.append(parent_id)
node_queue.append(current_node.__class__.objects.get(pk=parent_id)) # node_queue.append(current_node.__class__.objects.get(pk=parent_id))
dirty = True # dirty = True
if fill and depth > 1 and prev_depth and depth > prev_depth and parent_id not in nodes_list: # if fill and depth > 1 and prev_depth and depth > prev_depth and parent_id not in nodes_list:
nodes_list.append(parent_id) # nodes_list.append(parent_id)
node_queue.append(current_node.__class__.objects.get(pk=parent_id)) # node_queue.append(current_node.__class__.objects.get(pk=parent_id))
dirty = True # dirty = True
if not dirty: # if not dirty:
working_node = node_queue.pop() # working_node = node_queue.pop()
if start_depth is None: # if start_depth is None:
start_depth = depth # start_depth = depth
open = (depth and (prev_depth is None or depth > prev_depth)) # open = (depth and (prev_depth is None or depth > prev_depth))
if prev_depth is not None and depth < prev_depth: # if prev_depth is not None and depth < prev_depth:
info['close'] = list(range(0, prev_depth - depth)) # info['close'] = list(range(0, prev_depth - depth))
info = {'open': open, 'close': [], 'level': depth - start_depth} # info = {'open': open, 'close': [], 'level': depth - start_depth}
result.append((working_node, info,)) # result.append((working_node, info,))
prev_depth = depth # prev_depth = depth
if start_depth and start_depth > 0: # if start_depth and start_depth > 0:
info['close'] = list(range(0, prev_depth - start_depth + 1)) # info['close'] = list(range(0, prev_depth - start_depth + 1))
return result # 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>