Merge pull request #2431 from smilerz/pytest_fixes

Pytest fixes
This commit is contained in:
vabene1111 2023-04-25 15:59:15 +02:00 committed by GitHub
commit 7830ddd4e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 647 additions and 377 deletions

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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']]

View File

@ -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

View File

@ -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

View File

@ -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>