mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-04 21:58:54 -05:00
remove facets and treeselect
This commit is contained in:
@@ -1,14 +1,11 @@
|
||||
import json
|
||||
from collections import Counter
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
|
||||
from django.core.cache import cache, caches
|
||||
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value,
|
||||
When)
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Avg, Case, Count, Exists, F, Max, OuterRef, Q, Subquery, Value, When
|
||||
from django.db.models.functions import Coalesce, Lower, Substr
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
||||
from cookbook.managers import DICTIONARY
|
||||
@@ -20,15 +17,17 @@ from recipes import settings
|
||||
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
|
||||
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
||||
class RecipeSearch():
|
||||
_postgres = settings.DATABASES['default']['ENGINE'] in [
|
||||
'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
|
||||
_postgres = settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
|
||||
|
||||
def __init__(self, request, **params):
|
||||
self._request = request
|
||||
self._queryset = None
|
||||
if f := params.get('filter', None):
|
||||
custom_filter = CustomFilter.objects.filter(id=f, space=self._request.space).filter(Q(created_by=self._request.user) |
|
||||
Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first()
|
||||
custom_filter = (
|
||||
CustomFilter.objects.filter(id=f, space=self._request.space)
|
||||
.filter(Q(created_by=self._request.user) | Q(shared=self._request.user) | Q(recipebook__shared=self._request.user))
|
||||
.first()
|
||||
)
|
||||
if custom_filter:
|
||||
self._params = {**json.loads(custom_filter.search)}
|
||||
self._original_params = {**(params or {})}
|
||||
@@ -101,24 +100,18 @@ class RecipeSearch():
|
||||
self._search_type = self._search_prefs.search or 'plain'
|
||||
if self._string:
|
||||
if self._postgres:
|
||||
self._unaccent_include = self._search_prefs.unaccent.values_list(
|
||||
'field', flat=True)
|
||||
self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
|
||||
else:
|
||||
self._unaccent_include = []
|
||||
self._icontains_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
|
||||
self._istartswith_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
|
||||
self._icontains_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
|
||||
self._istartswith_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
|
||||
self._trigram_include = None
|
||||
self._fulltext_include = None
|
||||
self._trigram = False
|
||||
if self._postgres and self._string:
|
||||
self._language = DICTIONARY.get(
|
||||
translation.get_language(), 'simple')
|
||||
self._trigram_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
|
||||
self._fulltext_include = self._search_prefs.fulltext.values_list(
|
||||
'field', flat=True) or None
|
||||
self._language = DICTIONARY.get(translation.get_language(), 'simple')
|
||||
self._trigram_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
|
||||
self._fulltext_include = self._search_prefs.fulltext.values_list('field', flat=True) or None
|
||||
|
||||
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
|
||||
self._trigram = True
|
||||
@@ -178,7 +171,6 @@ class RecipeSearch():
|
||||
|
||||
# if a sort order is provided by user - use that order
|
||||
if self._sort_order:
|
||||
|
||||
if not isinstance(self._sort_order, list):
|
||||
order += [self._sort_order]
|
||||
else:
|
||||
@@ -218,24 +210,18 @@ class RecipeSearch():
|
||||
self._queryset = self._queryset.filter(query_filter).distinct()
|
||||
if self._fulltext_include:
|
||||
if self._fuzzy_match is None:
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=Coalesce(Max(self.search_rank), 0.0))
|
||||
self._queryset = self._queryset.annotate(score=Coalesce(Max(self.search_rank), 0.0))
|
||||
else:
|
||||
self._queryset = self._queryset.annotate(
|
||||
rank=Coalesce(Max(self.search_rank), 0.0))
|
||||
self._queryset = self._queryset.annotate(rank=Coalesce(Max(self.search_rank), 0.0))
|
||||
|
||||
if self._fuzzy_match is not None:
|
||||
simularity = self._fuzzy_match.filter(
|
||||
pk=OuterRef('pk')).values('simularity')
|
||||
simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity')
|
||||
if not self._fulltext_include:
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=Coalesce(Subquery(simularity), 0.0))
|
||||
self._queryset = self._queryset.annotate(score=Coalesce(Subquery(simularity), 0.0))
|
||||
else:
|
||||
self._queryset = self._queryset.annotate(
|
||||
simularity=Coalesce(Subquery(simularity), 0.0))
|
||||
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
|
||||
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=F('rank') + F('simularity'))
|
||||
self._queryset = self._queryset.annotate(score=F('rank') + F('simularity'))
|
||||
else:
|
||||
query_filter = Q()
|
||||
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
|
||||
@@ -244,78 +230,69 @@ class RecipeSearch():
|
||||
|
||||
def _cooked_on_filter(self, cooked_date=None):
|
||||
if self._sort_includes('lastcooked') or cooked_date:
|
||||
lessthan = self._sort_includes(
|
||||
'-lastcooked') or '-' in (cooked_date or [])[:1]
|
||||
lessthan = self._sort_includes('-lastcooked') or '-' in (cooked_date or [])[:1]
|
||||
if lessthan:
|
||||
default = timezone.now() - timedelta(days=100000)
|
||||
else:
|
||||
default = timezone.now()
|
||||
self._queryset = self._queryset.annotate(lastcooked=Coalesce(
|
||||
Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default)))
|
||||
self._queryset = self._queryset.annotate(
|
||||
lastcooked=Coalesce(Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default))
|
||||
)
|
||||
if cooked_date is None:
|
||||
return
|
||||
|
||||
cooked_date = date(*[int(x)
|
||||
for x in cooked_date.split('-') if x != ''])
|
||||
cooked_date = date(*[int(x)for x in cooked_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
||||
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
|
||||
self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
|
||||
|
||||
def _created_on_filter(self, created_date=None):
|
||||
if created_date is None:
|
||||
return
|
||||
lessthan = '-' in created_date[:1]
|
||||
created_date = date(*[int(x)
|
||||
for x in created_date.split('-') if x != ''])
|
||||
created_date = date(*[int(x) for x in created_date.split('-') if x != ''])
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
created_at__date__lte=created_date)
|
||||
self._queryset = self._queryset.filter(created_at__date__lte=created_date)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
created_at__date__gte=created_date)
|
||||
self._queryset = self._queryset.filter(created_at__date__gte=created_date)
|
||||
|
||||
def _updated_on_filter(self, updated_date=None):
|
||||
if updated_date is None:
|
||||
return
|
||||
lessthan = '-' in updated_date[:1]
|
||||
updated_date = date(*[int(x)
|
||||
for x in updated_date.split('-') if x != ''])
|
||||
updated_date = date(*[int(x)for x in updated_date.split('-') if x != ''])
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
updated_at__date__lte=updated_date)
|
||||
self._queryset = self._queryset.filter(updated_at__date__lte=updated_date)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
updated_at__date__gte=updated_date)
|
||||
self._queryset = self._queryset.filter(updated_at__date__gte=updated_date)
|
||||
|
||||
def _viewed_on_filter(self, viewed_date=None):
|
||||
if self._sort_includes('lastviewed') or viewed_date:
|
||||
longTimeAgo = timezone.now() - timedelta(days=100000)
|
||||
self._queryset = self._queryset.annotate(lastviewed=Coalesce(
|
||||
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo)))
|
||||
self._queryset = self._queryset.annotate(
|
||||
lastviewed=Coalesce(Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo))
|
||||
)
|
||||
if viewed_date is None:
|
||||
return
|
||||
lessthan = '-' in viewed_date[:1]
|
||||
viewed_date = date(*[int(x)
|
||||
for x in viewed_date.split('-') if x != ''])
|
||||
viewed_date = date(*[int(x)for x in viewed_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
self._queryset = self._queryset.filter(lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
|
||||
def _new_recipes(self, new_days=7):
|
||||
# TODO make new days a user-setting
|
||||
if not self._new:
|
||||
return
|
||||
self._queryset = (
|
||||
self._queryset.annotate(new_recipe=Case(
|
||||
When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')), default=Value(0), ))
|
||||
self._queryset = self._queryset.annotate(
|
||||
new_recipe=Case(
|
||||
When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')),
|
||||
default=Value(0),
|
||||
)
|
||||
)
|
||||
|
||||
def _recently_viewed(self, num_recent=None):
|
||||
@@ -325,34 +302,35 @@ class RecipeSearch():
|
||||
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__pk'))), Value(0)))
|
||||
return
|
||||
|
||||
num_recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space).values(
|
||||
'recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
|
||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(
|
||||
pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
||||
num_recent_recipes = (
|
||||
ViewLog.objects.filter(created_by=self._request.user, space=self._request.space)
|
||||
.values('recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
|
||||
)
|
||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
||||
|
||||
def _favorite_recipes(self, times_cooked=None):
|
||||
if self._sort_includes('favorite') or times_cooked:
|
||||
less_than = '-' in (times_cooked or []
|
||||
) and not self._sort_includes('-favorite')
|
||||
less_than = '-' in (times_cooked or []) and not self._sort_includes('-favorite')
|
||||
if less_than:
|
||||
default = 1000
|
||||
else:
|
||||
default = 0
|
||||
favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk')
|
||||
).values('recipe').annotate(count=Count('pk', distinct=True)).values('count')
|
||||
self._queryset = self._queryset.annotate(
|
||||
favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
favorite_recipes = (
|
||||
CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk'))
|
||||
.values('recipe')
|
||||
.annotate(count=Count('pk', distinct=True))
|
||||
.values('count')
|
||||
)
|
||||
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
if times_cooked is None:
|
||||
return
|
||||
|
||||
if times_cooked == '0':
|
||||
self._queryset = self._queryset.filter(favorite=0)
|
||||
elif less_than:
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(
|
||||
times_cooked.replace('-', ''))).exclude(favorite=0)
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(times_cooked.replace('-', ''))).exclude(favorite=0)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
favorite__gte=int(times_cooked))
|
||||
self._queryset = self._queryset.filter(favorite__gte=int(times_cooked))
|
||||
|
||||
def keyword_filters(self, **kwargs):
|
||||
if all([kwargs[x] is None for x in kwargs]):
|
||||
@@ -385,8 +363,7 @@ class RecipeSearch():
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_and)
|
||||
if 'not' in kw_filter:
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
|
||||
def food_filters(self, **kwargs):
|
||||
if all([kwargs[x] is None for x in kwargs]):
|
||||
@@ -400,8 +377,7 @@ class RecipeSearch():
|
||||
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
|
||||
if 'or' in fd_filter:
|
||||
if self._include_children:
|
||||
f_or = Q(
|
||||
steps__ingredients__food__in=Food.include_descendants(foods))
|
||||
f_or = Q(steps__ingredients__food__in=Food.include_descendants(foods))
|
||||
else:
|
||||
f_or = Q(steps__ingredients__food__in=foods)
|
||||
|
||||
@@ -413,8 +389,7 @@ class RecipeSearch():
|
||||
recipes = Recipe.objects.all()
|
||||
for food in foods:
|
||||
if self._include_children:
|
||||
f_and = Q(
|
||||
steps__ingredients__food__in=food.get_descendants_and_self())
|
||||
f_and = Q(steps__ingredients__food__in=food.get_descendants_and_self())
|
||||
else:
|
||||
f_and = Q(steps__ingredients__food=food)
|
||||
if 'not' in fd_filter:
|
||||
@@ -422,8 +397,7 @@ class RecipeSearch():
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_and)
|
||||
if 'not' in fd_filter:
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
|
||||
def unit_filters(self, units=None, operator=True):
|
||||
if operator != True:
|
||||
@@ -432,8 +406,7 @@ class RecipeSearch():
|
||||
return
|
||||
if not isinstance(units, list):
|
||||
units = [units]
|
||||
self._queryset = self._queryset.filter(
|
||||
steps__ingredients__unit__in=units)
|
||||
self._queryset = self._queryset.filter(steps__ingredients__unit__in=units)
|
||||
|
||||
def rating_filter(self, rating=None):
|
||||
if rating or self._sort_includes('rating'):
|
||||
@@ -479,14 +452,11 @@ class RecipeSearch():
|
||||
recipes = Recipe.objects.all()
|
||||
for book in kwargs[bk_filter]:
|
||||
if 'not' in bk_filter:
|
||||
recipes = recipes.filter(
|
||||
recipebookentry__book__id=book)
|
||||
recipes = recipes.filter(recipebookentry__book__id=book)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
recipebookentry__book__id=book)
|
||||
self._queryset = self._queryset.filter(recipebookentry__book__id=book)
|
||||
if 'not' in bk_filter:
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
|
||||
def step_filters(self, steps=None, operator=True):
|
||||
if operator != True:
|
||||
@@ -505,25 +475,20 @@ class RecipeSearch():
|
||||
rank = []
|
||||
if 'name' in self._fulltext_include:
|
||||
vectors.append('name_search_vector')
|
||||
rank.append(SearchRank('name_search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('name_search_vector', self.search_query, cover_density=True))
|
||||
if 'description' in self._fulltext_include:
|
||||
vectors.append('desc_search_vector')
|
||||
rank.append(SearchRank('desc_search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('desc_search_vector', self.search_query, cover_density=True))
|
||||
if 'steps__instruction' in self._fulltext_include:
|
||||
vectors.append('steps__search_vector')
|
||||
rank.append(SearchRank('steps__search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('steps__search_vector', self.search_query, cover_density=True))
|
||||
if 'keywords__name' in self._fulltext_include:
|
||||
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
|
||||
vectors.append('keywords__name__unaccent')
|
||||
rank.append(SearchRank('keywords__name__unaccent',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('keywords__name__unaccent', self.search_query, cover_density=True))
|
||||
if 'steps__ingredients__food__name' in self._fulltext_include:
|
||||
vectors.append('steps__ingredients__food__name__unaccent')
|
||||
rank.append(SearchRank('steps__ingredients__food__name',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('steps__ingredients__food__name', self.search_query, cover_density=True))
|
||||
|
||||
for r in rank:
|
||||
if self.search_rank is None:
|
||||
@@ -531,8 +496,7 @@ class RecipeSearch():
|
||||
else:
|
||||
self.search_rank += r
|
||||
# modifying queryset will annotation creates duplicate results
|
||||
self._filters.append(Q(id__in=Recipe.objects.annotate(
|
||||
vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
|
||||
self._filters.append(Q(id__in=Recipe.objects.annotate(vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
|
||||
|
||||
def build_text_filters(self, string=None):
|
||||
if not string:
|
||||
@@ -557,15 +521,19 @@ class RecipeSearch():
|
||||
trigram += TrigramSimilarity(f, self._string)
|
||||
else:
|
||||
trigram = TrigramSimilarity(f, self._string)
|
||||
self._fuzzy_match = Recipe.objects.annotate(trigram=trigram).distinct(
|
||||
).annotate(simularity=Max('trigram')).values('id', 'simularity').filter(simularity__gt=self._search_prefs.trigram_threshold)
|
||||
self._fuzzy_match = (
|
||||
Recipe.objects.annotate(trigram=trigram)
|
||||
.distinct()
|
||||
.annotate(simularity=Max('trigram'))
|
||||
.values('id', 'simularity')
|
||||
.filter(simularity__gt=self._search_prefs.trigram_threshold)
|
||||
)
|
||||
self._filters += [Q(pk__in=self._fuzzy_match.values('pk'))]
|
||||
|
||||
def _makenow_filter(self, missing=None):
|
||||
if missing is None or (isinstance(missing, bool) and missing == False):
|
||||
return
|
||||
shopping_users = [
|
||||
*self._request.user.get_shopping_share(), self._request.user]
|
||||
shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
|
||||
|
||||
onhand_filter = (
|
||||
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
|
||||
@@ -575,264 +543,255 @@ class RecipeSearch():
|
||||
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
||||
)
|
||||
makenow_recipes = Recipe.objects.annotate(
|
||||
count_food=Count('steps__ingredients__food__pk', filter=Q(
|
||||
steps__ingredients__food__isnull=False), distinct=True),
|
||||
count_onhand=Count('steps__ingredients__food__pk',
|
||||
filter=onhand_filter, distinct=True),
|
||||
count_ignore_shopping=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True,
|
||||
steps__ingredients__food__recipe__isnull=True), distinct=True),
|
||||
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(
|
||||
shopping_users), then=Value(1)), default=Value(0)),
|
||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(
|
||||
shopping_users), then=Value(1)), default=Value(0))
|
||||
count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
|
||||
count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
|
||||
count_ignore_shopping=Count(
|
||||
'steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True, steps__ingredients__food__recipe__isnull=True), distinct=True
|
||||
),
|
||||
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users), then=Value(1)), default=Value(0)),
|
||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)), default=Value(0))
|
||||
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood=missing)
|
||||
self._queryset = self._queryset.distinct().filter(
|
||||
id__in=makenow_recipes.values('id'))
|
||||
self._queryset = self._queryset.distinct().filter(id__in=makenow_recipes.values('id'))
|
||||
|
||||
@staticmethod
|
||||
def __children_substitute_filter(shopping_users=None):
|
||||
children_onhand_subquery = Food.objects.filter(
|
||||
path__startswith=OuterRef('path'),
|
||||
depth__gt=OuterRef('depth'),
|
||||
onhand_users__in=shopping_users
|
||||
children_onhand_subquery = Food.objects.filter(path__startswith=OuterRef('path'), depth__gt=OuterRef('depth'), onhand_users__in=shopping_users)
|
||||
return (
|
||||
Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
|
||||
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
|
||||
)
|
||||
.exclude(depth=1, numchild=0)
|
||||
.filter(substitute_children=True)
|
||||
.annotate(child_onhand_count=Exists(children_onhand_subquery))
|
||||
.filter(child_onhand_count=True)
|
||||
)
|
||||
return Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
|
||||
Q(onhand_users__in=shopping_users)
|
||||
| Q(ignore_shopping=True, recipe__isnull=True)
|
||||
| Q(substitute__onhand_users__in=shopping_users)
|
||||
).exclude(depth=1, numchild=0
|
||||
).filter(substitute_children=True
|
||||
).annotate(child_onhand_count=Exists(children_onhand_subquery)
|
||||
).filter(child_onhand_count=True)
|
||||
|
||||
@staticmethod
|
||||
def __sibling_substitute_filter(shopping_users=None):
|
||||
sibling_onhand_subquery = Food.objects.filter(
|
||||
path__startswith=Substr(
|
||||
OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
|
||||
depth=OuterRef('depth'),
|
||||
onhand_users__in=shopping_users
|
||||
path__startswith=Substr(OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)), depth=OuterRef('depth'), onhand_users__in=shopping_users
|
||||
)
|
||||
return Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
|
||||
Q(onhand_users__in=shopping_users)
|
||||
| Q(ignore_shopping=True, recipe__isnull=True)
|
||||
| Q(substitute__onhand_users__in=shopping_users)
|
||||
).exclude(depth=1, numchild=0
|
||||
).filter(substitute_siblings=True
|
||||
).annotate(sibling_onhand=Exists(sibling_onhand_subquery)
|
||||
).filter(sibling_onhand=True)
|
||||
|
||||
|
||||
class RecipeFacet():
|
||||
class CacheEmpty(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, request, queryset=None, hash_key=None, cache_timeout=3600):
|
||||
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(self._queryset.query))
|
||||
self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}"
|
||||
self._cache_timeout = cache_timeout
|
||||
self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, {})
|
||||
if self._cache is None and self._queryset is None:
|
||||
raise self.CacheEmpty("No queryset provided and cache empty")
|
||||
|
||||
self.Keywords = self._cache.get('Keywords', None)
|
||||
self.Foods = self._cache.get('Foods', None)
|
||||
self.Books = self._cache.get('Books', None)
|
||||
self.Ratings = self._cache.get('Ratings', None)
|
||||
# TODO Move Recent to recipe annotation/serializer: requrires change in RecipeSearch(), RecipeSearchView.vue and serializer
|
||||
self.Recent = self._cache.get('Recent', None)
|
||||
|
||||
if self._queryset is not None:
|
||||
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 is not None:
|
||||
self._recipe_list = self._cache.get('recipe_list', [])
|
||||
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(self._SEARCH_CACHE_KEY,
|
||||
self._cache, self._cache_timeout)
|
||||
|
||||
def get_facets(self, from_cache=False):
|
||||
if from_cache:
|
||||
return {
|
||||
'cache_key': self.hash_key or '',
|
||||
'Ratings': self.Ratings or {},
|
||||
'Recent': self.Recent or [],
|
||||
'Keywords': self.Keywords or [],
|
||||
'Foods': self.Foods or [],
|
||||
'Books': self.Books or []
|
||||
}
|
||||
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
|
||||
return (
|
||||
Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
|
||||
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
|
||||
)
|
||||
.exclude(depth=1, numchild=0)
|
||||
.filter(substitute_siblings=True)
|
||||
.annotate(sibling_onhand=Exists(sibling_onhand_subquery))
|
||||
.filter(sibling_onhand=True)
|
||||
)
|
||||
|
||||
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:
|
||||
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(
|
||||
depth=1)).filter(space=self._request.space).distinct()
|
||||
# class RecipeFacet():
|
||||
# class CacheEmpty(Exception):
|
||||
# pass
|
||||
|
||||
# set keywords to root objects only
|
||||
keywords = self._keyword_queryset(keywords)
|
||||
self.Keywords = [{**x, 'children': None}
|
||||
if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.set_cache('Keywords', self.Keywords)
|
||||
return self.Keywords
|
||||
# def __init__(self, request, queryset=None, hash_key=None, cache_timeout=3600):
|
||||
# if hash_key is None and queryset is None:
|
||||
# raise ValueError(_("One of queryset or hash_key must be provided"))
|
||||
|
||||
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()
|
||||
# self._request = request
|
||||
# self._queryset = queryset
|
||||
# self.hash_key = hash_key or str(hash(self._queryset.query))
|
||||
# self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}"
|
||||
# self._cache_timeout = cache_timeout
|
||||
# self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, {})
|
||||
# if self._cache is None and self._queryset is None:
|
||||
# raise self.CacheEmpty("No queryset provided and cache empty")
|
||||
|
||||
# set keywords to root objects only
|
||||
foods = self._food_queryset(foods)
|
||||
# self.Keywords = self._cache.get('Keywords', None)
|
||||
# self.Foods = self._cache.get('Foods', None)
|
||||
# self.Books = self._cache.get('Books', None)
|
||||
# self.Ratings = self._cache.get('Ratings', None)
|
||||
# # TODO Move Recent to recipe annotation/serializer: requrires change in RecipeSearch(), RecipeSearchView.vue and serializer
|
||||
# self.Recent = self._cache.get('Recent', None)
|
||||
|
||||
self.Foods = [{**x, 'children': None}
|
||||
if x['numchild'] > 0 else x for x in list(foods)]
|
||||
self.set_cache('Foods', self.Foods)
|
||||
return self.Foods
|
||||
# if self._queryset is not None:
|
||||
# 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 is not None:
|
||||
# self._recipe_list = self._cache.get('recipe_list', [])
|
||||
# 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),
|
||||
# }
|
||||
|
||||
def get_ratings(self):
|
||||
if self.Ratings is None:
|
||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
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))
|
||||
else:
|
||||
self.Rating = {}
|
||||
self.set_cache('Ratings', self.Ratings)
|
||||
return self.Ratings
|
||||
# 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
|
||||
|
||||
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
|
||||
# }
|
||||
# caches['default'].set(self._SEARCH_CACHE_KEY,
|
||||
# self._cache, self._cache_timeout)
|
||||
|
||||
def add_food_children(self, id):
|
||||
try:
|
||||
food = Food.objects.get(id=id)
|
||||
nodes = food.get_ancestors()
|
||||
except Food.DoesNotExist:
|
||||
return self.get_facets()
|
||||
foods = self._food_queryset(food.get_children(), food)
|
||||
deep_search = self.Foods
|
||||
for node in nodes:
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == node.id), None)
|
||||
deep_search = deep_search[index]['children']
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == food.id), None)
|
||||
deep_search[index]['children'] = [
|
||||
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
||||
self.set_cache('Foods', self.Foods)
|
||||
return self.get_facets()
|
||||
# def get_facets(self, from_cache=False):
|
||||
# if from_cache:
|
||||
# return {
|
||||
# 'cache_key': self.hash_key or '',
|
||||
# 'Ratings': self.Ratings or {},
|
||||
# 'Recent': self.Recent or [],
|
||||
# 'Keywords': self.Keywords or [],
|
||||
# 'Foods': self.Foods or [],
|
||||
# 'Books': self.Books or []
|
||||
# }
|
||||
# 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 add_keyword_children(self, id):
|
||||
try:
|
||||
keyword = Keyword.objects.get(id=id)
|
||||
nodes = keyword.get_ancestors()
|
||||
except Keyword.DoesNotExist:
|
||||
return self.get_facets()
|
||||
keywords = self._keyword_queryset(keyword.get_children(), keyword)
|
||||
deep_search = self.Keywords
|
||||
for node in nodes:
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == node.id), None)
|
||||
deep_search = deep_search[index]['children']
|
||||
index = next((i for i, x in enumerate(deep_search)
|
||||
if x["id"] == keyword.id), None)
|
||||
deep_search[index]['children'] = [
|
||||
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.set_cache('Keywords', self.Keywords)
|
||||
return self.get_facets()
|
||||
# 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 _recipe_count_queryset(self, field, depth=1, steplen=4):
|
||||
return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path'), f'{field}__depth__gte': depth}, id__in=self._recipe_list, space=self._request.space
|
||||
).annotate(count=Coalesce(Func('pk', function='Count'), 0)).values('count')
|
||||
# def get_books(self):
|
||||
# if self.Books is None:
|
||||
# self.Books = []
|
||||
# return self.Books
|
||||
|
||||
def _keyword_queryset(self, queryset, keyword=None):
|
||||
depth = getattr(keyword, 'depth', 0) + 1
|
||||
steplen = depth * Keyword.steplen
|
||||
# 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()
|
||||
|
||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0)
|
||||
).filter(depth=depth, count__gt=0
|
||||
).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
||||
else:
|
||||
return queryset.filter(depth=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
||||
# # set keywords to root objects only
|
||||
# keywords = self._keyword_queryset(keywords)
|
||||
# self.Keywords = [{**x, 'children': None}
|
||||
# if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
# self.set_cache('Keywords', self.Keywords)
|
||||
# return self.Keywords
|
||||
|
||||
def _food_queryset(self, queryset, food=None):
|
||||
depth = getattr(food, 'depth', 0) + 1
|
||||
steplen = depth * Food.steplen
|
||||
# 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()
|
||||
|
||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0)
|
||||
).filter(depth__lte=depth, count__gt=0
|
||||
).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
||||
else:
|
||||
return queryset.filter(depth__lte=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
||||
# # set keywords to root objects only
|
||||
# foods = self._food_queryset(foods)
|
||||
|
||||
# self.Foods = [{**x, 'children': None}
|
||||
# if x['numchild'] > 0 else x for x in list(foods)]
|
||||
# self.set_cache('Foods', self.Foods)
|
||||
# return self.Foods
|
||||
|
||||
# def get_ratings(self):
|
||||
# if self.Ratings is None:
|
||||
# if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
# 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))
|
||||
# else:
|
||||
# self.Rating = {}
|
||||
# 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 add_food_children(self, id):
|
||||
# try:
|
||||
# food = Food.objects.get(id=id)
|
||||
# nodes = food.get_ancestors()
|
||||
# except Food.DoesNotExist:
|
||||
# return self.get_facets()
|
||||
# foods = self._food_queryset(food.get_children(), food)
|
||||
# deep_search = self.Foods
|
||||
# for node in nodes:
|
||||
# index = next((i for i, x in enumerate(
|
||||
# deep_search) if x["id"] == node.id), None)
|
||||
# deep_search = deep_search[index]['children']
|
||||
# index = next((i for i, x in enumerate(
|
||||
# deep_search) if x["id"] == food.id), None)
|
||||
# deep_search[index]['children'] = [
|
||||
# {**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
||||
# self.set_cache('Foods', self.Foods)
|
||||
# return self.get_facets()
|
||||
|
||||
# def add_keyword_children(self, id):
|
||||
# try:
|
||||
# keyword = Keyword.objects.get(id=id)
|
||||
# nodes = keyword.get_ancestors()
|
||||
# except Keyword.DoesNotExist:
|
||||
# return self.get_facets()
|
||||
# keywords = self._keyword_queryset(keyword.get_children(), keyword)
|
||||
# deep_search = self.Keywords
|
||||
# for node in nodes:
|
||||
# index = next((i for i, x in enumerate(
|
||||
# deep_search) if x["id"] == node.id), None)
|
||||
# deep_search = deep_search[index]['children']
|
||||
# index = next((i for i, x in enumerate(deep_search)
|
||||
# if x["id"] == keyword.id), None)
|
||||
# deep_search[index]['children'] = [
|
||||
# {**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
# self.set_cache('Keywords', self.Keywords)
|
||||
# return self.get_facets()
|
||||
|
||||
# def _recipe_count_queryset(self, field, depth=1, steplen=4):
|
||||
# return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path'), f'{field}__depth__gte': depth}, id__in=self._recipe_list, space=self._request.space
|
||||
# ).annotate(count=Coalesce(Func('pk', function='Count'), 0)).values('count')
|
||||
|
||||
# def _keyword_queryset(self, queryset, keyword=None):
|
||||
# depth = getattr(keyword, 'depth', 0) + 1
|
||||
# steplen = depth * Keyword.steplen
|
||||
|
||||
# if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
# return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0)
|
||||
# ).filter(depth=depth, count__gt=0
|
||||
# ).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
||||
# else:
|
||||
# return queryset.filter(depth=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
||||
|
||||
# def _food_queryset(self, queryset, food=None):
|
||||
# depth = getattr(food, 'depth', 0) + 1
|
||||
# steplen = depth * Food.steplen
|
||||
|
||||
# if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
# return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0)
|
||||
# ).filter(depth__lte=depth, count__gt=0
|
||||
# ).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
||||
# else:
|
||||
# return queryset.filter(depth__lte=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# import random
|
||||
import re
|
||||
import traceback
|
||||
from html import unescape
|
||||
|
||||
from django.core.cache import caches
|
||||
from django.utils.dateparse import parse_duration
|
||||
from django.utils.translation import gettext as _
|
||||
from isodate import parse_duration as iso_parse_duration
|
||||
@@ -11,15 +9,9 @@ from isodate.isoerror import ISO8601Error
|
||||
from pytube import YouTube
|
||||
from recipe_scrapers._utils import get_host_name, get_minutes
|
||||
|
||||
# from cookbook.helper import recipe_url_import as helper
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.models import Automation, Keyword, PropertyType
|
||||
|
||||
# from unicodedata import decomposition
|
||||
|
||||
|
||||
# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
|
||||
|
||||
|
||||
def get_from_scraper(scrape, request):
|
||||
# converting the scrape_me object to the existing json format based on ld+json
|
||||
@@ -147,7 +139,7 @@ def get_from_scraper(scrape, request):
|
||||
recipe_json['steps'] = []
|
||||
try:
|
||||
for i in parse_instructions(scrape.instructions()):
|
||||
recipe_json['steps'].append({'instruction': i, 'ingredients': [], 'show_ingredients_table': request.user.userpreference.show_step_ingredients,})
|
||||
recipe_json['steps'].append({'instruction': i, 'ingredients': [], 'show_ingredients_table': request.user.userpreference.show_step_ingredients, })
|
||||
except Exception:
|
||||
pass
|
||||
if len(recipe_json['steps']) == 0:
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.db.models import F, OuterRef, Q, Subquery, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
||||
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
|
||||
SupermarketCategoryRelation)
|
||||
from recipes import settings
|
||||
|
||||
|
||||
def shopping_helper(qs, request):
|
||||
@@ -47,7 +44,7 @@ class RecipeShoppingEditor():
|
||||
self.mealplan = self._kwargs.get('mealplan', None)
|
||||
if type(self.mealplan) in [int, float]:
|
||||
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
|
||||
if type(self.mealplan) == dict:
|
||||
if isinstance(self.mealplan, dict):
|
||||
self.mealplan = MealPlan.objects.filter(id=self.mealplan['id'], space=self.space).first()
|
||||
self.id = self._kwargs.get('id', None)
|
||||
|
||||
@@ -69,11 +66,12 @@ class RecipeShoppingEditor():
|
||||
|
||||
@property
|
||||
def _recipe_servings(self):
|
||||
return getattr(self.recipe, 'servings', None) or getattr(getattr(self.mealplan, 'recipe', None), 'servings', None) or getattr(getattr(self._shopping_list_recipe, 'recipe', None), 'servings', None)
|
||||
return getattr(self.recipe, 'servings', None) or getattr(getattr(self.mealplan, 'recipe', None), 'servings',
|
||||
None) or getattr(getattr(self._shopping_list_recipe, 'recipe', None), 'servings', None)
|
||||
|
||||
@property
|
||||
def _servings_factor(self):
|
||||
return Decimal(self.servings)/Decimal(self._recipe_servings)
|
||||
return Decimal(self.servings) / Decimal(self._recipe_servings)
|
||||
|
||||
@property
|
||||
def _shared_users(self):
|
||||
@@ -90,9 +88,10 @@ class RecipeShoppingEditor():
|
||||
|
||||
def get_recipe_ingredients(self, id, exclude_onhand=False):
|
||||
if exclude_onhand:
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(food__onhand_users__id__in=[x.id for x in self._shared_users])
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(
|
||||
food__onhand_users__id__in=[x.id for x in self._shared_users])
|
||||
else:
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
|
||||
|
||||
@property
|
||||
def _include_related(self):
|
||||
@@ -109,7 +108,7 @@ class RecipeShoppingEditor():
|
||||
self.servings = float(servings)
|
||||
|
||||
if mealplan := kwargs.get('mealplan', None):
|
||||
if type(mealplan) == dict:
|
||||
if isinstance(mealplan, dict):
|
||||
self.mealplan = MealPlan.objects.filter(id=mealplan['id'], space=self.space).first()
|
||||
else:
|
||||
self.mealplan = mealplan
|
||||
@@ -170,13 +169,13 @@ class RecipeShoppingEditor():
|
||||
try:
|
||||
self._shopping_list_recipe.delete()
|
||||
return True
|
||||
except:
|
||||
except BaseException:
|
||||
return False
|
||||
|
||||
def _add_ingredients(self, ingredients=None):
|
||||
if not ingredients:
|
||||
return
|
||||
elif type(ingredients) == list:
|
||||
elif isinstance(ingredients, list):
|
||||
ingredients = Ingredient.objects.filter(id__in=ingredients)
|
||||
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
|
||||
add_ingredients = ingredients.exclude(id__in=existing)
|
||||
@@ -315,4 +314,4 @@ class RecipeShoppingEditor():
|
||||
# )
|
||||
|
||||
# # return all shopping list items
|
||||
# return list_recipe
|
||||
# return list_recipe
|
||||
|
||||
Reference in New Issue
Block a user