remove facets and treeselect
This commit is contained in:
parent
d33b0d2254
commit
9ee4be621b
@ -45,8 +45,7 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
model = UserPreference
|
||||
fields = (
|
||||
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
|
||||
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed',
|
||||
'show_step_ingredients',
|
||||
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed', 'show_step_ingredients',
|
||||
)
|
||||
|
||||
labels = {
|
||||
@ -67,21 +66,19 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
|
||||
help_texts = {
|
||||
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
|
||||
|
||||
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
|
||||
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
|
||||
'use_fractions': _(
|
||||
'Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'),
|
||||
|
||||
'use_kj': _('Display nutritional energy amounts in joules instead of calories'),
|
||||
'use_kj': _('Display nutritional energy amounts in joules instead of calories'),
|
||||
'plan_share': _('Users with whom newly created meal plans should be shared by default.'),
|
||||
'shopping_share': _('Users with whom to share shopping lists.'),
|
||||
'ingredient_decimals': _('Number of decimals to round ingredients.'),
|
||||
'comments': _('If you want to be able to create and see comments underneath recipes.'),
|
||||
'ingredient_decimals': _('Number of decimals to round ingredients.'),
|
||||
'comments': _('If you want to be able to create and see comments underneath recipes.'),
|
||||
'shopping_auto_sync': _(
|
||||
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
|
||||
'of mobile data. If lower than instance limit it is reset when saving.'
|
||||
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
|
||||
'of mobile data. If lower than instance limit it is reset when saving.'
|
||||
),
|
||||
'sticky_navbar': _('Makes the navbar stick to the top of the page.'),
|
||||
'sticky_navbar': _('Makes the navbar stick to the top of the page.'),
|
||||
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
|
||||
'mealplan_autoexclude_onhand': _('Exclude ingredients that are on hand.'),
|
||||
'left_handed': _('Will optimize the UI for use with your left hand.'),
|
||||
@ -187,6 +184,7 @@ class MultipleFileField(forms.FileField):
|
||||
result = single_file_clean(data, initial)
|
||||
return result
|
||||
|
||||
|
||||
class ImportForm(ImportExportBase):
|
||||
files = MultipleFileField(required=True)
|
||||
duplicates = forms.BooleanField(help_text=_(
|
||||
@ -352,9 +350,8 @@ class MealPlanForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
help_texts = {
|
||||
'shared': _('You can list default users to share recipes with in the settings.'),
|
||||
'shared': _('You can list default users to share recipes with in the settings.'),
|
||||
'note': _('You can use markdown to format this field. See the <a href="/docs/markdown/">docs here</a>')
|
||||
|
||||
}
|
||||
|
||||
widgets = {
|
||||
@ -509,8 +506,8 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
||||
help_texts = {
|
||||
'shopping_share': _('Users will see all items you add to your shopping list. They must add you to see items on their list.'),
|
||||
'shopping_auto_sync': _(
|
||||
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
|
||||
'of mobile data. If lower than instance limit it is reset when saving.'
|
||||
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
|
||||
'of mobile data. If lower than instance limit it is reset when saving.'
|
||||
),
|
||||
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
|
||||
'mealplan_autoinclude_related': _('When adding a meal plan to the shopping list (manually or automatically), include all related recipes.'),
|
||||
@ -554,11 +551,10 @@ class SpacePreferenceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Space
|
||||
|
||||
fields = ('food_inherit', 'reset_food_inherit', 'show_facet_count', 'use_plural')
|
||||
fields = ('food_inherit', 'reset_food_inherit', 'use_plural')
|
||||
|
||||
help_texts = {
|
||||
'food_inherit': _('Fields on food that should be inherited by default.'),
|
||||
'show_facet_count': _('Show recipe counts on search filters'),
|
||||
'use_plural': _('Use the plural form for units and food inside this space.'),
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,11 @@
|
||||
import json
|
||||
from collections import Counter
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
|
||||
from django.core.cache import cache, caches
|
||||
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value,
|
||||
When)
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Avg, Case, Count, Exists, F, Max, OuterRef, Q, Subquery, Value, When
|
||||
from django.db.models.functions import Coalesce, Lower, Substr
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
||||
from cookbook.managers import DICTIONARY
|
||||
@ -20,15 +17,17 @@ from recipes import settings
|
||||
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
|
||||
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
||||
class RecipeSearch():
|
||||
_postgres = settings.DATABASES['default']['ENGINE'] in [
|
||||
'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
|
||||
_postgres = settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
|
||||
|
||||
def __init__(self, request, **params):
|
||||
self._request = request
|
||||
self._queryset = None
|
||||
if f := params.get('filter', None):
|
||||
custom_filter = CustomFilter.objects.filter(id=f, space=self._request.space).filter(Q(created_by=self._request.user) |
|
||||
Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first()
|
||||
custom_filter = (
|
||||
CustomFilter.objects.filter(id=f, space=self._request.space)
|
||||
.filter(Q(created_by=self._request.user) | Q(shared=self._request.user) | Q(recipebook__shared=self._request.user))
|
||||
.first()
|
||||
)
|
||||
if custom_filter:
|
||||
self._params = {**json.loads(custom_filter.search)}
|
||||
self._original_params = {**(params or {})}
|
||||
@ -101,24 +100,18 @@ class RecipeSearch():
|
||||
self._search_type = self._search_prefs.search or 'plain'
|
||||
if self._string:
|
||||
if self._postgres:
|
||||
self._unaccent_include = self._search_prefs.unaccent.values_list(
|
||||
'field', flat=True)
|
||||
self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
|
||||
else:
|
||||
self._unaccent_include = []
|
||||
self._icontains_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
|
||||
self._istartswith_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
|
||||
self._icontains_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
|
||||
self._istartswith_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
|
||||
self._trigram_include = None
|
||||
self._fulltext_include = None
|
||||
self._trigram = False
|
||||
if self._postgres and self._string:
|
||||
self._language = DICTIONARY.get(
|
||||
translation.get_language(), 'simple')
|
||||
self._trigram_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
|
||||
self._fulltext_include = self._search_prefs.fulltext.values_list(
|
||||
'field', flat=True) or None
|
||||
self._language = DICTIONARY.get(translation.get_language(), 'simple')
|
||||
self._trigram_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
|
||||
self._fulltext_include = self._search_prefs.fulltext.values_list('field', flat=True) or None
|
||||
|
||||
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
|
||||
self._trigram = True
|
||||
@ -178,7 +171,6 @@ class RecipeSearch():
|
||||
|
||||
# if a sort order is provided by user - use that order
|
||||
if self._sort_order:
|
||||
|
||||
if not isinstance(self._sort_order, list):
|
||||
order += [self._sort_order]
|
||||
else:
|
||||
@ -218,24 +210,18 @@ class RecipeSearch():
|
||||
self._queryset = self._queryset.filter(query_filter).distinct()
|
||||
if self._fulltext_include:
|
||||
if self._fuzzy_match is None:
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=Coalesce(Max(self.search_rank), 0.0))
|
||||
self._queryset = self._queryset.annotate(score=Coalesce(Max(self.search_rank), 0.0))
|
||||
else:
|
||||
self._queryset = self._queryset.annotate(
|
||||
rank=Coalesce(Max(self.search_rank), 0.0))
|
||||
self._queryset = self._queryset.annotate(rank=Coalesce(Max(self.search_rank), 0.0))
|
||||
|
||||
if self._fuzzy_match is not None:
|
||||
simularity = self._fuzzy_match.filter(
|
||||
pk=OuterRef('pk')).values('simularity')
|
||||
simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity')
|
||||
if not self._fulltext_include:
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=Coalesce(Subquery(simularity), 0.0))
|
||||
self._queryset = self._queryset.annotate(score=Coalesce(Subquery(simularity), 0.0))
|
||||
else:
|
||||
self._queryset = self._queryset.annotate(
|
||||
simularity=Coalesce(Subquery(simularity), 0.0))
|
||||
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
|
||||
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=F('rank') + F('simularity'))
|
||||
self._queryset = self._queryset.annotate(score=F('rank') + F('simularity'))
|
||||
else:
|
||||
query_filter = Q()
|
||||
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
|
||||
@ -244,78 +230,69 @@ class RecipeSearch():
|
||||
|
||||
def _cooked_on_filter(self, cooked_date=None):
|
||||
if self._sort_includes('lastcooked') or cooked_date:
|
||||
lessthan = self._sort_includes(
|
||||
'-lastcooked') or '-' in (cooked_date or [])[:1]
|
||||
lessthan = self._sort_includes('-lastcooked') or '-' in (cooked_date or [])[:1]
|
||||
if lessthan:
|
||||
default = timezone.now() - timedelta(days=100000)
|
||||
else:
|
||||
default = timezone.now()
|
||||
self._queryset = self._queryset.annotate(lastcooked=Coalesce(
|
||||
Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default)))
|
||||
self._queryset = self._queryset.annotate(
|
||||
lastcooked=Coalesce(Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default))
|
||||
)
|
||||
if cooked_date is None:
|
||||
return
|
||||
|
||||
cooked_date = date(*[int(x)
|
||||
for x in cooked_date.split('-') if x != ''])
|
||||
cooked_date = date(*[int(x)for x in cooked_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
||||
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
|
||||
self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
|
||||
|
||||
def _created_on_filter(self, created_date=None):
|
||||
if created_date is None:
|
||||
return
|
||||
lessthan = '-' in created_date[:1]
|
||||
created_date = date(*[int(x)
|
||||
for x in created_date.split('-') if x != ''])
|
||||
created_date = date(*[int(x) for x in created_date.split('-') if x != ''])
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
created_at__date__lte=created_date)
|
||||
self._queryset = self._queryset.filter(created_at__date__lte=created_date)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
created_at__date__gte=created_date)
|
||||
self._queryset = self._queryset.filter(created_at__date__gte=created_date)
|
||||
|
||||
def _updated_on_filter(self, updated_date=None):
|
||||
if updated_date is None:
|
||||
return
|
||||
lessthan = '-' in updated_date[:1]
|
||||
updated_date = date(*[int(x)
|
||||
for x in updated_date.split('-') if x != ''])
|
||||
updated_date = date(*[int(x)for x in updated_date.split('-') if x != ''])
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
updated_at__date__lte=updated_date)
|
||||
self._queryset = self._queryset.filter(updated_at__date__lte=updated_date)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
updated_at__date__gte=updated_date)
|
||||
self._queryset = self._queryset.filter(updated_at__date__gte=updated_date)
|
||||
|
||||
def _viewed_on_filter(self, viewed_date=None):
|
||||
if self._sort_includes('lastviewed') or viewed_date:
|
||||
longTimeAgo = timezone.now() - timedelta(days=100000)
|
||||
self._queryset = self._queryset.annotate(lastviewed=Coalesce(
|
||||
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo)))
|
||||
self._queryset = self._queryset.annotate(
|
||||
lastviewed=Coalesce(Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo))
|
||||
)
|
||||
if viewed_date is None:
|
||||
return
|
||||
lessthan = '-' in viewed_date[:1]
|
||||
viewed_date = date(*[int(x)
|
||||
for x in viewed_date.split('-') if x != ''])
|
||||
viewed_date = date(*[int(x)for x in viewed_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
self._queryset = self._queryset.filter(lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
|
||||
def _new_recipes(self, new_days=7):
|
||||
# TODO make new days a user-setting
|
||||
if not self._new:
|
||||
return
|
||||
self._queryset = (
|
||||
self._queryset.annotate(new_recipe=Case(
|
||||
When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')), default=Value(0), ))
|
||||
self._queryset = self._queryset.annotate(
|
||||
new_recipe=Case(
|
||||
When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')),
|
||||
default=Value(0),
|
||||
)
|
||||
)
|
||||
|
||||
def _recently_viewed(self, num_recent=None):
|
||||
@ -325,34 +302,35 @@ class RecipeSearch():
|
||||
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__pk'))), Value(0)))
|
||||
return
|
||||
|
||||
num_recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space).values(
|
||||
'recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
|
||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(
|
||||
pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
||||
num_recent_recipes = (
|
||||
ViewLog.objects.filter(created_by=self._request.user, space=self._request.space)
|
||||
.values('recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
|
||||
)
|
||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
||||
|
||||
def _favorite_recipes(self, times_cooked=None):
|
||||
if self._sort_includes('favorite') or times_cooked:
|
||||
less_than = '-' in (times_cooked or []
|
||||
) and not self._sort_includes('-favorite')
|
||||
less_than = '-' in (times_cooked or []) and not self._sort_includes('-favorite')
|
||||
if less_than:
|
||||
default = 1000
|
||||
else:
|
||||
default = 0
|
||||
favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk')
|
||||
).values('recipe').annotate(count=Count('pk', distinct=True)).values('count')
|
||||
self._queryset = self._queryset.annotate(
|
||||
favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
favorite_recipes = (
|
||||
CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk'))
|
||||
.values('recipe')
|
||||
.annotate(count=Count('pk', distinct=True))
|
||||
.values('count')
|
||||
)
|
||||
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
if times_cooked is None:
|
||||
return
|
||||
|
||||
if times_cooked == '0':
|
||||
self._queryset = self._queryset.filter(favorite=0)
|
||||
elif less_than:
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(
|
||||
times_cooked.replace('-', ''))).exclude(favorite=0)
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(times_cooked.replace('-', ''))).exclude(favorite=0)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
favorite__gte=int(times_cooked))
|
||||
self._queryset = self._queryset.filter(favorite__gte=int(times_cooked))
|
||||
|
||||
def keyword_filters(self, **kwargs):
|
||||
if all([kwargs[x] is None for x in kwargs]):
|
||||
@ -385,8 +363,7 @@ class RecipeSearch():
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_and)
|
||||
if 'not' in kw_filter:
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
|
||||
def food_filters(self, **kwargs):
|
||||
if all([kwargs[x] is None for x in kwargs]):
|
||||
@ -400,8 +377,7 @@ class RecipeSearch():
|
||||
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
|
||||
if 'or' in fd_filter:
|
||||
if self._include_children:
|
||||
f_or = Q(
|
||||
steps__ingredients__food__in=Food.include_descendants(foods))
|
||||
f_or = Q(steps__ingredients__food__in=Food.include_descendants(foods))
|
||||
else:
|
||||
f_or = Q(steps__ingredients__food__in=foods)
|
||||
|
||||
@ -413,8 +389,7 @@ class RecipeSearch():
|
||||
recipes = Recipe.objects.all()
|
||||
for food in foods:
|
||||
if self._include_children:
|
||||
f_and = Q(
|
||||
steps__ingredients__food__in=food.get_descendants_and_self())
|
||||
f_and = Q(steps__ingredients__food__in=food.get_descendants_and_self())
|
||||
else:
|
||||
f_and = Q(steps__ingredients__food=food)
|
||||
if 'not' in fd_filter:
|
||||
@ -422,8 +397,7 @@ class RecipeSearch():
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_and)
|
||||
if 'not' in fd_filter:
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
|
||||
def unit_filters(self, units=None, operator=True):
|
||||
if operator != True:
|
||||
@ -432,8 +406,7 @@ class RecipeSearch():
|
||||
return
|
||||
if not isinstance(units, list):
|
||||
units = [units]
|
||||
self._queryset = self._queryset.filter(
|
||||
steps__ingredients__unit__in=units)
|
||||
self._queryset = self._queryset.filter(steps__ingredients__unit__in=units)
|
||||
|
||||
def rating_filter(self, rating=None):
|
||||
if rating or self._sort_includes('rating'):
|
||||
@ -479,14 +452,11 @@ class RecipeSearch():
|
||||
recipes = Recipe.objects.all()
|
||||
for book in kwargs[bk_filter]:
|
||||
if 'not' in bk_filter:
|
||||
recipes = recipes.filter(
|
||||
recipebookentry__book__id=book)
|
||||
recipes = recipes.filter(recipebookentry__book__id=book)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(
|
||||
recipebookentry__book__id=book)
|
||||
self._queryset = self._queryset.filter(recipebookentry__book__id=book)
|
||||
if 'not' in bk_filter:
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
|
||||
def step_filters(self, steps=None, operator=True):
|
||||
if operator != True:
|
||||
@ -505,25 +475,20 @@ class RecipeSearch():
|
||||
rank = []
|
||||
if 'name' in self._fulltext_include:
|
||||
vectors.append('name_search_vector')
|
||||
rank.append(SearchRank('name_search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('name_search_vector', self.search_query, cover_density=True))
|
||||
if 'description' in self._fulltext_include:
|
||||
vectors.append('desc_search_vector')
|
||||
rank.append(SearchRank('desc_search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('desc_search_vector', self.search_query, cover_density=True))
|
||||
if 'steps__instruction' in self._fulltext_include:
|
||||
vectors.append('steps__search_vector')
|
||||
rank.append(SearchRank('steps__search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('steps__search_vector', self.search_query, cover_density=True))
|
||||
if 'keywords__name' in self._fulltext_include:
|
||||
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
|
||||
vectors.append('keywords__name__unaccent')
|
||||
rank.append(SearchRank('keywords__name__unaccent',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('keywords__name__unaccent', self.search_query, cover_density=True))
|
||||
if 'steps__ingredients__food__name' in self._fulltext_include:
|
||||
vectors.append('steps__ingredients__food__name__unaccent')
|
||||
rank.append(SearchRank('steps__ingredients__food__name',
|
||||
self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('steps__ingredients__food__name', self.search_query, cover_density=True))
|
||||
|
||||
for r in rank:
|
||||
if self.search_rank is None:
|
||||
@ -531,8 +496,7 @@ class RecipeSearch():
|
||||
else:
|
||||
self.search_rank += r
|
||||
# modifying queryset will annotation creates duplicate results
|
||||
self._filters.append(Q(id__in=Recipe.objects.annotate(
|
||||
vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
|
||||
self._filters.append(Q(id__in=Recipe.objects.annotate(vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
|
||||
|
||||
def build_text_filters(self, string=None):
|
||||
if not string:
|
||||
@ -557,15 +521,19 @@ class RecipeSearch():
|
||||
trigram += TrigramSimilarity(f, self._string)
|
||||
else:
|
||||
trigram = TrigramSimilarity(f, self._string)
|
||||
self._fuzzy_match = Recipe.objects.annotate(trigram=trigram).distinct(
|
||||
).annotate(simularity=Max('trigram')).values('id', 'simularity').filter(simularity__gt=self._search_prefs.trigram_threshold)
|
||||
self._fuzzy_match = (
|
||||
Recipe.objects.annotate(trigram=trigram)
|
||||
.distinct()
|
||||
.annotate(simularity=Max('trigram'))
|
||||
.values('id', 'simularity')
|
||||
.filter(simularity__gt=self._search_prefs.trigram_threshold)
|
||||
)
|
||||
self._filters += [Q(pk__in=self._fuzzy_match.values('pk'))]
|
||||
|
||||
def _makenow_filter(self, missing=None):
|
||||
if missing is None or (isinstance(missing, bool) and missing == False):
|
||||
return
|
||||
shopping_users = [
|
||||
*self._request.user.get_shopping_share(), self._request.user]
|
||||
shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
|
||||
|
||||
onhand_filter = (
|
||||
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
|
||||
@ -575,264 +543,255 @@ class RecipeSearch():
|
||||
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
||||
)
|
||||
makenow_recipes = Recipe.objects.annotate(
|
||||
count_food=Count('steps__ingredients__food__pk', filter=Q(
|
||||
steps__ingredients__food__isnull=False), distinct=True),
|
||||
count_onhand=Count('steps__ingredients__food__pk',
|
||||
filter=onhand_filter, distinct=True),
|
||||
count_ignore_shopping=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True,
|
||||
steps__ingredients__food__recipe__isnull=True), distinct=True),
|
||||
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(
|
||||
shopping_users), then=Value(1)), default=Value(0)),
|
||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(
|
||||
shopping_users), then=Value(1)), default=Value(0))
|
||||
count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
|
||||
count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
|
||||
count_ignore_shopping=Count(
|
||||
'steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True, steps__ingredients__food__recipe__isnull=True), distinct=True
|
||||
),
|
||||
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users), then=Value(1)), default=Value(0)),
|
||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)), default=Value(0))
|
||||
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood=missing)
|
||||
self._queryset = self._queryset.distinct().filter(
|
||||
id__in=makenow_recipes.values('id'))
|
||||
self._queryset = self._queryset.distinct().filter(id__in=makenow_recipes.values('id'))
|
||||
|
||||
@staticmethod
|
||||
def __children_substitute_filter(shopping_users=None):
|
||||
children_onhand_subquery = Food.objects.filter(
|
||||
path__startswith=OuterRef('path'),
|
||||
depth__gt=OuterRef('depth'),
|
||||
onhand_users__in=shopping_users
|
||||
children_onhand_subquery = Food.objects.filter(path__startswith=OuterRef('path'), depth__gt=OuterRef('depth'), onhand_users__in=shopping_users)
|
||||
return (
|
||||
Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
|
||||
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
|
||||
)
|
||||
.exclude(depth=1, numchild=0)
|
||||
.filter(substitute_children=True)
|
||||
.annotate(child_onhand_count=Exists(children_onhand_subquery))
|
||||
.filter(child_onhand_count=True)
|
||||
)
|
||||
return Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
|
||||
Q(onhand_users__in=shopping_users)
|
||||
| Q(ignore_shopping=True, recipe__isnull=True)
|
||||
| Q(substitute__onhand_users__in=shopping_users)
|
||||
).exclude(depth=1, numchild=0
|
||||
).filter(substitute_children=True
|
||||
).annotate(child_onhand_count=Exists(children_onhand_subquery)
|
||||
).filter(child_onhand_count=True)
|
||||
|
||||
@staticmethod
|
||||
def __sibling_substitute_filter(shopping_users=None):
|
||||
sibling_onhand_subquery = Food.objects.filter(
|
||||
path__startswith=Substr(
|
||||
OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
|
||||
depth=OuterRef('depth'),
|
||||
onhand_users__in=shopping_users
|
||||
path__startswith=Substr(OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)), depth=OuterRef('depth'), onhand_users__in=shopping_users
|
||||
)
|
||||
return Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
|
||||
Q(onhand_users__in=shopping_users)
|
||||
| Q(ignore_shopping=True, recipe__isnull=True)
|
||||
| Q(substitute__onhand_users__in=shopping_users)
|
||||
).exclude(depth=1, numchild=0
|
||||
).filter(substitute_siblings=True
|
||||
).annotate(sibling_onhand=Exists(sibling_onhand_subquery)
|
||||
).filter(sibling_onhand=True)
|
||||
|
||||
|
||||
class RecipeFacet():
|
||||
class CacheEmpty(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, request, queryset=None, hash_key=None, cache_timeout=3600):
|
||||
if hash_key is None and queryset is None:
|
||||
raise ValueError(_("One of queryset or hash_key must be provided"))
|
||||
|
||||
self._request = request
|
||||
self._queryset = queryset
|
||||
self.hash_key = hash_key or str(hash(self._queryset.query))
|
||||
self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}"
|
||||
self._cache_timeout = cache_timeout
|
||||
self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, {})
|
||||
if self._cache is None and self._queryset is None:
|
||||
raise self.CacheEmpty("No queryset provided and cache empty")
|
||||
|
||||
self.Keywords = self._cache.get('Keywords', None)
|
||||
self.Foods = self._cache.get('Foods', None)
|
||||
self.Books = self._cache.get('Books', None)
|
||||
self.Ratings = self._cache.get('Ratings', None)
|
||||
# TODO Move Recent to recipe annotation/serializer: requrires change in RecipeSearch(), RecipeSearchView.vue and serializer
|
||||
self.Recent = self._cache.get('Recent', None)
|
||||
|
||||
if self._queryset is not None:
|
||||
self._recipe_list = list(
|
||||
self._queryset.values_list('id', flat=True))
|
||||
self._search_params = {
|
||||
'keyword_list': self._request.query_params.getlist('keywords', []),
|
||||
'food_list': self._request.query_params.getlist('foods', []),
|
||||
'book_list': self._request.query_params.getlist('book', []),
|
||||
'search_keywords_or': str2bool(self._request.query_params.get('keywords_or', True)),
|
||||
'search_foods_or': str2bool(self._request.query_params.get('foods_or', True)),
|
||||
'search_books_or': str2bool(self._request.query_params.get('books_or', True)),
|
||||
'space': self._request.space,
|
||||
}
|
||||
elif self.hash_key is not None:
|
||||
self._recipe_list = self._cache.get('recipe_list', [])
|
||||
self._search_params = {
|
||||
'keyword_list': self._cache.get('keyword_list', None),
|
||||
'food_list': self._cache.get('food_list', None),
|
||||
'book_list': self._cache.get('book_list', None),
|
||||
'search_keywords_or': self._cache.get('search_keywords_or', None),
|
||||
'search_foods_or': self._cache.get('search_foods_or', None),
|
||||
'search_books_or': self._cache.get('search_books_or', None),
|
||||
'space': self._cache.get('space', None),
|
||||
}
|
||||
|
||||
self._cache = {
|
||||
**self._search_params,
|
||||
'recipe_list': self._recipe_list,
|
||||
'Ratings': self.Ratings,
|
||||
'Recent': self.Recent,
|
||||
'Keywords': self.Keywords,
|
||||
'Foods': self.Foods,
|
||||
'Books': self.Books
|
||||
|
||||
}
|
||||
caches['default'].set(self._SEARCH_CACHE_KEY,
|
||||
self._cache, self._cache_timeout)
|
||||
|
||||
def get_facets(self, from_cache=False):
|
||||
if from_cache:
|
||||
return {
|
||||
'cache_key': self.hash_key or '',
|
||||
'Ratings': self.Ratings or {},
|
||||
'Recent': self.Recent or [],
|
||||
'Keywords': self.Keywords or [],
|
||||
'Foods': self.Foods or [],
|
||||
'Books': self.Books or []
|
||||
}
|
||||
return {
|
||||
'cache_key': self.hash_key,
|
||||
'Ratings': self.get_ratings(),
|
||||
'Recent': self.get_recent(),
|
||||
'Keywords': self.get_keywords(),
|
||||
'Foods': self.get_foods(),
|
||||
'Books': self.get_books()
|
||||
}
|
||||
|
||||
def set_cache(self, key, value):
|
||||
self._cache = {**self._cache, key: value}
|
||||
caches['default'].set(
|
||||
self._SEARCH_CACHE_KEY,
|
||||
self._cache,
|
||||
self._cache_timeout
|
||||
return (
|
||||
Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
|
||||
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
|
||||
)
|
||||
.exclude(depth=1, numchild=0)
|
||||
.filter(substitute_siblings=True)
|
||||
.annotate(sibling_onhand=Exists(sibling_onhand_subquery))
|
||||
.filter(sibling_onhand=True)
|
||||
)
|
||||
|
||||
def get_books(self):
|
||||
if self.Books is None:
|
||||
self.Books = []
|
||||
return self.Books
|
||||
|
||||
def get_keywords(self):
|
||||
if self.Keywords is None:
|
||||
if self._search_params['search_keywords_or']:
|
||||
keywords = Keyword.objects.filter(
|
||||
space=self._request.space).distinct()
|
||||
else:
|
||||
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(
|
||||
depth=1)).filter(space=self._request.space).distinct()
|
||||
# class RecipeFacet():
|
||||
# class CacheEmpty(Exception):
|
||||
# pass
|
||||
|
||||
# set keywords to root objects only
|
||||
keywords = self._keyword_queryset(keywords)
|
||||
self.Keywords = [{**x, 'children': None}
|
||||
if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.set_cache('Keywords', self.Keywords)
|
||||
return self.Keywords
|
||||
# def __init__(self, request, queryset=None, hash_key=None, cache_timeout=3600):
|
||||
# if hash_key is None and queryset is None:
|
||||
# raise ValueError(_("One of queryset or hash_key must be provided"))
|
||||
|
||||
def get_foods(self):
|
||||
if self.Foods is None:
|
||||
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
||||
if self._search_params['search_foods_or']:
|
||||
foods = Food.objects.filter(
|
||||
space=self._request.space).distinct()
|
||||
else:
|
||||
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(
|
||||
depth=1)).filter(space=self._request.space).distinct()
|
||||
# self._request = request
|
||||
# self._queryset = queryset
|
||||
# self.hash_key = hash_key or str(hash(self._queryset.query))
|
||||
# self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}"
|
||||
# self._cache_timeout = cache_timeout
|
||||
# self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, {})
|
||||
# if self._cache is None and self._queryset is None:
|
||||
# raise self.CacheEmpty("No queryset provided and cache empty")
|
||||
|
||||
# set keywords to root objects only
|
||||
foods = self._food_queryset(foods)
|
||||
# self.Keywords = self._cache.get('Keywords', None)
|
||||
# self.Foods = self._cache.get('Foods', None)
|
||||
# self.Books = self._cache.get('Books', None)
|
||||
# self.Ratings = self._cache.get('Ratings', None)
|
||||
# # TODO Move Recent to recipe annotation/serializer: requrires change in RecipeSearch(), RecipeSearchView.vue and serializer
|
||||
# self.Recent = self._cache.get('Recent', None)
|
||||
|
||||
self.Foods = [{**x, 'children': None}
|
||||
if x['numchild'] > 0 else x for x in list(foods)]
|
||||
self.set_cache('Foods', self.Foods)
|
||||
return self.Foods
|
||||
# if self._queryset is not None:
|
||||
# self._recipe_list = list(
|
||||
# self._queryset.values_list('id', flat=True))
|
||||
# self._search_params = {
|
||||
# 'keyword_list': self._request.query_params.getlist('keywords', []),
|
||||
# 'food_list': self._request.query_params.getlist('foods', []),
|
||||
# 'book_list': self._request.query_params.getlist('book', []),
|
||||
# 'search_keywords_or': str2bool(self._request.query_params.get('keywords_or', True)),
|
||||
# 'search_foods_or': str2bool(self._request.query_params.get('foods_or', True)),
|
||||
# 'search_books_or': str2bool(self._request.query_params.get('books_or', True)),
|
||||
# 'space': self._request.space,
|
||||
# }
|
||||
# elif self.hash_key is not None:
|
||||
# self._recipe_list = self._cache.get('recipe_list', [])
|
||||
# self._search_params = {
|
||||
# 'keyword_list': self._cache.get('keyword_list', None),
|
||||
# 'food_list': self._cache.get('food_list', None),
|
||||
# 'book_list': self._cache.get('book_list', None),
|
||||
# 'search_keywords_or': self._cache.get('search_keywords_or', None),
|
||||
# 'search_foods_or': self._cache.get('search_foods_or', None),
|
||||
# 'search_books_or': self._cache.get('search_books_or', None),
|
||||
# 'space': self._cache.get('space', None),
|
||||
# }
|
||||
|
||||
def get_ratings(self):
|
||||
if self.Ratings is None:
|
||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
if self._queryset is None:
|
||||
self._queryset = Recipe.objects.filter(
|
||||
id__in=self._recipe_list)
|
||||
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(
|
||||
cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
|
||||
self.Ratings = dict(Counter(r.rating for r in rating_qs))
|
||||
else:
|
||||
self.Rating = {}
|
||||
self.set_cache('Ratings', self.Ratings)
|
||||
return self.Ratings
|
||||
# self._cache = {
|
||||
# **self._search_params,
|
||||
# 'recipe_list': self._recipe_list,
|
||||
# 'Ratings': self.Ratings,
|
||||
# 'Recent': self.Recent,
|
||||
# 'Keywords': self.Keywords,
|
||||
# 'Foods': self.Foods,
|
||||
# 'Books': self.Books
|
||||
|
||||
def get_recent(self):
|
||||
if self.Recent is None:
|
||||
# TODO make days of recent recipe a setting
|
||||
recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space, created_at__gte=timezone.now() - timedelta(days=14)
|
||||
).values_list('recipe__pk', flat=True)
|
||||
self.Recent = list(recent_recipes)
|
||||
self.set_cache('Recent', self.Recent)
|
||||
return self.Recent
|
||||
# }
|
||||
# caches['default'].set(self._SEARCH_CACHE_KEY,
|
||||
# self._cache, self._cache_timeout)
|
||||
|
||||
def add_food_children(self, id):
|
||||
try:
|
||||
food = Food.objects.get(id=id)
|
||||
nodes = food.get_ancestors()
|
||||
except Food.DoesNotExist:
|
||||
return self.get_facets()
|
||||
foods = self._food_queryset(food.get_children(), food)
|
||||
deep_search = self.Foods
|
||||
for node in nodes:
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == node.id), None)
|
||||
deep_search = deep_search[index]['children']
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == food.id), None)
|
||||
deep_search[index]['children'] = [
|
||||
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
||||
self.set_cache('Foods', self.Foods)
|
||||
return self.get_facets()
|
||||
# def get_facets(self, from_cache=False):
|
||||
# if from_cache:
|
||||
# return {
|
||||
# 'cache_key': self.hash_key or '',
|
||||
# 'Ratings': self.Ratings or {},
|
||||
# 'Recent': self.Recent or [],
|
||||
# 'Keywords': self.Keywords or [],
|
||||
# 'Foods': self.Foods or [],
|
||||
# 'Books': self.Books or []
|
||||
# }
|
||||
# return {
|
||||
# 'cache_key': self.hash_key,
|
||||
# 'Ratings': self.get_ratings(),
|
||||
# 'Recent': self.get_recent(),
|
||||
# 'Keywords': self.get_keywords(),
|
||||
# 'Foods': self.get_foods(),
|
||||
# 'Books': self.get_books()
|
||||
# }
|
||||
|
||||
def add_keyword_children(self, id):
|
||||
try:
|
||||
keyword = Keyword.objects.get(id=id)
|
||||
nodes = keyword.get_ancestors()
|
||||
except Keyword.DoesNotExist:
|
||||
return self.get_facets()
|
||||
keywords = self._keyword_queryset(keyword.get_children(), keyword)
|
||||
deep_search = self.Keywords
|
||||
for node in nodes:
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == node.id), None)
|
||||
deep_search = deep_search[index]['children']
|
||||
index = next((i for i, x in enumerate(deep_search)
|
||||
if x["id"] == keyword.id), None)
|
||||
deep_search[index]['children'] = [
|
||||
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.set_cache('Keywords', self.Keywords)
|
||||
return self.get_facets()
|
||||
# def set_cache(self, key, value):
|
||||
# self._cache = {**self._cache, key: value}
|
||||
# caches['default'].set(
|
||||
# self._SEARCH_CACHE_KEY,
|
||||
# self._cache,
|
||||
# self._cache_timeout
|
||||
# )
|
||||
|
||||
def _recipe_count_queryset(self, field, depth=1, steplen=4):
|
||||
return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path'), f'{field}__depth__gte': depth}, id__in=self._recipe_list, space=self._request.space
|
||||
).annotate(count=Coalesce(Func('pk', function='Count'), 0)).values('count')
|
||||
# def get_books(self):
|
||||
# if self.Books is None:
|
||||
# self.Books = []
|
||||
# return self.Books
|
||||
|
||||
def _keyword_queryset(self, queryset, keyword=None):
|
||||
depth = getattr(keyword, 'depth', 0) + 1
|
||||
steplen = depth * Keyword.steplen
|
||||
# def get_keywords(self):
|
||||
# if self.Keywords is None:
|
||||
# if self._search_params['search_keywords_or']:
|
||||
# keywords = Keyword.objects.filter(
|
||||
# space=self._request.space).distinct()
|
||||
# else:
|
||||
# keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(
|
||||
# depth=1)).filter(space=self._request.space).distinct()
|
||||
|
||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0)
|
||||
).filter(depth=depth, count__gt=0
|
||||
).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
||||
else:
|
||||
return queryset.filter(depth=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
||||
# # set keywords to root objects only
|
||||
# keywords = self._keyword_queryset(keywords)
|
||||
# self.Keywords = [{**x, 'children': None}
|
||||
# if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
# self.set_cache('Keywords', self.Keywords)
|
||||
# return self.Keywords
|
||||
|
||||
def _food_queryset(self, queryset, food=None):
|
||||
depth = getattr(food, 'depth', 0) + 1
|
||||
steplen = depth * Food.steplen
|
||||
# def get_foods(self):
|
||||
# if self.Foods is None:
|
||||
# # # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
||||
# if self._search_params['search_foods_or']:
|
||||
# foods = Food.objects.filter(
|
||||
# space=self._request.space).distinct()
|
||||
# else:
|
||||
# foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(
|
||||
# depth=1)).filter(space=self._request.space).distinct()
|
||||
|
||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0)
|
||||
).filter(depth__lte=depth, count__gt=0
|
||||
).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
||||
else:
|
||||
return queryset.filter(depth__lte=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
||||
# # set keywords to root objects only
|
||||
# foods = self._food_queryset(foods)
|
||||
|
||||
# self.Foods = [{**x, 'children': None}
|
||||
# if x['numchild'] > 0 else x for x in list(foods)]
|
||||
# self.set_cache('Foods', self.Foods)
|
||||
# return self.Foods
|
||||
|
||||
# def get_ratings(self):
|
||||
# if self.Ratings is None:
|
||||
# if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
# if self._queryset is None:
|
||||
# self._queryset = Recipe.objects.filter(
|
||||
# id__in=self._recipe_list)
|
||||
# rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(
|
||||
# cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
|
||||
# self.Ratings = dict(Counter(r.rating for r in rating_qs))
|
||||
# else:
|
||||
# self.Rating = {}
|
||||
# self.set_cache('Ratings', self.Ratings)
|
||||
# return self.Ratings
|
||||
|
||||
# def get_recent(self):
|
||||
# if self.Recent is None:
|
||||
# # TODO make days of recent recipe a setting
|
||||
# recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space, created_at__gte=timezone.now() - timedelta(days=14)
|
||||
# ).values_list('recipe__pk', flat=True)
|
||||
# self.Recent = list(recent_recipes)
|
||||
# self.set_cache('Recent', self.Recent)
|
||||
# return self.Recent
|
||||
|
||||
# def add_food_children(self, id):
|
||||
# try:
|
||||
# food = Food.objects.get(id=id)
|
||||
# nodes = food.get_ancestors()
|
||||
# except Food.DoesNotExist:
|
||||
# return self.get_facets()
|
||||
# foods = self._food_queryset(food.get_children(), food)
|
||||
# deep_search = self.Foods
|
||||
# for node in nodes:
|
||||
# index = next((i for i, x in enumerate(
|
||||
# deep_search) if x["id"] == node.id), None)
|
||||
# deep_search = deep_search[index]['children']
|
||||
# index = next((i for i, x in enumerate(
|
||||
# deep_search) if x["id"] == food.id), None)
|
||||
# deep_search[index]['children'] = [
|
||||
# {**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
||||
# self.set_cache('Foods', self.Foods)
|
||||
# return self.get_facets()
|
||||
|
||||
# def add_keyword_children(self, id):
|
||||
# try:
|
||||
# keyword = Keyword.objects.get(id=id)
|
||||
# nodes = keyword.get_ancestors()
|
||||
# except Keyword.DoesNotExist:
|
||||
# return self.get_facets()
|
||||
# keywords = self._keyword_queryset(keyword.get_children(), keyword)
|
||||
# deep_search = self.Keywords
|
||||
# for node in nodes:
|
||||
# index = next((i for i, x in enumerate(
|
||||
# deep_search) if x["id"] == node.id), None)
|
||||
# deep_search = deep_search[index]['children']
|
||||
# index = next((i for i, x in enumerate(deep_search)
|
||||
# if x["id"] == keyword.id), None)
|
||||
# deep_search[index]['children'] = [
|
||||
# {**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
# self.set_cache('Keywords', self.Keywords)
|
||||
# return self.get_facets()
|
||||
|
||||
# def _recipe_count_queryset(self, field, depth=1, steplen=4):
|
||||
# return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path'), f'{field}__depth__gte': depth}, id__in=self._recipe_list, space=self._request.space
|
||||
# ).annotate(count=Coalesce(Func('pk', function='Count'), 0)).values('count')
|
||||
|
||||
# def _keyword_queryset(self, queryset, keyword=None):
|
||||
# depth = getattr(keyword, 'depth', 0) + 1
|
||||
# steplen = depth * Keyword.steplen
|
||||
|
||||
# if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
# return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0)
|
||||
# ).filter(depth=depth, count__gt=0
|
||||
# ).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
||||
# else:
|
||||
# return queryset.filter(depth=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
||||
|
||||
# def _food_queryset(self, queryset, food=None):
|
||||
# depth = getattr(food, 'depth', 0) + 1
|
||||
# steplen = depth * Food.steplen
|
||||
|
||||
# if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
# return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0)
|
||||
# ).filter(depth__lte=depth, count__gt=0
|
||||
# ).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
||||
# else:
|
||||
# return queryset.filter(depth__lte=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
||||
|
@ -1,9 +1,7 @@
|
||||
# import random
|
||||
import re
|
||||
import traceback
|
||||
from html import unescape
|
||||
|
||||
from django.core.cache import caches
|
||||
from django.utils.dateparse import parse_duration
|
||||
from django.utils.translation import gettext as _
|
||||
from isodate import parse_duration as iso_parse_duration
|
||||
@ -11,15 +9,9 @@ from isodate.isoerror import ISO8601Error
|
||||
from pytube import YouTube
|
||||
from recipe_scrapers._utils import get_host_name, get_minutes
|
||||
|
||||
# from cookbook.helper import recipe_url_import as helper
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.models import Automation, Keyword, PropertyType
|
||||
|
||||
# from unicodedata import decomposition
|
||||
|
||||
|
||||
# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
|
||||
|
||||
|
||||
def get_from_scraper(scrape, request):
|
||||
# converting the scrape_me object to the existing json format based on ld+json
|
||||
@ -147,7 +139,7 @@ def get_from_scraper(scrape, request):
|
||||
recipe_json['steps'] = []
|
||||
try:
|
||||
for i in parse_instructions(scrape.instructions()):
|
||||
recipe_json['steps'].append({'instruction': i, 'ingredients': [], 'show_ingredients_table': request.user.userpreference.show_step_ingredients,})
|
||||
recipe_json['steps'].append({'instruction': i, 'ingredients': [], 'show_ingredients_table': request.user.userpreference.show_step_ingredients, })
|
||||
except Exception:
|
||||
pass
|
||||
if len(recipe_json['steps']) == 0:
|
||||
|
@ -1,16 +1,13 @@
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.db.models import F, OuterRef, Q, Subquery, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
||||
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
|
||||
SupermarketCategoryRelation)
|
||||
from recipes import settings
|
||||
|
||||
|
||||
def shopping_helper(qs, request):
|
||||
@ -47,7 +44,7 @@ class RecipeShoppingEditor():
|
||||
self.mealplan = self._kwargs.get('mealplan', None)
|
||||
if type(self.mealplan) in [int, float]:
|
||||
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
|
||||
if type(self.mealplan) == dict:
|
||||
if isinstance(self.mealplan, dict):
|
||||
self.mealplan = MealPlan.objects.filter(id=self.mealplan['id'], space=self.space).first()
|
||||
self.id = self._kwargs.get('id', None)
|
||||
|
||||
@ -69,11 +66,12 @@ class RecipeShoppingEditor():
|
||||
|
||||
@property
|
||||
def _recipe_servings(self):
|
||||
return getattr(self.recipe, 'servings', None) or getattr(getattr(self.mealplan, 'recipe', None), 'servings', None) or getattr(getattr(self._shopping_list_recipe, 'recipe', None), 'servings', None)
|
||||
return getattr(self.recipe, 'servings', None) or getattr(getattr(self.mealplan, 'recipe', None), 'servings',
|
||||
None) or getattr(getattr(self._shopping_list_recipe, 'recipe', None), 'servings', None)
|
||||
|
||||
@property
|
||||
def _servings_factor(self):
|
||||
return Decimal(self.servings)/Decimal(self._recipe_servings)
|
||||
return Decimal(self.servings) / Decimal(self._recipe_servings)
|
||||
|
||||
@property
|
||||
def _shared_users(self):
|
||||
@ -90,9 +88,10 @@ class RecipeShoppingEditor():
|
||||
|
||||
def get_recipe_ingredients(self, id, exclude_onhand=False):
|
||||
if exclude_onhand:
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(food__onhand_users__id__in=[x.id for x in self._shared_users])
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(
|
||||
food__onhand_users__id__in=[x.id for x in self._shared_users])
|
||||
else:
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
|
||||
|
||||
@property
|
||||
def _include_related(self):
|
||||
@ -109,7 +108,7 @@ class RecipeShoppingEditor():
|
||||
self.servings = float(servings)
|
||||
|
||||
if mealplan := kwargs.get('mealplan', None):
|
||||
if type(mealplan) == dict:
|
||||
if isinstance(mealplan, dict):
|
||||
self.mealplan = MealPlan.objects.filter(id=mealplan['id'], space=self.space).first()
|
||||
else:
|
||||
self.mealplan = mealplan
|
||||
@ -170,13 +169,13 @@ class RecipeShoppingEditor():
|
||||
try:
|
||||
self._shopping_list_recipe.delete()
|
||||
return True
|
||||
except:
|
||||
except BaseException:
|
||||
return False
|
||||
|
||||
def _add_ingredients(self, ingredients=None):
|
||||
if not ingredients:
|
||||
return
|
||||
elif type(ingredients) == list:
|
||||
elif isinstance(ingredients, list):
|
||||
ingredients = Ingredient.objects.filter(id__in=ingredients)
|
||||
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
|
||||
add_ingredients = ingredients.exclude(id__in=existing)
|
||||
@ -315,4 +314,4 @@ class RecipeShoppingEditor():
|
||||
# )
|
||||
|
||||
# # return all shopping list items
|
||||
# return list_recipe
|
||||
# return list_recipe
|
||||
|
@ -1,20 +1,15 @@
|
||||
import base64
|
||||
import gzip
|
||||
import json
|
||||
import re
|
||||
from gettext import gettext as _
|
||||
from io import BytesIO
|
||||
|
||||
import requests
|
||||
import validators
|
||||
import yaml
|
||||
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import (get_from_scraper, get_images_from_soup,
|
||||
iso_duration_to_minutes)
|
||||
from cookbook.helper.scrapers.scrapers import text_scraper
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
||||
from cookbook.models import Ingredient, Recipe, Step
|
||||
|
||||
|
||||
class CookBookApp(Integration):
|
||||
@ -47,7 +42,8 @@ class CookBookApp(Integration):
|
||||
pass
|
||||
|
||||
# assuming import files only contain single step
|
||||
step = Step.objects.create(instruction=recipe_json['steps'][0]['instruction'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
|
||||
step = Step.objects.create(instruction=recipe_json['steps'][0]['instruction'], space=self.request.space,
|
||||
show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
|
||||
|
||||
if 'nutrition' in recipe_json:
|
||||
step.instruction = step.instruction + '\n\n' + recipe_json['nutrition']
|
||||
@ -62,7 +58,7 @@ class CookBookApp(Integration):
|
||||
if unit := ingredient.get('unit', None):
|
||||
u = ingredient_parser.get_unit(unit.get('name', None))
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=ingredient.get('amount', None), note=ingredient.get('note', None), original_text=ingredient.get('original_text', None), space=self.request.space,
|
||||
food=f, unit=u, amount=ingredient.get('amount', None), note=ingredient.get('note', None), original_text=ingredient.get('original_text', None), space=self.request.space,
|
||||
))
|
||||
|
||||
if len(images) > 0:
|
||||
|
@ -1,4 +1,3 @@
|
||||
import traceback
|
||||
import datetime
|
||||
import traceback
|
||||
import uuid
|
||||
@ -18,8 +17,7 @@ from lxml import etree
|
||||
|
||||
from cookbook.helper.image_processing import handle_image
|
||||
from cookbook.models import Keyword, Recipe
|
||||
from recipes.settings import DEBUG
|
||||
from recipes.settings import EXPORT_FILE_CACHE_DURATION
|
||||
from recipes.settings import DEBUG, EXPORT_FILE_CACHE_DURATION
|
||||
|
||||
|
||||
class Integration:
|
||||
@ -63,12 +61,10 @@ class Integration:
|
||||
space=request.space
|
||||
)
|
||||
|
||||
|
||||
|
||||
def do_export(self, recipes, el):
|
||||
|
||||
with scope(space=self.request.space):
|
||||
el.total_recipes = len(recipes)
|
||||
el.total_recipes = len(recipes)
|
||||
el.cache_duration = EXPORT_FILE_CACHE_DURATION
|
||||
el.save()
|
||||
|
||||
@ -80,7 +76,7 @@ class Integration:
|
||||
export_file = file
|
||||
|
||||
else:
|
||||
#zip the files if there is more then one file
|
||||
# zip the files if there is more then one file
|
||||
export_filename = self.get_export_file_name()
|
||||
export_stream = BytesIO()
|
||||
export_obj = ZipFile(export_stream, 'w')
|
||||
@ -91,8 +87,7 @@ class Integration:
|
||||
export_obj.close()
|
||||
export_file = export_stream.getvalue()
|
||||
|
||||
|
||||
cache.set('export_file_'+str(el.pk), {'filename': export_filename, 'file': export_file}, EXPORT_FILE_CACHE_DURATION)
|
||||
cache.set('export_file_' + str(el.pk), {'filename': export_filename, 'file': export_file}, EXPORT_FILE_CACHE_DURATION)
|
||||
el.running = False
|
||||
el.save()
|
||||
|
||||
@ -100,7 +95,6 @@ class Integration:
|
||||
response['Content-Disposition'] = 'attachment; filename="' + export_filename + '"'
|
||||
return response
|
||||
|
||||
|
||||
def import_file_name_filter(self, zip_info_object):
|
||||
"""
|
||||
Since zipfile.namelist() returns all files in all subdirectories this function allows filtering of files
|
||||
@ -164,7 +158,7 @@ class Integration:
|
||||
|
||||
for z in file_list:
|
||||
try:
|
||||
if not hasattr(z, 'filename') or type(z) == Tag:
|
||||
if not hasattr(z, 'filename') or isinstance(z, Tag):
|
||||
recipe = self.get_recipe_from_file(z)
|
||||
else:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
@ -298,7 +292,6 @@ class Integration:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def get_export_file_name(self, format='zip'):
|
||||
return "export_{}.{}".format(datetime.datetime.now().strftime("%Y-%m-%d"), format)
|
||||
|
||||
|
@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.1.10 on 2023-09-07 18:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0200_alter_propertytype_options_remove_keyword_icon_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='space',
|
||||
name='show_facet_count',
|
||||
),
|
||||
]
|
@ -185,7 +185,6 @@ class TreeModel(MP_Node):
|
||||
:param filter: Filter (include) the descendants nodes with the provided Q filter
|
||||
"""
|
||||
descendants = Q()
|
||||
# TODO filter the queryset nodes to exclude descendants of objects in the queryset
|
||||
nodes = queryset.values('path', 'depth')
|
||||
for node in nodes:
|
||||
descendants |= Q(path__startswith=node['path'], depth__gt=node['depth'])
|
||||
@ -265,7 +264,6 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
no_sharing_limit = models.BooleanField(default=False)
|
||||
demo = models.BooleanField(default=False)
|
||||
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
|
||||
show_facet_count = models.BooleanField(default=False)
|
||||
|
||||
internal_note = models.TextField(blank=True, null=True)
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import random
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
@ -7,34 +6,34 @@ from gettext import gettext as _
|
||||
from html import escape
|
||||
from smtplib import SMTPException
|
||||
|
||||
from django.contrib.auth.models import Group, User, AnonymousUser
|
||||
from django.contrib.auth.models import AnonymousUser, Group, User
|
||||
from django.core.cache import caches
|
||||
from django.core.mail import send_mail
|
||||
from django.db.models import Avg, Q, QuerySet, Sum
|
||||
from django.db.models import Q, QuerySet, Sum
|
||||
from django.http import BadHeaderError
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django_scopes import scopes_disabled
|
||||
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
|
||||
from PIL import Image
|
||||
from oauth2_provider.models import AccessToken
|
||||
from PIL import Image
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
|
||||
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
|
||||
from cookbook.helper.HelperFunctions import str2bool
|
||||
from cookbook.helper.property_helper import FoodPropertyHelper
|
||||
from cookbook.helper.permission_helper import above_space_limit
|
||||
from cookbook.helper.property_helper import FoodPropertyHelper
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
|
||||
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter,
|
||||
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink,
|
||||
Keyword, MealPlan, MealType, NutritionInformation, Recipe, RecipeBook,
|
||||
RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
|
||||
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
|
||||
SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, Property,
|
||||
PropertyType, Property)
|
||||
Keyword, MealPlan, MealType, NutritionInformation, Property,
|
||||
PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport,
|
||||
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
|
||||
Step, Storage, Supermarket, SupermarketCategory,
|
||||
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
||||
UserFile, UserPreference, UserSpace, ViewLog)
|
||||
from cookbook.templatetags.custom_tags import markdown
|
||||
from recipes.settings import AWS_ENABLED, MEDIA_URL
|
||||
|
||||
@ -60,7 +59,7 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
if str2bool(
|
||||
self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||
return fields
|
||||
except (AttributeError, KeyError) as e:
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
try:
|
||||
del fields['image']
|
||||
@ -104,9 +103,9 @@ class CustomDecimalField(serializers.Field):
|
||||
return round(value, 2).normalize()
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if type(data) == int or type(data) == float:
|
||||
if isinstance(data, int) or isinstance(data, float):
|
||||
return data
|
||||
elif type(data) == str:
|
||||
elif isinstance(data, str):
|
||||
if data == '':
|
||||
return 0
|
||||
try:
|
||||
@ -146,11 +145,11 @@ class SpaceFilterSerializer(serializers.ListSerializer):
|
||||
def to_representation(self, data):
|
||||
if self.context.get('request', None) is None:
|
||||
return
|
||||
if (type(data) == QuerySet and data.query.is_sliced):
|
||||
if (isinstance(data, QuerySet) and data.query.is_sliced):
|
||||
# if query is sliced it came from api request not nested serializer
|
||||
return super().to_representation(data)
|
||||
if self.child.Meta.model == User:
|
||||
if type(self.context['request'].user) == AnonymousUser:
|
||||
if isinstance(self.context['request'].user, AnonymousUser):
|
||||
data = []
|
||||
else:
|
||||
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
|
||||
@ -302,7 +301,7 @@ class SpaceSerializer(WritableNestedModelSerializer):
|
||||
model = Space
|
||||
fields = (
|
||||
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
|
||||
'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb',
|
||||
'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
|
||||
'image', 'use_plural',)
|
||||
read_only_fields = (
|
||||
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
|
||||
@ -449,7 +448,7 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
|
||||
class Meta:
|
||||
model = Keyword
|
||||
fields = (
|
||||
'id', 'name', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
|
||||
'id', 'name', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
|
||||
'updated_at', 'full_name')
|
||||
read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
|
||||
|
||||
@ -528,7 +527,7 @@ class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer,
|
||||
|
||||
class Meta:
|
||||
model = PropertyType
|
||||
fields = ('id', 'name', 'unit', 'description', 'order', 'open_data_slug')
|
||||
fields = ('id', 'name', 'unit', 'description', 'order', 'open_data_slug')
|
||||
|
||||
|
||||
class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
||||
@ -636,7 +635,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
validated_data['recipe'] = Recipe.objects.get(**recipe)
|
||||
|
||||
# assuming if on hand for user also onhand for shopping_share users
|
||||
if not onhand is None:
|
||||
if onhand is not None:
|
||||
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
|
||||
if self.instance:
|
||||
onhand_users = self.instance.onhand_users.all()
|
||||
@ -669,7 +668,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
# assuming if on hand for user also onhand for shopping_share users
|
||||
onhand = validated_data.get('food_onhand', None)
|
||||
reset_inherit = self.initial_data.get('reset_inherit', False)
|
||||
if not onhand is None:
|
||||
if onhand is not None:
|
||||
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
|
||||
if onhand:
|
||||
validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users
|
||||
@ -764,7 +763,7 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
def get_step_recipe_data(self, obj):
|
||||
# check if root type is recipe to prevent infinite recursion
|
||||
# can be improved later to allow multi level embedding
|
||||
if obj.step_recipe and type(self.parent.root) == RecipeSerializer:
|
||||
if obj.step_recipe and isinstance(self.parent.root, RecipeSerializer):
|
||||
return StepRecipeSerializer(obj.step_recipe, context={'request': self.context['request']}).data
|
||||
|
||||
class Meta:
|
||||
@ -956,8 +955,7 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
||||
def create(self, validated_data):
|
||||
book = validated_data['book']
|
||||
recipe = validated_data['recipe']
|
||||
if not book.get_owner() == self.context['request'].user and not self.context[
|
||||
'request'].user in book.get_shared():
|
||||
if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
|
||||
raise NotFound(detail=None, code=None)
|
||||
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
|
||||
return obj
|
||||
@ -1023,10 +1021,10 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
value = value.quantize(
|
||||
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
||||
return (
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
) + f' ({value:.2g})'
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
@ -6,16 +6,17 @@ from rest_framework import permissions, routers
|
||||
from rest_framework.schemas import get_schema_view
|
||||
|
||||
from cookbook.helper import dal
|
||||
from recipes.settings import DEBUG, PLUGINS
|
||||
from cookbook.version_info import TANDOOR_VERSION
|
||||
from recipes.settings import DEBUG, PLUGINS
|
||||
|
||||
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe,
|
||||
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage,
|
||||
Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UserFile,
|
||||
get_model_name, UserSpace, Space, PropertyType, UnitConversion)
|
||||
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan,
|
||||
PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList,
|
||||
Space, Step, Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit,
|
||||
UnitConversion, UserFile, UserSpace, get_model_name)
|
||||
from .views import api, data, delete, edit, import_export, lists, new, telegram, views
|
||||
from .views.api import CustomAuthToken, ImportOpenData
|
||||
|
||||
|
||||
# extend DRF default router class to allow including additional routers
|
||||
class DefaultRouter(routers.DefaultRouter):
|
||||
def extend(self, r):
|
||||
@ -131,7 +132,6 @@ urlpatterns = [
|
||||
path('api/backup/', api.get_backup, name='api_backup'),
|
||||
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
|
||||
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
|
||||
path('api/get_facets/', api.get_facets, name='api_get_facets'),
|
||||
path('api/reset-food-inheritance/', api.reset_food_inheritance, name='api_reset_food_inheritance'),
|
||||
path('api/switch-active-space/<int:space_id>/', api.switch_active_space, name='api_switch_active_space'),
|
||||
path('api/download-file/<int:file_id>/', api.download_file, name='api_download_file'),
|
||||
|
@ -3,7 +3,6 @@ import io
|
||||
import json
|
||||
import mimetypes
|
||||
import pathlib
|
||||
import random
|
||||
import re
|
||||
import threading
|
||||
import traceback
|
||||
@ -15,7 +14,6 @@ from zipfile import ZipFile
|
||||
|
||||
import requests
|
||||
import validators
|
||||
from PIL import UnidentifiedImageError
|
||||
from annoying.decorators import ajax_request
|
||||
from annoying.functions import get_object_or_None
|
||||
from django.contrib import messages
|
||||
@ -24,7 +22,7 @@ from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.core.cache import caches
|
||||
from django.core.exceptions import FieldError, ValidationError
|
||||
from django.core.files import File
|
||||
from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When, Avg, Max
|
||||
from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.db.models.functions import Coalesce, Lower
|
||||
from django.db.models.signals import post_save
|
||||
@ -36,6 +34,7 @@ from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
from icalendar import Calendar, Event
|
||||
from oauth2_provider.models import AccessToken
|
||||
from PIL import UnidentifiedImageError
|
||||
from recipe_scrapers import scrape_me
|
||||
from recipe_scrapers._exceptions import NoSchemaFoundInWildMode
|
||||
from requests.exceptions import MissingSchema
|
||||
@ -58,35 +57,41 @@ from cookbook.helper.HelperFunctions import str2bool
|
||||
from cookbook.helper.image_processing import handle_image
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.open_data_importer import OpenDataImporter
|
||||
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner,
|
||||
CustomIsOwnerReadOnly, CustomIsShared,
|
||||
CustomIsSpaceOwner, CustomIsUser, group_required,
|
||||
is_space_owner, switch_user_active_space, above_space_limit,
|
||||
CustomRecipePermission, CustomUserPermission,
|
||||
CustomTokenHasReadWriteScope, CustomTokenHasScope, has_group_permission, IsReadOnlyDRF)
|
||||
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch
|
||||
from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup, clean_dict
|
||||
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, CustomIsOwnerReadOnly,
|
||||
CustomIsShared, CustomIsSpaceOwner, CustomIsUser,
|
||||
CustomRecipePermission, CustomTokenHasReadWriteScope,
|
||||
CustomTokenHasScope, CustomUserPermission,
|
||||
IsReadOnlyDRF, above_space_limit, group_required,
|
||||
has_group_permission, is_space_owner,
|
||||
switch_user_active_space)
|
||||
from cookbook.helper.recipe_search import RecipeSearch
|
||||
from cookbook.helper.recipe_url_import import (clean_dict, get_from_youtube_scraper,
|
||||
get_images_from_soup)
|
||||
from cookbook.helper.scrapers.scrapers import text_scraper
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
|
||||
from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food,
|
||||
FoodInheritField, ImportLog, Ingredient, InviteLink, Keyword, MealPlan,
|
||||
MealType, Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
|
||||
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
|
||||
SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, PropertyType, Property)
|
||||
MealType, Property, PropertyType, Recipe, RecipeBook, RecipeBookEntry,
|
||||
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
|
||||
Step, Storage, Supermarket, SupermarketCategory,
|
||||
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
||||
UserFile, UserPreference, UserSpace, ViewLog)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.local import Local
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema
|
||||
from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSerializer,
|
||||
from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
|
||||
AutoMealPlanSerializer, BookmarkletImportListSerializer,
|
||||
BookmarkletImportSerializer, CookLogSerializer,
|
||||
CustomFilterSerializer, ExportLogSerializer,
|
||||
FoodInheritFieldSerializer, FoodSerializer,
|
||||
FoodShoppingUpdateSerializer, GroupSerializer, ImportLogSerializer,
|
||||
IngredientSerializer, IngredientSimpleSerializer,
|
||||
InviteLinkSerializer, KeywordSerializer, MealPlanSerializer,
|
||||
MealTypeSerializer, RecipeBookEntrySerializer,
|
||||
RecipeBookSerializer, RecipeFromSourceSerializer,
|
||||
FoodShoppingUpdateSerializer, FoodSimpleSerializer,
|
||||
GroupSerializer, ImportLogSerializer, IngredientSerializer,
|
||||
IngredientSimpleSerializer, InviteLinkSerializer,
|
||||
KeywordSerializer, MealPlanSerializer, MealTypeSerializer,
|
||||
PropertySerializer, PropertyTypeSerializer,
|
||||
RecipeBookEntrySerializer, RecipeBookSerializer,
|
||||
RecipeExportSerializer, RecipeFromSourceSerializer,
|
||||
RecipeImageSerializer, RecipeOverviewSerializer, RecipeSerializer,
|
||||
RecipeShoppingUpdateSerializer, RecipeSimpleSerializer,
|
||||
ShoppingListAutoSyncSerializer, ShoppingListEntrySerializer,
|
||||
@ -94,11 +99,9 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
|
||||
SpaceSerializer, StepSerializer, StorageSerializer,
|
||||
SupermarketCategoryRelationSerializer,
|
||||
SupermarketCategorySerializer, SupermarketSerializer,
|
||||
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
||||
UserFileSerializer, UserSerializer, UserPreferenceSerializer,
|
||||
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer,
|
||||
RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer,
|
||||
PropertySerializer, AutoMealPlanSerializer)
|
||||
SyncLogSerializer, SyncSerializer, UnitConversionSerializer,
|
||||
UnitSerializer, UserFileSerializer, UserPreferenceSerializer,
|
||||
UserSerializer, UserSpaceSerializer, ViewLogSerializer)
|
||||
from cookbook.views.import_export import get_integration
|
||||
from recipes import settings
|
||||
|
||||
@ -186,7 +189,8 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
||||
if query is not None and query not in ["''", '']:
|
||||
if fuzzy and (settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
||||
'django.db.backends.postgresql']):
|
||||
if self.request.user.is_authenticated and any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||
if self.request.user.is_authenticated and any(
|
||||
[self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
|
||||
else:
|
||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
|
||||
@ -367,7 +371,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
|
||||
child.move(parent, f'{node_location}-child')
|
||||
content = {'msg': _(f'{child.name} was moved successfully to parent {parent.name}')}
|
||||
return Response(content, status=status.HTTP_200_OK)
|
||||
except (PathOverflow, InvalidMoveToDescendant, InvalidPosition) as e:
|
||||
except (PathOverflow, InvalidMoveToDescendant, InvalidPosition):
|
||||
content = {'error': True, 'msg': _('An error occurred attempting to move ') + child.name}
|
||||
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -775,8 +779,7 @@ class StepViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
pagination_class = DefaultPagination
|
||||
query_params = [
|
||||
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'), qtype='int'),
|
||||
QueryParam(name='query', description=_('Query string matched (fuzzy) against object name.'), qtype='string'),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
@ -799,7 +802,6 @@ class RecipePagination(PageNumberPagination):
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
if queryset is None:
|
||||
raise Exception
|
||||
self.facets = RecipeFacet(request, queryset=queryset)
|
||||
return super().paginate_queryset(queryset, request, view)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
@ -808,7 +810,6 @@ class RecipePagination(PageNumberPagination):
|
||||
('next', self.get_next_link()),
|
||||
('previous', self.get_previous_link()),
|
||||
('results', data),
|
||||
('facets', self.facets.get_facets(from_cache=True))
|
||||
]))
|
||||
|
||||
|
||||
@ -820,63 +821,33 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
pagination_class = RecipePagination
|
||||
|
||||
query_params = [
|
||||
QueryParam(name='query', description=_(
|
||||
'Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
|
||||
QueryParam(name='keywords', description=_(
|
||||
'ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_or',
|
||||
description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_and',
|
||||
description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_or_not',
|
||||
description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_and_not',
|
||||
description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='foods_or',
|
||||
description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), qtype='int'),
|
||||
QueryParam(name='foods_and',
|
||||
description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_or_not',
|
||||
description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_and_not',
|
||||
description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='query', description=_('Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
|
||||
QueryParam(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'), qtype='int'),
|
||||
QueryParam(name='keywords_or', description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'), qtype='int'),
|
||||
QueryParam(name='keywords_and', description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'), qtype='int'),
|
||||
QueryParam(name='keywords_or_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'), qtype='int'),
|
||||
QueryParam(name='keywords_and_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'), qtype='int'),
|
||||
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'), qtype='int'),
|
||||
QueryParam(name='foods_or', description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), qtype='int'),
|
||||
QueryParam(name='foods_and', description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_or_not', description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_and_not', description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='units', description=_('ID of unit a recipe should have.'), qtype='int'),
|
||||
QueryParam(name='rating', description=_(
|
||||
'Rating a recipe should have or greater. [0 - 5] Negative value filters rating less than.'), qtype='int'),
|
||||
QueryParam(name='rating', description=_('Rating a recipe should have or greater. [0 - 5] Negative value filters rating less than.'), qtype='int'),
|
||||
QueryParam(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.')),
|
||||
QueryParam(name='books_or',
|
||||
description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), qtype='int'),
|
||||
QueryParam(name='books_and',
|
||||
description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='books_or_not',
|
||||
description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
|
||||
QueryParam(name='books_and_not',
|
||||
description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='internal',
|
||||
description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
||||
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='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='updatedon', description=_(
|
||||
'Filter recipes updated 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>'']')),
|
||||
QueryParam(name='books_or', description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), qtype='int'),
|
||||
QueryParam(name='books_and', description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='books_or_not', description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
|
||||
QueryParam(name='books_and_not', description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='internal', description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
||||
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='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='updatedon', description=_('Filter recipes updated 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()
|
||||
|
||||
@ -1095,17 +1066,10 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ShoppingListEntrySerializer
|
||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||
query_params = [
|
||||
QueryParam(name='id',
|
||||
description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'),
|
||||
qtype='int'),
|
||||
QueryParam(
|
||||
name='checked',
|
||||
description=_(
|
||||
'Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
||||
),
|
||||
QueryParam(name='supermarket',
|
||||
description=_('Returns the shopping list entries sorted by supermarket category order.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='int'),
|
||||
QueryParam(name='checked', description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
||||
),
|
||||
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
@ -1344,12 +1308,10 @@ def recipe_from_source(request):
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
elif url and not data:
|
||||
if re.match('^(https?://)?(www\.youtube\.com|youtu\.be)/.+$', url):
|
||||
if re.match('^(https?://)?(www\\.youtube\\.com|youtu\\.be)/.+$', url):
|
||||
if validators.url(url, public=True):
|
||||
return Response({
|
||||
'recipe_json': get_from_youtube_scraper(url, request),
|
||||
# 'recipe_tree': '',
|
||||
# 'recipe_html': '',
|
||||
'recipe_images': [],
|
||||
}, status=status.HTTP_200_OK)
|
||||
if re.match(
|
||||
@ -1412,8 +1374,6 @@ def recipe_from_source(request):
|
||||
if scrape:
|
||||
return Response({
|
||||
'recipe_json': helper.get_from_scraper(scrape, request),
|
||||
# 'recipe_tree': recipe_tree,
|
||||
# 'recipe_html': recipe_html,
|
||||
'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))),
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
@ -1437,7 +1397,7 @@ def reset_food_inheritance(request):
|
||||
try:
|
||||
Food.reset_inheritance(space=request.space)
|
||||
return Response({'message': 'success', }, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -1457,7 +1417,7 @@ def switch_active_space(request, space_id):
|
||||
return Response(UserSpaceSerializer().to_representation(instance=user_space), status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response("not found", status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -1482,7 +1442,7 @@ def download_file(request, file_id):
|
||||
response['Content-Disposition'] = 'attachment; filename="' + uf.name + '.zip"'
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -1707,25 +1667,3 @@ def ingredient_from_string(request):
|
||||
},
|
||||
status=200
|
||||
)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def get_facets(request):
|
||||
key = request.GET.get('hash', None)
|
||||
food = request.GET.get('food', None)
|
||||
keyword = request.GET.get('keyword', None)
|
||||
facets = RecipeFacet(request, hash_key=key)
|
||||
|
||||
if food:
|
||||
results = facets.add_food_children(food)
|
||||
elif keyword:
|
||||
results = facets.add_keyword_children(keyword)
|
||||
else:
|
||||
results = facets.get_facets()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
'facets': results,
|
||||
},
|
||||
status=200
|
||||
)
|
||||
|
@ -1,16 +1,13 @@
|
||||
import re
|
||||
import threading
|
||||
from io import BytesIO
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.forms import ExportForm, ImportExportBase, ImportForm
|
||||
from cookbook.helper.permission_helper import group_required, above_space_limit
|
||||
from cookbook.forms import ExportForm, ImportExportBase
|
||||
from cookbook.helper.permission_helper import group_required
|
||||
from cookbook.helper.recipe_search import RecipeSearch
|
||||
from cookbook.integration.cheftap import ChefTap
|
||||
from cookbook.integration.chowdown import Chowdown
|
||||
@ -34,7 +31,7 @@ from cookbook.integration.recipesage import RecipeSage
|
||||
from cookbook.integration.rezeptsuitede import Rezeptsuitede
|
||||
from cookbook.integration.rezkonv import RezKonv
|
||||
from cookbook.integration.saffron import Saffron
|
||||
from cookbook.models import ExportLog, ImportLog, Recipe, UserPreference
|
||||
from cookbook.models import ExportLog, Recipe
|
||||
from recipes import settings
|
||||
|
||||
|
||||
|
@ -10,7 +10,6 @@
|
||||
"dependencies": {
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@popperjs/core": "^2.11.7",
|
||||
"@riophae/vue-treeselect": "^0.4.0",
|
||||
"@vue/cli": "^5.0.8",
|
||||
"@vue/composition-api": "1.7.1",
|
||||
"axios": "^1.2.0",
|
||||
@ -37,7 +36,7 @@
|
||||
"vue-multiselect": "^2.1.6",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-sanitize": "^0.2.2",
|
||||
"vue-simple-calendar": "5.0.1",
|
||||
"vue-simple-calendar": "TandoorRecipes/vue-simple-calendar#lastvue2",
|
||||
"vue-template-compiler": "2.7.14",
|
||||
"vue2-touch-events": "^3.2.2",
|
||||
"vuedraggable": "^2.24.3",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,6 @@
|
||||
"file_upload_disabled": "",
|
||||
"warning_space_delete": "",
|
||||
"food_inherit_info": "",
|
||||
"facet_count_info": "",
|
||||
"step_time_minutes": "",
|
||||
"confirm_delete": "",
|
||||
"import_running": "",
|
||||
|
@ -16,7 +16,6 @@
|
||||
"file_upload_disabled": "Nahrávání souborů není povoleno pro Váš prostor.",
|
||||
"warning_space_delete": "Můžete smazat váš prostor včetně všech receptů, nákupních seznamů, jídelníčků a všeho ostatního, co jste vytvořili. Tuto akci nemůžete vzít zpět! Jste si jisti, že chcete pokračovat?",
|
||||
"food_inherit_info": "Pole potravin, která budou standardně zděděna.",
|
||||
"facet_count_info": "Zobraz počet receptů u filtrů vyhledávání.",
|
||||
"step_time_minutes": "Nastavte čas v minutách",
|
||||
"confirm_delete": "Jste si jisti že chcete odstranit tento {objekt}?",
|
||||
"import_running": "Probíhá import, čekejte prosím!",
|
||||
|
@ -446,7 +446,6 @@
|
||||
"Valid Until": "Gyldig indtil",
|
||||
"Private_Recipe_Help": "Opskriften er kun synlig for dig, og dem som den er delt med.",
|
||||
"food_inherit_info": "Felter på mad som skal nedarves automatisk.",
|
||||
"facet_count_info": "Vis opskriftsantal på søgeresultater.",
|
||||
"Copy Link": "Kopier link",
|
||||
"Copy Token": "Kopier token",
|
||||
"show_ingredient_overview": "Vis en liste af alle ingredienser i starten af en opskrift.",
|
||||
|
@ -411,7 +411,6 @@
|
||||
"warning_space_delete": "Du kannst deinen Space inklusive all deiner Rezepte, Shoppinglisten, Essensplänen und allem anderen, das du erstellt hast löschen. Dieser Schritt kann nicht rückgängig gemacht werden! Bist du sicher, dass du das tun möchtest?",
|
||||
"Copy Link": "Link Kopieren",
|
||||
"Users": "Benutzer",
|
||||
"facet_count_info": "Zeige die Anzahl der Rezepte auf den Suchfiltern.",
|
||||
"Copy Token": "Kopiere Token",
|
||||
"Invites": "Einladungen",
|
||||
"Message": "Nachricht",
|
||||
|
@ -17,7 +17,6 @@
|
||||
"recipe_property_info": "Μπορείτε επίσης να προσθέσετε ιδιότητες σε φαγητά ώστε να υπολογίζονται αυτόματα βάσει της συνταγής σας!",
|
||||
"warning_space_delete": "Μπορείτε να διαγράψετε τον χώρο σας μαζί με όλες τις συνταγές, τις λίστες αγορών, τα προγράμματα γευμάτων και οτιδήποτε άλλο έχετε δημιουργήσει. Η ενέργεια αυτή είναι μη αναστρέψιμη! Θέλετε σίγουρα να το κάνετε;",
|
||||
"food_inherit_info": "Πεδία σε φαγητά τα οποία πρέπει να κληρονομούνται αυτόματα.",
|
||||
"facet_count_info": "Εμφάνιση του αριθμού των συνταγών στα φίλτρα αναζήτησης.",
|
||||
"step_time_minutes": "Χρόνος βήματος σε λεπτά",
|
||||
"confirm_delete": "Θέλετε σίγουρα να διαγράψετε αυτό το {object};",
|
||||
"import_running": "Εισαγωγή σε εξέλιξη, παρακαλώ περιμένετε!",
|
||||
|
@ -17,7 +17,6 @@
|
||||
"recipe_property_info": "You can also add properties to foods to calculate them automatically based on your recipe!",
|
||||
"warning_space_delete": "You can delete your space including all recipes, shopping lists, meal plans and whatever else you have created. This cannot be undone! Are you sure you want to do this ?",
|
||||
"food_inherit_info": "Fields on food that should be inherited by default.",
|
||||
"facet_count_info": "Show recipe counts on search filters.",
|
||||
"step_time_minutes": "Step time in minutes",
|
||||
"confirm_delete": "Are you sure you want to delete this {object}?",
|
||||
"import_running": "Import running, please wait!",
|
||||
|
@ -419,7 +419,6 @@
|
||||
"Users": "Usuarios",
|
||||
"Invites": "Invitaciones",
|
||||
"food_inherit_info": "Campos que han de ser heredados por defecto.",
|
||||
"facet_count_info": "Mostrar contadores de receta en los filtros de búsqueda.",
|
||||
"Copy Link": "Copiar Enlace",
|
||||
"Copy Token": "Copiar Token",
|
||||
"Create_New_Shopping_Category": "Añadir nueva Categoría de Compras",
|
||||
|
@ -418,7 +418,6 @@
|
||||
"Message": "Message",
|
||||
"Sticky_Nav_Help": "Toujours afficher le menu de navigation en haut de l’écran.",
|
||||
"Combine_All_Steps": "Combiner toutes les étapes en un seul champ.",
|
||||
"facet_count_info": "Afficher les compteurs de recette sur les filtres de recherche.",
|
||||
"Decimals": "Décimales",
|
||||
"plan_share_desc": "Les nouvelles entrées de menu de la semaine seront partagées automatiquement avec des utilisateurs sélectionnés.",
|
||||
"Use_Kj": "Utiliser kJ au lieu de kcal",
|
||||
|
@ -17,7 +17,6 @@
|
||||
"recipe_property_info": "ניתן גם להוסיף ערכים למאכלים בכדי לחשב אוטומטית בהתאם למתכון שלך!",
|
||||
"warning_space_delete": "ניתן למחיק את המרחב כולל כל המתכונים, רשימות קניות, תוכניות אוכל וכל מה שנוצר. פעולה זו הינה בלתי הפיכה! האם אתה בטוח ?",
|
||||
"food_inherit_info": "ערכים על אוכל שאמורים להיות תורשתיים כברירת מחדל.",
|
||||
"facet_count_info": "הצג מספר מתכונים בסנני החיפוש.",
|
||||
"step_time_minutes": "זמן הצעד בדקות",
|
||||
"confirm_delete": "האם אתה בטוח רוצה למחק את {object}?",
|
||||
"import_running": "ייבוא מתבצע, נא להמתין!",
|
||||
|
@ -16,7 +16,6 @@
|
||||
"file_upload_disabled": "Unggahan file tidak diaktifkan untuk ruang Anda.",
|
||||
"warning_space_delete": "Anda dapat menghapus ruang Anda termasuk semua resep, daftar belanja, rencana makan, dan apa pun yang telah Anda buat. Ini tidak dapat dibatalkan! Apakah Anda yakin ingin melakukan ini?",
|
||||
"food_inherit_info": "Bidang pada makanan yang harus diwarisi secara default.",
|
||||
"facet_count_info": "Tampilkan jumlah resep pada filter pencarian.",
|
||||
"step_time_minutes": "Langkah waktu dalam menit",
|
||||
"confirm_delete": "Anda yakin ingin menghapus {object} ini?",
|
||||
"import_running": "Impor berjalan, harap tunggu!",
|
||||
|
@ -304,7 +304,6 @@
|
||||
"tree_select": "Usa selezione ad albero",
|
||||
"sql_debug": "Debug SQL",
|
||||
"remember_search": "Ricorda ricerca",
|
||||
"facet_count_info": "Mostra il conteggio delle ricette nei filtri di ricerca.",
|
||||
"warning_space_delete": "Stai per eliminare la tua istanza che include tutte le ricette, liste della spesa, piani alimentari e tutto ciò che hai creato. Questa azione non può essere annullata! Sei sicuro di voler procedere?",
|
||||
"food_inherit_info": "Campi di alimenti che devono essere ereditati per impostazione predefinita.",
|
||||
"enable_expert": "Abilita modalità esperto",
|
||||
|
@ -16,7 +16,6 @@
|
||||
"file_upload_disabled": "Opplasting av filer er ikke aktivert i området ditt.",
|
||||
"warning_space_delete": "Du kan slette området, inkludert alle oppskrifter, handlelister, måltidsplaner og alt annet du har opprettet. Dette kan ikke angres! Er du sikker på at du vil gjøre dette?",
|
||||
"food_inherit_info": "Felter på matvarer som skal arves som standard.",
|
||||
"facet_count_info": "Vis oppskriftsantall i søkefilter.",
|
||||
"step_time_minutes": "Tid for trinn, i minutter",
|
||||
"confirm_delete": "Er du sikker på at du vil slette dette {object}?",
|
||||
"import_running": "Importering pågår. Vennligst vent!",
|
||||
|
@ -464,7 +464,6 @@
|
||||
"Valid Until": "Geldig tot",
|
||||
"warning_space_delete": "Je kunt jouw space verwijderen inclusief alle recepten, boodschappenlijstjes, maaltijdplannen en alles wat je verder aangemaakt hebt. Dit kan niet ongedaan worden gemaakt! Weet je het zeker?",
|
||||
"food_inherit_info": "Voedselvelden die standaard geërfd worden.",
|
||||
"facet_count_info": "Geef receptenaantal bij zoekfilters weer.",
|
||||
"Split_All_Steps": "Splits alle rijen in aparte stappen.",
|
||||
"Combine_All_Steps": "Voeg alle stappen samen tot een veld.",
|
||||
"Plural": "Meervoud",
|
||||
|
@ -420,7 +420,6 @@
|
||||
"Users": "Użytkownicy",
|
||||
"Invites": "Zaprasza",
|
||||
"food_inherit_info": "Pola w pożywieniu, które powinny być domyślnie dziedziczone.",
|
||||
"facet_count_info": "Pokaż ilości przepisów w filtrach wyszukiwania.",
|
||||
"Copy Token": "Kopiuj Token",
|
||||
"Message": "Wiadomość",
|
||||
"reset_food_inheritance": "Zresetuj dziedziczenie",
|
||||
|
@ -382,7 +382,6 @@
|
||||
"err_deleting_protected_resource": "O objeto que você está tentando deletar ainda está sendo utilizado, portanto não pode ser deletado.",
|
||||
"food_inherit_info": "Campos no alimento que devem ser herdados por padrão.",
|
||||
"warning_space_delete": "Pode eliminar o seu espaço incluindo todas as receitas, listas de compras, planos de refeição e tudo o que tenha criado. Isto não pode ser desfeito! Tem a certeza que quer fazer isto?",
|
||||
"facet_count_info": "Mostrar quantidade de receitas nos filtros de busca.",
|
||||
"Plural": "",
|
||||
"plural_short": "",
|
||||
"Use_Plural_Unit_Always": "",
|
||||
|
@ -387,7 +387,6 @@
|
||||
"Copy Token": "Copiar Token",
|
||||
"warning_space_delete": "Você pode deletar seu espaço, inclusive todas as receitas, listas de mercado, planos de comida e tudo mais que você criou. Esta ação não poderá ser desfeita! Você tem certeza que quer fazer isto?",
|
||||
"food_inherit_info": "Campos no alimento que devem ser herdados por padrão.",
|
||||
"facet_count_info": "Mostrar quantidade de receitas nos filtros de busca.",
|
||||
"Plural": "",
|
||||
"plural_short": "",
|
||||
"Use_Plural_Unit_Always": "",
|
||||
|
@ -343,7 +343,6 @@
|
||||
"Undefined": "Nedefinit",
|
||||
"Select": "Selectare",
|
||||
"food_inherit_info": "Câmpuri pe alimente care ar trebui să fie moștenite în mod implicit.",
|
||||
"facet_count_info": "Afișarea numărului de rețete pe filtrele de căutare.",
|
||||
"Amount": "Cantitate",
|
||||
"Auto_Sort_Help": "Mutați toate ingredientele la cel mai potrivit pas.",
|
||||
"search_create_help_text": "Creați o rețetă nouă direct în Tandoor.",
|
||||
|
@ -343,7 +343,6 @@
|
||||
"DelayFor": "Отложить на {hours} часов",
|
||||
"New_Entry": "Новая запись",
|
||||
"GroupBy": "Сгруппировать по",
|
||||
"facet_count_info": "Показывать количество рецептов в фильтрах поиска.",
|
||||
"food_inherit_info": "Поля для продуктов питания, которые должны наследоваться по умолчанию.",
|
||||
"warning_space_delete": "Вы можете удалить свое пространство, включая все рецепты, списки покупок, планы питания и все остальное, что вы создали. Этого нельзя отменить! Вы уверены, что хотите это сделать?",
|
||||
"Description_Replace": "Изменить описание"
|
||||
|
@ -302,7 +302,6 @@
|
||||
"Description_Replace": "Zamenjaj Opis",
|
||||
"recipe_property_info": "Živilom lahko dodate tudi lastnosti, ki se samodejno izračunajo na podlagi vašega recepta!",
|
||||
"warning_space_delete": "Izbrišete lahko svoj prostor, vključno z vsemi recepti, nakupovalnimi seznami, načrti obrokov in vsem drugim, kar ste ustvarili. Tega ni mogoče preklicati! Ste prepričani, da želite to storiti?",
|
||||
"facet_count_info": "Prikaži število receptov v iskalnih filtrih.",
|
||||
"per_serving": "na porcijo",
|
||||
"Ingredient Editor": "Urejevalnik Sestavin",
|
||||
"Instruction_Replace": "Zamenjaj Navodila",
|
||||
|
@ -425,7 +425,6 @@
|
||||
"reusable_help_text": "Bör inbjudningslänken vara användbar för mer än en användare.",
|
||||
"Ingredient Editor": "Ingrediensredigerare",
|
||||
"warning_space_delete": "Du kan ta bort ditt utrymme inklusive alla recept, inköpslistor, måltidsplaner och allt annat du har skapat. Detta kan inte ångras! Är du säker på att du vill göra detta?",
|
||||
"facet_count_info": "Visa recept antal på sökfilter.",
|
||||
"food_inherit_info": "Fält på mat som ska ärvas som standard.",
|
||||
"Auto_Sort": "Automatisk Sortering",
|
||||
"Day": "Dag",
|
||||
|
@ -16,7 +16,6 @@
|
||||
"file_upload_disabled": "Alanınız için dosya yükleme aktif değil.",
|
||||
"warning_space_delete": "Tüm tarifler, alışveriş listeleri, yemek planları ve oluşturduğunuz her şey dahil olmak üzere silinecektir. Bu geri alınamaz! Bunu yapmak istediğinizden emin misiniz?",
|
||||
"food_inherit_info": "",
|
||||
"facet_count_info": "",
|
||||
"step_time_minutes": "Dakika olarak adım süresi",
|
||||
"confirm_delete": "",
|
||||
"import_running": "",
|
||||
|
@ -421,7 +421,6 @@
|
||||
"Use_Plural_Food_Simple": "",
|
||||
"plural_usage_info": "",
|
||||
"warning_space_delete": "Ви можете видалити ваш простір разом зі всіма рецептами, списками покупок, планами харчування і всім іншим, що ви створили. Ця дія незворотня! Ви впевнені, що бажаєте це зробити?",
|
||||
"facet_count_info": "Показати кількість рецептів на полі пошуку.",
|
||||
"Amount": "Кількість",
|
||||
"Auto_Sort": "Автоматичне сортування",
|
||||
"Auto_Sort_Help": "Перемістити всі інгредієнти до більш підходящого кроку.",
|
||||
|
@ -414,7 +414,6 @@
|
||||
"Warning_Delete_Supermarket_Category": "删除超市类别也会删除与食品的所有关系。 你确定吗?",
|
||||
"New_Supermarket": "创建新超市",
|
||||
"warning_space_delete": "您可以删除您的空间,包括所有食谱、购物清单、膳食计划以及您创建的任何其他内容。 这不能被撤消! 你确定要这么做吗 ?",
|
||||
"facet_count_info": "在搜索筛选器上显示食谱计数。",
|
||||
"Hide_as_header": "隐藏标题",
|
||||
"food_inherit_info": "默认情况下应继承的食物上的字段。",
|
||||
"Custom Filter": "自定义筛选器",
|
||||
|
File diff suppressed because it is too large
Load Diff
29063
vue/yarn.lock
29063
vue/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user