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.core.cache import caches
from django.db.models import Avg, Case, Count, Func, Max, Q, Subquery, Value, When
from django.db.models.functions import Coalesce
from django.db.models import Avg, Case, Count, Func, Max, OuterRef, Q, Subquery, Value, When
from django.db.models.functions import Coalesce, Substr
from django.utils import timezone, translation
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
# 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)
else:
# 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 search_foods_or:
# 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)
else:
# 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)
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)
@ -196,189 +201,375 @@ def search_recipes(request, queryset, params):
return queryset
# 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):
"""
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
class CacheEmpty(Exception):
pass
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 = {
'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']
class RecipeFacet():
def __init__(self, request, queryset=None, hash_key=None, cache_timeout=600):
if hash_key is None and queryset is None:
raise ValueError(_("One of queryset or hash_key must be provided"))
self._request = request
self._queryset = queryset
self.hash_key = hash_key or str(hash(frozenset(self._queryset.values_list('pk'))))
self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}"
self._cache_timeout = cache_timeout
self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, None)
if self._cache is None and self._queryset is None:
raise CacheEmpty("No queryset provided and cache empty")
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)
return facets
caches['default'].set(self._SEARCH_CACHE_KEY, self._cache, self._cache_timeout)
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 {}
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()
}
# 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).annotate(recipe_count=Count('recipe'))
else:
keywords = Keyword.objects.filter(recipe__in=recipe_list, space=request.space).annotate(recipe_count=Count('recipe'))
# 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)
def set_cache(self, key, value):
self._cache = {**self._cache, key: value}
caches['default'].set(
self._SEARCH_CACHE_KEY,
self._cache,
self._cache_timeout
)
# # 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).annotate(recipe_count=Count('ingredient'))
else:
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)
def get_books(self):
if self.Books is None:
self.Books = []
return self.Books
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
facets['Foods'] = fill_annotated_parents(food_a, food_list)
# TODO add book facet
facets['Books'] = []
c['Keywords'] = facets['Keywords']
c['Foods'] = facets['Foods']
c['Books'] = facets['Books']
caches['default'].set(SEARCH_CACHE_KEY, c, cache_timeout)
return facets
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:
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
# Subquery that counts recipes for keyword including children
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):
tree_list = []
parent = []
i = 0
level = -1
for r in annotation:
expand = False
# # 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
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
# # 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)
if str(r[0].id) in filters:
expand = True
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
# 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
while j < level:
# 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)
if expand:
annotation[parent[j]][1]['isDefaultExpanded'] = True
j += 1
if level == 0:
tree_list.append(annotation[i][1])
elif level > 0:
annotation[parent[level - 1]][1].setdefault('children', []).append(annotation[i][1])
i += 1
return tree_list
# # 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 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):
"""
Gets an annotated list from a queryset.
:param root:
# def fill_annotated_parents(annotation, filters):
# tree_list = []
# parent = []
# 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:
Will fill in gaps in annotation where nodes between children
and ancestors are not included in the queryset.
"""
# if str(r[0].id) in filters:
# expand = True
# 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 = [], {}
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
# while j < level:
# # 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)
# if expand:
# annotation[parent[j]][1]['isDefaultExpanded'] = True
# j += 1
# if level == 0:
# tree_list.append(annotation[i][1])
# elif level > 0:
# annotation[parent[level - 1]][1].setdefault('children', []).append(annotation[i][1])
# i += 1
# return tree_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)
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 annotated_qs(qs, root=False, fill=False):
# """
# Gets an annotated list from a queryset.
# :param root:
# Will backfill in annotation to include all parents to root node.
# :param fill:
# Will fill in gaps in annotation where nodes between children
# and ancestors are not included in the queryset.
# """
# 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):

View File

@ -38,7 +38,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, Cus
CustomIsShare, CustomIsShared, CustomIsUser,
group_required)
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.shopping_helper import list_from_recipe, shopping_helper
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodInheritField,
@ -604,7 +604,7 @@ class RecipePagination(PageNumberPagination):
max_page_size = 100
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)
def get_paginated_response(self, data):
@ -613,7 +613,7 @@ class RecipePagination(PageNumberPagination):
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('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 = search_recipes(self.request, self.queryset, self.request.GET)
return super().get_queryset()
return super().get_queryset().prefetch_related('cooklog_set')
def list(self, request, *args, **kwargs):
if self.request.GET.get('debug', False):
@ -1132,10 +1131,13 @@ def ingredient_from_string(request):
@group_required('user')
def get_facets(request):
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(
{
'facets': get_facet(request=request, use_cache=False, hash_key=key),
'facets': facets.get_facets(),
},
status=200
)

View File

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

View File

@ -99,7 +99,7 @@
:options="facets.Keywords"
:flat="true"
searchNested
multiple
:multiple="true"
:placeholder="$t('Keywords')"
:normalizer="normalizer"
@input="refreshData(false)"
@ -123,10 +123,11 @@
<b-input-group class="mt-2">
<treeselect
v-model="settings.search_foods"
:options="facets.Foods"
:options="foodFacet"
:load-options="loadFoodChildren"
:flat="true"
searchNested
multiple
:multiple="true"
:placeholder="$t('Ingredients')"
:normalizer="normalizer"
@input="refreshData(false)"
@ -243,7 +244,7 @@ import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprec
import RecipeCard from "@/components/RecipeCard"
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 RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
@ -290,6 +291,16 @@ export default {
}
},
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 () {
return [
{ id: 5, label: "⭐⭐⭐⭐⭐" + " (" + (this.facets.Ratings?.["5.0"] ?? 0) + ")" },
@ -403,6 +414,7 @@ export default {
this.pagination_count = result.data.count
this.facets = result.data.facets
console.log(this.facets)
if (this.facets?.cache_key) {
this.getFacets(this.facets.cache_key)
}
@ -480,7 +492,7 @@ export default {
}
},
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 }
})
},
@ -512,6 +524,35 @@ export default {
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>