refactor get_facets as RecipeFacets class
This commit is contained in:
parent
f9b04a3f1e
commit
20d61160ba
@ -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):
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import time
|
||||
from os import getenv
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user