commit
7830ddd4e9
@ -3,9 +3,9 @@ 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
|
||||
from django.core.cache import caches
|
||||
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value, When, FilteredRelation)
|
||||
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.db.models.functions import Coalesce, Lower, Substr
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.translation import gettext as _
|
||||
@ -20,7 +20,8 @@ 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
|
||||
@ -45,7 +46,8 @@ class RecipeSearch():
|
||||
cache.set(CACHE_KEY, self._search_prefs, timeout=10)
|
||||
else:
|
||||
self._search_prefs = SearchPreference()
|
||||
self._string = self._params.get('query').strip() if self._params.get('query', None) else None
|
||||
self._string = self._params.get('query').strip(
|
||||
) if self._params.get('query', None) else None
|
||||
self._rating = self._params.get('rating', None)
|
||||
self._keywords = {
|
||||
'or': self._params.get('keywords_or', None) or self._params.get('keywords', None),
|
||||
@ -74,7 +76,8 @@ class RecipeSearch():
|
||||
self._random = str2bool(self._params.get('random', False))
|
||||
self._new = str2bool(self._params.get('new', False))
|
||||
self._num_recent = int(self._params.get('num_recent', 0))
|
||||
self._include_children = str2bool(self._params.get('include_children', None))
|
||||
self._include_children = str2bool(
|
||||
self._params.get('include_children', None))
|
||||
self._timescooked = self._params.get('timescooked', None)
|
||||
self._cookedon = self._params.get('cookedon', None)
|
||||
self._createdon = self._params.get('createdon', None)
|
||||
@ -95,18 +98,24 @@ 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
|
||||
@ -182,8 +191,10 @@ class RecipeSearch():
|
||||
# otherwise sort by the remaining order_by attributes or favorite by default
|
||||
else:
|
||||
order += default_order
|
||||
order[:] = [Lower('name').asc() if x == 'name' else x for x in order]
|
||||
order[:] = [Lower('name').desc() if x == '-name' else x for x in order]
|
||||
order[:] = [Lower('name').asc() if x ==
|
||||
'name' else x for x in order]
|
||||
order[:] = [Lower('name').desc() if x ==
|
||||
'-name' else x for x in order]
|
||||
self.orderby = order
|
||||
|
||||
def string_filters(self, string=None):
|
||||
@ -200,21 +211,28 @@ class RecipeSearch():
|
||||
for f in self._filters:
|
||||
query_filter |= f
|
||||
|
||||
self._queryset = self._queryset.filter(query_filter).distinct() # this creates duplicate records which can screw up other aggregates, see makenow for workaround
|
||||
# this creates duplicate records which can screw up other aggregates, see makenow for workaround
|
||||
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)]:
|
||||
@ -223,7 +241,8 @@ 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:
|
||||
@ -233,32 +252,41 @@ class RecipeSearch():
|
||||
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:
|
||||
@ -268,12 +296,15 @@ class RecipeSearch():
|
||||
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
|
||||
@ -293,27 +324,32 @@ class RecipeSearch():
|
||||
|
||||
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)))
|
||||
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 []) or 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))
|
||||
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[1:])).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]):
|
||||
@ -346,7 +382,8 @@ 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]):
|
||||
@ -360,7 +397,8 @@ 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)
|
||||
|
||||
@ -372,7 +410,8 @@ 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:
|
||||
@ -380,7 +419,8 @@ 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:
|
||||
@ -389,7 +429,8 @@ 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'):
|
||||
@ -399,14 +440,16 @@ class RecipeSearch():
|
||||
else:
|
||||
default = 0
|
||||
# TODO make ratings a settings user-only vs all-users
|
||||
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
|
||||
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(
|
||||
cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
|
||||
if rating is None:
|
||||
return
|
||||
|
||||
if rating == '0':
|
||||
self._queryset = self._queryset.filter(rating=0)
|
||||
elif lessthan:
|
||||
self._queryset = self._queryset.filter(rating__lte=int(rating[1:])).exclude(rating=0)
|
||||
self._queryset = self._queryset.filter(
|
||||
rating__lte=int(rating[1:])).exclude(rating=0)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(rating__gte=int(rating))
|
||||
|
||||
@ -434,11 +477,14 @@ 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:
|
||||
@ -446,7 +492,7 @@ class RecipeSearch():
|
||||
if not steps:
|
||||
return
|
||||
if not isinstance(steps, list):
|
||||
steps = [unistepsts]
|
||||
steps = [steps]
|
||||
self._queryset = self._queryset.filter(steps__id__in=steps)
|
||||
|
||||
def build_fulltext_filters(self, string=None):
|
||||
@ -457,20 +503,25 @@ 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:
|
||||
@ -478,7 +529,8 @@ 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:
|
||||
@ -510,23 +562,30 @@ class RecipeSearch():
|
||||
def _makenow_filter(self, missing=None):
|
||||
if missing is None or (type(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
|
||||
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users) # or substitute food onhand
|
||||
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
|
||||
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
||||
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
|
||||
# or substitute food onhand
|
||||
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users)
|
||||
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
|
||||
| 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_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))
|
||||
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):
|
||||
@ -547,7 +606,8 @@ class RecipeSearch():
|
||||
@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)),
|
||||
path__startswith=Substr(
|
||||
OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
|
||||
depth=OuterRef('depth'),
|
||||
onhand_users__in=shopping_users
|
||||
)
|
||||
@ -586,7 +646,8 @@ class RecipeFacet():
|
||||
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._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', []),
|
||||
@ -618,7 +679,8 @@ class RecipeFacet():
|
||||
'Books': self.Books
|
||||
|
||||
}
|
||||
caches['default'].set(self._SEARCH_CACHE_KEY, self._cache, self._cache_timeout)
|
||||
caches['default'].set(self._SEARCH_CACHE_KEY,
|
||||
self._cache, self._cache_timeout)
|
||||
|
||||
def get_facets(self, from_cache=False):
|
||||
if from_cache:
|
||||
@ -655,13 +717,16 @@ class RecipeFacet():
|
||||
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()
|
||||
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()
|
||||
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(
|
||||
depth=1)).filter(space=self._request.space).distinct()
|
||||
|
||||
# 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.Keywords = [{**x, 'children': None}
|
||||
if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.set_cache('Keywords', self.Keywords)
|
||||
return self.Keywords
|
||||
|
||||
@ -669,28 +734,28 @@ class RecipeFacet():
|
||||
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()
|
||||
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()
|
||||
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(
|
||||
depth=1)).filter(space=self._request.space).distinct()
|
||||
|
||||
# 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.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_books(self):
|
||||
if self.Books is None:
|
||||
self.Books = []
|
||||
return self.Books
|
||||
|
||||
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._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 = {}
|
||||
@ -715,10 +780,13 @@ class RecipeFacet():
|
||||
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)
|
||||
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)]
|
||||
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()
|
||||
|
||||
@ -731,10 +799,13 @@ class RecipeFacet():
|
||||
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)
|
||||
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)]
|
||||
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()
|
||||
|
||||
|
@ -6,7 +6,7 @@ from django.urls import reverse
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from pytest_factoryboy import LazyFixture, register
|
||||
|
||||
from cookbook.models import Food, FoodInheritField, Ingredient, ShoppingList, ShoppingListEntry
|
||||
from cookbook.models import Food, Ingredient, ShoppingListEntry
|
||||
from cookbook.tests.factories import (FoodFactory, IngredientFactory, ShoppingListEntryFactory,
|
||||
SupermarketCategoryFactory)
|
||||
|
||||
@ -56,23 +56,32 @@ def obj_tree_1(request, space_1):
|
||||
params = request.param # request.param is a magic variable
|
||||
except AttributeError:
|
||||
params = {}
|
||||
objs = []
|
||||
inherit = params.pop('inherit', False)
|
||||
objs.extend(FoodFactory.create_batch(3, space=space_1, **params))
|
||||
FoodFactory.create_batch(3, space=space_1, **params)
|
||||
objs = Food.objects.values_list('id', flat=True)
|
||||
obj_id = objs[1]
|
||||
child_id = objs[0]
|
||||
parent_id = objs[2]
|
||||
|
||||
# set all foods to inherit everything
|
||||
if inherit:
|
||||
inherit = Food.inheritable_fields
|
||||
Through = Food.objects.filter(space=space_1).first().inherit_fields.through
|
||||
Through = Food.objects.filter(
|
||||
space=space_1).first().inherit_fields.through
|
||||
for i in inherit:
|
||||
Through.objects.bulk_create([
|
||||
Through(food_id=x, foodinheritfield_id=i.id)
|
||||
for x in Food.objects.filter(space=space_1).values_list('id', flat=True)
|
||||
])
|
||||
|
||||
objs[0].move(objs[1], node_location)
|
||||
objs[1].move(objs[2], node_location)
|
||||
return Food.objects.get(id=objs[1].id) # whenever you move/merge a tree it's safest to re-get the object
|
||||
Food.objects.get(id=child_id).move(
|
||||
Food.objects.get(id=obj_id), node_location)
|
||||
|
||||
Food.objects.get(id=obj_id).move(
|
||||
Food.objects.get(id=parent_id), node_location)
|
||||
|
||||
# whenever you move/merge a tree it's safest to re-get the object
|
||||
return Food.objects.get(id=obj_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
@ -107,19 +116,23 @@ def test_list_filter(obj_1, obj_2, u1_s1):
|
||||
assert obj_2.name in [x['name'] for x in response['results']]
|
||||
assert response['results'][0]['name'] < response['results'][1]['name']
|
||||
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?page_size=1').content)
|
||||
response = json.loads(
|
||||
u1_s1.get(f'{reverse(LIST_URL)}?page_size=1').content)
|
||||
assert len(response['results']) == 1
|
||||
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
|
||||
assert len(response['results']) == 1
|
||||
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=''&limit=1').content)
|
||||
response = json.loads(
|
||||
u1_s1.get(f'{reverse(LIST_URL)}?query=''&limit=1').content)
|
||||
assert len(response['results']) == 1
|
||||
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
|
||||
response = json.loads(
|
||||
u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
|
||||
assert response['count'] == 0
|
||||
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[:-4]}').content)
|
||||
response = json.loads(
|
||||
u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[:-4]}').content)
|
||||
assert response['count'] == 1
|
||||
|
||||
|
||||
@ -262,8 +275,9 @@ def test_integrity(u1_s1, recipe_1_s1):
|
||||
|
||||
def test_move(u1_s1, obj_tree_1, obj_2, obj_3, space_1):
|
||||
with scope(space=space_1):
|
||||
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
|
||||
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
|
||||
parent = obj_tree_1.get_parent()
|
||||
child = obj_tree_1.get_descendants()[0]
|
||||
assert parent.get_num_children() == 1
|
||||
assert parent.get_descendant_count() == 2
|
||||
assert Food.get_root_nodes().filter(space=space_1).count() == 2
|
||||
@ -295,8 +309,9 @@ def test_move(u1_s1, obj_tree_1, obj_2, obj_3, space_1):
|
||||
|
||||
def test_move_errors(u1_s1, obj_tree_1, obj_3, space_1):
|
||||
with scope(space=space_1):
|
||||
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
|
||||
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
|
||||
parent = obj_tree_1.get_parent()
|
||||
child = obj_tree_1.get_descendants()[0]
|
||||
# move child to root
|
||||
r = u1_s1.put(reverse(MOVE_URL, args=[obj_tree_1.id, 0]))
|
||||
assert r.status_code == 200
|
||||
@ -351,7 +366,7 @@ def test_merge_shopping_entries(obj_tree_1, u1_s1, space_1):
|
||||
with scope(space=space_1):
|
||||
parent = obj_tree_1.get_parent()
|
||||
child = obj_tree_1.get_descendants()[0]
|
||||
ShoppingListEntryFactory.create(food=parent, space=space_1)
|
||||
ShoppingListEntryFactory.create(food=parent, space=space_1)
|
||||
ShoppingListEntryFactory.create(food=child, space=space_1)
|
||||
assert parent.get_num_children() == 1
|
||||
assert parent.get_descendant_count() == 2
|
||||
@ -371,8 +386,10 @@ def test_merge_shopping_entries(obj_tree_1, u1_s1, space_1):
|
||||
assert obj_tree_1.shopping_entries.count() == 1 # now has child's ingredient
|
||||
|
||||
|
||||
def test_merge(u1_s1, obj_tree_1, obj_1, obj_3, space_1):
|
||||
def test_merge(u1_s1, obj_tree_1, obj_1, obj_3, space_1):
|
||||
with scope(space=space_1):
|
||||
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
|
||||
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
|
||||
parent = obj_tree_1.get_parent()
|
||||
child = obj_tree_1.get_descendants()[0]
|
||||
assert parent.get_num_children() == 1
|
||||
@ -416,8 +433,9 @@ def test_merge(u1_s1, obj_tree_1, obj_1, obj_3, space_1):
|
||||
|
||||
def test_merge_errors(u1_s1, obj_tree_1, obj_3, space_1):
|
||||
with scope(space=space_1):
|
||||
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
|
||||
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
|
||||
parent = obj_tree_1.get_parent()
|
||||
child = obj_tree_1.get_descendants()[0]
|
||||
|
||||
# attempt to merge with non-existent parent
|
||||
r = u1_s1.put(
|
||||
@ -451,44 +469,63 @@ def test_merge_errors(u1_s1, obj_tree_1, obj_3, space_1):
|
||||
|
||||
def test_root_filter(obj_tree_1, obj_2, obj_3, u1_s1):
|
||||
with scope(space=obj_tree_1.space):
|
||||
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
|
||||
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
|
||||
parent = obj_tree_1.get_parent()
|
||||
child = obj_tree_1.get_descendants()[0]
|
||||
|
||||
# should return root objects in the space (obj_1, obj_2), ignoring query filters
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root=0').content)
|
||||
assert len(response['results']) == 2
|
||||
|
||||
# django_tree bypasses ORM - best to retrieve all changed objects
|
||||
with scopes_disabled():
|
||||
obj_2.move(parent, node_location)
|
||||
obj_2 = Food.objects.get(id=obj_2.id)
|
||||
parent = Food.objects.get(id=parent.id)
|
||||
# should return direct children of parent (obj_tree_1, obj_2), ignoring query filters
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={parent.id}').content)
|
||||
response = json.loads(
|
||||
u1_s1.get(f'{reverse(LIST_URL)}?root={parent.id}').content)
|
||||
assert response['count'] == 2
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={parent.id}&query={obj_2.name[4:]}').content)
|
||||
response = json.loads(u1_s1.get(
|
||||
f'{reverse(LIST_URL)}?root={parent.id}&query={obj_2.name[4:]}').content)
|
||||
assert response['count'] == 2
|
||||
|
||||
|
||||
def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1):
|
||||
with scope(space=obj_tree_1.space):
|
||||
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
|
||||
parent = obj_tree_1.get_parent()
|
||||
child = obj_tree_1.get_descendants()[0]
|
||||
obj_2.move(parent, node_location)
|
||||
obj_2 = Food.objects.get(id=obj_2.id)
|
||||
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
|
||||
parent = Food.objects.get(id=parent.id)
|
||||
|
||||
# should return full tree starting at parent (obj_tree_1, obj_2), ignoring query filters
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}').content)
|
||||
response = json.loads(
|
||||
u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}').content)
|
||||
assert response['count'] == 4
|
||||
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}&query={obj_2.name[4:]}').content)
|
||||
response = json.loads(u1_s1.get(
|
||||
f'{reverse(LIST_URL)}?tree={parent.id}&query={obj_2.name[4:]}').content)
|
||||
assert response['count'] == 4
|
||||
|
||||
|
||||
# This is more about the model than the API - should this be moved to a different test?
|
||||
@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [
|
||||
({'has_category': True, 'inherit': True}, 'supermarket_category', True, 'cat_1'),
|
||||
({'has_category': True, 'inherit': False}, 'supermarket_category', False, 'cat_1'),
|
||||
({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'),
|
||||
({'ignore_shopping': True, 'inherit': False}, 'ignore_shopping', False, 'false'),
|
||||
({'substitute_children': True, 'inherit': True}, 'substitute_children', True, 'false'),
|
||||
({'substitute_children': True, 'inherit': False}, 'substitute_children', False, 'false'),
|
||||
({'substitute_siblings': True, 'inherit': True}, 'substitute_siblings', True, 'false'),
|
||||
({'substitute_siblings': True, 'inherit': False}, 'substitute_siblings', False, 'false'),
|
||||
({'has_category': True, 'inherit': True},
|
||||
'supermarket_category', True, 'cat_1'),
|
||||
({'has_category': True, 'inherit': False},
|
||||
'supermarket_category', False, 'cat_1'),
|
||||
({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'),
|
||||
({'ignore_shopping': True, 'inherit': False},
|
||||
'ignore_shopping', False, 'false'),
|
||||
({'substitute_children': True, 'inherit': True},
|
||||
'substitute_children', True, 'false'),
|
||||
({'substitute_children': True, 'inherit': False},
|
||||
'substitute_children', False, 'false'),
|
||||
({'substitute_siblings': True, 'inherit': True},
|
||||
'substitute_siblings', True, 'false'),
|
||||
({'substitute_siblings': True, 'inherit': False},
|
||||
'substitute_siblings', False, 'false'),
|
||||
], indirect=['obj_tree_1']) # indirect=True populates magic variable request.param of obj_tree_1 with the parameter
|
||||
def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
|
||||
with scope(space=obj_tree_1.space):
|
||||
@ -498,8 +535,10 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
|
||||
new_val = request.getfixturevalue(new_val)
|
||||
# if this test passes it demonstrates that inheritance works
|
||||
# when moving to a parent as each food is created with a different category
|
||||
assert (getattr(parent, field) == getattr(obj_tree_1, field)) in [inherit, True]
|
||||
assert (getattr(obj_tree_1, field) == getattr(child, field)) in [inherit, True]
|
||||
assert (getattr(parent, field) == getattr(
|
||||
obj_tree_1, field)) in [inherit, True]
|
||||
assert (getattr(obj_tree_1, field) == getattr(
|
||||
child, field)) in [inherit, True]
|
||||
# change parent to a new value
|
||||
setattr(parent, field, new_val)
|
||||
with scope(space=parent.space):
|
||||
@ -515,7 +554,8 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("obj_tree_1", [
|
||||
({'has_category': True, 'inherit': False, 'ignore_shopping': True, 'substitute_children': True, 'substitute_siblings': True}),
|
||||
({'has_category': True, 'inherit': False, 'ignore_shopping': True,
|
||||
'substitute_children': True, 'substitute_siblings': True}),
|
||||
], indirect=['obj_tree_1'])
|
||||
@pytest.mark.parametrize("global_reset", [True, False])
|
||||
@pytest.mark.parametrize("field", ['ignore_shopping', 'substitute_children', 'substitute_siblings', 'supermarket_category'])
|
||||
@ -534,10 +574,13 @@ def test_reset_inherit_space_fields(obj_tree_1, space_1, global_reset, field):
|
||||
assert getattr(parent, field) != getattr(obj_tree_1, field)
|
||||
|
||||
if global_reset:
|
||||
space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields
|
||||
# set default inherit fields
|
||||
space_1.food_inherit.add(
|
||||
*Food.inheritable_fields.values_list('id', flat=True))
|
||||
parent.reset_inheritance(space=space_1)
|
||||
else:
|
||||
obj_tree_1.child_inherit_fields.set(Food.inheritable_fields.values_list('id', flat=True))
|
||||
obj_tree_1.child_inherit_fields.set(
|
||||
Food.inheritable_fields.values_list('id', flat=True))
|
||||
obj_tree_1.save()
|
||||
parent.reset_inheritance(space=space_1, food=obj_tree_1)
|
||||
# djangotree bypasses ORM and need to be retrieved again
|
||||
@ -545,12 +588,14 @@ def test_reset_inherit_space_fields(obj_tree_1, space_1, global_reset, field):
|
||||
parent = Food.objects.get(id=parent.id)
|
||||
child = Food.objects.get(id=child.id)
|
||||
|
||||
assert (getattr(parent, field) == getattr(obj_tree_1, field)) == global_reset
|
||||
assert (getattr(parent, field) == getattr(
|
||||
obj_tree_1, field)) == global_reset
|
||||
assert getattr(obj_tree_1, field) == getattr(child, field)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("obj_tree_1", [
|
||||
({'has_category': True, 'inherit': False, 'ignore_shopping': True, 'substitute_children': True, 'substitute_siblings': True}),
|
||||
({'has_category': True, 'inherit': False, 'ignore_shopping': True,
|
||||
'substitute_children': True, 'substitute_siblings': True}),
|
||||
], indirect=['obj_tree_1'])
|
||||
@pytest.mark.parametrize("field", ['ignore_shopping', 'substitute_children', 'substitute_siblings', 'supermarket_category'])
|
||||
def test_reset_inherit_no_food_instances(obj_tree_1, space_1, field):
|
||||
@ -558,13 +603,17 @@ def test_reset_inherit_no_food_instances(obj_tree_1, space_1, field):
|
||||
parent = obj_tree_1.get_parent()
|
||||
Food.objects.all().delete()
|
||||
|
||||
space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields
|
||||
# set default inherit fields
|
||||
space_1.food_inherit.add(
|
||||
*Food.inheritable_fields.values_list('id', flat=True))
|
||||
parent.reset_inheritance(space=space_1)
|
||||
|
||||
|
||||
def test_onhand(obj_1, u1_s1, u2_s1):
|
||||
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
|
||||
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
|
||||
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
|
||||
'food_onhand'] == False
|
||||
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
|
||||
'food_onhand'] == False
|
||||
|
||||
u1_s1.patch(
|
||||
reverse(
|
||||
@ -574,10 +623,13 @@ def test_onhand(obj_1, u1_s1, u2_s1):
|
||||
{'food_onhand': True},
|
||||
content_type='application/json'
|
||||
)
|
||||
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True
|
||||
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
|
||||
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
|
||||
'food_onhand'] == True
|
||||
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
|
||||
'food_onhand'] == False
|
||||
|
||||
user1 = auth.get_user(u1_s1)
|
||||
user2 = auth.get_user(u2_s1)
|
||||
user1.userpreference.shopping_share.add(user2)
|
||||
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True
|
||||
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
|
||||
'food_onhand'] == True
|
||||
|
@ -1,20 +1,14 @@
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
import factory
|
||||
import pytest
|
||||
# work around for bug described here https://stackoverflow.com/a/70312265/15762829
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.forms import model_to_dict
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from pytest_factoryboy import LazyFixture, register
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import Food, Ingredient, ShoppingListEntry, Step
|
||||
from cookbook.tests.factories import (IngredientFactory, MealPlanFactory, RecipeFactory,
|
||||
StepFactory, UserFactory)
|
||||
from cookbook.models import Food, Ingredient
|
||||
from cookbook.tests.factories import MealPlanFactory, RecipeFactory, StepFactory, UserFactory
|
||||
|
||||
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
||||
'django.db.backends.postgresql']:
|
||||
@ -32,9 +26,12 @@ def user2(request, u1_s1):
|
||||
except AttributeError:
|
||||
params = {}
|
||||
user = auth.get_user(u1_s1)
|
||||
user.userpreference.mealplan_autoadd_shopping = params.get('mealplan_autoadd_shopping', True)
|
||||
user.userpreference.mealplan_autoinclude_related = params.get('mealplan_autoinclude_related', True)
|
||||
user.userpreference.mealplan_autoexclude_onhand = params.get('mealplan_autoexclude_onhand', True)
|
||||
user.userpreference.mealplan_autoadd_shopping = params.get(
|
||||
'mealplan_autoadd_shopping', True)
|
||||
user.userpreference.mealplan_autoinclude_related = params.get(
|
||||
'mealplan_autoinclude_related', True)
|
||||
user.userpreference.mealplan_autoexclude_onhand = params.get(
|
||||
'mealplan_autoexclude_onhand', True)
|
||||
user.userpreference.save()
|
||||
return u1_s1
|
||||
|
||||
@ -50,7 +47,6 @@ def recipe(request, space_1, u1_s1):
|
||||
return RecipeFactory(**params)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['g1_s1', 204],
|
||||
['u1_s1', 204],
|
||||
@ -59,11 +55,14 @@ def recipe(request, space_1, u1_s1):
|
||||
])
|
||||
@pytest.mark.parametrize("recipe, sle_count", [
|
||||
({}, 10),
|
||||
({'steps__recipe_count': 1}, 20), # shopping list from recipe with StepRecipe
|
||||
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19), # shopping list from recipe with food recipe
|
||||
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29), # shopping list from recipe with StepRecipe and food recipe
|
||||
# shopping list from recipe with StepRecipe
|
||||
({'steps__recipe_count': 1}, 20),
|
||||
# shopping list from recipe with food recipe
|
||||
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19),
|
||||
# shopping list from recipe with StepRecipe and food recipe
|
||||
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29),
|
||||
], indirect=['recipe'])
|
||||
def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1):
|
||||
def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
user = auth.get_user(c)
|
||||
user.userpreference.mealplan_autoadd_shopping = True
|
||||
@ -78,16 +77,20 @@ def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1):
|
||||
if r.status_code == 204: # skip anonymous user
|
||||
|
||||
r = json.loads(c.get(reverse(SHOPPING_LIST_URL)).content)
|
||||
assert len(r) == sle_count # recipe factory creates 10 ingredients by default
|
||||
# recipe factory creates 10 ingredients by default
|
||||
assert len(r) == sle_count
|
||||
assert [x['created_by']['id'] for x in r].count(user.id) == sle_count
|
||||
# user in space can't see shopping list
|
||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
|
||||
assert len(json.loads(
|
||||
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
|
||||
user.userpreference.shopping_share.add(auth.get_user(u2_s1))
|
||||
# after share, user in space can see shopping list
|
||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
||||
assert len(json.loads(
|
||||
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
||||
# confirm that the author of the recipe doesn't have access to shopping list
|
||||
if c != u1_s1:
|
||||
assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
|
||||
assert len(json.loads(
|
||||
u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
|
||||
|
||||
r = c.get(url)
|
||||
assert r.status_code == 405
|
||||
@ -99,9 +102,12 @@ def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1):
|
||||
|
||||
@pytest.mark.parametrize("recipe, sle_count", [
|
||||
({}, 10),
|
||||
({'steps__recipe_count': 1}, 20), # shopping list from recipe with StepRecipe
|
||||
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19), # shopping list from recipe with food recipe
|
||||
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29), # shopping list from recipe with StepRecipe and food recipe
|
||||
# shopping list from recipe with StepRecipe
|
||||
({'steps__recipe_count': 1}, 20),
|
||||
# shopping list from recipe with food recipe
|
||||
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19),
|
||||
# shopping list from recipe with StepRecipe and food recipe
|
||||
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29),
|
||||
], indirect=['recipe'])
|
||||
@pytest.mark.parametrize("use_mealplan", [(False), (True), ])
|
||||
def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u2_s1):
|
||||
@ -115,31 +121,33 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
|
||||
user.userpreference.save()
|
||||
|
||||
if use_mealplan:
|
||||
mealplan = MealPlanFactory(space=recipe.space, created_by=user, servings=recipe.servings, recipe=recipe)
|
||||
mealplan = MealPlanFactory(
|
||||
space=recipe.space, created_by=user, servings=recipe.servings, recipe=recipe)
|
||||
else:
|
||||
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}))
|
||||
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
|
||||
assert [x['created_by']['id'] for x in r].count(user.id) == sle_count
|
||||
all_ing = [x['ingredient'] for x in r]
|
||||
keep_ing = all_ing[1:-1] # remove first and last element
|
||||
del keep_ing[int(len(keep_ing)/2)] # remove a middle element
|
||||
del keep_ing[int(len(keep_ing) / 2)] # remove a middle element
|
||||
list_recipe = r[0]['list_recipe']
|
||||
amount_sum = sum([x['amount'] for x in r])
|
||||
|
||||
# test modifying shopping list as different user
|
||||
# test increasing servings size of recipe shopping list
|
||||
if use_mealplan:
|
||||
mealplan.servings = 2*recipe.servings
|
||||
mealplan.servings = 2 * recipe.servings
|
||||
mealplan.save()
|
||||
else:
|
||||
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
|
||||
{'list_recipe': list_recipe, 'servings': 2*recipe.servings},
|
||||
{'list_recipe': list_recipe, 'servings': 2 * recipe.servings},
|
||||
content_type='application/json'
|
||||
)
|
||||
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
|
||||
assert sum([x['amount'] for x in r]) == amount_sum * 2
|
||||
assert len(r) == sle_count
|
||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
||||
assert len(json.loads(
|
||||
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
||||
|
||||
# testing decreasing servings size of recipe shopping list
|
||||
if use_mealplan:
|
||||
@ -153,7 +161,8 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
|
||||
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
|
||||
assert sum([x['amount'] for x in r]) == amount_sum * .5
|
||||
assert len(r) == sle_count
|
||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
||||
assert len(json.loads(
|
||||
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
||||
|
||||
# test removing 3 items from shopping list
|
||||
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
|
||||
@ -162,7 +171,8 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
|
||||
)
|
||||
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
|
||||
assert len(r) == sle_count - 3
|
||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count - 3
|
||||
assert len(json.loads(
|
||||
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count - 3
|
||||
|
||||
# add all ingredients to existing shopping list - don't change serving size
|
||||
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
|
||||
@ -172,14 +182,16 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
|
||||
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
|
||||
assert sum([x['amount'] for x in r]) == amount_sum * .5
|
||||
assert len(r) == sle_count
|
||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
||||
assert len(json.loads(
|
||||
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
||||
|
||||
|
||||
@pytest.mark.parametrize("user2, sle_count", [
|
||||
({'mealplan_autoadd_shopping': False}, (0, 18)),
|
||||
({'mealplan_autoinclude_related': False}, (9, 9)),
|
||||
({'mealplan_autoexclude_onhand': False}, (20, 20)),
|
||||
({'mealplan_autoexclude_onhand': False, 'mealplan_autoinclude_related': False}, (10, 10)),
|
||||
({'mealplan_autoexclude_onhand': False,
|
||||
'mealplan_autoinclude_related': False}, (10, 10)),
|
||||
], indirect=['user2'])
|
||||
@pytest.mark.parametrize("use_mealplan", [(False), (True), ])
|
||||
@pytest.mark.parametrize("recipe", [({'steps__recipe_count': 1})], indirect=['recipe'])
|
||||
@ -191,20 +203,24 @@ def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2):
|
||||
food = Food.objects.get(id=ingredients[2].food.id)
|
||||
food.onhand_users.add(user)
|
||||
food.save()
|
||||
food = recipe.steps.exclude(step_recipe=None).first().step_recipe.steps.first().ingredients.first().food
|
||||
food = recipe.steps.exclude(step_recipe=None).first(
|
||||
).step_recipe.steps.first().ingredients.first().food
|
||||
food = Food.objects.get(id=food.id)
|
||||
food.onhand_users.add(user)
|
||||
food.save()
|
||||
|
||||
if use_mealplan:
|
||||
mealplan = MealPlanFactory(space=recipe.space, created_by=user, servings=recipe.servings, recipe=recipe)
|
||||
assert len(json.loads(user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[0]
|
||||
MealPlanFactory(
|
||||
space=recipe.space, created_by=user, servings=recipe.servings, recipe=recipe)
|
||||
assert len(json.loads(
|
||||
user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[0]
|
||||
else:
|
||||
user2.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}))
|
||||
assert len(json.loads(user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[1]
|
||||
assert len(json.loads(
|
||||
user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[1]
|
||||
|
||||
|
||||
def test_shopping_recipe_mixed_authors(u1_s1, u2_s1,space_1):
|
||||
def test_shopping_recipe_mixed_authors(u1_s1, u2_s1, space_1):
|
||||
with scopes_disabled():
|
||||
user1 = auth.get_user(u1_s1)
|
||||
user2 = auth.get_user(u2_s1)
|
||||
@ -213,15 +229,19 @@ def test_shopping_recipe_mixed_authors(u1_s1, u2_s1,space_1):
|
||||
recipe1 = RecipeFactory(created_by=user1, space=space)
|
||||
recipe2 = RecipeFactory(created_by=user2, space=space)
|
||||
recipe3 = RecipeFactory(created_by=user3, space=space)
|
||||
food = Food.objects.get(id=recipe1.steps.first().ingredients.first().food.id)
|
||||
food = Food.objects.get(
|
||||
id=recipe1.steps.first().ingredients.first().food.id)
|
||||
food.recipe = recipe2
|
||||
food.save()
|
||||
recipe1.steps.add(StepFactory(step_recipe=recipe3, ingredients__count=0, space=space))
|
||||
recipe1.steps.add(StepFactory(step_recipe=recipe3,
|
||||
ingredients__count=0, space=space))
|
||||
recipe1.save()
|
||||
|
||||
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe1.id}))
|
||||
assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 29
|
||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
|
||||
assert len(json.loads(
|
||||
u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 29
|
||||
assert len(json.loads(
|
||||
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("recipe", [{'steps__ingredients__header': 1}], indirect=['recipe'])
|
||||
@ -230,4 +250,5 @@ def test_shopping_with_header_ingredient(u1_s1, recipe):
|
||||
# recipe.step_set.first().ingredient_set.add(IngredientFactory(ingredients__header=1))
|
||||
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}))
|
||||
assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 10
|
||||
assert len(json.loads(u1_s1.get(reverse('api:ingredient-list')).content)['results']) == 11
|
||||
assert len(json.loads(
|
||||
u1_s1.get(reverse('api:ingredient-list')).content)['results']) == 11
|
||||
|
@ -5,12 +5,11 @@ import uuid
|
||||
|
||||
import pytest
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django_scopes import scopes_disabled
|
||||
from pytest_factoryboy import LazyFixture, register
|
||||
from pytest_factoryboy import register
|
||||
|
||||
from cookbook.models import Food, Ingredient, Recipe, Space, Step, Unit
|
||||
from cookbook.tests.factories import FoodFactory, SpaceFactory, UserFactory
|
||||
from cookbook.models import Food, Ingredient, Recipe, Step, Unit
|
||||
from cookbook.tests.factories import SpaceFactory, UserFactory
|
||||
|
||||
register(SpaceFactory, 'space_1')
|
||||
register(SpaceFactory, 'space_2')
|
||||
@ -60,8 +59,10 @@ def get_random_recipe(space_1, u1_s1):
|
||||
internal=True,
|
||||
)
|
||||
|
||||
s1 = Step.objects.create(name=str(uuid.uuid4()), instruction=str(uuid.uuid4()), space=space_1, )
|
||||
s2 = Step.objects.create(name=str(uuid.uuid4()), instruction=str(uuid.uuid4()), space=space_1, )
|
||||
s1 = Step.objects.create(name=str(uuid.uuid4()),
|
||||
instruction=str(uuid.uuid4()), space=space_1, )
|
||||
s2 = Step.objects.create(name=str(uuid.uuid4()),
|
||||
instruction=str(uuid.uuid4()), space=space_1, )
|
||||
|
||||
r.steps.add(s1)
|
||||
r.steps.add(s2)
|
||||
@ -70,8 +71,10 @@ def get_random_recipe(space_1, u1_s1):
|
||||
s1.ingredients.add(
|
||||
Ingredient.objects.create(
|
||||
amount=1,
|
||||
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0],
|
||||
unit=Unit.objects.create(name=str(uuid.uuid4()), space=space_1, ),
|
||||
food=Food.objects.get_or_create(
|
||||
name=str(uuid.uuid4()), space=space_1)[0],
|
||||
unit=Unit.objects.create(
|
||||
name=str(uuid.uuid4()), space=space_1, ),
|
||||
note=str(uuid.uuid4()),
|
||||
space=space_1,
|
||||
)
|
||||
@ -80,8 +83,10 @@ def get_random_recipe(space_1, u1_s1):
|
||||
s2.ingredients.add(
|
||||
Ingredient.objects.create(
|
||||
amount=1,
|
||||
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0],
|
||||
unit=Unit.objects.create(name=str(uuid.uuid4()), space=space_1, ),
|
||||
food=Food.objects.get_or_create(
|
||||
name=str(uuid.uuid4()), space=space_1)[0],
|
||||
unit=Unit.objects.create(
|
||||
name=str(uuid.uuid4()), space=space_1, ),
|
||||
note=str(uuid.uuid4()),
|
||||
space=space_1,
|
||||
)
|
||||
@ -99,8 +104,10 @@ def get_random_json_recipe():
|
||||
{
|
||||
"instruction": str(uuid.uuid4()),
|
||||
"ingredients": [
|
||||
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(uuid.uuid4())}, "amount": random.randint(0, 10)},
|
||||
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(uuid.uuid4())}, "amount": random.randint(0, 10)},
|
||||
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(
|
||||
uuid.uuid4())}, "amount": random.randint(0, 10)},
|
||||
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(
|
||||
uuid.uuid4())}, "amount": random.randint(0, 10)},
|
||||
],
|
||||
}
|
||||
],
|
||||
@ -133,7 +140,8 @@ def validate_recipe(expected, recipe):
|
||||
for key in expected_lists:
|
||||
for k in expected_lists[key]:
|
||||
try:
|
||||
print('comparing ', any([dict_compare(k, i) for i in target_lists[key]]))
|
||||
print('comparing ', any([dict_compare(k, i)
|
||||
for i in target_lists[key]]))
|
||||
assert any([dict_compare(k, i) for i in target_lists[key]])
|
||||
except AssertionError:
|
||||
for result in [dict_compare(k, i, details=True) for i in target_lists[key]]:
|
||||
@ -152,7 +160,8 @@ def dict_compare(d1, d2, details=False):
|
||||
added = d1_keys - d2_keys
|
||||
removed = d2_keys - d1_keys
|
||||
modified = {o: (d1[o], d2[o]) for o in not_dicts if d1[o] != d2[o]}
|
||||
modified_dicts = {o: (d1[o], d2[o]) for o in sub_dicts if not d1[o].items() <= d2[o].items()}
|
||||
modified_dicts = {o: (d1[o], d2[o])
|
||||
for o in sub_dicts if not d1[o].items() <= d2[o].items()}
|
||||
if details:
|
||||
return added, removed, modified, modified_dicts
|
||||
else:
|
||||
@ -173,12 +182,12 @@ def transpose(text, number=2):
|
||||
positions = random.sample(range(len(tokens[token_pos])), number)
|
||||
|
||||
# swap the positions
|
||||
l = list(tokens[token_pos])
|
||||
lt = list(tokens[token_pos])
|
||||
for first, second in zip(positions[::2], positions[1::2]):
|
||||
l[first], l[second] = l[second], l[first]
|
||||
lt[first], lt[second] = lt[second], lt[first]
|
||||
|
||||
# replace original tokens with swapped
|
||||
tokens[token_pos] = ''.join(l)
|
||||
tokens[token_pos] = ''.join(lt)
|
||||
|
||||
# return text with the swapped token
|
||||
return ' '.join(tokens)
|
||||
|
@ -4,13 +4,12 @@ from decimal import Decimal
|
||||
|
||||
import factory
|
||||
import pytest
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django_scopes import scopes_disabled
|
||||
from faker import Factory as FakerFactory
|
||||
from pytest_factoryboy import register
|
||||
|
||||
from cookbook.models import Recipe, Step, UserSpace
|
||||
from cookbook.models import UserSpace
|
||||
|
||||
# this code will run immediately prior to creating the model object useful when you want a reverse relationship
|
||||
# log = factory.RelatedFactory(
|
||||
@ -53,7 +52,8 @@ class SpaceFactory(factory.django.DjangoModelFactory):
|
||||
class UserFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
"""User factory."""
|
||||
username = factory.LazyAttribute(lambda x: faker.simple_profile()['username'])
|
||||
username = factory.LazyAttribute(
|
||||
lambda x: faker.simple_profile()['username'])
|
||||
first_name = factory.LazyAttribute(lambda x: faker.first_name())
|
||||
last_name = factory.LazyAttribute(lambda x: faker.last_name())
|
||||
email = factory.LazyAttribute(lambda x: faker.email())
|
||||
@ -65,7 +65,8 @@ class UserFactory(factory.django.DjangoModelFactory):
|
||||
return
|
||||
|
||||
if extracted:
|
||||
us = UserSpace.objects.create(space=self.space, user=self, active=True)
|
||||
us = UserSpace.objects.create(
|
||||
space=self.space, user=self, active=True)
|
||||
us.groups.add(Group.objects.get(name=extracted))
|
||||
|
||||
@factory.post_generation
|
||||
@ -75,10 +76,12 @@ class UserFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
if extracted:
|
||||
for prefs in extracted:
|
||||
self.userpreference[prefs] = extracted[prefs]/0 # intentionally break so it can be debugged later
|
||||
# intentionally break so it can be debugged later
|
||||
self.userpreference[prefs] = extracted[prefs] / 0
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
django_get_or_create = ('username', 'space',)
|
||||
|
||||
|
||||
@register
|
||||
@ -98,18 +101,22 @@ class SupermarketCategoryFactory(factory.django.DjangoModelFactory):
|
||||
class FoodFactory(factory.django.DjangoModelFactory):
|
||||
"""Food factory."""
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10)[:128])
|
||||
plural_name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=3, variable_nb_words=False))
|
||||
plural_name = factory.LazyAttribute(
|
||||
lambda x: faker.sentence(nb_words=3, variable_nb_words=False))
|
||||
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
|
||||
supermarket_category = factory.Maybe(
|
||||
factory.LazyAttribute(lambda x: x.has_category),
|
||||
yes_declaration=factory.SubFactory(SupermarketCategoryFactory, space=factory.SelfAttribute('..space')),
|
||||
factory.LazyAttribute(lambda x: x.has_category),
|
||||
yes_declaration=factory.SubFactory(
|
||||
SupermarketCategoryFactory, space=factory.SelfAttribute('..space')),
|
||||
no_declaration=None
|
||||
)
|
||||
recipe = factory.Maybe(
|
||||
factory.LazyAttribute(lambda x: x.has_recipe),
|
||||
yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
|
||||
factory.LazyAttribute(lambda x: x.has_recipe),
|
||||
yes_declaration=factory.SubFactory(
|
||||
'cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
|
||||
no_declaration=None
|
||||
)
|
||||
path = None
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
@factory.post_generation
|
||||
@ -127,17 +134,19 @@ class FoodFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
class Meta:
|
||||
model = 'cookbook.Food'
|
||||
django_get_or_create = ('name', 'plural_name', 'space',)
|
||||
django_get_or_create = ('name', 'plural_name', 'path', 'space',)
|
||||
|
||||
|
||||
@register
|
||||
class RecipeBookFactory(factory.django.DjangoModelFactory):
|
||||
"""RecipeBook factory."""
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=3, variable_nb_words=False))
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(
|
||||
nb_words=3, variable_nb_words=False))
|
||||
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
|
||||
icon = None
|
||||
# shared = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_by = factory.SubFactory(
|
||||
UserFactory, space=factory.SelfAttribute('..space'))
|
||||
filter = None
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
@ -149,7 +158,8 @@ class RecipeBookFactory(factory.django.DjangoModelFactory):
|
||||
@register
|
||||
class RecipeBookEntryFactory(factory.django.DjangoModelFactory):
|
||||
"""RecipeBookEntry factory."""
|
||||
book = factory.SubFactory(RecipeBookFactory, space=factory.SelfAttribute('..recipe.space'))
|
||||
book = factory.SubFactory(
|
||||
RecipeBookFactory, space=factory.SelfAttribute('..recipe.space'))
|
||||
recipe = None
|
||||
|
||||
class Meta:
|
||||
@ -173,7 +183,8 @@ class UnitFactory(factory.django.DjangoModelFactory):
|
||||
@register
|
||||
class KeywordFactory(factory.django.DjangoModelFactory):
|
||||
"""Keyword factory."""
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=2, variable_nb_words=False))
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(
|
||||
nb_words=2, variable_nb_words=False))
|
||||
# icon = models.CharField(max_length=16, blank=True, null=True)
|
||||
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
@ -184,15 +195,17 @@ class KeywordFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
class Meta:
|
||||
model = 'cookbook.Keyword'
|
||||
django_get_or_create = ('name', 'space',)
|
||||
django_get_or_create = ('name', 'space')
|
||||
exclude = ('num')
|
||||
|
||||
|
||||
@register
|
||||
class IngredientFactory(factory.django.DjangoModelFactory):
|
||||
"""Ingredient factory."""
|
||||
food = factory.SubFactory(FoodFactory, space=factory.SelfAttribute('..space'))
|
||||
unit = factory.SubFactory(UnitFactory, space=factory.SelfAttribute('..space'))
|
||||
food = factory.SubFactory(
|
||||
FoodFactory, space=factory.SelfAttribute('..space'))
|
||||
unit = factory.SubFactory(
|
||||
UnitFactory, space=factory.SelfAttribute('..space'))
|
||||
amount = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=10))
|
||||
note = factory.LazyAttribute(lambda x: faker.sentence(nb_words=8))
|
||||
is_header = False
|
||||
@ -210,7 +223,8 @@ class MealTypeFactory(factory.django.DjangoModelFactory):
|
||||
# icon =
|
||||
color = factory.LazyAttribute(lambda x: faker.safe_hex_color())
|
||||
default = False
|
||||
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_by = factory.SubFactory(
|
||||
UserFactory, space=factory.SelfAttribute('..space'))
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
class Meta:
|
||||
@ -220,14 +234,18 @@ class MealTypeFactory(factory.django.DjangoModelFactory):
|
||||
@register
|
||||
class MealPlanFactory(factory.django.DjangoModelFactory):
|
||||
recipe = factory.Maybe(
|
||||
factory.LazyAttribute(lambda x: x.has_recipe),
|
||||
yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
|
||||
factory.LazyAttribute(lambda x: x.has_recipe),
|
||||
yes_declaration=factory.SubFactory(
|
||||
'cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
|
||||
no_declaration=None
|
||||
)
|
||||
servings = factory.LazyAttribute(lambda x: Decimal(faker.random_int(min=1, max=1000)/100))
|
||||
servings = factory.LazyAttribute(
|
||||
lambda x: Decimal(faker.random_int(min=1, max=1000) / 100))
|
||||
title = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5))
|
||||
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
|
||||
meal_type = factory.SubFactory(MealTypeFactory, space=factory.SelfAttribute('..space'))
|
||||
created_by = factory.SubFactory(
|
||||
UserFactory, space=factory.SelfAttribute('..space'))
|
||||
meal_type = factory.SubFactory(
|
||||
MealTypeFactory, space=factory.SelfAttribute('..space'))
|
||||
note = factory.LazyAttribute(lambda x: faker.paragraph())
|
||||
date = factory.LazyAttribute(lambda x: faker.future_date())
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
@ -243,12 +261,14 @@ class MealPlanFactory(factory.django.DjangoModelFactory):
|
||||
class ShoppingListRecipeFactory(factory.django.DjangoModelFactory):
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5))
|
||||
recipe = factory.Maybe(
|
||||
factory.LazyAttribute(lambda x: x.has_recipe),
|
||||
yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
|
||||
factory.LazyAttribute(lambda x: x.has_recipe),
|
||||
yes_declaration=factory.SubFactory(
|
||||
'cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
|
||||
no_declaration=None
|
||||
)
|
||||
servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=10))
|
||||
mealplan = factory.SubFactory(MealPlanFactory, space=factory.SelfAttribute('..space'))
|
||||
mealplan = factory.SubFactory(
|
||||
MealPlanFactory, space=factory.SelfAttribute('..space'))
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
class Params:
|
||||
@ -263,26 +283,33 @@ class ShoppingListEntryFactory(factory.django.DjangoModelFactory):
|
||||
"""ShoppingListEntry factory."""
|
||||
|
||||
list_recipe = factory.Maybe(
|
||||
factory.LazyAttribute(lambda x: x.has_mealplan),
|
||||
yes_declaration=factory.SubFactory(ShoppingListRecipeFactory, space=factory.SelfAttribute('..space')),
|
||||
factory.LazyAttribute(lambda x: x.has_mealplan),
|
||||
yes_declaration=factory.SubFactory(
|
||||
ShoppingListRecipeFactory, space=factory.SelfAttribute('..space')),
|
||||
no_declaration=None
|
||||
)
|
||||
food = factory.SubFactory(FoodFactory, space=factory.SelfAttribute('..space'))
|
||||
unit = factory.SubFactory(UnitFactory, space=factory.SelfAttribute('..space'))
|
||||
food = factory.SubFactory(
|
||||
FoodFactory, space=factory.SelfAttribute('..space'))
|
||||
unit = factory.SubFactory(
|
||||
UnitFactory, space=factory.SelfAttribute('..space'))
|
||||
# # ingredient = factory.SubFactory(IngredientFactory)
|
||||
amount = factory.LazyAttribute(lambda x: Decimal(faker.random_int(min=1, max=100))/10)
|
||||
amount = factory.LazyAttribute(
|
||||
lambda x: Decimal(faker.random_int(min=1, max=100)) / 10)
|
||||
order = factory.Sequence(int)
|
||||
checked = False
|
||||
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_by = factory.SubFactory(
|
||||
UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_at = factory.LazyAttribute(lambda x: faker.past_date())
|
||||
completed_at = None
|
||||
delay_until = None
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs): # override create to prevent auto_add_now from changing the created_at date
|
||||
# override create to prevent auto_add_now from changing the created_at date
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
created_at = kwargs.pop('created_at', None)
|
||||
obj = super(ShoppingListEntryFactory, cls)._create(target_class, *args, **kwargs)
|
||||
obj = super(ShoppingListEntryFactory, cls)._create(
|
||||
target_class, *args, **kwargs)
|
||||
if created_at is not None:
|
||||
obj.created_at = created_at
|
||||
obj.save()
|
||||
@ -298,7 +325,8 @@ class ShoppingListEntryFactory(factory.django.DjangoModelFactory):
|
||||
@register
|
||||
class StepFactory(factory.django.DjangoModelFactory):
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5))
|
||||
instruction = factory.LazyAttribute(lambda x: ''.join(faker.paragraphs(nb=5)))
|
||||
instruction = factory.LazyAttribute(
|
||||
lambda x: ''.join(faker.paragraphs(nb=5)))
|
||||
# TODO add optional recipe food, make dependent on recipe, make number of recipes a Params
|
||||
ingredients__count = 10 # default number of ingredients to add
|
||||
ingredients__header = 0
|
||||
@ -330,14 +358,16 @@ class StepFactory(factory.django.DjangoModelFactory):
|
||||
for i in range(num_ing):
|
||||
if num_food_recipe > 0:
|
||||
has_recipe = True
|
||||
num_food_recipe = num_food_recipe-1
|
||||
num_food_recipe = num_food_recipe - 1
|
||||
else:
|
||||
has_recipe = False
|
||||
self.ingredients.add(IngredientFactory(space=self.space, food__has_recipe=has_recipe))
|
||||
self.ingredients.add(IngredientFactory(
|
||||
space=self.space, food__has_recipe=has_recipe))
|
||||
num_header = kwargs.get('header', 0)
|
||||
if num_header > 0:
|
||||
for i in range(num_header):
|
||||
self.ingredients.add(IngredientFactory(food=None, unit=None, amount=0, is_header=True, space=self.space))
|
||||
self.ingredients.add(IngredientFactory(
|
||||
food=None, unit=None, amount=0, is_header=True, space=self.space))
|
||||
elif extracted:
|
||||
for ing in extracted:
|
||||
self.ingredients.add(ing)
|
||||
@ -351,20 +381,27 @@ class RecipeFactory(factory.django.DjangoModelFactory):
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=7))
|
||||
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
|
||||
servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=20))
|
||||
servings_text = factory.LazyAttribute(lambda x: faker.sentence(nb_words=1)) # TODO generate list of expected servings text that can be iterated through
|
||||
# TODO generate list of expected servings text that can be iterated through
|
||||
servings_text = factory.LazyAttribute(lambda x: faker.sentence(nb_words=1))
|
||||
keywords__count = 5 # default number of keywords to generate
|
||||
steps__count = 1 # default number of steps to create
|
||||
steps__recipe_count = 0 # default number of step recipes to create
|
||||
steps__food_recipe_count = {} # by default, don't create food recipes, to override {'steps__food_recipe_count': {'step': 0, 'count': 1}}
|
||||
working_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360))
|
||||
waiting_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360))
|
||||
# by default, don't create food recipes, to override {'steps__food_recipe_count': {'step': 0, 'count': 1}}
|
||||
steps__food_recipe_count = {}
|
||||
working_time = factory.LazyAttribute(
|
||||
lambda x: faker.random_int(min=0, max=360))
|
||||
waiting_time = factory.LazyAttribute(
|
||||
lambda x: faker.random_int(min=0, max=360))
|
||||
internal = False
|
||||
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_at = factory.LazyAttribute(lambda x: faker.date_between_dates(date_start=date(2000, 1, 1), date_end=date(2020, 12, 31)))
|
||||
created_by = factory.SubFactory(
|
||||
UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_at = factory.LazyAttribute(lambda x: faker.date_between_dates(
|
||||
date_start=date(2000, 1, 1), date_end=date(2020, 12, 31)))
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs): # override create to prevent auto_add_now from changing the created_at date
|
||||
# override create to prevent auto_add_now from changing the created_at date
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
created_at = kwargs.pop('created_at', None)
|
||||
# updated_at = kwargs.pop('updated_at', None)
|
||||
obj = super(RecipeFactory, cls)._create(target_class, *args, **kwargs)
|
||||
@ -401,11 +438,13 @@ class RecipeFactory(factory.django.DjangoModelFactory):
|
||||
ing_recipe_count = 0
|
||||
if food_recipe_count.get('step', None) == i:
|
||||
ing_recipe_count = food_recipe_count.get('count', 0)
|
||||
self.steps.add(StepFactory(space=self.space, ingredients__food_recipe_count=ing_recipe_count, ingredients__header=num_ing_headers))
|
||||
num_ing_headers+-1
|
||||
self.steps.add(StepFactory(
|
||||
space=self.space, ingredients__food_recipe_count=ing_recipe_count, ingredients__header=num_ing_headers))
|
||||
num_ing_headers + - 1
|
||||
if num_recipe_steps > 0:
|
||||
for j in range(num_recipe_steps):
|
||||
self.steps.add(StepFactory(space=self.space, step_recipe__has_recipe=True, ingredients__count=0))
|
||||
self.steps.add(StepFactory(
|
||||
space=self.space, step_recipe__has_recipe=True, ingredients__count=0))
|
||||
if extracted and (num_steps + num_recipe_steps == 0):
|
||||
for step in extracted:
|
||||
self.steps.add(step)
|
||||
@ -428,15 +467,18 @@ class RecipeFactory(factory.django.DjangoModelFactory):
|
||||
@register
|
||||
class CookLogFactory(factory.django.DjangoModelFactory):
|
||||
"""CookLog factory."""
|
||||
recipe = factory.SubFactory(RecipeFactory, space=factory.SelfAttribute('..space'))
|
||||
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
|
||||
recipe = factory.SubFactory(
|
||||
RecipeFactory, space=factory.SelfAttribute('..space'))
|
||||
created_by = factory.SubFactory(
|
||||
UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_at = factory.LazyAttribute(lambda x: faker.date_this_decade())
|
||||
rating = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=5))
|
||||
servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=32))
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs): # override create to prevent auto_add_now from changing the created_at date
|
||||
# override create to prevent auto_add_now from changing the created_at date
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
created_at = kwargs.pop('created_at', None)
|
||||
obj = super(CookLogFactory, cls)._create(target_class, *args, **kwargs)
|
||||
if created_at is not None:
|
||||
@ -451,13 +493,17 @@ class CookLogFactory(factory.django.DjangoModelFactory):
|
||||
@register
|
||||
class ViewLogFactory(factory.django.DjangoModelFactory):
|
||||
"""ViewLog factory."""
|
||||
recipe = factory.SubFactory(RecipeFactory, space=factory.SelfAttribute('..space'))
|
||||
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_at = factory.LazyAttribute(lambda x: faker.past_datetime(start_date='-365d'))
|
||||
recipe = factory.SubFactory(
|
||||
RecipeFactory, space=factory.SelfAttribute('..space'))
|
||||
created_by = factory.SubFactory(
|
||||
UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_at = factory.LazyAttribute(
|
||||
lambda x: faker.past_datetime(start_date='-365d'))
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs): # override create to prevent auto_add_now from changing the created_at date
|
||||
# override create to prevent auto_add_now from changing the created_at date
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
created_at = kwargs.pop('created_at', None)
|
||||
obj = super(ViewLogFactory, cls)._create(target_class, *args, **kwargs)
|
||||
if created_at is not None:
|
||||
|
@ -11,6 +11,11 @@ from cookbook.tests.factories import FoodFactory, RecipeFactory
|
||||
# TODO returns recipes with all ingredients via child substitute
|
||||
# TODO returns recipes with all ingredients via sibling substitute
|
||||
|
||||
if (Food.node_order_by):
|
||||
node_location = 'sorted-child'
|
||||
else:
|
||||
node_location = 'last-child'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def recipes(space_1):
|
||||
@ -19,7 +24,8 @@ def recipes(space_1):
|
||||
|
||||
@pytest.fixture
|
||||
def makenow_recipe(request, space_1):
|
||||
onhand_user = auth.get_user(request.getfixturevalue(request.param.get('onhand_users', 'u1_s1')))
|
||||
onhand_user = auth.get_user(request.getfixturevalue(
|
||||
request.param.get('onhand_users', 'u1_s1')))
|
||||
|
||||
recipe = RecipeFactory.create(space=space_1)
|
||||
for food in Food.objects.filter(ingredient__step__recipe=recipe.id):
|
||||
@ -55,13 +61,16 @@ def test_makenow_ignoreshopping(recipes, makenow_recipe, user1, space_1):
|
||||
request = type('', (object,), {'space': space_1, 'user': user1})()
|
||||
search = RecipeSearch(request, makenow='true')
|
||||
with scope(space=space_1):
|
||||
food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first()
|
||||
food = Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id).first()
|
||||
food.onhand_users.clear()
|
||||
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
||||
food.ignore_shopping = True
|
||||
food.save()
|
||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, ignore_shopping=True).count() == 1
|
||||
assert Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
||||
assert Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id, ignore_shopping=True).count() == 1
|
||||
search = search.get_queryset(Recipe.objects.all())
|
||||
assert search.count() == 1
|
||||
assert search.first().id == makenow_recipe.id
|
||||
@ -74,13 +83,17 @@ def test_makenow_substitute(recipes, makenow_recipe, user1, space_1):
|
||||
request = type('', (object,), {'space': space_1, 'user': user1})()
|
||||
search = RecipeSearch(request, makenow='true')
|
||||
with scope(space=space_1):
|
||||
food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first()
|
||||
food = Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id).first()
|
||||
onhand_user = food.onhand_users.first()
|
||||
food.onhand_users.clear()
|
||||
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
||||
food.substitute.add(FoodFactory.create(space=space_1, onhand_users=[onhand_user]))
|
||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, substitute__isnull=False).count() == 1
|
||||
food.substitute.add(FoodFactory.create(
|
||||
space=space_1, onhand_users=[onhand_user]))
|
||||
assert Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
||||
assert Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id, substitute__isnull=False).count() == 1
|
||||
|
||||
search = search.get_queryset(Recipe.objects.all())
|
||||
assert search.count() == 1
|
||||
@ -94,16 +107,20 @@ def test_makenow_child_substitute(recipes, makenow_recipe, user1, space_1):
|
||||
request = type('', (object,), {'space': space_1, 'user': user1})()
|
||||
search = RecipeSearch(request, makenow='true')
|
||||
with scope(space=space_1):
|
||||
food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first()
|
||||
food = Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id).first()
|
||||
onhand_user = food.onhand_users.first()
|
||||
food.onhand_users.clear()
|
||||
food.substitute_children = True
|
||||
food.save()
|
||||
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
||||
new_food = FoodFactory.create(space=space_1, onhand_users=[onhand_user])
|
||||
new_food.move(food, 'first-child')
|
||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, numchild__gt=0).count() == 1
|
||||
new_food = FoodFactory.create(
|
||||
space=space_1, onhand_users=[onhand_user])
|
||||
new_food.move(food, node_location)
|
||||
assert Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
||||
assert Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id, numchild__gt=0).count() == 1
|
||||
search = search.get_queryset(Recipe.objects.all())
|
||||
assert search.count() == 1
|
||||
assert search.first().id == makenow_recipe.id
|
||||
@ -116,18 +133,22 @@ def test_makenow_sibling_substitute(recipes, makenow_recipe, user1, space_1):
|
||||
request = type('', (object,), {'space': space_1, 'user': user1})()
|
||||
search = RecipeSearch(request, makenow='true')
|
||||
with scope(space=space_1):
|
||||
food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first()
|
||||
food = Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id).first()
|
||||
onhand_user = food.onhand_users.first()
|
||||
food.onhand_users.clear()
|
||||
food.substitute_siblings = True
|
||||
food.save()
|
||||
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
||||
new_parent = FoodFactory.create(space=space_1)
|
||||
new_sibling = FoodFactory.create(space=space_1, onhand_users=[onhand_user])
|
||||
new_sibling.move(new_parent, 'first-child')
|
||||
food.move(new_parent, 'first-child')
|
||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, depth=2).count() == 1
|
||||
new_sibling = FoodFactory.create(
|
||||
space=space_1, onhand_users=[onhand_user])
|
||||
new_sibling.move(new_parent, node_location)
|
||||
food.move(new_parent, node_location)
|
||||
assert Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
||||
assert Food.objects.filter(
|
||||
ingredient__step__recipe=makenow_recipe.id, depth=2).count() == 1
|
||||
search = search.get_queryset(Recipe.objects.all())
|
||||
assert search.count() == 1
|
||||
assert search.first().id == makenow_recipe.id
|
||||
|
@ -7,9 +7,9 @@ from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from django_scopes import scope
|
||||
|
||||
from cookbook.models import Food, Recipe, SearchFields
|
||||
from cookbook.models import Recipe, SearchFields
|
||||
from cookbook.tests.conftest import transpose
|
||||
from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFactory,
|
||||
KeywordFactory, RecipeBookEntryFactory, RecipeFactory,
|
||||
@ -23,7 +23,8 @@ from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFac
|
||||
# TODO makenow with above filters
|
||||
# TODO test search food/keywords including/excluding children
|
||||
LIST_URL = 'api:recipe-list'
|
||||
sqlite = settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
|
||||
sqlite = settings.DATABASES['default']['ENGINE'] not in [
|
||||
'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -50,26 +51,43 @@ def user1(request, space_1, u1_s1, unaccent):
|
||||
if params.get('fuzzy_lookups', False):
|
||||
user.searchpreference.lookup = True
|
||||
misspelled_result = 1
|
||||
else:
|
||||
user.searchpreference.lookup = False
|
||||
|
||||
if params.get('fuzzy_search', False):
|
||||
user.searchpreference.trigram.set(SearchFields.objects.all())
|
||||
misspelled_result = 1
|
||||
else:
|
||||
user.searchpreference.trigram.set([])
|
||||
|
||||
if params.get('icontains', False):
|
||||
user.searchpreference.icontains.set(SearchFields.objects.all())
|
||||
search_term = 'ghijklmn'
|
||||
else:
|
||||
user.searchpreference.icontains.set([])
|
||||
|
||||
if params.get('istartswith', False):
|
||||
user.searchpreference.istartswith.set(SearchFields.objects.all())
|
||||
search_term = 'abcdef'
|
||||
else:
|
||||
user.searchpreference.istartswith.set([])
|
||||
|
||||
if params.get('unaccent', False):
|
||||
user.searchpreference.unaccent.set(SearchFields.objects.all())
|
||||
misspelled_result *= 2
|
||||
result *= 2
|
||||
else:
|
||||
user.searchpreference.unaccent.set([])
|
||||
|
||||
# full text vectors are hard coded to use unaccent - put this after unaccent to override result
|
||||
if params.get('fulltext', False):
|
||||
user.searchpreference.fulltext.set(SearchFields.objects.all())
|
||||
# user.searchpreference.search = 'websearch'
|
||||
search_term = 'ghijklmn uvwxyz'
|
||||
result = 2
|
||||
else:
|
||||
user.searchpreference.fulltext.set([])
|
||||
|
||||
user.searchpreference.save()
|
||||
misspelled_term = transpose(search_term, number=3)
|
||||
return (u1_s1, result, misspelled_result, search_term, misspelled_term, params)
|
||||
@ -104,7 +122,8 @@ def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1):
|
||||
obj2 = FoodFactory.create(name=accent, space=space_1)
|
||||
recipe1.steps.first().ingredients.add(IngredientFactory.create(food=obj1))
|
||||
recipe2.steps.first().ingredients.add(IngredientFactory.create(food=obj2))
|
||||
recipe3.steps.first().ingredients.add(IngredientFactory.create(food=obj1), IngredientFactory.create(food=obj2))
|
||||
recipe3.steps.first().ingredients.add(IngredientFactory.create(
|
||||
food=obj1), IngredientFactory.create(food=obj2))
|
||||
if request.param.get('keyword', None):
|
||||
obj1 = KeywordFactory.create(name=unaccent, space=space_1)
|
||||
obj2 = KeywordFactory.create(name=accent, space=space_1)
|
||||
@ -125,7 +144,8 @@ def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1):
|
||||
obj2 = UnitFactory.create(name=accent, space=space_1)
|
||||
recipe1.steps.first().ingredients.add(IngredientFactory.create(unit=obj1))
|
||||
recipe2.steps.first().ingredients.add(IngredientFactory.create(unit=obj2))
|
||||
recipe3.steps.first().ingredients.add(IngredientFactory.create(unit=obj1), IngredientFactory.create(unit=obj2))
|
||||
recipe3.steps.first().ingredients.add(IngredientFactory.create(
|
||||
unit=obj1), IngredientFactory.create(unit=obj2))
|
||||
if request.param.get('name', None):
|
||||
recipe1.name = unaccent
|
||||
recipe2.name = accent
|
||||
@ -145,21 +165,32 @@ def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1):
|
||||
i2.save()
|
||||
|
||||
if request.param.get('viewedon', None):
|
||||
ViewLogFactory.create(recipe=recipe1, created_by=user1, created_at=days_3, space=space_1)
|
||||
ViewLogFactory.create(recipe=recipe2, created_by=user1, created_at=days_30, space=space_1)
|
||||
ViewLogFactory.create(recipe=recipe3, created_by=user2, created_at=days_15, space=space_1)
|
||||
ViewLogFactory.create(recipe=recipe1, created_by=user1,
|
||||
created_at=days_3, space=space_1)
|
||||
ViewLogFactory.create(recipe=recipe2, created_by=user1,
|
||||
created_at=days_30, space=space_1)
|
||||
ViewLogFactory.create(recipe=recipe3, created_by=user2,
|
||||
created_at=days_15, space=space_1)
|
||||
if request.param.get('cookedon', None):
|
||||
CookLogFactory.create(recipe=recipe1, created_by=user1, created_at=days_3, space=space_1)
|
||||
CookLogFactory.create(recipe=recipe2, created_by=user1, created_at=days_30, space=space_1)
|
||||
CookLogFactory.create(recipe=recipe3, created_by=user2, created_at=days_15, space=space_1)
|
||||
CookLogFactory.create(recipe=recipe1, created_by=user1,
|
||||
created_at=days_3, space=space_1)
|
||||
CookLogFactory.create(recipe=recipe2, created_by=user1,
|
||||
created_at=days_30, space=space_1)
|
||||
CookLogFactory.create(recipe=recipe3, created_by=user2,
|
||||
created_at=days_15, space=space_1)
|
||||
if request.param.get('timescooked', None):
|
||||
CookLogFactory.create_batch(5, recipe=recipe1, created_by=user1, space=space_1)
|
||||
CookLogFactory.create(recipe=recipe2, created_by=user1, space=space_1)
|
||||
CookLogFactory.create_batch(3, recipe=recipe3, created_by=user2, space=space_1)
|
||||
CookLogFactory.create_batch(
|
||||
5, recipe=recipe1, created_by=user1, space=space_1)
|
||||
CookLogFactory.create(recipe=recipe2, created_by=user1, space=space_1)
|
||||
CookLogFactory.create_batch(
|
||||
3, recipe=recipe3, created_by=user2, space=space_1)
|
||||
if request.param.get('rating', None):
|
||||
CookLogFactory.create(recipe=recipe1, created_by=user1, rating=5.0, space=space_1)
|
||||
CookLogFactory.create(recipe=recipe2, created_by=user1, rating=1.0, space=space_1)
|
||||
CookLogFactory.create(recipe=recipe3, created_by=user2, rating=3.0, space=space_1)
|
||||
CookLogFactory.create(
|
||||
recipe=recipe1, created_by=user1, rating=5.0, space=space_1)
|
||||
CookLogFactory.create(
|
||||
recipe=recipe2, created_by=user1, rating=1.0, space=space_1)
|
||||
CookLogFactory.create(
|
||||
recipe=recipe3, created_by=user2, rating=3.0, space=space_1)
|
||||
|
||||
return (recipe1, recipe2, recipe3, obj1, obj2, request.param)
|
||||
|
||||
@ -188,7 +219,8 @@ def test_search_or_and_not(found_recipe, param_type, operator, recipes, u1_s1, s
|
||||
assert found_recipe[1].id in [x['id'] for x in r['results']]
|
||||
assert found_recipe[2].id in [x['id'] for x in r['results']]
|
||||
|
||||
r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1}&{param2}').content)
|
||||
r = json.loads(u1_s1.get(reverse(LIST_URL) +
|
||||
f'?{param1}&{param2}').content)
|
||||
assert r['count'] == operator[1]
|
||||
assert found_recipe[2].id in [x['id'] for x in r['results']]
|
||||
|
||||
@ -203,7 +235,8 @@ def test_search_or_and_not(found_recipe, param_type, operator, recipes, u1_s1, s
|
||||
assert found_recipe[1].id not in [x['id'] for x in r['results']]
|
||||
assert found_recipe[2].id not in [x['id'] for x in r['results']]
|
||||
|
||||
r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1_not}&{param2_not}').content)
|
||||
r = json.loads(u1_s1.get(reverse(LIST_URL) +
|
||||
f'?{param1_not}&{param2_not}').content)
|
||||
assert r['count'] == 10 + operator[2]
|
||||
assert found_recipe[2].id not in [x['id'] for x in r['results']]
|
||||
|
||||
@ -227,13 +260,14 @@ def test_search_units(found_recipe, recipes, u1_s1, space_1):
|
||||
assert found_recipe[1].id in [x['id'] for x in r['results']]
|
||||
assert found_recipe[2].id in [x['id'] for x in r['results']]
|
||||
|
||||
r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1}&{param2}').content)
|
||||
r = json.loads(u1_s1.get(reverse(LIST_URL) +
|
||||
f'?{param1}&{param2}').content)
|
||||
assert r['count'] == 3
|
||||
assert found_recipe[2].id in [x['id'] for x in r['results']]
|
||||
|
||||
|
||||
@pytest.mark.skipif(sqlite, reason="requires PostgreSQL")
|
||||
@pytest.mark.parametrize("user1", itertools.product(
|
||||
@pytest.mark.parametrize("user1", itertools.product(
|
||||
[
|
||||
('fuzzy_search', True), ('fuzzy_search', False),
|
||||
('fuzzy_lookups', True), ('fuzzy_lookups', False)
|
||||
@ -245,22 +279,26 @@ def test_search_units(found_recipe, recipes, u1_s1, space_1):
|
||||
({'keyword': True}, 'keyword'),
|
||||
({'food': True}, 'food'),
|
||||
], indirect=['found_recipe'])
|
||||
def test_fuzzy_lookup(found_recipe, recipes, param_type, user1, space_1):
|
||||
def test_fuzzy_lookup(found_recipe, recipes, param_type, user1, space_1):
|
||||
with scope(space=space_1):
|
||||
list_url = f'api:{param_type}-list'
|
||||
param1 = f"query={user1[3]}"
|
||||
param2 = f"query={user1[4]}"
|
||||
|
||||
r = json.loads(user1[0].get(reverse(list_url) + f'?{param1}&limit=2').content)
|
||||
assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[3].id, found_recipe[4].id]]) == user1[1]
|
||||
r = json.loads(user1[0].get(reverse(list_url) +
|
||||
f'?{param1}&limit=2').content)
|
||||
assert len([x['id'] for x in r['results'] if x['id'] in [
|
||||
found_recipe[3].id, found_recipe[4].id]]) == user1[1]
|
||||
|
||||
r = json.loads(user1[0].get(reverse(list_url) + f'?{param2}&limit=10').content)
|
||||
assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[3].id, found_recipe[4].id]]) == user1[2]
|
||||
r = json.loads(user1[0].get(reverse(list_url) +
|
||||
f'?{param2}&limit=10').content)
|
||||
assert len([x['id'] for x in r['results'] if x['id'] in [
|
||||
found_recipe[3].id, found_recipe[4].id]]) == user1[2]
|
||||
|
||||
# commenting this out for general use - it is really slow
|
||||
# it should be run on occasion to ensure everything still works
|
||||
# @pytest.mark.skipif(sqlite and True, reason="requires PostgreSQL")
|
||||
# @pytest.mark.parametrize("user1", itertools.product(
|
||||
# @pytest.mark.parametrize("user1", itertools.product(
|
||||
# [
|
||||
# ('fuzzy_search', True), ('fuzzy_search', False),
|
||||
# ('fulltext', True), ('fulltext', False),
|
||||
@ -276,29 +314,35 @@ def test_fuzzy_lookup(found_recipe, recipes, param_type, user1, space_1):
|
||||
# ({'keyword': True}),
|
||||
# ({'food': True}),
|
||||
# ], indirect=['found_recipe'])
|
||||
# def test_search_string(found_recipe, recipes, user1, space_1):
|
||||
# # user array contains: user client, expected count of search, expected count of mispelled search, search string, mispelled search string, user search preferences
|
||||
# def test_search_string(found_recipe, recipes, user1, space_1):
|
||||
# with scope(space=space_1):
|
||||
# param1 = f"query={user1[3]}"
|
||||
# param2 = f"query={user1[4]}"
|
||||
|
||||
# r = json.loads(user1[0].get(reverse(LIST_URL) + f'?{param1}').content)
|
||||
# assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[0].id, found_recipe[1].id]]) == user1[1]
|
||||
# assert len([x['id'] for x in r['results'] if x['id'] in [
|
||||
# found_recipe[0].id, found_recipe[1].id]]) == user1[1]
|
||||
|
||||
# r = json.loads(user1[0].get(reverse(LIST_URL) + f'?{param2}').content)
|
||||
# assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[0].id, found_recipe[1].id]]) == user1[2]
|
||||
# assert len([x['id'] for x in r['results'] if x['id'] in [
|
||||
# found_recipe[0].id, found_recipe[1].id]]) == user1[2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("found_recipe, param_type, result", [
|
||||
({'viewedon': True}, 'viewedon', (1, 1)),
|
||||
({'cookedon': True}, 'cookedon', (1, 1)),
|
||||
({'createdon': True}, 'createdon', (2, 12)), # created dates are not filtered by user
|
||||
({'createdon': True}, 'updatedon', (2, 12)), # updated dates are not filtered by user
|
||||
# created dates are not filtered by user
|
||||
({'createdon': True}, 'createdon', (2, 12)),
|
||||
# updated dates are not filtered by user
|
||||
({'createdon': True}, 'updatedon', (2, 12)),
|
||||
], indirect=['found_recipe'])
|
||||
def test_search_date(found_recipe, recipes, param_type, result, u1_s1, u2_s1, space_1):
|
||||
# force updated_at to equal created_at datetime
|
||||
with scope(space=space_1):
|
||||
for recipe in Recipe.objects.all():
|
||||
Recipe.objects.filter(id=recipe.id).update(updated_at=recipe.created_at)
|
||||
Recipe.objects.filter(id=recipe.id).update(
|
||||
updated_at=recipe.created_at)
|
||||
|
||||
date = (timezone.now() - timedelta(days=15)).strftime("%Y-%m-%d")
|
||||
param1 = f"?{param_type}={date}"
|
||||
@ -321,34 +365,34 @@ def test_search_date(found_recipe, recipes, param_type, result, u1_s1, u2_s1, sp
|
||||
assert found_recipe[2].id in [x['id'] for x in r['results']]
|
||||
|
||||
|
||||
# TODO this is somehow screwed, probably the search itself, dont want to fix it for now
|
||||
# @pytest.mark.parametrize("found_recipe, param_type", [
|
||||
# ({'rating': True}, 'rating'),
|
||||
# ({'timescooked': True}, 'timescooked'),
|
||||
# ], indirect=['found_recipe'])
|
||||
# def test_search_count(found_recipe, recipes, param_type, u1_s1, u2_s1, space_1):
|
||||
# param1 = f'?{param_type}=3'
|
||||
# param2 = f'?{param_type}=-3'
|
||||
# param3 = f'?{param_type}=0'
|
||||
#
|
||||
# r = json.loads(u1_s1.get(reverse(LIST_URL) + param1).content)
|
||||
# assert r['count'] == 1
|
||||
# assert found_recipe[0].id in [x['id'] for x in r['results']]
|
||||
#
|
||||
# r = json.loads(u1_s1.get(reverse(LIST_URL) + param2).content)
|
||||
# assert r['count'] == 1
|
||||
# assert found_recipe[1].id in [x['id'] for x in r['results']]
|
||||
#
|
||||
# # test search for not rated/cooked
|
||||
# r = json.loads(u1_s1.get(reverse(LIST_URL) + param3).content)
|
||||
# assert r['count'] == 11
|
||||
# assert (found_recipe[0].id or found_recipe[1].id) not in [x['id'] for x in r['results']]
|
||||
#
|
||||
# # test matched returns for lte and gte searches
|
||||
# r = json.loads(u2_s1.get(reverse(LIST_URL) + param1).content)
|
||||
# assert r['count'] == 1
|
||||
# assert found_recipe[2].id in [x['id'] for x in r['results']]
|
||||
#
|
||||
# r = json.loads(u2_s1.get(reverse(LIST_URL) + param2).content)
|
||||
# assert r['count'] == 1
|
||||
# assert found_recipe[2].id in [x['id'] for x in r['results']]
|
||||
@pytest.mark.parametrize("found_recipe, param_type", [
|
||||
({'rating': True}, 'rating'),
|
||||
({'timescooked': True}, 'timescooked'),
|
||||
], indirect=['found_recipe'])
|
||||
def test_search_count(found_recipe, recipes, param_type, u1_s1, u2_s1, space_1):
|
||||
param1 = f'?{param_type}=3'
|
||||
param2 = f'?{param_type}=-3'
|
||||
param3 = f'?{param_type}=0'
|
||||
|
||||
r = json.loads(u1_s1.get(reverse(LIST_URL) + param1).content)
|
||||
assert r['count'] == 1
|
||||
assert found_recipe[0].id in [x['id'] for x in r['results']]
|
||||
|
||||
r = json.loads(u1_s1.get(reverse(LIST_URL) + param2).content)
|
||||
assert r['count'] == 1
|
||||
assert found_recipe[1].id in [x['id'] for x in r['results']]
|
||||
|
||||
# test search for not rated/cooked
|
||||
r = json.loads(u1_s1.get(reverse(LIST_URL) + param3).content)
|
||||
assert r['count'] == 11
|
||||
assert (found_recipe[0].id or found_recipe[1].id) not in [
|
||||
x['id'] for x in r['results']]
|
||||
|
||||
# test matched returns for lte and gte searches
|
||||
r = json.loads(u2_s1.get(reverse(LIST_URL) + param1).content)
|
||||
assert r['count'] == 1
|
||||
assert found_recipe[2].id in [x['id'] for x in r['results']]
|
||||
|
||||
r = json.loads(u2_s1.get(reverse(LIST_URL) + param2).content)
|
||||
assert r['count'] == 1
|
||||
assert found_recipe[2].id in [x['id'] for x in r['results']]
|
||||
|
@ -24,7 +24,8 @@ load_dotenv()
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Get vars from .env files
|
||||
SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv('SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV'
|
||||
SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv(
|
||||
'SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV'
|
||||
|
||||
DEBUG = bool(int(os.getenv('DEBUG', True)))
|
||||
DEBUG_TOOLBAR = bool(int(os.getenv('DEBUG_TOOLBAR', True)))
|
||||
@ -35,9 +36,11 @@ SOCIAL_DEFAULT_GROUP = os.getenv('SOCIAL_DEFAULT_GROUP', 'guest')
|
||||
SPACE_DEFAULT_MAX_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0))
|
||||
SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0))
|
||||
SPACE_DEFAULT_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 0))
|
||||
SPACE_DEFAULT_ALLOW_SHARING = bool(int(os.getenv('SPACE_DEFAULT_ALLOW_SHARING', True)))
|
||||
SPACE_DEFAULT_ALLOW_SHARING = bool(
|
||||
int(os.getenv('SPACE_DEFAULT_ALLOW_SHARING', True)))
|
||||
|
||||
INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
|
||||
INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(
|
||||
',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
|
||||
|
||||
# allow djangos wsgi server to server mediafiles
|
||||
GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', True)))
|
||||
@ -51,9 +54,11 @@ KJ_PREF_DEFAULT = bool(int(os.getenv('KJ_PREF_DEFAULT', False)))
|
||||
STICKY_NAV_PREF_DEFAULT = bool(int(os.getenv('STICKY_NAV_PREF_DEFAULT', True)))
|
||||
|
||||
# minimum interval that users can set for automatic sync of shopping lists
|
||||
SHOPPING_MIN_AUTOSYNC_INTERVAL = int(os.getenv('SHOPPING_MIN_AUTOSYNC_INTERVAL', 5))
|
||||
SHOPPING_MIN_AUTOSYNC_INTERVAL = int(
|
||||
os.getenv('SHOPPING_MIN_AUTOSYNC_INTERVAL', 5))
|
||||
|
||||
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(',') if os.getenv('ALLOWED_HOSTS') else ['*']
|
||||
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(
|
||||
',') if os.getenv('ALLOWED_HOSTS') else ['*']
|
||||
|
||||
if os.getenv('CSRF_TRUSTED_ORIGINS'):
|
||||
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',')
|
||||
@ -131,7 +136,8 @@ try:
|
||||
plugin_module = f'recipes.plugins.{d}.apps.{app_config_classname}'
|
||||
if plugin_module not in INSTALLED_APPS:
|
||||
INSTALLED_APPS.append(plugin_module)
|
||||
plugin_class = getattr(sys.modules[apps_path], app_config_classname)
|
||||
plugin_class = getattr(
|
||||
sys.modules[apps_path], app_config_classname)
|
||||
plugin_config = {
|
||||
'name': plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name,
|
||||
'module': f'recipes.plugins.{d}',
|
||||
@ -148,7 +154,8 @@ except Exception:
|
||||
if DEBUG:
|
||||
print('ERROR failed to initialize plugins')
|
||||
|
||||
SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split(',') if os.getenv('SOCIAL_PROVIDERS') else []
|
||||
SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split(
|
||||
',') if os.getenv('SOCIAL_PROVIDERS') else []
|
||||
SOCIALACCOUNT_EMAIL_VERIFICATION = 'none'
|
||||
INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS
|
||||
|
||||
@ -194,7 +201,8 @@ if DEBUG_TOOLBAR:
|
||||
INSTALLED_APPS += ('debug_toolbar',)
|
||||
|
||||
SORT_TREE_BY_NAME = bool(int(os.getenv('SORT_TREE_BY_NAME', False)))
|
||||
DISABLE_TREE_FIX_STARTUP = bool(int(os.getenv('DISABLE_TREE_FIX_STARTUP', False)))
|
||||
DISABLE_TREE_FIX_STARTUP = bool(
|
||||
int(os.getenv('DISABLE_TREE_FIX_STARTUP', False)))
|
||||
|
||||
if bool(int(os.getenv('SQL_DEBUG', False))):
|
||||
MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware',)
|
||||
@ -225,10 +233,12 @@ if LDAP_AUTH:
|
||||
'last_name': 'sn',
|
||||
'email': 'mail',
|
||||
}
|
||||
AUTH_LDAP_ALWAYS_UPDATE_USER = bool(int(os.getenv('AUTH_LDAP_ALWAYS_UPDATE_USER', True)))
|
||||
AUTH_LDAP_ALWAYS_UPDATE_USER = bool(
|
||||
int(os.getenv('AUTH_LDAP_ALWAYS_UPDATE_USER', True)))
|
||||
AUTH_LDAP_CACHE_TIMEOUT = int(os.getenv('AUTH_LDAP_CACHE_TIMEOUT', 3600))
|
||||
if 'AUTH_LDAP_TLS_CACERTFILE' in os.environ:
|
||||
AUTH_LDAP_GLOBAL_OPTIONS = {ldap.OPT_X_TLS_CACERTFILE: os.getenv('AUTH_LDAP_TLS_CACERTFILE')}
|
||||
AUTH_LDAP_GLOBAL_OPTIONS = {
|
||||
ldap.OPT_X_TLS_CACERTFILE: os.getenv('AUTH_LDAP_TLS_CACERTFILE')}
|
||||
if DEBUG:
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
@ -249,7 +259,8 @@ ACCOUNT_ADAPTER = 'cookbook.helper.AllAuthCustomAdapter'
|
||||
|
||||
if REVERSE_PROXY_AUTH:
|
||||
MIDDLEWARE.insert(8, 'recipes.middleware.CustomRemoteUser')
|
||||
AUTHENTICATION_BACKENDS.append('django.contrib.auth.backends.RemoteUserBackend')
|
||||
AUTHENTICATION_BACKENDS.append(
|
||||
'django.contrib.auth.backends.RemoteUserBackend')
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
|
||||
@ -444,7 +455,8 @@ LANGUAGES = [
|
||||
|
||||
SCRIPT_NAME = os.getenv('SCRIPT_NAME', '')
|
||||
# path for django_js_reverse to generate the javascript file containing all urls. Only done because the default command (collectstatic_js_reverse) fails to update the manifest
|
||||
JS_REVERSE_OUTPUT_PATH = os.path.join(BASE_DIR, "cookbook/static/django_js_reverse")
|
||||
JS_REVERSE_OUTPUT_PATH = os.path.join(
|
||||
BASE_DIR, "cookbook/static/django_js_reverse")
|
||||
JS_REVERSE_SCRIPT_PREFIX = os.getenv('JS_REVERSE_SCRIPT_PREFIX', SCRIPT_NAME)
|
||||
|
||||
STATIC_URL = os.getenv('STATIC_URL', '/static/')
|
||||
@ -499,4 +511,5 @@ EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '')
|
||||
EMAIL_USE_TLS = bool(int(os.getenv('EMAIL_USE_TLS', False)))
|
||||
EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False)))
|
||||
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
|
||||
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv('ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
|
||||
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv(
|
||||
'ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
|
||||
|
@ -30,11 +30,11 @@ Jinja2==3.1.2
|
||||
django-webpack-loader==1.8.1
|
||||
git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82
|
||||
django-allauth==0.52.0
|
||||
recipe-scrapers==14.35.0
|
||||
recipe-scrapers==14.36.1
|
||||
django-scopes==1.2.0.post1
|
||||
pytest==7.2.2
|
||||
pytest==7.3.1
|
||||
pytest-django==4.5.2
|
||||
django-treebeard==4.5.1
|
||||
django-treebeard==4.7
|
||||
django-cors-headers==3.13.0
|
||||
django-storages==1.13.2
|
||||
boto3==1.26.41
|
||||
@ -42,7 +42,7 @@ django-prometheus==2.2.0
|
||||
django-hCaptcha==0.2.0
|
||||
python-ldap==3.4.3
|
||||
django-auth-ldap==4.2.0
|
||||
pytest-factoryboy==2.5.0
|
||||
pytest-factoryboy==2.5.1
|
||||
pyppeteer==1.0.2
|
||||
validators==0.20.0
|
||||
pytube==12.1.0
|
||||
pytube==12.1.0
|
||||
|
@ -12,18 +12,17 @@
|
||||
<i class="far fa-check-circle text-primary" v-if="!ingredient.checked"></i>
|
||||
</td>
|
||||
<td class="text-nowrap" @click="done">
|
||||
<span v-if="ingredient.amount !== 0 && !ingredient.no_amount"
|
||||
v-html="calculateAmount(ingredient.amount)"></span>
|
||||
<span v-if="ingredient.amount !== 0 && !ingredient.no_amount" v-html="calculateAmount(ingredient.amount)"></span>
|
||||
</td>
|
||||
<td @click="done">
|
||||
<template v-if="ingredient.unit !== null && !ingredient.no_amount">
|
||||
<template >
|
||||
<template>
|
||||
<template v-if="ingredient.unit.plural_name === '' || ingredient.unit.plural_name === null">
|
||||
<span>{{ ingredient.unit.name }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="ingredient.always_use_plural_unit">{{ ingredient.unit.plural_name}}</span>
|
||||
<span v-else-if="(ingredient.amount * this.ingredient_factor) > 1">{{ ingredient.unit.plural_name }}</span>
|
||||
<span v-if="ingredient.always_use_plural_unit">{{ ingredient.unit.plural_name }}</span>
|
||||
<span v-else-if="ingredient.amount * this.ingredient_factor > 1">{{ ingredient.unit.plural_name }}</span>
|
||||
<span v-else>{{ ingredient.unit.name }}</span>
|
||||
</template>
|
||||
</template>
|
||||
@ -31,11 +30,10 @@
|
||||
</td>
|
||||
<td @click="done">
|
||||
<template v-if="ingredient.food !== null">
|
||||
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)"
|
||||
v-if="ingredient.food.recipe !== null" target="_blank"
|
||||
rel="noopener noreferrer">{{ ingredient.food.name }}</a>
|
||||
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)" v-if="ingredient.food.recipe !== null" target="_blank" rel="noopener noreferrer">{{
|
||||
ingredient.food.name
|
||||
}}</a>
|
||||
<template v-if="ingredient.food.recipe === null">
|
||||
|
||||
<template>
|
||||
<template v-if="ingredient.food.plural_name === '' || ingredient.food.plural_name === null">
|
||||
<span>{{ ingredient.food.name }}</span>
|
||||
@ -43,7 +41,7 @@
|
||||
<template v-else>
|
||||
<span v-if="ingredient.always_use_plural_food">{{ ingredient.food.plural_name }}</span>
|
||||
<span v-else-if="ingredient.no_amount">{{ ingredient.food.name }}</span>
|
||||
<span v-else-if="(ingredient.amount * this.ingredient_factor) > 1">{{ ingredient.food.plural_name }}</span>
|
||||
<span v-else-if="ingredient.amount * this.ingredient_factor > 1">{{ ingredient.food.plural_name }}</span>
|
||||
<span v-else>{{ ingredient.food.name }}</span>
|
||||
</template>
|
||||
</template>
|
||||
@ -51,35 +49,32 @@
|
||||
</template>
|
||||
</td>
|
||||
<td v-if="detailed">
|
||||
<div v-if="ingredient.note">
|
||||
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable p-0 pl-md-2 pr-md-2">
|
||||
<template v-if="ingredient.note">
|
||||
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable py-0 px-2">
|
||||
<i class="far fa-comment"></i>
|
||||
</span>
|
||||
|
||||
<div class="d-none d-print-block"><i class="far fa-comment-alt d-print-none"></i> {{
|
||||
ingredient.note
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-none d-print-block"><i class="far fa-comment-alt d-print-none"></i> {{ ingredient.note }}</div>
|
||||
</template>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {calculateAmount, ResolveUrlMixin} from "@/utils/utils"
|
||||
import { calculateAmount, ResolveUrlMixin } from "@/utils/utils"
|
||||
|
||||
import Vue from "vue"
|
||||
import VueSanitize from "vue-sanitize";
|
||||
import VueSanitize from "vue-sanitize"
|
||||
|
||||
Vue.use(VueSanitize);
|
||||
Vue.use(VueSanitize)
|
||||
|
||||
export default {
|
||||
name: "IngredientComponent",
|
||||
props: {
|
||||
ingredient: Object,
|
||||
ingredient_factor: {type: Number, default: 1},
|
||||
detailed: {type: Boolean, default: true},
|
||||
ingredient_factor: { type: Number, default: 1 },
|
||||
detailed: { type: Boolean, default: true },
|
||||
},
|
||||
mixins: [ResolveUrlMixin],
|
||||
data() {
|
||||
@ -88,9 +83,7 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
calculateAmount: function (x) {
|
||||
return this.$sanitize(calculateAmount(x, this.ingredient_factor))
|
||||
@ -106,9 +99,9 @@ export default {
|
||||
<style scoped>
|
||||
/* increase size of hover/touchable space without changing spacing */
|
||||
.touchable {
|
||||
padding-right: 2em;
|
||||
padding-left: 2em;
|
||||
margin-right: -2em;
|
||||
margin-left: -2em;
|
||||
/* padding-right: 2em;
|
||||
padding-left: 2em; */
|
||||
margin-right: -1em;
|
||||
margin-left: -1em;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user