more recipe search tests
This commit is contained in:
@ -1,8 +1,8 @@
|
||||
import json
|
||||
from collections import Counter
|
||||
from datetime import timedelta
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.contrib.postgres.search import SearchQuery, SearchRank, TrigramSimilarity
|
||||
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
|
||||
from django.core.cache import caches
|
||||
from django.db.models import Avg, Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When
|
||||
from django.db.models.functions import Coalesce, Lower, Substr
|
||||
@ -13,7 +13,8 @@ from cookbook.filters import RecipeFilter
|
||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
||||
from cookbook.helper.permission_helper import has_group_permission
|
||||
from cookbook.managers import DICTIONARY
|
||||
from cookbook.models import CookLog, CustomFilter, Food, Keyword, Recipe, SearchPreference, ViewLog
|
||||
from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, SearchFields,
|
||||
SearchPreference, ViewLog)
|
||||
from recipes import settings
|
||||
|
||||
|
||||
@ -66,10 +67,12 @@ class RecipeSearch():
|
||||
self._internal = str2bool(self._params.get('internal', None))
|
||||
self._random = str2bool(self._params.get('random', False))
|
||||
self._new = str2bool(self._params.get('new', False))
|
||||
self._last_viewed = int(self._params.get('last_viewed', 0))
|
||||
self._num_recent = int(self._params.get('num_recent', 0))
|
||||
self._include_children = str2bool(self._params.get('include_children', None))
|
||||
self._timescooked = self._params.get('timescooked', None)
|
||||
self._lastcooked = self._params.get('lastcooked', None)
|
||||
self._cookedon = self._params.get('cookedon', None)
|
||||
self._createdon = self._params.get('createdon', None)
|
||||
self._viewedon = self._params.get('viewedon', None)
|
||||
# this supports hidden feature to find recipes missing X ingredients
|
||||
try:
|
||||
self._makenow = int(makenow := self._params.get('makenow', None))
|
||||
@ -81,16 +84,16 @@ class RecipeSearch():
|
||||
|
||||
self._search_type = self._search_prefs.search or 'plain'
|
||||
if self._string:
|
||||
unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
|
||||
self._icontains_include = [x + '__unaccent' if x in unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
|
||||
self._istartswith_include = [x + '__unaccent' if x in unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
|
||||
self._unaccent_include = self._search_prefs.unaccent.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 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)
|
||||
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
|
||||
@ -99,11 +102,7 @@ class RecipeSearch():
|
||||
search_type=self._search_type,
|
||||
config=self._language,
|
||||
)
|
||||
self.search_rank = (
|
||||
SearchRank('name_search_vector', self.search_query, cover_density=True)
|
||||
+ SearchRank('desc_search_vector', self.search_query, cover_density=True)
|
||||
# + SearchRank('steps__search_vector', self.search_query, cover_density=True) # Is a large performance drag
|
||||
)
|
||||
self.search_rank = None
|
||||
self.orderby = []
|
||||
self._default_sort = ['-favorite'] # TODO add user setting
|
||||
self._filters = None
|
||||
@ -112,8 +111,10 @@ class RecipeSearch():
|
||||
def get_queryset(self, queryset):
|
||||
self._queryset = queryset
|
||||
self._build_sort_order()
|
||||
self._recently_viewed(num_recent=self._last_viewed)
|
||||
self._last_cooked(lastcooked=self._lastcooked)
|
||||
self._recently_viewed(num_recent=self._num_recent)
|
||||
self._cooked_on_filter(cooked_date=self._cookedon)
|
||||
self._created_on_filter(created_date=self._createdon)
|
||||
self._viewed_on_filter(viewed_date=self._viewedon)
|
||||
self._favorite_recipes(timescooked=self._timescooked)
|
||||
self._new_recipes()
|
||||
self.keyword_filters(**self._keywords)
|
||||
@ -144,7 +145,7 @@ class RecipeSearch():
|
||||
# TODO add userpreference for default sort order and replace '-favorite'
|
||||
default_order = ['-favorite']
|
||||
# recent and new_recipe are always first; they float a few recipes to the top
|
||||
if self._last_viewed:
|
||||
if self._num_recent:
|
||||
order += ['-recent']
|
||||
if self._new:
|
||||
order += ['-new_recipe']
|
||||
@ -162,7 +163,7 @@ class RecipeSearch():
|
||||
if '-score' in order:
|
||||
order.remove('-score')
|
||||
# if no sort order provided prioritize text search, followed by the default search
|
||||
elif self._postgres and self._string:
|
||||
elif self._postgres and self._string and (self._trigram or self._fulltext_include):
|
||||
order += ['-score', *default_order]
|
||||
# otherwise sort by the remaining order_by attributes or favorite by default
|
||||
else:
|
||||
@ -180,13 +181,11 @@ class RecipeSearch():
|
||||
self.build_fulltext_filters(self._string)
|
||||
self.build_trigram(self._string)
|
||||
|
||||
query_filter = Q()
|
||||
if self._filters:
|
||||
query_filter = None
|
||||
for f in self._filters:
|
||||
if query_filter:
|
||||
query_filter |= f
|
||||
else:
|
||||
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
|
||||
if self._fulltext_include:
|
||||
if self._fuzzy_match is None:
|
||||
@ -197,27 +196,56 @@ class RecipeSearch():
|
||||
if self._fuzzy_match is not None:
|
||||
simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity')
|
||||
if not self._fulltext_include:
|
||||
self._queryset = self._queryset.annotate(score=Coalesce(Subquery(Max(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))
|
||||
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
|
||||
self._queryset = self._queryset.annotate(score=Sum(F('rank')+F('simularity')))
|
||||
else:
|
||||
self._queryset = self._queryset.filter(name__icontains=self._string)
|
||||
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)]:
|
||||
query_filter |= Q(**{"%s" % f: self._string})
|
||||
self._queryset = self._queryset.filter(query_filter).distinct()
|
||||
|
||||
def _last_cooked(self, lastcooked=None):
|
||||
if self._sort_includes('lastcooked') or lastcooked:
|
||||
def _cooked_on_filter(self, cooked_date=None):
|
||||
if self._sort_includes('lastcooked') or cooked_date:
|
||||
longTimeAgo = timezone.now() - timedelta(days=100000)
|
||||
self._queryset = self._queryset.annotate(lastcooked=Coalesce(
|
||||
Max(Case(When(created_by=self._request.user, space=self._request.space, then='cooklog__created_at'))), Value(longTimeAgo)))
|
||||
if lastcooked is None:
|
||||
Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(longTimeAgo)))
|
||||
if cooked_date is None:
|
||||
return
|
||||
lessthan = '-' in lastcooked[:1]
|
||||
lessthan = '-' in cooked_date[:1]
|
||||
cooked_date = date(*[int(x) for x in cooked_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(lastcooked__lte=lastcooked[1:]).exclude(lastcooked=longTimeAgo)
|
||||
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=longTimeAgo)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(lastcooked__gte=lastcooked).exclude(lastcooked=longTimeAgo)
|
||||
self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=longTimeAgo)
|
||||
|
||||
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 != ''])
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(created_at__date__lte=created_date)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(created_at__date__gte=created_date)
|
||||
|
||||
def _viewed_on_filter(self, viewed_date=None):
|
||||
if self._sort_includes('lastviewed') or viewed_date:
|
||||
longTimeAgo = timezone.now() - timedelta(days=100000)
|
||||
self._queryset = self._queryset.annotate(lastviewed=Coalesce(
|
||||
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo)))
|
||||
if viewed_date is None:
|
||||
return
|
||||
lessthan = '-' in viewed_date[:1]
|
||||
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)
|
||||
else:
|
||||
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
|
||||
@ -232,12 +260,12 @@ class RecipeSearch():
|
||||
if not num_recent:
|
||||
if self._sort_includes('lastviewed'):
|
||||
self._queryset = self._queryset.annotate(lastviewed=Coalesce(
|
||||
Max(Case(When(created_by=self._request.user, space=self._request.space, then='viewlog__pk'))), Value(0)))
|
||||
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__pk'))), Value(0)))
|
||||
return
|
||||
|
||||
last_viewed_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]
|
||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=last_viewed_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, timescooked=None):
|
||||
if self._sort_includes('favorite') or timescooked:
|
||||
@ -388,18 +416,32 @@ class RecipeSearch():
|
||||
if not string:
|
||||
return
|
||||
if self._fulltext_include:
|
||||
if not self._filters:
|
||||
self._filters = []
|
||||
vectors = []
|
||||
rank = []
|
||||
if 'name' in self._fulltext_include:
|
||||
self._filters += [Q(name_search_vector=self.search_query)]
|
||||
vectors.append('name_search_vector')
|
||||
rank.append(SearchRank('name_search_vector', self.search_query, cover_density=True))
|
||||
if 'description' in self._fulltext_include:
|
||||
self._filters += [Q(desc_search_vector=self.search_query)]
|
||||
vectors.append('desc_search_vector')
|
||||
rank.append(SearchRank('desc_search_vector', self.search_query, cover_density=True))
|
||||
if 'steps__instruction' in self._fulltext_include:
|
||||
self._filters += [Q(steps__search_vector=self.search_query)]
|
||||
vectors.append('steps__search_vector')
|
||||
rank.append(SearchRank('steps__search_vector', self.search_query, cover_density=True))
|
||||
if 'keywords__name' in self._fulltext_include:
|
||||
self._filters += [Q(keywords__in=Keyword.objects.filter(name__search=self.search_query))]
|
||||
# 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))
|
||||
if 'steps__ingredients__food__name' in self._fulltext_include:
|
||||
self._filters += [Q(steps__ingredients__food__in=Food.objects.filter(name__search=self.search_query))]
|
||||
vectors.append('steps__ingredients__food__name__unaccent')
|
||||
rank.append(SearchRank('steps__ingredients__food__name', self.search_query, cover_density=True))
|
||||
|
||||
for r in rank:
|
||||
if self.search_rank is None:
|
||||
self.search_rank = r
|
||||
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))))
|
||||
|
||||
def build_text_filters(self, string=None):
|
||||
if not string:
|
||||
@ -653,7 +695,7 @@ class RecipeFacet():
|
||||
|
||||
def _recipe_count_queryset(self, field, depth=1, steplen=4):
|
||||
return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space
|
||||
).values(child=Substr(f'{field}__path', 1, steplen)
|
||||
).values(child=Substr(f'{field}__path', 1, steplen*depth)
|
||||
).annotate(count=Count('pk', distinct=True)).values('count')
|
||||
|
||||
def _keyword_queryset(self, queryset, keyword=None):
|
||||
|
@ -1106,7 +1106,7 @@ class SearchPreference(models.Model, PermissionModelMixin):
|
||||
istartswith = models.ManyToManyField(SearchFields, related_name="istartswith_fields", blank=True)
|
||||
trigram = models.ManyToManyField(SearchFields, related_name="trigram_fields", blank=True, default=nameSearchField)
|
||||
fulltext = models.ManyToManyField(SearchFields, related_name="fulltext_fields", blank=True)
|
||||
trigram_threshold = models.DecimalField(default=0.1, decimal_places=2, max_digits=3)
|
||||
trigram_threshold = models.DecimalField(default=0.2, decimal_places=2, max_digits=3)
|
||||
|
||||
|
||||
class UserFile(ExportModelOperationsMixin('user_files'), models.Model, PermissionModelMixin):
|
||||
|
@ -37,6 +37,7 @@ def update_recipe_search_vector(sender, instance=None, created=False, **kwargs):
|
||||
if SQLITE:
|
||||
return
|
||||
language = DICTIONARY.get(translation.get_language(), 'simple')
|
||||
# these indexed fields are space wide, reading user preferences would lead to inconsistent behavior
|
||||
instance.name_search_vector = SearchVector('name__unaccent', weight='A', config=language)
|
||||
instance.desc_search_vector = SearchVector('description__unaccent', weight='C', config=language)
|
||||
try:
|
||||
|
@ -158,6 +158,31 @@ def dict_compare(d1, d2, details=False):
|
||||
return any([not added, not removed, not modified, not modified_dicts])
|
||||
|
||||
|
||||
def transpose(text, number=2):
|
||||
|
||||
# select random token
|
||||
tokens = text.split()
|
||||
positions = list(i for i, e in enumerate(tokens) if len(e) > 1)
|
||||
|
||||
if positions:
|
||||
|
||||
token_pos = random.choice(positions)
|
||||
|
||||
# select random positions in token
|
||||
positions = random.sample(range(len(tokens[token_pos])), number)
|
||||
|
||||
# swap the positions
|
||||
l = list(tokens[token_pos])
|
||||
for first, second in zip(positions[::2], positions[1::2]):
|
||||
l[first], l[second] = l[second], l[first]
|
||||
|
||||
# replace original tokens with swapped
|
||||
tokens[token_pos] = ''.join(l)
|
||||
|
||||
# return text with the swapped token
|
||||
return ' '.join(tokens)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def recipe_1_s1(space_1, u1_s1):
|
||||
return get_random_recipe(space_1, u1_s1)
|
||||
|
@ -277,6 +277,15 @@ class ShoppingListEntryFactory(factory.django.DjangoModelFactory):
|
||||
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
|
||||
created_at = kwargs.pop('created_at', None)
|
||||
obj = super(ShoppingListEntryFactory, cls)._create(target_class, *args, **kwargs)
|
||||
if created_at is not None:
|
||||
obj.created_at = created_at
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
class Params:
|
||||
has_mealplan = False
|
||||
|
||||
@ -324,8 +333,6 @@ class StepFactory(factory.django.DjangoModelFactory):
|
||||
has_recipe = False
|
||||
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))
|
||||
@ -354,6 +361,15 @@ class RecipeFactory(factory.django.DjangoModelFactory):
|
||||
created_at = factory.LazyAttribute(lambda x: faker.date_this_decade())
|
||||
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
|
||||
created_at = kwargs.pop('created_at', None)
|
||||
obj = super(RecipeFactory, cls)._create(target_class, *args, **kwargs)
|
||||
if created_at is not None:
|
||||
obj.created_at = created_at
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
@factory.post_generation
|
||||
def keywords(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
@ -404,3 +420,47 @@ class RecipeFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
class Meta:
|
||||
model = 'cookbook.Recipe'
|
||||
|
||||
|
||||
@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'))
|
||||
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
|
||||
created_at = kwargs.pop('created_at', None)
|
||||
obj = super(CookLogFactory, cls)._create(target_class, *args, **kwargs)
|
||||
if created_at is not None:
|
||||
obj.created_at = created_at
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
model = 'cookbook.CookLog'
|
||||
|
||||
|
||||
@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'))
|
||||
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
|
||||
created_at = kwargs.pop('created_at', None)
|
||||
obj = super(ViewLogFactory, cls)._create(target_class, *args, **kwargs)
|
||||
if created_at is not None:
|
||||
obj.created_at = created_at
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
model = 'cookbook.ViewLog'
|
||||
|
@ -1,58 +1,78 @@
|
||||
import itertools
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
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 cookbook.models import Food, Recipe, SearchFields
|
||||
from cookbook.tests.factories import (FoodFactory, IngredientFactory, KeywordFactory,
|
||||
RecipeBookEntryFactory, RecipeFactory, UnitFactory)
|
||||
|
||||
# TODO recipe name/description/instructions/keyword/book/food test search with icontains, istarts with/ full text(?? probably when word changes based on conjugation??), trigram, unaccent
|
||||
|
||||
from cookbook.tests.conftest import transpose
|
||||
from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFactory,
|
||||
KeywordFactory, RecipeBookEntryFactory, RecipeFactory,
|
||||
UnitFactory, ViewLogFactory)
|
||||
|
||||
# TODO test combining any/all of the above
|
||||
# TODO search rating as user or when another user rated
|
||||
# TODO search last cooked
|
||||
# TODO changing lsat_viewed ## to return on search
|
||||
# TODO test sort_by
|
||||
# TODO test sort_by new X number of recipes are new within last Y days
|
||||
# TODO test loading custom filter
|
||||
# TODO test loading custom filter with overrided params
|
||||
# TODO makenow with above filters
|
||||
# TODO test search for number of times cooked (self vs others)
|
||||
# TODO test including children
|
||||
# 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']
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accent():
|
||||
return "àèìòù"
|
||||
return "àbçđêf ğĦìĵķĽmñ öPqŕşŧ úvŵxyž"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unaccent():
|
||||
return "aeiou"
|
||||
return "abcdef ghijklmn opqrst uvwxyz"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user1(request, space_1, u1_s1):
|
||||
def user1(request, space_1, u1_s1, unaccent):
|
||||
user = auth.get_user(u1_s1)
|
||||
params = {x[0]: x[1] for x in request.param}
|
||||
try:
|
||||
params = {x[0]: x[1] for x in request.param}
|
||||
except AttributeError:
|
||||
params = {}
|
||||
result = 1
|
||||
misspelled_result = 0
|
||||
search_term = unaccent
|
||||
|
||||
if params.get('fuzzy_lookups', False):
|
||||
user.searchpreference.lookup = True
|
||||
misspelled_result = 1
|
||||
if params.get('fuzzy_search', False):
|
||||
user.searchpreference.trigram.set(SearchFields.objects.all())
|
||||
misspelled_result = 1
|
||||
|
||||
if params.get('icontains', False):
|
||||
user.searchpreference.icontains.set(SearchFields.objects.all())
|
||||
search_term = 'ghijklmn'
|
||||
if params.get('istartswith', False):
|
||||
user.searchpreference.istartswith.set(SearchFields.objects.all())
|
||||
search_term = 'abcdef'
|
||||
if params.get('unaccent', False):
|
||||
user.searchpreference.unaccent.set(SearchFields.objects.all())
|
||||
misspelled_result *= 2
|
||||
result *= 2
|
||||
# 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:
|
||||
result = 1
|
||||
|
||||
user.userpreference.save()
|
||||
return (u1_s1, result, params)
|
||||
user.searchpreference.save()
|
||||
misspelled_term = transpose(search_term, number=3)
|
||||
return (u1_s1, result, misspelled_result, search_term, misspelled_term, params)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -61,15 +81,21 @@ def recipes(space_1):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def found_recipe(request, space_1, accent, unaccent):
|
||||
def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1):
|
||||
user1 = auth.get_user(u1_s1)
|
||||
user2 = auth.get_user(u2_s1)
|
||||
days_3 = timezone.now() - timedelta(days=3)
|
||||
days_15 = timezone.now() - timedelta(days=15)
|
||||
days_30 = timezone.now() - timedelta(days=30)
|
||||
recipe1 = RecipeFactory.create(space=space_1)
|
||||
recipe2 = RecipeFactory.create(space=space_1)
|
||||
recipe3 = RecipeFactory.create(space=space_1)
|
||||
obj1 = None
|
||||
obj2 = None
|
||||
|
||||
if request.param.get('food', None):
|
||||
obj1 = FoodFactory.create(name=unaccent, space=space_1)
|
||||
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))
|
||||
@ -79,6 +105,10 @@ def found_recipe(request, space_1, accent, unaccent):
|
||||
recipe1.keywords.add(obj1)
|
||||
recipe2.keywords.add(obj2)
|
||||
recipe3.keywords.add(obj1, obj2)
|
||||
recipe1.name = unaccent
|
||||
recipe2.name = accent
|
||||
recipe1.save()
|
||||
recipe2.save()
|
||||
if request.param.get('book', None):
|
||||
obj1 = RecipeBookEntryFactory.create(recipe=recipe1).book
|
||||
obj2 = RecipeBookEntryFactory.create(recipe=recipe2).book
|
||||
@ -87,12 +117,51 @@ def found_recipe(request, space_1, accent, unaccent):
|
||||
if request.param.get('unit', None):
|
||||
obj1 = UnitFactory.create(name=unaccent, space=space_1)
|
||||
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))
|
||||
if request.param.get('name', None):
|
||||
recipe1.name = unaccent
|
||||
recipe2.name = accent
|
||||
recipe1.save()
|
||||
recipe2.save()
|
||||
if request.param.get('description', None):
|
||||
recipe1.description = unaccent
|
||||
recipe2.description = accent
|
||||
recipe1.save()
|
||||
recipe2.save()
|
||||
if request.param.get('instruction', None):
|
||||
i1 = recipe1.steps.first()
|
||||
i2 = recipe2.steps.first()
|
||||
i1.instruction = unaccent
|
||||
i2.instruction = accent
|
||||
i1.save()
|
||||
i2.save()
|
||||
if request.param.get('createdon', None):
|
||||
recipe1.created_at = days_3
|
||||
recipe2.created_at = days_30
|
||||
recipe3.created_at = days_15
|
||||
recipe1.save()
|
||||
recipe2.save()
|
||||
recipe3.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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
return (recipe1, recipe2, recipe3, obj1, obj2)
|
||||
return (recipe1, recipe2, recipe3, obj1, obj2, request.param)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("found_recipe, param_type", [
|
||||
@ -101,7 +170,7 @@ def found_recipe(request, space_1, accent, unaccent):
|
||||
({'book': True}, 'books'),
|
||||
], indirect=['found_recipe'])
|
||||
@pytest.mark.parametrize('operator', [('_or', 3, 0), ('_and', 1, 2), ])
|
||||
def test_search_or_and_not(found_recipe, param_type, operator, recipes, user1, space_1):
|
||||
def test_search_or_and_not(found_recipe, param_type, operator, recipes, u1_s1, space_1):
|
||||
with scope(space=space_1):
|
||||
param1 = f"{param_type}{operator[0]}={found_recipe[3].id}"
|
||||
param2 = f"{param_type}{operator[0]}={found_recipe[4].id}"
|
||||
@ -163,9 +232,12 @@ def test_search_units(found_recipe, recipes, u1_s1, space_1):
|
||||
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(
|
||||
[('fuzzy_lookups', True), ('fuzzy_lookups', False)],
|
||||
[('fuzzy_search', True), ('fuzzy_search', False)],
|
||||
[
|
||||
('fuzzy_search', True), ('fuzzy_search', False),
|
||||
('fuzzy_lookups', True), ('fuzzy_lookups', False)
|
||||
],
|
||||
[('unaccent', True), ('unaccent', False)]
|
||||
), indirect=['user1'])
|
||||
@pytest.mark.parametrize("found_recipe, param_type", [
|
||||
@ -176,12 +248,99 @@ def test_search_units(found_recipe, recipes, u1_s1, 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 = "query=aeiou"
|
||||
param2 = "query=aoieu"
|
||||
param1 = f"query={user1[3]}"
|
||||
param2 = f"query={user1[4]}"
|
||||
|
||||
# test fuzzy off - also need search settings on/off
|
||||
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}').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]
|
||||
|
||||
|
||||
@pytest.mark.skipif(sqlite, 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'])
|
||||
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", [
|
||||
({'viewedon': True}, 'viewedon', (1, 1)),
|
||||
({'cookedon': True}, 'cookedon', (1, 1)),
|
||||
({'createdon': True}, 'createdon', (2, 12)), # created dates are not filtered by user
|
||||
], indirect=['found_recipe'])
|
||||
def test_search_date(found_recipe, recipes, param_type, result, u1_s1, u2_s1, space_1):
|
||||
date = (timezone.now() - timedelta(days=15)).strftime("%Y-%m-%d")
|
||||
param1 = f"?{param_type}={date}"
|
||||
param2 = f"?{param_type}=-{date}"
|
||||
r = json.loads(u1_s1.get(reverse(LIST_URL) + f'{param1}').content)
|
||||
assert r['count'] == result[0]
|
||||
assert found_recipe[0].id in [x['id'] for x in r['results']]
|
||||
|
||||
r = json.loads(u1_s1.get(reverse(LIST_URL) + f'{param2}').content)
|
||||
assert r['count'] == result[1]
|
||||
assert found_recipe[1].id in [x['id'] for x in r['results']]
|
||||
|
||||
# test today's date returns for lte and gte searches
|
||||
r = json.loads(u2_s1.get(reverse(LIST_URL) + f'{param1}').content)
|
||||
assert r['count'] == result[0]
|
||||
assert found_recipe[2].id in [x['id'] for x in r['results']]
|
||||
|
||||
r = json.loads(u2_s1.get(reverse(LIST_URL) + f'{param2}').content)
|
||||
assert r['count'] == result[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']]
|
||||
|
@ -652,7 +652,9 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
QueryParam(name='random', description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='new', description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='timescooked', description=_('Filter recipes cooked X times or more. Negative values returns cooked less than X times'), qtype='int'),
|
||||
QueryParam(name='lastcooked', description=_('Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='cookedon', description=_('Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='createdon', description=_('Filter recipes created on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='viewedon', description=_('Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='makenow', description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']')),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
@ -361,17 +361,18 @@ def user_settings(request):
|
||||
sp.istartswith.clear()
|
||||
sp.trigram.set([SearchFields.objects.get(name='Name')])
|
||||
sp.fulltext.clear()
|
||||
sp.trigram_threshold = 0.1
|
||||
sp.trigram_threshold = 0.2
|
||||
|
||||
if search_form.cleaned_data['preset'] == 'precise':
|
||||
sp.search = SearchPreference.WEB
|
||||
sp.lookup = True
|
||||
sp.unaccent.set(SearchFields.objects.all())
|
||||
sp.icontains.clear()
|
||||
# full text on food is very slow, add search_vector field and index it (including Admin functions and postsave signal to rebuild index)
|
||||
sp.icontains.set([SearchFields.objects.get(name__in=['Name', 'Ingredients'])])
|
||||
sp.istartswith.set([SearchFields.objects.get(name='Name')])
|
||||
sp.trigram.clear()
|
||||
sp.fulltext.set(SearchFields.objects.all())
|
||||
sp.trigram_threshold = 0.1
|
||||
sp.fulltext.set(SearchFields.objects.filter(name__in=['Ingredients']))
|
||||
sp.trigram_threshold = 0.2
|
||||
|
||||
sp.save()
|
||||
elif 'shopping_form' in request.POST:
|
||||
|
@ -122,7 +122,7 @@
|
||||
<b-form-checkbox switch v-model="ui.show_makenow" id="popover-show_makenow" size="sm"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-if="ui.enable_expert" v-bind:label="$t('last_cooked')" label-for="popover-show_sortby" label-cols="8" class="mb-1">
|
||||
<b-form-checkbox switch v-model="ui.show_lastcooked" id="popover-show_lastcooked" size="sm"></b-form-checkbox>
|
||||
<b-form-checkbox switch v-model="ui.show_cookedon" id="popover-show_cookedon" size="sm"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
|
||||
@ -393,7 +393,7 @@
|
||||
</div>
|
||||
|
||||
<!-- special switches -->
|
||||
<div class="row g-0" v-if="ui.show_timescooked || ui.show_makenow || ui.show_lastcooked">
|
||||
<div class="row g-0" v-if="ui.show_timescooked || ui.show_makenow || ui.show_cookedon">
|
||||
<div class="col-12">
|
||||
<b-input-group class="mt-2">
|
||||
<!-- times cooked -->
|
||||
@ -410,9 +410,9 @@
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
<!-- date cooked -->
|
||||
<b-input-group-append v-if="ui.show_lastcooked">
|
||||
<b-input-group-append v-if="ui.show_cookedon">
|
||||
<b-form-datepicker
|
||||
v-model="search.lastcooked"
|
||||
v-model="search.cookedon"
|
||||
:max="yesterday"
|
||||
no-highlight-today
|
||||
reset-button
|
||||
@ -422,8 +422,8 @@
|
||||
@input="refreshData(false)"
|
||||
/>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="search.lastcooked_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
|
||||
<span class="text-uppercase" v-if="search.lastcooked_gte">>=</span>
|
||||
<b-form-checkbox v-model="search.cookedon_gte" name="check-button" @change="refreshData(false)" class="shadow-none" switch style="width: 4em">
|
||||
<span class="text-uppercase" v-if="search.cookedon_gte">>=</span>
|
||||
<span class="text-uppercase" v-else><=</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
@ -563,8 +563,12 @@ export default {
|
||||
timescooked: undefined,
|
||||
timescooked_gte: true,
|
||||
makenow: false,
|
||||
lastcooked: undefined,
|
||||
lastcooked_gte: true,
|
||||
cookedon: undefined,
|
||||
cookedon_gte: true,
|
||||
createdon: undefined,
|
||||
createdon_gte: true,
|
||||
viewedon: undefined,
|
||||
viewedon_gte: true,
|
||||
sort_order: [],
|
||||
pagination_page: 1,
|
||||
expert_mode: false,
|
||||
@ -594,7 +598,7 @@ export default {
|
||||
show_sortby: false,
|
||||
show_timescooked: false,
|
||||
show_makenow: false,
|
||||
show_lastcooked: false,
|
||||
show_cookedon: false,
|
||||
include_children: true,
|
||||
},
|
||||
pagination_count: 0,
|
||||
@ -681,7 +685,7 @@ export default {
|
||||
const field = [
|
||||
[this.$t("search_rank"), "score"],
|
||||
[this.$t("Name"), "name"],
|
||||
[this.$t("last_cooked"), "lastcooked"],
|
||||
[this.$t("last_cooked"), "cookedon"],
|
||||
[this.$t("Rating"), "rating"],
|
||||
[this.$t("times_cooked"), "favorite"],
|
||||
[this.$t("date_created"), "created_at"],
|
||||
@ -876,7 +880,7 @@ export default {
|
||||
this.search.pagination_page = 1
|
||||
this.search.timescooked = undefined
|
||||
this.search.makenow = false
|
||||
this.search.lastcooked = undefined
|
||||
this.search.cookedon = undefined
|
||||
|
||||
let fieldnum = {
|
||||
keywords: 1,
|
||||
@ -976,9 +980,17 @@ export default {
|
||||
if (rating !== undefined && !this.search.search_rating_gte) {
|
||||
rating = rating * -1
|
||||
}
|
||||
let lastcooked = this.search.lastcooked || undefined
|
||||
if (lastcooked !== undefined && !this.search.lastcooked_gte) {
|
||||
lastcooked = "-" + lastcooked
|
||||
let cookedon = this.search.cookedon || undefined
|
||||
if (cookedon !== undefined && !this.search.cookedon_gte) {
|
||||
cookedon = "-" + cookedon
|
||||
}
|
||||
let createdon = this.search.createdon || undefined
|
||||
if (createdon !== undefined && !this.search.createdon_gte) {
|
||||
createdon = "-" + createdon
|
||||
}
|
||||
let viewedon = this.search.viewedon || undefined
|
||||
if (viewedon !== undefined && !this.search.viewedon_gte) {
|
||||
viewedon = "-" + viewedon
|
||||
}
|
||||
let timescooked = parseInt(this.search.timescooked)
|
||||
if (isNaN(timescooked)) {
|
||||
@ -999,7 +1011,9 @@ export default {
|
||||
random: this.random_search,
|
||||
timescooked: timescooked,
|
||||
makenow: this.search.makenow || undefined,
|
||||
lastcooked: lastcooked,
|
||||
cookedon: cookedon,
|
||||
createdon: createdon,
|
||||
viewedon: viewedon,
|
||||
page: this.search.pagination_page,
|
||||
pageSize: this.ui.page_size,
|
||||
}
|
||||
@ -1009,7 +1023,7 @@ export default {
|
||||
include_children: this.ui.include_children,
|
||||
}
|
||||
if (!this.searchFiltered()) {
|
||||
params.options.query.last_viewed = this.ui.recently_viewed
|
||||
params.options.query.num_recent = this.ui.recently_viewed //TODO refactor as num_recent
|
||||
params._new = this.ui.sort_by_new
|
||||
}
|
||||
if (this.search.search_filter) {
|
||||
@ -1030,12 +1044,12 @@ export default {
|
||||
this.search?.search_rating !== undefined ||
|
||||
(this.search.timescooked !== undefined && this.search.timescooked !== "") ||
|
||||
this.search.makenow !== false ||
|
||||
(this.search.lastcooked !== undefined && this.search.lastcooked !== "")
|
||||
(this.search.cookedon !== undefined && this.search.cookedon !== "")
|
||||
|
||||
if (ignore_string) {
|
||||
return filtered
|
||||
} else {
|
||||
return filtered || this.search?.search_input != "" || this.search.sort_order.length <= 1
|
||||
return filtered || this.search?.search_input != "" || this.search.sort_order.length >= 1
|
||||
}
|
||||
},
|
||||
addFields(field) {
|
||||
|
@ -530,7 +530,9 @@ export class Models {
|
||||
"random",
|
||||
"_new",
|
||||
"timescooked",
|
||||
"lastcooked",
|
||||
"cookedon",
|
||||
"createdon",
|
||||
"viewedon",
|
||||
"makenow",
|
||||
"page",
|
||||
"pageSize",
|
||||
|
Reference in New Issue
Block a user