fixed pytests
This commit is contained in:
parent
8c3195937b
commit
595de0c5a1
@ -3,9 +3,9 @@ from collections import Counter
|
|||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
|
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache, caches
|
||||||
from django.core.cache import caches
|
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value,
|
||||||
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value, When, FilteredRelation)
|
When)
|
||||||
from django.db.models.functions import Coalesce, Lower, Substr
|
from django.db.models.functions import Coalesce, Lower, Substr
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.translation import gettext as _
|
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 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
|
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
||||||
class RecipeSearch():
|
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):
|
def __init__(self, request, **params):
|
||||||
self._request = request
|
self._request = request
|
||||||
@ -45,7 +46,8 @@ class RecipeSearch():
|
|||||||
cache.set(CACHE_KEY, self._search_prefs, timeout=10)
|
cache.set(CACHE_KEY, self._search_prefs, timeout=10)
|
||||||
else:
|
else:
|
||||||
self._search_prefs = SearchPreference()
|
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._rating = self._params.get('rating', None)
|
||||||
self._keywords = {
|
self._keywords = {
|
||||||
'or': self._params.get('keywords_or', None) or self._params.get('keywords', None),
|
'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._random = str2bool(self._params.get('random', False))
|
||||||
self._new = str2bool(self._params.get('new', False))
|
self._new = str2bool(self._params.get('new', False))
|
||||||
self._num_recent = int(self._params.get('num_recent', 0))
|
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._timescooked = self._params.get('timescooked', None)
|
||||||
self._cookedon = self._params.get('cookedon', None)
|
self._cookedon = self._params.get('cookedon', None)
|
||||||
self._createdon = self._params.get('createdon', None)
|
self._createdon = self._params.get('createdon', None)
|
||||||
@ -95,18 +98,24 @@ class RecipeSearch():
|
|||||||
self._search_type = self._search_prefs.search or 'plain'
|
self._search_type = self._search_prefs.search or 'plain'
|
||||||
if self._string:
|
if self._string:
|
||||||
if self._postgres:
|
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:
|
else:
|
||||||
self._unaccent_include = []
|
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._icontains_include = [
|
||||||
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)]
|
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._trigram_include = None
|
||||||
self._fulltext_include = None
|
self._fulltext_include = None
|
||||||
self._trigram = False
|
self._trigram = False
|
||||||
if self._postgres and self._string:
|
if self._postgres and self._string:
|
||||||
self._language = DICTIONARY.get(translation.get_language(), 'simple')
|
self._language = DICTIONARY.get(
|
||||||
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)]
|
translation.get_language(), 'simple')
|
||||||
self._fulltext_include = self._search_prefs.fulltext.values_list('field', flat=True) or None
|
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:
|
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
|
||||||
self._trigram = True
|
self._trigram = True
|
||||||
@ -182,8 +191,10 @@ class RecipeSearch():
|
|||||||
# otherwise sort by the remaining order_by attributes or favorite by default
|
# otherwise sort by the remaining order_by attributes or favorite by default
|
||||||
else:
|
else:
|
||||||
order += default_order
|
order += default_order
|
||||||
order[:] = [Lower('name').asc() if x == 'name' else x for x in order]
|
order[:] = [Lower('name').asc() if x ==
|
||||||
order[:] = [Lower('name').desc() if x == '-name' else x for x in order]
|
'name' else x for x in order]
|
||||||
|
order[:] = [Lower('name').desc() if x ==
|
||||||
|
'-name' else x for x in order]
|
||||||
self.orderby = order
|
self.orderby = order
|
||||||
|
|
||||||
def string_filters(self, string=None):
|
def string_filters(self, string=None):
|
||||||
@ -200,21 +211,28 @@ class RecipeSearch():
|
|||||||
for f in self._filters:
|
for f in self._filters:
|
||||||
query_filter |= f
|
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._fulltext_include:
|
||||||
if self._fuzzy_match is None:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
else:
|
||||||
query_filter = Q()
|
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)]:
|
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):
|
def _cooked_on_filter(self, cooked_date=None):
|
||||||
if self._sort_includes('lastcooked') or cooked_date:
|
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:
|
if lessthan:
|
||||||
default = timezone.now() - timedelta(days=100000)
|
default = timezone.now() - timedelta(days=100000)
|
||||||
else:
|
else:
|
||||||
@ -233,32 +252,41 @@ class RecipeSearch():
|
|||||||
if cooked_date is None:
|
if cooked_date is None:
|
||||||
return
|
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:
|
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:
|
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):
|
def _created_on_filter(self, created_date=None):
|
||||||
if created_date is None:
|
if created_date is None:
|
||||||
return
|
return
|
||||||
lessthan = '-' in created_date[:1]
|
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:
|
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:
|
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):
|
def _updated_on_filter(self, updated_date=None):
|
||||||
if updated_date is None:
|
if updated_date is None:
|
||||||
return
|
return
|
||||||
lessthan = '-' in updated_date[:1]
|
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:
|
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:
|
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):
|
def _viewed_on_filter(self, viewed_date=None):
|
||||||
if self._sort_includes('lastviewed') or viewed_date:
|
if self._sort_includes('lastviewed') or viewed_date:
|
||||||
@ -268,12 +296,15 @@ class RecipeSearch():
|
|||||||
if viewed_date is None:
|
if viewed_date is None:
|
||||||
return
|
return
|
||||||
lessthan = '-' in viewed_date[:1]
|
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:
|
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:
|
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):
|
def _new_recipes(self, new_days=7):
|
||||||
# TODO make new days a user-setting
|
# 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(
|
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]
|
'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):
|
def _favorite_recipes(self, times_cooked=None):
|
||||||
if self._sort_includes('favorite') or times_cooked:
|
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:
|
if less_than:
|
||||||
default = 1000
|
default = 1000
|
||||||
else:
|
else:
|
||||||
default = 0
|
default = 0
|
||||||
favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk')
|
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')
|
).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:
|
if times_cooked is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if times_cooked == '0':
|
if times_cooked == '0':
|
||||||
self._queryset = self._queryset.filter(favorite=0)
|
self._queryset = self._queryset.filter(favorite=0)
|
||||||
elif less_than:
|
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:
|
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):
|
def keyword_filters(self, **kwargs):
|
||||||
if all([kwargs[x] is None for x in kwargs]):
|
if all([kwargs[x] is None for x in kwargs]):
|
||||||
@ -346,7 +382,8 @@ class RecipeSearch():
|
|||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.filter(f_and)
|
self._queryset = self._queryset.filter(f_and)
|
||||||
if 'not' in kw_filter:
|
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):
|
def food_filters(self, **kwargs):
|
||||||
if all([kwargs[x] is None for x in 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])
|
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
|
||||||
if 'or' in fd_filter:
|
if 'or' in fd_filter:
|
||||||
if self._include_children:
|
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:
|
else:
|
||||||
f_or = Q(steps__ingredients__food__in=foods)
|
f_or = Q(steps__ingredients__food__in=foods)
|
||||||
|
|
||||||
@ -372,7 +410,8 @@ class RecipeSearch():
|
|||||||
recipes = Recipe.objects.all()
|
recipes = Recipe.objects.all()
|
||||||
for food in foods:
|
for food in foods:
|
||||||
if self._include_children:
|
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:
|
else:
|
||||||
f_and = Q(steps__ingredients__food=food)
|
f_and = Q(steps__ingredients__food=food)
|
||||||
if 'not' in fd_filter:
|
if 'not' in fd_filter:
|
||||||
@ -380,7 +419,8 @@ class RecipeSearch():
|
|||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.filter(f_and)
|
self._queryset = self._queryset.filter(f_and)
|
||||||
if 'not' in fd_filter:
|
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):
|
def unit_filters(self, units=None, operator=True):
|
||||||
if operator != True:
|
if operator != True:
|
||||||
@ -389,7 +429,8 @@ class RecipeSearch():
|
|||||||
return
|
return
|
||||||
if not isinstance(units, list):
|
if not isinstance(units, list):
|
||||||
units = [units]
|
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):
|
def rating_filter(self, rating=None):
|
||||||
if rating or self._sort_includes('rating'):
|
if rating or self._sort_includes('rating'):
|
||||||
@ -399,14 +440,16 @@ class RecipeSearch():
|
|||||||
else:
|
else:
|
||||||
default = 0
|
default = 0
|
||||||
# TODO make ratings a settings user-only vs all-users
|
# 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:
|
if rating is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if rating == '0':
|
if rating == '0':
|
||||||
self._queryset = self._queryset.filter(rating=0)
|
self._queryset = self._queryset.filter(rating=0)
|
||||||
elif lessthan:
|
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:
|
else:
|
||||||
self._queryset = self._queryset.filter(rating__gte=int(rating))
|
self._queryset = self._queryset.filter(rating__gte=int(rating))
|
||||||
|
|
||||||
@ -434,11 +477,14 @@ class RecipeSearch():
|
|||||||
recipes = Recipe.objects.all()
|
recipes = Recipe.objects.all()
|
||||||
for book in kwargs[bk_filter]:
|
for book in kwargs[bk_filter]:
|
||||||
if 'not' in bk_filter:
|
if 'not' in bk_filter:
|
||||||
recipes = recipes.filter(recipebookentry__book__id=book)
|
recipes = recipes.filter(
|
||||||
|
recipebookentry__book__id=book)
|
||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.filter(recipebookentry__book__id=book)
|
self._queryset = self._queryset.filter(
|
||||||
|
recipebookentry__book__id=book)
|
||||||
if 'not' in bk_filter:
|
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):
|
def step_filters(self, steps=None, operator=True):
|
||||||
if operator != True:
|
if operator != True:
|
||||||
@ -446,7 +492,7 @@ class RecipeSearch():
|
|||||||
if not steps:
|
if not steps:
|
||||||
return
|
return
|
||||||
if not isinstance(steps, list):
|
if not isinstance(steps, list):
|
||||||
steps = [unistepsts]
|
steps = [steps]
|
||||||
self._queryset = self._queryset.filter(steps__id__in=steps)
|
self._queryset = self._queryset.filter(steps__id__in=steps)
|
||||||
|
|
||||||
def build_fulltext_filters(self, string=None):
|
def build_fulltext_filters(self, string=None):
|
||||||
@ -457,20 +503,25 @@ class RecipeSearch():
|
|||||||
rank = []
|
rank = []
|
||||||
if 'name' in self._fulltext_include:
|
if 'name' in self._fulltext_include:
|
||||||
vectors.append('name_search_vector')
|
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:
|
if 'description' in self._fulltext_include:
|
||||||
vectors.append('desc_search_vector')
|
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:
|
if 'steps__instruction' in self._fulltext_include:
|
||||||
vectors.append('steps__search_vector')
|
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:
|
if 'keywords__name' in self._fulltext_include:
|
||||||
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
|
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
|
||||||
vectors.append('keywords__name__unaccent')
|
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:
|
if 'steps__ingredients__food__name' in self._fulltext_include:
|
||||||
vectors.append('steps__ingredients__food__name__unaccent')
|
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:
|
for r in rank:
|
||||||
if self.search_rank is None:
|
if self.search_rank is None:
|
||||||
@ -478,7 +529,8 @@ class RecipeSearch():
|
|||||||
else:
|
else:
|
||||||
self.search_rank += r
|
self.search_rank += r
|
||||||
# modifying queryset will annotation creates duplicate results
|
# 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):
|
def build_text_filters(self, string=None):
|
||||||
if not string:
|
if not string:
|
||||||
@ -510,23 +562,30 @@ class RecipeSearch():
|
|||||||
def _makenow_filter(self, missing=None):
|
def _makenow_filter(self, missing=None):
|
||||||
if missing is None or (type(missing) == bool and missing == False):
|
if missing is None or (type(missing) == bool and missing == False):
|
||||||
return
|
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 = (
|
onhand_filter = (
|
||||||
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
|
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
|
# 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.__children_substitute_filter(shopping_users))
|
||||||
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
||||||
)
|
)
|
||||||
makenow_recipes = Recipe.objects.annotate(
|
makenow_recipes = Recipe.objects.annotate(
|
||||||
count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
|
count_food=Count('steps__ingredients__food__pk', filter=Q(
|
||||||
count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
|
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,
|
count_ignore_shopping=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True,
|
||||||
steps__ingredients__food__recipe__isnull=True), distinct=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_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(
|
||||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)), default=Value(0))
|
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)
|
).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
|
@staticmethod
|
||||||
def __children_substitute_filter(shopping_users=None):
|
def __children_substitute_filter(shopping_users=None):
|
||||||
@ -547,7 +606,8 @@ class RecipeSearch():
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def __sibling_substitute_filter(shopping_users=None):
|
def __sibling_substitute_filter(shopping_users=None):
|
||||||
sibling_onhand_subquery = Food.objects.filter(
|
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'),
|
depth=OuterRef('depth'),
|
||||||
onhand_users__in=shopping_users
|
onhand_users__in=shopping_users
|
||||||
)
|
)
|
||||||
@ -586,7 +646,8 @@ class RecipeFacet():
|
|||||||
self.Recent = self._cache.get('Recent', None)
|
self.Recent = self._cache.get('Recent', None)
|
||||||
|
|
||||||
if self._queryset is not 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 = {
|
self._search_params = {
|
||||||
'keyword_list': self._request.query_params.getlist('keywords', []),
|
'keyword_list': self._request.query_params.getlist('keywords', []),
|
||||||
'food_list': self._request.query_params.getlist('foods', []),
|
'food_list': self._request.query_params.getlist('foods', []),
|
||||||
@ -618,7 +679,8 @@ class RecipeFacet():
|
|||||||
'Books': self.Books
|
'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):
|
def get_facets(self, from_cache=False):
|
||||||
if from_cache:
|
if from_cache:
|
||||||
@ -655,13 +717,16 @@ class RecipeFacet():
|
|||||||
def get_keywords(self):
|
def get_keywords(self):
|
||||||
if self.Keywords is None:
|
if self.Keywords is None:
|
||||||
if self._search_params['search_keywords_or']:
|
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:
|
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
|
# set keywords to root objects only
|
||||||
keywords = self._keyword_queryset(keywords)
|
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)
|
self.set_cache('Keywords', self.Keywords)
|
||||||
return self.Keywords
|
return self.Keywords
|
||||||
|
|
||||||
@ -669,28 +734,28 @@ class RecipeFacet():
|
|||||||
if self.Foods is None:
|
if self.Foods is None:
|
||||||
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
||||||
if self._search_params['search_foods_or']:
|
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:
|
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
|
# set keywords to root objects only
|
||||||
foods = self._food_queryset(foods)
|
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)
|
self.set_cache('Foods', self.Foods)
|
||||||
return self.Foods
|
return self.Foods
|
||||||
|
|
||||||
def get_books(self):
|
|
||||||
if self.Books is None:
|
|
||||||
self.Books = []
|
|
||||||
return self.Books
|
|
||||||
|
|
||||||
def get_ratings(self):
|
def get_ratings(self):
|
||||||
if self.Ratings is None:
|
if self.Ratings is None:
|
||||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
if not self._request.space.demo and self._request.space.show_facet_count:
|
||||||
if self._queryset is None:
|
if self._queryset is None:
|
||||||
self._queryset = Recipe.objects.filter(id__in=self._recipe_list)
|
self._queryset = Recipe.objects.filter(
|
||||||
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
|
id__in=self._recipe_list)
|
||||||
|
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(
|
||||||
|
cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
|
||||||
self.Ratings = dict(Counter(r.rating for r in rating_qs))
|
self.Ratings = dict(Counter(r.rating for r in rating_qs))
|
||||||
else:
|
else:
|
||||||
self.Rating = {}
|
self.Rating = {}
|
||||||
@ -715,10 +780,13 @@ class RecipeFacet():
|
|||||||
foods = self._food_queryset(food.get_children(), food)
|
foods = self._food_queryset(food.get_children(), food)
|
||||||
deep_search = self.Foods
|
deep_search = self.Foods
|
||||||
for node in nodes:
|
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']
|
deep_search = deep_search[index]['children']
|
||||||
index = next((i for i, x in enumerate(deep_search) if x["id"] == food.id), None)
|
index = next((i for i, x in enumerate(
|
||||||
deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
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)
|
self.set_cache('Foods', self.Foods)
|
||||||
return self.get_facets()
|
return self.get_facets()
|
||||||
|
|
||||||
@ -731,10 +799,13 @@ class RecipeFacet():
|
|||||||
keywords = self._keyword_queryset(keyword.get_children(), keyword)
|
keywords = self._keyword_queryset(keyword.get_children(), keyword)
|
||||||
deep_search = self.Keywords
|
deep_search = self.Keywords
|
||||||
for node in nodes:
|
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']
|
deep_search = deep_search[index]['children']
|
||||||
index = next((i for i, x in enumerate(deep_search) if x["id"] == keyword.id), None)
|
index = next((i for i, x in enumerate(deep_search)
|
||||||
deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
|
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)
|
self.set_cache('Keywords', self.Keywords)
|
||||||
return self.get_facets()
|
return self.get_facets()
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from django.urls import reverse
|
|||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from pytest_factoryboy import LazyFixture, register
|
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,
|
from cookbook.tests.factories import (FoodFactory, IngredientFactory, ShoppingListEntryFactory,
|
||||||
SupermarketCategoryFactory)
|
SupermarketCategoryFactory)
|
||||||
|
|
||||||
@ -433,6 +433,8 @@ 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):
|
def test_merge_errors(u1_s1, obj_tree_1, obj_3, space_1):
|
||||||
with scope(space=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()
|
parent = obj_tree_1.get_parent()
|
||||||
|
|
||||||
# attempt to merge with non-existent parent
|
# attempt to merge with non-existent parent
|
||||||
|
@ -5,12 +5,11 @@ import uuid
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.contrib.auth.models import Group, User
|
|
||||||
from django_scopes import scopes_disabled
|
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.models import Food, Ingredient, Recipe, Step, Unit
|
||||||
from cookbook.tests.factories import FoodFactory, SpaceFactory, UserFactory
|
from cookbook.tests.factories import SpaceFactory, UserFactory
|
||||||
|
|
||||||
register(SpaceFactory, 'space_1')
|
register(SpaceFactory, 'space_1')
|
||||||
register(SpaceFactory, 'space_2')
|
register(SpaceFactory, 'space_2')
|
||||||
@ -60,8 +59,10 @@ def get_random_recipe(space_1, u1_s1):
|
|||||||
internal=True,
|
internal=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
s1 = Step.objects.create(name=str(uuid.uuid4()), instruction=str(uuid.uuid4()), space=space_1, )
|
s1 = Step.objects.create(name=str(uuid.uuid4()),
|
||||||
s2 = Step.objects.create(name=str(uuid.uuid4()), instruction=str(uuid.uuid4()), space=space_1, )
|
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(s1)
|
||||||
r.steps.add(s2)
|
r.steps.add(s2)
|
||||||
@ -70,8 +71,10 @@ def get_random_recipe(space_1, u1_s1):
|
|||||||
s1.ingredients.add(
|
s1.ingredients.add(
|
||||||
Ingredient.objects.create(
|
Ingredient.objects.create(
|
||||||
amount=1,
|
amount=1,
|
||||||
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0],
|
food=Food.objects.get_or_create(
|
||||||
unit=Unit.objects.create(name=str(uuid.uuid4()), space=space_1, ),
|
name=str(uuid.uuid4()), space=space_1)[0],
|
||||||
|
unit=Unit.objects.create(
|
||||||
|
name=str(uuid.uuid4()), space=space_1, ),
|
||||||
note=str(uuid.uuid4()),
|
note=str(uuid.uuid4()),
|
||||||
space=space_1,
|
space=space_1,
|
||||||
)
|
)
|
||||||
@ -80,8 +83,10 @@ def get_random_recipe(space_1, u1_s1):
|
|||||||
s2.ingredients.add(
|
s2.ingredients.add(
|
||||||
Ingredient.objects.create(
|
Ingredient.objects.create(
|
||||||
amount=1,
|
amount=1,
|
||||||
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0],
|
food=Food.objects.get_or_create(
|
||||||
unit=Unit.objects.create(name=str(uuid.uuid4()), space=space_1, ),
|
name=str(uuid.uuid4()), space=space_1)[0],
|
||||||
|
unit=Unit.objects.create(
|
||||||
|
name=str(uuid.uuid4()), space=space_1, ),
|
||||||
note=str(uuid.uuid4()),
|
note=str(uuid.uuid4()),
|
||||||
space=space_1,
|
space=space_1,
|
||||||
)
|
)
|
||||||
@ -99,8 +104,10 @@ def get_random_json_recipe():
|
|||||||
{
|
{
|
||||||
"instruction": str(uuid.uuid4()),
|
"instruction": str(uuid.uuid4()),
|
||||||
"ingredients": [
|
"ingredients": [
|
||||||
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(uuid.uuid4())}, "amount": random.randint(0, 10)},
|
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(
|
||||||
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(uuid.uuid4())}, "amount": random.randint(0, 10)},
|
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 key in expected_lists:
|
||||||
for k in expected_lists[key]:
|
for k in expected_lists[key]:
|
||||||
try:
|
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]])
|
assert any([dict_compare(k, i) for i in target_lists[key]])
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
for result in [dict_compare(k, i, details=True) for i in target_lists[key]]:
|
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
|
added = d1_keys - d2_keys
|
||||||
removed = d2_keys - d1_keys
|
removed = d2_keys - d1_keys
|
||||||
modified = {o: (d1[o], d2[o]) for o in not_dicts if d1[o] != d2[o]}
|
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:
|
if details:
|
||||||
return added, removed, modified, modified_dicts
|
return added, removed, modified, modified_dicts
|
||||||
else:
|
else:
|
||||||
@ -173,12 +182,12 @@ def transpose(text, number=2):
|
|||||||
positions = random.sample(range(len(tokens[token_pos])), number)
|
positions = random.sample(range(len(tokens[token_pos])), number)
|
||||||
|
|
||||||
# swap the positions
|
# swap the positions
|
||||||
l = list(tokens[token_pos])
|
lt = list(tokens[token_pos])
|
||||||
for first, second in zip(positions[::2], positions[1::2]):
|
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
|
# replace original tokens with swapped
|
||||||
tokens[token_pos] = ''.join(l)
|
tokens[token_pos] = ''.join(lt)
|
||||||
|
|
||||||
# return text with the swapped token
|
# return text with the swapped token
|
||||||
return ' '.join(tokens)
|
return ' '.join(tokens)
|
||||||
|
@ -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 child substitute
|
||||||
# TODO returns recipes with all ingredients via sibling 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
|
@pytest.fixture
|
||||||
def recipes(space_1):
|
def recipes(space_1):
|
||||||
@ -19,7 +24,8 @@ def recipes(space_1):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def makenow_recipe(request, space_1):
|
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)
|
recipe = RecipeFactory.create(space=space_1)
|
||||||
for food in Food.objects.filter(ingredient__step__recipe=recipe.id):
|
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})()
|
request = type('', (object,), {'space': space_1, 'user': user1})()
|
||||||
search = RecipeSearch(request, makenow='true')
|
search = RecipeSearch(request, makenow='true')
|
||||||
with scope(space=space_1):
|
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()
|
food.onhand_users.clear()
|
||||||
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
||||||
food.ignore_shopping = True
|
food.ignore_shopping = True
|
||||||
food.save()
|
food.save()
|
||||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
assert Food.objects.filter(
|
||||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, ignore_shopping=True).count() == 1
|
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())
|
search = search.get_queryset(Recipe.objects.all())
|
||||||
assert search.count() == 1
|
assert search.count() == 1
|
||||||
assert search.first().id == makenow_recipe.id
|
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})()
|
request = type('', (object,), {'space': space_1, 'user': user1})()
|
||||||
search = RecipeSearch(request, makenow='true')
|
search = RecipeSearch(request, makenow='true')
|
||||||
with scope(space=space_1):
|
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()
|
onhand_user = food.onhand_users.first()
|
||||||
food.onhand_users.clear()
|
food.onhand_users.clear()
|
||||||
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
||||||
food.substitute.add(FoodFactory.create(space=space_1, onhand_users=[onhand_user]))
|
food.substitute.add(FoodFactory.create(
|
||||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
space=space_1, onhand_users=[onhand_user]))
|
||||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, substitute__isnull=False).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, substitute__isnull=False).count() == 1
|
||||||
|
|
||||||
search = search.get_queryset(Recipe.objects.all())
|
search = search.get_queryset(Recipe.objects.all())
|
||||||
assert search.count() == 1
|
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})()
|
request = type('', (object,), {'space': space_1, 'user': user1})()
|
||||||
search = RecipeSearch(request, makenow='true')
|
search = RecipeSearch(request, makenow='true')
|
||||||
with scope(space=space_1):
|
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()
|
onhand_user = food.onhand_users.first()
|
||||||
food.onhand_users.clear()
|
food.onhand_users.clear()
|
||||||
food.substitute_children = True
|
food.substitute_children = True
|
||||||
food.save()
|
food.save()
|
||||||
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
||||||
new_food = FoodFactory.create(space=space_1, onhand_users=[onhand_user])
|
new_food = FoodFactory.create(
|
||||||
new_food.move(food, 'first-child')
|
space=space_1, onhand_users=[onhand_user])
|
||||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
new_food.move(food, node_location)
|
||||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, numchild__gt=0).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, numchild__gt=0).count() == 1
|
||||||
search = search.get_queryset(Recipe.objects.all())
|
search = search.get_queryset(Recipe.objects.all())
|
||||||
assert search.count() == 1
|
assert search.count() == 1
|
||||||
assert search.first().id == makenow_recipe.id
|
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})()
|
request = type('', (object,), {'space': space_1, 'user': user1})()
|
||||||
search = RecipeSearch(request, makenow='true')
|
search = RecipeSearch(request, makenow='true')
|
||||||
with scope(space=space_1):
|
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()
|
onhand_user = food.onhand_users.first()
|
||||||
food.onhand_users.clear()
|
food.onhand_users.clear()
|
||||||
food.substitute_siblings = True
|
food.substitute_siblings = True
|
||||||
food.save()
|
food.save()
|
||||||
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
assert search.get_queryset(Recipe.objects.all()).count() == 0
|
||||||
new_parent = FoodFactory.create(space=space_1)
|
new_parent = FoodFactory.create(space=space_1)
|
||||||
new_sibling = FoodFactory.create(space=space_1, onhand_users=[onhand_user])
|
new_sibling = FoodFactory.create(
|
||||||
new_sibling.move(new_parent, 'first-child')
|
space=space_1, onhand_users=[onhand_user])
|
||||||
food.move(new_parent, 'first-child')
|
new_sibling.move(new_parent, node_location)
|
||||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
|
food.move(new_parent, node_location)
|
||||||
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, depth=2).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, depth=2).count() == 1
|
||||||
search = search.get_queryset(Recipe.objects.all())
|
search = search.get_queryset(Recipe.objects.all())
|
||||||
assert search.count() == 1
|
assert search.count() == 1
|
||||||
assert search.first().id == makenow_recipe.id
|
assert search.first().id == makenow_recipe.id
|
||||||
|
@ -7,9 +7,9 @@ from django.conf import settings
|
|||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
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.conftest import transpose
|
||||||
from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFactory,
|
from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFactory,
|
||||||
KeywordFactory, RecipeBookEntryFactory, RecipeFactory,
|
KeywordFactory, RecipeBookEntryFactory, RecipeFactory,
|
||||||
@ -23,7 +23,8 @@ from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFac
|
|||||||
# TODO makenow with above filters
|
# TODO makenow with above filters
|
||||||
# TODO test search food/keywords including/excluding children
|
# TODO test search food/keywords including/excluding children
|
||||||
LIST_URL = 'api:recipe-list'
|
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
|
@pytest.fixture
|
||||||
@ -50,26 +51,43 @@ def user1(request, space_1, u1_s1, unaccent):
|
|||||||
if params.get('fuzzy_lookups', False):
|
if params.get('fuzzy_lookups', False):
|
||||||
user.searchpreference.lookup = True
|
user.searchpreference.lookup = True
|
||||||
misspelled_result = 1
|
misspelled_result = 1
|
||||||
|
else:
|
||||||
|
user.searchpreference.lookup = False
|
||||||
|
|
||||||
if params.get('fuzzy_search', False):
|
if params.get('fuzzy_search', False):
|
||||||
user.searchpreference.trigram.set(SearchFields.objects.all())
|
user.searchpreference.trigram.set(SearchFields.objects.all())
|
||||||
misspelled_result = 1
|
misspelled_result = 1
|
||||||
|
else:
|
||||||
|
user.searchpreference.trigram.set([])
|
||||||
|
|
||||||
if params.get('icontains', False):
|
if params.get('icontains', False):
|
||||||
user.searchpreference.icontains.set(SearchFields.objects.all())
|
user.searchpreference.icontains.set(SearchFields.objects.all())
|
||||||
search_term = 'ghijklmn'
|
search_term = 'ghijklmn'
|
||||||
|
else:
|
||||||
|
user.searchpreference.icontains.set([])
|
||||||
|
|
||||||
if params.get('istartswith', False):
|
if params.get('istartswith', False):
|
||||||
user.searchpreference.istartswith.set(SearchFields.objects.all())
|
user.searchpreference.istartswith.set(SearchFields.objects.all())
|
||||||
search_term = 'abcdef'
|
search_term = 'abcdef'
|
||||||
|
else:
|
||||||
|
user.searchpreference.istartswith.set([])
|
||||||
|
|
||||||
if params.get('unaccent', False):
|
if params.get('unaccent', False):
|
||||||
user.searchpreference.unaccent.set(SearchFields.objects.all())
|
user.searchpreference.unaccent.set(SearchFields.objects.all())
|
||||||
misspelled_result *= 2
|
misspelled_result *= 2
|
||||||
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
|
# full text vectors are hard coded to use unaccent - put this after unaccent to override result
|
||||||
if params.get('fulltext', False):
|
if params.get('fulltext', False):
|
||||||
user.searchpreference.fulltext.set(SearchFields.objects.all())
|
user.searchpreference.fulltext.set(SearchFields.objects.all())
|
||||||
# user.searchpreference.search = 'websearch'
|
# user.searchpreference.search = 'websearch'
|
||||||
search_term = 'ghijklmn uvwxyz'
|
search_term = 'ghijklmn uvwxyz'
|
||||||
result = 2
|
result = 2
|
||||||
|
else:
|
||||||
|
user.searchpreference.fulltext.set([])
|
||||||
|
|
||||||
user.searchpreference.save()
|
user.searchpreference.save()
|
||||||
misspelled_term = transpose(search_term, number=3)
|
misspelled_term = transpose(search_term, number=3)
|
||||||
return (u1_s1, result, misspelled_result, search_term, misspelled_term, params)
|
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)
|
obj2 = FoodFactory.create(name=accent, space=space_1)
|
||||||
recipe1.steps.first().ingredients.add(IngredientFactory.create(food=obj1))
|
recipe1.steps.first().ingredients.add(IngredientFactory.create(food=obj1))
|
||||||
recipe2.steps.first().ingredients.add(IngredientFactory.create(food=obj2))
|
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):
|
if request.param.get('keyword', None):
|
||||||
obj1 = KeywordFactory.create(name=unaccent, space=space_1)
|
obj1 = KeywordFactory.create(name=unaccent, space=space_1)
|
||||||
obj2 = KeywordFactory.create(name=accent, 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)
|
obj2 = UnitFactory.create(name=accent, space=space_1)
|
||||||
recipe1.steps.first().ingredients.add(IngredientFactory.create(unit=obj1))
|
recipe1.steps.first().ingredients.add(IngredientFactory.create(unit=obj1))
|
||||||
recipe2.steps.first().ingredients.add(IngredientFactory.create(unit=obj2))
|
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):
|
if request.param.get('name', None):
|
||||||
recipe1.name = unaccent
|
recipe1.name = unaccent
|
||||||
recipe2.name = accent
|
recipe2.name = accent
|
||||||
@ -145,21 +165,32 @@ def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1):
|
|||||||
i2.save()
|
i2.save()
|
||||||
|
|
||||||
if request.param.get('viewedon', None):
|
if request.param.get('viewedon', None):
|
||||||
ViewLogFactory.create(recipe=recipe1, created_by=user1, created_at=days_3, space=space_1)
|
ViewLogFactory.create(recipe=recipe1, created_by=user1,
|
||||||
ViewLogFactory.create(recipe=recipe2, created_by=user1, created_at=days_30, space=space_1)
|
created_at=days_3, space=space_1)
|
||||||
ViewLogFactory.create(recipe=recipe3, created_by=user2, created_at=days_15, 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):
|
if request.param.get('cookedon', None):
|
||||||
CookLogFactory.create(recipe=recipe1, created_by=user1, created_at=days_3, space=space_1)
|
CookLogFactory.create(recipe=recipe1, created_by=user1,
|
||||||
CookLogFactory.create(recipe=recipe2, created_by=user1, created_at=days_30, space=space_1)
|
created_at=days_3, space=space_1)
|
||||||
CookLogFactory.create(recipe=recipe3, created_by=user2, created_at=days_15, 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):
|
if request.param.get('timescooked', None):
|
||||||
CookLogFactory.create_batch(5, recipe=recipe1, created_by=user1, 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(recipe=recipe2, created_by=user1, space=space_1)
|
||||||
CookLogFactory.create_batch(3, recipe=recipe3, created_by=user2, space=space_1)
|
CookLogFactory.create_batch(
|
||||||
|
3, recipe=recipe3, created_by=user2, space=space_1)
|
||||||
if request.param.get('rating', None):
|
if request.param.get('rating', None):
|
||||||
CookLogFactory.create(recipe=recipe1, created_by=user1, rating=5.0, space=space_1)
|
CookLogFactory.create(
|
||||||
CookLogFactory.create(recipe=recipe2, created_by=user1, rating=1.0, space=space_1)
|
recipe=recipe1, created_by=user1, rating=5.0, space=space_1)
|
||||||
CookLogFactory.create(recipe=recipe3, created_by=user2, rating=3.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)
|
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[1].id in [x['id'] for x in r['results']]
|
||||||
assert found_recipe[2].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 r['count'] == operator[1]
|
||||||
assert found_recipe[2].id in [x['id'] for x in r['results']]
|
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[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']]
|
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 r['count'] == 10 + operator[2]
|
||||||
assert found_recipe[2].id not in [x['id'] for x in r['results']]
|
assert found_recipe[2].id not in [x['id'] for x in r['results']]
|
||||||
|
|
||||||
@ -227,7 +260,8 @@ 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[1].id in [x['id'] for x in r['results']]
|
||||||
assert found_recipe[2].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 r['count'] == 3
|
||||||
assert found_recipe[2].id in [x['id'] for x in r['results']]
|
assert found_recipe[2].id in [x['id'] for x in r['results']]
|
||||||
|
|
||||||
@ -251,56 +285,64 @@ def test_fuzzy_lookup(found_recipe, recipes, param_type, user1, space_1):
|
|||||||
param1 = f"query={user1[3]}"
|
param1 = f"query={user1[3]}"
|
||||||
param2 = f"query={user1[4]}"
|
param2 = f"query={user1[4]}"
|
||||||
|
|
||||||
r = json.loads(user1[0].get(reverse(list_url) + f'?{param1}&limit=2').content)
|
r = json.loads(user1[0].get(reverse(list_url) +
|
||||||
assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[3].id, found_recipe[4].id]]) == user1[1]
|
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)
|
r = json.loads(user1[0].get(reverse(list_url) +
|
||||||
assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[3].id, found_recipe[4].id]]) == user1[2]
|
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
|
# commenting this out for general use - it is really slow
|
||||||
# it should be run on occasion to ensure everything still works
|
# 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(
|
||||||
|
# [
|
||||||
|
# ('fuzzy_search', True), ('fuzzy_search', False),
|
||||||
|
# ('fulltext', True), ('fulltext', False),
|
||||||
|
# ('icontains', True), ('icontains', False),
|
||||||
|
# ('istartswith', True), ('istartswith', False),
|
||||||
|
# ],
|
||||||
|
# [('unaccent', True), ('unaccent', False)]
|
||||||
|
# ), indirect=['user1'])
|
||||||
|
# @pytest.mark.parametrize("found_recipe", [
|
||||||
|
# ({'name': True}),
|
||||||
|
# ({'description': True}),
|
||||||
|
# ({'instruction': True}),
|
||||||
|
# ({'keyword': True}),
|
||||||
|
# ({'food': True}),
|
||||||
|
# ], indirect=['found_recipe'])
|
||||||
|
# # 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]
|
||||||
|
|
||||||
@pytest.mark.skipif(sqlite and True, reason="requires PostgreSQL")
|
# r = json.loads(user1[0].get(reverse(LIST_URL) + f'?{param2}').content)
|
||||||
@pytest.mark.parametrize("user1", itertools.product(
|
# assert len([x['id'] for x in r['results'] if x['id'] in [
|
||||||
[
|
# found_recipe[0].id, found_recipe[1].id]]) == user1[2]
|
||||||
('fuzzy_search', True), ('fuzzy_search', False),
|
|
||||||
('fulltext', True), ('fulltext', False),
|
|
||||||
('icontains', True), ('icontains', False),
|
|
||||||
('istartswith', True), ('istartswith', False),
|
|
||||||
],
|
|
||||||
[('unaccent', True), ('unaccent', False)]
|
|
||||||
), indirect=['user1'])
|
|
||||||
@pytest.mark.parametrize("found_recipe", [
|
|
||||||
({'name': True}),
|
|
||||||
({'description': True}),
|
|
||||||
({'instruction': True}),
|
|
||||||
({'keyword': True}),
|
|
||||||
({'food': True}),
|
|
||||||
], indirect=['found_recipe'])
|
|
||||||
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]
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("found_recipe, param_type, result", [
|
@pytest.mark.parametrize("found_recipe, param_type, result", [
|
||||||
({'viewedon': True}, 'viewedon', (1, 1)),
|
({'viewedon': True}, 'viewedon', (1, 1)),
|
||||||
({'cookedon': True}, 'cookedon', (1, 1)),
|
({'cookedon': True}, 'cookedon', (1, 1)),
|
||||||
({'createdon': True}, 'createdon', (2, 12)), # created dates are not filtered by user
|
# created dates are not filtered by user
|
||||||
({'createdon': True}, 'updatedon', (2, 12)), # updated 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'])
|
], indirect=['found_recipe'])
|
||||||
def test_search_date(found_recipe, recipes, param_type, result, u1_s1, u2_s1, space_1):
|
def test_search_date(found_recipe, recipes, param_type, result, u1_s1, u2_s1, space_1):
|
||||||
# force updated_at to equal created_at datetime
|
# force updated_at to equal created_at datetime
|
||||||
with scope(space=space_1):
|
with scope(space=space_1):
|
||||||
for recipe in Recipe.objects.all():
|
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")
|
date = (timezone.now() - timedelta(days=15)).strftime("%Y-%m-%d")
|
||||||
param1 = f"?{param_type}={date}"
|
param1 = f"?{param_type}={date}"
|
||||||
@ -323,7 +365,6 @@ 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']]
|
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", [
|
@pytest.mark.parametrize("found_recipe, param_type", [
|
||||||
({'rating': True}, 'rating'),
|
({'rating': True}, 'rating'),
|
||||||
({'timescooked': True}, 'timescooked'),
|
({'timescooked': True}, 'timescooked'),
|
||||||
@ -344,7 +385,8 @@ def test_search_count(found_recipe, recipes, param_type, u1_s1, u2_s1, space_1):
|
|||||||
# test search for not rated/cooked
|
# test search for not rated/cooked
|
||||||
r = json.loads(u1_s1.get(reverse(LIST_URL) + param3).content)
|
r = json.loads(u1_s1.get(reverse(LIST_URL) + param3).content)
|
||||||
assert r['count'] == 11
|
assert r['count'] == 11
|
||||||
assert (found_recipe[0].id or found_recipe[1].id) not in [x['id'] for x in r['results']]
|
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
|
# test matched returns for lte and gte searches
|
||||||
r = json.loads(u2_s1.get(reverse(LIST_URL) + param1).content)
|
r = json.loads(u2_s1.get(reverse(LIST_URL) + param1).content)
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td v-if="detailed">
|
<td v-if="detailed">
|
||||||
<div v-if="ingredient.note">
|
<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">
|
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable p-0">
|
||||||
<i class="far fa-comment"></i>
|
<i class="far fa-comment"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ export default {
|
|||||||
.touchable {
|
.touchable {
|
||||||
padding-right: 2em;
|
padding-right: 2em;
|
||||||
padding-left: 3em;
|
padding-left: 3em;
|
||||||
margin-right: -3em;
|
margin-right: -2em;
|
||||||
margin-left: -2em;
|
margin-left: -3em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
Reference in New Issue
Block a user