Merge pull request #2623 from smilerz/remove_facets
remove facets and treeselect
This commit is contained in:
commit
a2f9ef2e74
@ -45,8 +45,7 @@ class UserPreferenceForm(forms.ModelForm):
|
|||||||
model = UserPreference
|
model = UserPreference
|
||||||
fields = (
|
fields = (
|
||||||
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
|
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
|
||||||
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed',
|
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed', 'show_step_ingredients',
|
||||||
'show_step_ingredients',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
@ -67,21 +66,19 @@ class UserPreferenceForm(forms.ModelForm):
|
|||||||
|
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
|
'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': _(
|
'use_fractions': _(
|
||||||
'Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'),
|
'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.'),
|
'plan_share': _('Users with whom newly created meal plans should be shared by default.'),
|
||||||
'shopping_share': _('Users with whom to share shopping lists.'),
|
'shopping_share': _('Users with whom to share shopping lists.'),
|
||||||
'ingredient_decimals': _('Number of decimals to round ingredients.'),
|
'ingredient_decimals': _('Number of decimals to round ingredients.'),
|
||||||
'comments': _('If you want to be able to create and see comments underneath recipes.'),
|
'comments': _('If you want to be able to create and see comments underneath recipes.'),
|
||||||
'shopping_auto_sync': _(
|
'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 '
|
'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.'
|
'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_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
|
||||||
'mealplan_autoexclude_onhand': _('Exclude ingredients that are on hand.'),
|
'mealplan_autoexclude_onhand': _('Exclude ingredients that are on hand.'),
|
||||||
'left_handed': _('Will optimize the UI for use with your left 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)
|
result = single_file_clean(data, initial)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class ImportForm(ImportExportBase):
|
class ImportForm(ImportExportBase):
|
||||||
files = MultipleFileField(required=True)
|
files = MultipleFileField(required=True)
|
||||||
duplicates = forms.BooleanField(help_text=_(
|
duplicates = forms.BooleanField(help_text=_(
|
||||||
@ -325,7 +323,6 @@ class ImportRecipeForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO deprecate
|
|
||||||
class InviteLinkForm(forms.ModelForm):
|
class InviteLinkForm(forms.ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
user = kwargs.pop('user')
|
user = kwargs.pop('user')
|
||||||
@ -466,8 +463,8 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
|||||||
help_texts = {
|
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_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': _(
|
'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 '
|
'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.'
|
'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_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.'),
|
'mealplan_autoinclude_related': _('When adding a meal plan to the shopping list (manually or automatically), include all related recipes.'),
|
||||||
@ -511,11 +508,10 @@ class SpacePreferenceForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Space
|
model = Space
|
||||||
|
|
||||||
fields = ('food_inherit', 'reset_food_inherit', 'show_facet_count', 'use_plural')
|
fields = ('food_inherit', 'reset_food_inherit', 'use_plural')
|
||||||
|
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'food_inherit': _('Fields on food that should be inherited by default.'),
|
'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.'),
|
'use_plural': _('Use the plural form for units and food inside this space.'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import json
|
import json
|
||||||
from collections import Counter
|
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
|
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
|
||||||
from django.core.cache import cache, caches
|
from django.core.cache import cache
|
||||||
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value,
|
from django.db.models import Avg, Case, Count, Exists, F, Max, OuterRef, Q, Subquery, Value, When
|
||||||
When)
|
|
||||||
from django.db.models.functions import Coalesce, Lower, Substr
|
from django.db.models.functions import Coalesce, Lower, Substr
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
|
|
||||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
from cookbook.helper.HelperFunctions import Round, str2bool
|
||||||
from cookbook.managers import DICTIONARY
|
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 create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
|
||||||
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
||||||
class RecipeSearch():
|
class RecipeSearch():
|
||||||
_postgres = settings.DATABASES['default']['ENGINE'] in [
|
_postgres = settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
|
||||||
'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
|
|
||||||
|
|
||||||
def __init__(self, request, **params):
|
def __init__(self, request, **params):
|
||||||
self._request = request
|
self._request = request
|
||||||
self._queryset = None
|
self._queryset = None
|
||||||
if f := params.get('filter', 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) |
|
custom_filter = (
|
||||||
Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first()
|
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:
|
if custom_filter:
|
||||||
self._params = {**json.loads(custom_filter.search)}
|
self._params = {**json.loads(custom_filter.search)}
|
||||||
self._original_params = {**(params or {})}
|
self._original_params = {**(params or {})}
|
||||||
@ -101,24 +100,18 @@ class RecipeSearch():
|
|||||||
self._search_type = self._search_prefs.search or 'plain'
|
self._search_type = self._search_prefs.search or 'plain'
|
||||||
if self._string:
|
if self._string:
|
||||||
if self._postgres:
|
if self._postgres:
|
||||||
self._unaccent_include = self._search_prefs.unaccent.values_list(
|
self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
|
||||||
'field', flat=True)
|
|
||||||
else:
|
else:
|
||||||
self._unaccent_include = []
|
self._unaccent_include = []
|
||||||
self._icontains_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)]
|
||||||
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._istartswith_include = [
|
|
||||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
|
|
||||||
self._trigram_include = None
|
self._trigram_include = None
|
||||||
self._fulltext_include = None
|
self._fulltext_include = None
|
||||||
self._trigram = False
|
self._trigram = False
|
||||||
if self._postgres and self._string:
|
if self._postgres and self._string:
|
||||||
self._language = DICTIONARY.get(
|
self._language = DICTIONARY.get(translation.get_language(), 'simple')
|
||||||
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._trigram_include = [
|
self._fulltext_include = self._search_prefs.fulltext.values_list('field', flat=True) or None
|
||||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
|
|
||||||
self._fulltext_include = self._search_prefs.fulltext.values_list(
|
|
||||||
'field', flat=True) or None
|
|
||||||
|
|
||||||
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
|
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
|
||||||
self._trigram = True
|
self._trigram = True
|
||||||
@ -178,7 +171,6 @@ class RecipeSearch():
|
|||||||
|
|
||||||
# if a sort order is provided by user - use that order
|
# if a sort order is provided by user - use that order
|
||||||
if self._sort_order:
|
if self._sort_order:
|
||||||
|
|
||||||
if not isinstance(self._sort_order, list):
|
if not isinstance(self._sort_order, list):
|
||||||
order += [self._sort_order]
|
order += [self._sort_order]
|
||||||
else:
|
else:
|
||||||
@ -218,24 +210,18 @@ class RecipeSearch():
|
|||||||
self._queryset = self._queryset.filter(query_filter).distinct()
|
self._queryset = self._queryset.filter(query_filter).distinct()
|
||||||
if self._fulltext_include:
|
if self._fulltext_include:
|
||||||
if self._fuzzy_match is None:
|
if self._fuzzy_match is None:
|
||||||
self._queryset = self._queryset.annotate(
|
self._queryset = self._queryset.annotate(score=Coalesce(Max(self.search_rank), 0.0))
|
||||||
score=Coalesce(Max(self.search_rank), 0.0))
|
|
||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.annotate(
|
self._queryset = self._queryset.annotate(rank=Coalesce(Max(self.search_rank), 0.0))
|
||||||
rank=Coalesce(Max(self.search_rank), 0.0))
|
|
||||||
|
|
||||||
if self._fuzzy_match is not None:
|
if self._fuzzy_match is not None:
|
||||||
simularity = self._fuzzy_match.filter(
|
simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity')
|
||||||
pk=OuterRef('pk')).values('simularity')
|
|
||||||
if not self._fulltext_include:
|
if not self._fulltext_include:
|
||||||
self._queryset = self._queryset.annotate(
|
self._queryset = self._queryset.annotate(score=Coalesce(Subquery(simularity), 0.0))
|
||||||
score=Coalesce(Subquery(simularity), 0.0))
|
|
||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.annotate(
|
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
|
||||||
simularity=Coalesce(Subquery(simularity), 0.0))
|
|
||||||
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
|
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
|
||||||
self._queryset = self._queryset.annotate(
|
self._queryset = self._queryset.annotate(score=F('rank') + F('simularity'))
|
||||||
score=F('rank') + F('simularity'))
|
|
||||||
else:
|
else:
|
||||||
query_filter = Q()
|
query_filter = Q()
|
||||||
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
|
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
|
||||||
@ -244,78 +230,69 @@ class RecipeSearch():
|
|||||||
|
|
||||||
def _cooked_on_filter(self, cooked_date=None):
|
def _cooked_on_filter(self, cooked_date=None):
|
||||||
if self._sort_includes('lastcooked') or cooked_date:
|
if self._sort_includes('lastcooked') or cooked_date:
|
||||||
lessthan = self._sort_includes(
|
lessthan = self._sort_includes('-lastcooked') or '-' in (cooked_date or [])[:1]
|
||||||
'-lastcooked') or '-' in (cooked_date or [])[:1]
|
|
||||||
if lessthan:
|
if lessthan:
|
||||||
default = timezone.now() - timedelta(days=100000)
|
default = timezone.now() - timedelta(days=100000)
|
||||||
else:
|
else:
|
||||||
default = timezone.now()
|
default = timezone.now()
|
||||||
self._queryset = self._queryset.annotate(lastcooked=Coalesce(
|
self._queryset = self._queryset.annotate(
|
||||||
Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default)))
|
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:
|
if cooked_date is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
cooked_date = date(*[int(x)
|
cooked_date = date(*[int(x)for x in cooked_date.split('-') if x != ''])
|
||||||
for x in cooked_date.split('-') if x != ''])
|
|
||||||
|
|
||||||
if lessthan:
|
if lessthan:
|
||||||
self._queryset = self._queryset.filter(
|
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
||||||
lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
|
||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.filter(
|
self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
|
||||||
lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
|
|
||||||
|
|
||||||
def _created_on_filter(self, created_date=None):
|
def _created_on_filter(self, created_date=None):
|
||||||
if created_date is None:
|
if created_date is None:
|
||||||
return
|
return
|
||||||
lessthan = '-' in created_date[:1]
|
lessthan = '-' in created_date[:1]
|
||||||
created_date = date(*[int(x)
|
created_date = date(*[int(x) for x in created_date.split('-') if x != ''])
|
||||||
for x in created_date.split('-') if x != ''])
|
|
||||||
if lessthan:
|
if lessthan:
|
||||||
self._queryset = self._queryset.filter(
|
self._queryset = self._queryset.filter(created_at__date__lte=created_date)
|
||||||
created_at__date__lte=created_date)
|
|
||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.filter(
|
self._queryset = self._queryset.filter(created_at__date__gte=created_date)
|
||||||
created_at__date__gte=created_date)
|
|
||||||
|
|
||||||
def _updated_on_filter(self, updated_date=None):
|
def _updated_on_filter(self, updated_date=None):
|
||||||
if updated_date is None:
|
if updated_date is None:
|
||||||
return
|
return
|
||||||
lessthan = '-' in updated_date[:1]
|
lessthan = '-' in updated_date[:1]
|
||||||
updated_date = date(*[int(x)
|
updated_date = date(*[int(x)for x in updated_date.split('-') if x != ''])
|
||||||
for x in updated_date.split('-') if x != ''])
|
|
||||||
if lessthan:
|
if lessthan:
|
||||||
self._queryset = self._queryset.filter(
|
self._queryset = self._queryset.filter(updated_at__date__lte=updated_date)
|
||||||
updated_at__date__lte=updated_date)
|
|
||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.filter(
|
self._queryset = self._queryset.filter(updated_at__date__gte=updated_date)
|
||||||
updated_at__date__gte=updated_date)
|
|
||||||
|
|
||||||
def _viewed_on_filter(self, viewed_date=None):
|
def _viewed_on_filter(self, viewed_date=None):
|
||||||
if self._sort_includes('lastviewed') or viewed_date:
|
if self._sort_includes('lastviewed') or viewed_date:
|
||||||
longTimeAgo = timezone.now() - timedelta(days=100000)
|
longTimeAgo = timezone.now() - timedelta(days=100000)
|
||||||
self._queryset = self._queryset.annotate(lastviewed=Coalesce(
|
self._queryset = self._queryset.annotate(
|
||||||
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo)))
|
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:
|
if viewed_date is None:
|
||||||
return
|
return
|
||||||
lessthan = '-' in viewed_date[:1]
|
lessthan = '-' in viewed_date[:1]
|
||||||
viewed_date = date(*[int(x)
|
viewed_date = date(*[int(x)for x in viewed_date.split('-') if x != ''])
|
||||||
for x in viewed_date.split('-') if x != ''])
|
|
||||||
|
|
||||||
if lessthan:
|
if lessthan:
|
||||||
self._queryset = self._queryset.filter(
|
self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||||
lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
|
|
||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.filter(
|
self._queryset = self._queryset.filter(lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||||
lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
|
|
||||||
|
|
||||||
def _new_recipes(self, new_days=7):
|
def _new_recipes(self, new_days=7):
|
||||||
# TODO make new days a user-setting
|
# TODO make new days a user-setting
|
||||||
if not self._new:
|
if not self._new:
|
||||||
return
|
return
|
||||||
self._queryset = (
|
self._queryset = self._queryset.annotate(
|
||||||
self._queryset.annotate(new_recipe=Case(
|
new_recipe=Case(
|
||||||
When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')), default=Value(0), ))
|
When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')),
|
||||||
|
default=Value(0),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _recently_viewed(self, num_recent=None):
|
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)))
|
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__pk'))), Value(0)))
|
||||||
return
|
return
|
||||||
|
|
||||||
num_recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space).values(
|
num_recent_recipes = (
|
||||||
'recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
|
ViewLog.objects.filter(created_by=self._request.user, space=self._request.space)
|
||||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(
|
.values('recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
|
||||||
pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
)
|
||||||
|
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
||||||
|
|
||||||
def _favorite_recipes(self, times_cooked=None):
|
def _favorite_recipes(self, times_cooked=None):
|
||||||
if self._sort_includes('favorite') or times_cooked:
|
if self._sort_includes('favorite') or times_cooked:
|
||||||
less_than = '-' in (times_cooked or []
|
less_than = '-' in (times_cooked or []) and not self._sort_includes('-favorite')
|
||||||
) and not self._sort_includes('-favorite')
|
|
||||||
if less_than:
|
if less_than:
|
||||||
default = 1000
|
default = 1000
|
||||||
else:
|
else:
|
||||||
default = 0
|
default = 0
|
||||||
favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk')
|
favorite_recipes = (
|
||||||
).values('recipe').annotate(count=Count('pk', distinct=True)).values('count')
|
CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk'))
|
||||||
self._queryset = self._queryset.annotate(
|
.values('recipe')
|
||||||
favorite=Coalesce(Subquery(favorite_recipes), default))
|
.annotate(count=Count('pk', distinct=True))
|
||||||
|
.values('count')
|
||||||
|
)
|
||||||
|
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||||
if times_cooked is None:
|
if times_cooked is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if times_cooked == '0':
|
if times_cooked == '0':
|
||||||
self._queryset = self._queryset.filter(favorite=0)
|
self._queryset = self._queryset.filter(favorite=0)
|
||||||
elif less_than:
|
elif less_than:
|
||||||
self._queryset = self._queryset.filter(favorite__lte=int(
|
self._queryset = self._queryset.filter(favorite__lte=int(times_cooked.replace('-', ''))).exclude(favorite=0)
|
||||||
times_cooked.replace('-', ''))).exclude(favorite=0)
|
|
||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.filter(
|
self._queryset = self._queryset.filter(favorite__gte=int(times_cooked))
|
||||||
favorite__gte=int(times_cooked))
|
|
||||||
|
|
||||||
def keyword_filters(self, **kwargs):
|
def keyword_filters(self, **kwargs):
|
||||||
if all([kwargs[x] is None for x in kwargs]):
|
if all([kwargs[x] is None for x in kwargs]):
|
||||||
@ -385,8 +363,7 @@ class RecipeSearch():
|
|||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.filter(f_and)
|
self._queryset = self._queryset.filter(f_and)
|
||||||
if 'not' in kw_filter:
|
if 'not' in kw_filter:
|
||||||
self._queryset = self._queryset.exclude(
|
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||||
id__in=recipes.values('id'))
|
|
||||||
|
|
||||||
def food_filters(self, **kwargs):
|
def food_filters(self, **kwargs):
|
||||||
if all([kwargs[x] is None for x in kwargs]):
|
if all([kwargs[x] is None for x in kwargs]):
|
||||||
@ -400,8 +377,7 @@ class RecipeSearch():
|
|||||||
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
|
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
|
||||||
if 'or' in fd_filter:
|
if 'or' in fd_filter:
|
||||||
if self._include_children:
|
if self._include_children:
|
||||||
f_or = Q(
|
f_or = Q(steps__ingredients__food__in=Food.include_descendants(foods))
|
||||||
steps__ingredients__food__in=Food.include_descendants(foods))
|
|
||||||
else:
|
else:
|
||||||
f_or = Q(steps__ingredients__food__in=foods)
|
f_or = Q(steps__ingredients__food__in=foods)
|
||||||
|
|
||||||
@ -413,8 +389,7 @@ class RecipeSearch():
|
|||||||
recipes = Recipe.objects.all()
|
recipes = Recipe.objects.all()
|
||||||
for food in foods:
|
for food in foods:
|
||||||
if self._include_children:
|
if self._include_children:
|
||||||
f_and = Q(
|
f_and = Q(steps__ingredients__food__in=food.get_descendants_and_self())
|
||||||
steps__ingredients__food__in=food.get_descendants_and_self())
|
|
||||||
else:
|
else:
|
||||||
f_and = Q(steps__ingredients__food=food)
|
f_and = Q(steps__ingredients__food=food)
|
||||||
if 'not' in fd_filter:
|
if 'not' in fd_filter:
|
||||||
@ -422,8 +397,7 @@ class RecipeSearch():
|
|||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.filter(f_and)
|
self._queryset = self._queryset.filter(f_and)
|
||||||
if 'not' in fd_filter:
|
if 'not' in fd_filter:
|
||||||
self._queryset = self._queryset.exclude(
|
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||||
id__in=recipes.values('id'))
|
|
||||||
|
|
||||||
def unit_filters(self, units=None, operator=True):
|
def unit_filters(self, units=None, operator=True):
|
||||||
if operator != True:
|
if operator != True:
|
||||||
@ -432,8 +406,7 @@ class RecipeSearch():
|
|||||||
return
|
return
|
||||||
if not isinstance(units, list):
|
if not isinstance(units, list):
|
||||||
units = [units]
|
units = [units]
|
||||||
self._queryset = self._queryset.filter(
|
self._queryset = self._queryset.filter(steps__ingredients__unit__in=units)
|
||||||
steps__ingredients__unit__in=units)
|
|
||||||
|
|
||||||
def rating_filter(self, rating=None):
|
def rating_filter(self, rating=None):
|
||||||
if rating or self._sort_includes('rating'):
|
if rating or self._sort_includes('rating'):
|
||||||
@ -479,14 +452,11 @@ class RecipeSearch():
|
|||||||
recipes = Recipe.objects.all()
|
recipes = Recipe.objects.all()
|
||||||
for book in kwargs[bk_filter]:
|
for book in kwargs[bk_filter]:
|
||||||
if 'not' in bk_filter:
|
if 'not' in bk_filter:
|
||||||
recipes = recipes.filter(
|
recipes = recipes.filter(recipebookentry__book__id=book)
|
||||||
recipebookentry__book__id=book)
|
|
||||||
else:
|
else:
|
||||||
self._queryset = self._queryset.filter(
|
self._queryset = self._queryset.filter(recipebookentry__book__id=book)
|
||||||
recipebookentry__book__id=book)
|
|
||||||
if 'not' in bk_filter:
|
if 'not' in bk_filter:
|
||||||
self._queryset = self._queryset.exclude(
|
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||||
id__in=recipes.values('id'))
|
|
||||||
|
|
||||||
def step_filters(self, steps=None, operator=True):
|
def step_filters(self, steps=None, operator=True):
|
||||||
if operator != True:
|
if operator != True:
|
||||||
@ -505,25 +475,20 @@ class RecipeSearch():
|
|||||||
rank = []
|
rank = []
|
||||||
if 'name' in self._fulltext_include:
|
if 'name' in self._fulltext_include:
|
||||||
vectors.append('name_search_vector')
|
vectors.append('name_search_vector')
|
||||||
rank.append(SearchRank('name_search_vector',
|
rank.append(SearchRank('name_search_vector', self.search_query, cover_density=True))
|
||||||
self.search_query, cover_density=True))
|
|
||||||
if 'description' in self._fulltext_include:
|
if 'description' in self._fulltext_include:
|
||||||
vectors.append('desc_search_vector')
|
vectors.append('desc_search_vector')
|
||||||
rank.append(SearchRank('desc_search_vector',
|
rank.append(SearchRank('desc_search_vector', self.search_query, cover_density=True))
|
||||||
self.search_query, cover_density=True))
|
|
||||||
if 'steps__instruction' in self._fulltext_include:
|
if 'steps__instruction' in self._fulltext_include:
|
||||||
vectors.append('steps__search_vector')
|
vectors.append('steps__search_vector')
|
||||||
rank.append(SearchRank('steps__search_vector',
|
rank.append(SearchRank('steps__search_vector', self.search_query, cover_density=True))
|
||||||
self.search_query, cover_density=True))
|
|
||||||
if 'keywords__name' in self._fulltext_include:
|
if 'keywords__name' in self._fulltext_include:
|
||||||
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
|
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
|
||||||
vectors.append('keywords__name__unaccent')
|
vectors.append('keywords__name__unaccent')
|
||||||
rank.append(SearchRank('keywords__name__unaccent',
|
rank.append(SearchRank('keywords__name__unaccent', self.search_query, cover_density=True))
|
||||||
self.search_query, cover_density=True))
|
|
||||||
if 'steps__ingredients__food__name' in self._fulltext_include:
|
if 'steps__ingredients__food__name' in self._fulltext_include:
|
||||||
vectors.append('steps__ingredients__food__name__unaccent')
|
vectors.append('steps__ingredients__food__name__unaccent')
|
||||||
rank.append(SearchRank('steps__ingredients__food__name',
|
rank.append(SearchRank('steps__ingredients__food__name', self.search_query, cover_density=True))
|
||||||
self.search_query, cover_density=True))
|
|
||||||
|
|
||||||
for r in rank:
|
for r in rank:
|
||||||
if self.search_rank is None:
|
if self.search_rank is None:
|
||||||
@ -531,8 +496,7 @@ class RecipeSearch():
|
|||||||
else:
|
else:
|
||||||
self.search_rank += r
|
self.search_rank += r
|
||||||
# modifying queryset will annotation creates duplicate results
|
# modifying queryset will annotation creates duplicate results
|
||||||
self._filters.append(Q(id__in=Recipe.objects.annotate(
|
self._filters.append(Q(id__in=Recipe.objects.annotate(vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
|
||||||
vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
|
|
||||||
|
|
||||||
def build_text_filters(self, string=None):
|
def build_text_filters(self, string=None):
|
||||||
if not string:
|
if not string:
|
||||||
@ -557,15 +521,19 @@ class RecipeSearch():
|
|||||||
trigram += TrigramSimilarity(f, self._string)
|
trigram += TrigramSimilarity(f, self._string)
|
||||||
else:
|
else:
|
||||||
trigram = TrigramSimilarity(f, self._string)
|
trigram = TrigramSimilarity(f, self._string)
|
||||||
self._fuzzy_match = Recipe.objects.annotate(trigram=trigram).distinct(
|
self._fuzzy_match = (
|
||||||
).annotate(simularity=Max('trigram')).values('id', 'simularity').filter(simularity__gt=self._search_prefs.trigram_threshold)
|
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'))]
|
self._filters += [Q(pk__in=self._fuzzy_match.values('pk'))]
|
||||||
|
|
||||||
def _makenow_filter(self, missing=None):
|
def _makenow_filter(self, missing=None):
|
||||||
if missing is None or (isinstance(missing, bool) and missing == False):
|
if missing is None or (isinstance(missing, bool) and missing == False):
|
||||||
return
|
return
|
||||||
shopping_users = [
|
shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
|
||||||
*self._request.user.get_shopping_share(), self._request.user]
|
|
||||||
|
|
||||||
onhand_filter = (
|
onhand_filter = (
|
||||||
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
|
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
|
||||||
@ -575,264 +543,255 @@ class RecipeSearch():
|
|||||||
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
||||||
)
|
)
|
||||||
makenow_recipes = Recipe.objects.annotate(
|
makenow_recipes = Recipe.objects.annotate(
|
||||||
count_food=Count('steps__ingredients__food__pk', filter=Q(
|
count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
|
||||||
steps__ingredients__food__isnull=False), distinct=True),
|
count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
|
||||||
count_onhand=Count('steps__ingredients__food__pk',
|
count_ignore_shopping=Count(
|
||||||
filter=onhand_filter, distinct=True),
|
'steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True, steps__ingredients__food__recipe__isnull=True), 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_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(
|
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)), default=Value(0))
|
||||||
shopping_users), then=Value(1)), default=Value(0)),
|
|
||||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(
|
|
||||||
shopping_users), then=Value(1)), default=Value(0))
|
|
||||||
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood=missing)
|
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood=missing)
|
||||||
self._queryset = self._queryset.distinct().filter(
|
self._queryset = self._queryset.distinct().filter(id__in=makenow_recipes.values('id'))
|
||||||
id__in=makenow_recipes.values('id'))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __children_substitute_filter(shopping_users=None):
|
def __children_substitute_filter(shopping_users=None):
|
||||||
children_onhand_subquery = Food.objects.filter(
|
children_onhand_subquery = Food.objects.filter(path__startswith=OuterRef('path'), depth__gt=OuterRef('depth'), onhand_users__in=shopping_users)
|
||||||
path__startswith=OuterRef('path'),
|
return (
|
||||||
depth__gt=OuterRef('depth'),
|
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
|
||||||
onhand_users__in=shopping_users
|
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
|
@staticmethod
|
||||||
def __sibling_substitute_filter(shopping_users=None):
|
def __sibling_substitute_filter(shopping_users=None):
|
||||||
sibling_onhand_subquery = Food.objects.filter(
|
sibling_onhand_subquery = Food.objects.filter(
|
||||||
path__startswith=Substr(
|
path__startswith=Substr(OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)), depth=OuterRef('depth'), onhand_users__in=shopping_users
|
||||||
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
|
return (
|
||||||
Q(onhand_users__in=shopping_users)
|
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(ignore_shopping=True, recipe__isnull=True)
|
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
|
||||||
| Q(substitute__onhand_users__in=shopping_users)
|
)
|
||||||
).exclude(depth=1, numchild=0
|
.exclude(depth=1, numchild=0)
|
||||||
).filter(substitute_siblings=True
|
.filter(substitute_siblings=True)
|
||||||
).annotate(sibling_onhand=Exists(sibling_onhand_subquery)
|
.annotate(sibling_onhand=Exists(sibling_onhand_subquery))
|
||||||
).filter(sibling_onhand=True)
|
.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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_books(self):
|
|
||||||
if self.Books is None:
|
|
||||||
self.Books = []
|
|
||||||
return self.Books
|
|
||||||
|
|
||||||
def get_keywords(self):
|
# class RecipeFacet():
|
||||||
if self.Keywords is None:
|
# class CacheEmpty(Exception):
|
||||||
if self._search_params['search_keywords_or']:
|
# pass
|
||||||
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()
|
|
||||||
|
|
||||||
# set keywords to root objects only
|
# def __init__(self, request, queryset=None, hash_key=None, cache_timeout=3600):
|
||||||
keywords = self._keyword_queryset(keywords)
|
# if hash_key is None and queryset is None:
|
||||||
self.Keywords = [{**x, 'children': None}
|
# raise ValueError(_("One of queryset or hash_key must be provided"))
|
||||||
if x['numchild'] > 0 else x for x in list(keywords)]
|
|
||||||
self.set_cache('Keywords', self.Keywords)
|
|
||||||
return self.Keywords
|
|
||||||
|
|
||||||
def get_foods(self):
|
# self._request = request
|
||||||
if self.Foods is None:
|
# self._queryset = queryset
|
||||||
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
# self.hash_key = hash_key or str(hash(self._queryset.query))
|
||||||
if self._search_params['search_foods_or']:
|
# self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}"
|
||||||
foods = Food.objects.filter(
|
# self._cache_timeout = cache_timeout
|
||||||
space=self._request.space).distinct()
|
# self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, {})
|
||||||
else:
|
# if self._cache is None and self._queryset is None:
|
||||||
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(
|
# raise self.CacheEmpty("No queryset provided and cache empty")
|
||||||
depth=1)).filter(space=self._request.space).distinct()
|
|
||||||
|
|
||||||
# set keywords to root objects only
|
# self.Keywords = self._cache.get('Keywords', None)
|
||||||
foods = self._food_queryset(foods)
|
# 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 self._queryset is not None:
|
||||||
if x['numchild'] > 0 else x for x in list(foods)]
|
# self._recipe_list = list(
|
||||||
self.set_cache('Foods', self.Foods)
|
# self._queryset.values_list('id', flat=True))
|
||||||
return self.Foods
|
# 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):
|
# self._cache = {
|
||||||
if self.Ratings is None:
|
# **self._search_params,
|
||||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
# 'recipe_list': self._recipe_list,
|
||||||
if self._queryset is None:
|
# 'Ratings': self.Ratings,
|
||||||
self._queryset = Recipe.objects.filter(
|
# 'Recent': self.Recent,
|
||||||
id__in=self._recipe_list)
|
# 'Keywords': self.Keywords,
|
||||||
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(
|
# 'Foods': self.Foods,
|
||||||
cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
|
# 'Books': self.Books
|
||||||
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:
|
# caches['default'].set(self._SEARCH_CACHE_KEY,
|
||||||
# TODO make days of recent recipe a setting
|
# self._cache, self._cache_timeout)
|
||||||
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):
|
# def get_facets(self, from_cache=False):
|
||||||
try:
|
# if from_cache:
|
||||||
food = Food.objects.get(id=id)
|
# return {
|
||||||
nodes = food.get_ancestors()
|
# 'cache_key': self.hash_key or '',
|
||||||
except Food.DoesNotExist:
|
# 'Ratings': self.Ratings or {},
|
||||||
return self.get_facets()
|
# 'Recent': self.Recent or [],
|
||||||
foods = self._food_queryset(food.get_children(), food)
|
# 'Keywords': self.Keywords or [],
|
||||||
deep_search = self.Foods
|
# 'Foods': self.Foods or [],
|
||||||
for node in nodes:
|
# 'Books': self.Books or []
|
||||||
index = next((i for i, x in enumerate(
|
# }
|
||||||
deep_search) if x["id"] == node.id), None)
|
# return {
|
||||||
deep_search = deep_search[index]['children']
|
# 'cache_key': self.hash_key,
|
||||||
index = next((i for i, x in enumerate(
|
# 'Ratings': self.get_ratings(),
|
||||||
deep_search) if x["id"] == food.id), None)
|
# 'Recent': self.get_recent(),
|
||||||
deep_search[index]['children'] = [
|
# 'Keywords': self.get_keywords(),
|
||||||
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
# 'Foods': self.get_foods(),
|
||||||
self.set_cache('Foods', self.Foods)
|
# 'Books': self.get_books()
|
||||||
return self.get_facets()
|
# }
|
||||||
|
|
||||||
def add_keyword_children(self, id):
|
# def set_cache(self, key, value):
|
||||||
try:
|
# self._cache = {**self._cache, key: value}
|
||||||
keyword = Keyword.objects.get(id=id)
|
# caches['default'].set(
|
||||||
nodes = keyword.get_ancestors()
|
# self._SEARCH_CACHE_KEY,
|
||||||
except Keyword.DoesNotExist:
|
# self._cache,
|
||||||
return self.get_facets()
|
# self._cache_timeout
|
||||||
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):
|
# def get_books(self):
|
||||||
return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path'), f'{field}__depth__gte': depth}, id__in=self._recipe_list, space=self._request.space
|
# if self.Books is None:
|
||||||
).annotate(count=Coalesce(Func('pk', function='Count'), 0)).values('count')
|
# self.Books = []
|
||||||
|
# return self.Books
|
||||||
|
|
||||||
def _keyword_queryset(self, queryset, keyword=None):
|
# def get_keywords(self):
|
||||||
depth = getattr(keyword, 'depth', 0) + 1
|
# if self.Keywords is None:
|
||||||
steplen = depth * Keyword.steplen
|
# 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:
|
# # set keywords to root objects only
|
||||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0)
|
# keywords = self._keyword_queryset(keywords)
|
||||||
).filter(depth=depth, count__gt=0
|
# self.Keywords = [{**x, 'children': None}
|
||||||
).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
# if x['numchild'] > 0 else x for x in list(keywords)]
|
||||||
else:
|
# self.set_cache('Keywords', self.Keywords)
|
||||||
return queryset.filter(depth=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
# return self.Keywords
|
||||||
|
|
||||||
def _food_queryset(self, queryset, food=None):
|
# def get_foods(self):
|
||||||
depth = getattr(food, 'depth', 0) + 1
|
# if self.Foods is None:
|
||||||
steplen = depth * Food.steplen
|
# # # 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:
|
# # set keywords to root objects only
|
||||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0)
|
# foods = self._food_queryset(foods)
|
||||||
).filter(depth__lte=depth, count__gt=0
|
|
||||||
).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
|
# self.Foods = [{**x, 'children': None}
|
||||||
else:
|
# if x['numchild'] > 0 else x for x in list(foods)]
|
||||||
return queryset.filter(depth__lte=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
|
# 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,4 +1,3 @@
|
|||||||
# import random
|
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
from html import unescape
|
from html import unescape
|
||||||
@ -11,15 +10,9 @@ from isodate.isoerror import ISO8601Error
|
|||||||
from pytube import YouTube
|
from pytube import YouTube
|
||||||
from recipe_scrapers._utils import get_host_name, get_minutes
|
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.helper.ingredient_parser import IngredientParser
|
||||||
from cookbook.models import Automation, Keyword, PropertyType
|
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):
|
def get_from_scraper(scrape, request):
|
||||||
# converting the scrape_me object to the existing json format based on ld+json
|
# converting the scrape_me object to the existing json format based on ld+json
|
||||||
@ -147,7 +140,7 @@ def get_from_scraper(scrape, request):
|
|||||||
recipe_json['steps'] = []
|
recipe_json['steps'] = []
|
||||||
try:
|
try:
|
||||||
for i in parse_instructions(scrape.instructions()):
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if len(recipe_json['steps']) == 0:
|
if len(recipe_json['steps']) == 0:
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
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 import F, OuterRef, Q, Subquery, Value
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
|
||||||
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
|
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
|
||||||
SupermarketCategoryRelation)
|
SupermarketCategoryRelation)
|
||||||
from recipes import settings
|
|
||||||
|
|
||||||
|
|
||||||
def shopping_helper(qs, request):
|
def shopping_helper(qs, request):
|
||||||
@ -47,7 +44,7 @@ class RecipeShoppingEditor():
|
|||||||
self.mealplan = self._kwargs.get('mealplan', None)
|
self.mealplan = self._kwargs.get('mealplan', None)
|
||||||
if type(self.mealplan) in [int, float]:
|
if type(self.mealplan) in [int, float]:
|
||||||
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
|
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.mealplan = MealPlan.objects.filter(id=self.mealplan['id'], space=self.space).first()
|
||||||
self.id = self._kwargs.get('id', None)
|
self.id = self._kwargs.get('id', None)
|
||||||
|
|
||||||
@ -69,11 +66,12 @@ class RecipeShoppingEditor():
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def _recipe_servings(self):
|
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
|
@property
|
||||||
def _servings_factor(self):
|
def _servings_factor(self):
|
||||||
return Decimal(self.servings)/Decimal(self._recipe_servings)
|
return Decimal(self.servings) / Decimal(self._recipe_servings)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _shared_users(self):
|
def _shared_users(self):
|
||||||
@ -90,9 +88,10 @@ class RecipeShoppingEditor():
|
|||||||
|
|
||||||
def get_recipe_ingredients(self, id, exclude_onhand=False):
|
def get_recipe_ingredients(self, id, exclude_onhand=False):
|
||||||
if exclude_onhand:
|
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:
|
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
|
@property
|
||||||
def _include_related(self):
|
def _include_related(self):
|
||||||
@ -109,7 +108,7 @@ class RecipeShoppingEditor():
|
|||||||
self.servings = float(servings)
|
self.servings = float(servings)
|
||||||
|
|
||||||
if mealplan := kwargs.get('mealplan', None):
|
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()
|
self.mealplan = MealPlan.objects.filter(id=mealplan['id'], space=self.space).first()
|
||||||
else:
|
else:
|
||||||
self.mealplan = mealplan
|
self.mealplan = mealplan
|
||||||
@ -170,13 +169,13 @@ class RecipeShoppingEditor():
|
|||||||
try:
|
try:
|
||||||
self._shopping_list_recipe.delete()
|
self._shopping_list_recipe.delete()
|
||||||
return True
|
return True
|
||||||
except:
|
except BaseException:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _add_ingredients(self, ingredients=None):
|
def _add_ingredients(self, ingredients=None):
|
||||||
if not ingredients:
|
if not ingredients:
|
||||||
return
|
return
|
||||||
elif type(ingredients) == list:
|
elif isinstance(ingredients, list):
|
||||||
ingredients = Ingredient.objects.filter(id__in=ingredients)
|
ingredients = Ingredient.objects.filter(id__in=ingredients)
|
||||||
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
|
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
|
||||||
add_ingredients = ingredients.exclude(id__in=existing)
|
add_ingredients = ingredients.exclude(id__in=existing)
|
||||||
@ -315,4 +314,4 @@ class RecipeShoppingEditor():
|
|||||||
# )
|
# )
|
||||||
|
|
||||||
# # return all shopping list items
|
# # return all shopping list items
|
||||||
# return list_recipe
|
# return list_recipe
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
import base64
|
|
||||||
import gzip
|
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
from gettext import gettext as _
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import validators
|
import validators
|
||||||
import yaml
|
|
||||||
|
|
||||||
from cookbook.helper.ingredient_parser import IngredientParser
|
from cookbook.helper.ingredient_parser import IngredientParser
|
||||||
from cookbook.helper.recipe_url_import import (get_from_scraper, get_images_from_soup,
|
from cookbook.helper.recipe_url_import import (get_from_scraper, get_images_from_soup,
|
||||||
iso_duration_to_minutes)
|
iso_duration_to_minutes)
|
||||||
from cookbook.helper.scrapers.scrapers import text_scraper
|
from cookbook.helper.scrapers.scrapers import text_scraper
|
||||||
from cookbook.integration.integration import Integration
|
from cookbook.integration.integration import Integration
|
||||||
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
from cookbook.models import Ingredient, Recipe, Step
|
||||||
|
|
||||||
|
|
||||||
class CookBookApp(Integration):
|
class CookBookApp(Integration):
|
||||||
@ -47,7 +42,8 @@ class CookBookApp(Integration):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# assuming import files only contain single step
|
# 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:
|
if 'nutrition' in recipe_json:
|
||||||
step.instruction = step.instruction + '\n\n' + recipe_json['nutrition']
|
step.instruction = step.instruction + '\n\n' + recipe_json['nutrition']
|
||||||
@ -62,7 +58,7 @@ class CookBookApp(Integration):
|
|||||||
if unit := ingredient.get('unit', None):
|
if unit := ingredient.get('unit', None):
|
||||||
u = ingredient_parser.get_unit(unit.get('name', None))
|
u = ingredient_parser.get_unit(unit.get('name', None))
|
||||||
step.ingredients.add(Ingredient.objects.create(
|
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:
|
if len(images) > 0:
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import traceback
|
|
||||||
import datetime
|
import datetime
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
@ -18,8 +17,7 @@ from lxml import etree
|
|||||||
|
|
||||||
from cookbook.helper.image_processing import handle_image
|
from cookbook.helper.image_processing import handle_image
|
||||||
from cookbook.models import Keyword, Recipe
|
from cookbook.models import Keyword, Recipe
|
||||||
from recipes.settings import DEBUG
|
from recipes.settings import DEBUG, EXPORT_FILE_CACHE_DURATION
|
||||||
from recipes.settings import EXPORT_FILE_CACHE_DURATION
|
|
||||||
|
|
||||||
|
|
||||||
class Integration:
|
class Integration:
|
||||||
@ -63,12 +61,10 @@ class Integration:
|
|||||||
space=request.space
|
space=request.space
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def do_export(self, recipes, el):
|
def do_export(self, recipes, el):
|
||||||
|
|
||||||
with scope(space=self.request.space):
|
with scope(space=self.request.space):
|
||||||
el.total_recipes = len(recipes)
|
el.total_recipes = len(recipes)
|
||||||
el.cache_duration = EXPORT_FILE_CACHE_DURATION
|
el.cache_duration = EXPORT_FILE_CACHE_DURATION
|
||||||
el.save()
|
el.save()
|
||||||
|
|
||||||
@ -80,7 +76,7 @@ class Integration:
|
|||||||
export_file = file
|
export_file = file
|
||||||
|
|
||||||
else:
|
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_filename = self.get_export_file_name()
|
||||||
export_stream = BytesIO()
|
export_stream = BytesIO()
|
||||||
export_obj = ZipFile(export_stream, 'w')
|
export_obj = ZipFile(export_stream, 'w')
|
||||||
@ -91,8 +87,7 @@ class Integration:
|
|||||||
export_obj.close()
|
export_obj.close()
|
||||||
export_file = export_stream.getvalue()
|
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.running = False
|
||||||
el.save()
|
el.save()
|
||||||
|
|
||||||
@ -100,7 +95,6 @@ class Integration:
|
|||||||
response['Content-Disposition'] = 'attachment; filename="' + export_filename + '"'
|
response['Content-Disposition'] = 'attachment; filename="' + export_filename + '"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def import_file_name_filter(self, zip_info_object):
|
def import_file_name_filter(self, zip_info_object):
|
||||||
"""
|
"""
|
||||||
Since zipfile.namelist() returns all files in all subdirectories this function allows filtering of files
|
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:
|
for z in file_list:
|
||||||
try:
|
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)
|
recipe = self.get_recipe_from_file(z)
|
||||||
else:
|
else:
|
||||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||||
@ -298,7 +292,6 @@ class Integration:
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
def get_export_file_name(self, format='zip'):
|
def get_export_file_name(self, format='zip'):
|
||||||
return "export_{}.{}".format(datetime.datetime.now().strftime("%Y-%m-%d"), format)
|
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
|
:param filter: Filter (include) the descendants nodes with the provided Q filter
|
||||||
"""
|
"""
|
||||||
descendants = Q()
|
descendants = Q()
|
||||||
# TODO filter the queryset nodes to exclude descendants of objects in the queryset
|
|
||||||
nodes = queryset.values('path', 'depth')
|
nodes = queryset.values('path', 'depth')
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
descendants |= Q(path__startswith=node['path'], depth__gt=node['depth'])
|
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)
|
no_sharing_limit = models.BooleanField(default=False)
|
||||||
demo = models.BooleanField(default=False)
|
demo = models.BooleanField(default=False)
|
||||||
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
|
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
|
||||||
show_facet_count = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
internal_note = models.TextField(blank=True, null=True)
|
internal_note = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import random
|
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@ -7,34 +6,34 @@ from gettext import gettext as _
|
|||||||
from html import escape
|
from html import escape
|
||||||
from smtplib import SMTPException
|
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.cache import caches
|
||||||
from django.core.mail import send_mail
|
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.http import BadHeaderError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
|
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
|
||||||
from PIL import Image
|
|
||||||
from oauth2_provider.models import AccessToken
|
from oauth2_provider.models import AccessToken
|
||||||
|
from PIL import Image
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import NotFound, ValidationError
|
from rest_framework.exceptions import NotFound, ValidationError
|
||||||
|
|
||||||
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
|
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
|
||||||
from cookbook.helper.HelperFunctions import str2bool
|
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.permission_helper import above_space_limit
|
||||||
|
from cookbook.helper.property_helper import FoodPropertyHelper
|
||||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||||
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
|
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
|
||||||
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter,
|
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter,
|
||||||
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink,
|
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink,
|
||||||
Keyword, MealPlan, MealType, NutritionInformation, Recipe, RecipeBook,
|
Keyword, MealPlan, MealType, NutritionInformation, Property,
|
||||||
RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
|
PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport,
|
||||||
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
|
||||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
|
Step, Storage, Supermarket, SupermarketCategory,
|
||||||
SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, Property,
|
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
||||||
PropertyType, Property)
|
UserFile, UserPreference, UserSpace, ViewLog)
|
||||||
from cookbook.templatetags.custom_tags import markdown
|
from cookbook.templatetags.custom_tags import markdown
|
||||||
from recipes.settings import AWS_ENABLED, MEDIA_URL
|
from recipes.settings import AWS_ENABLED, MEDIA_URL
|
||||||
|
|
||||||
@ -60,7 +59,7 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
|||||||
if str2bool(
|
if str2bool(
|
||||||
self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||||
return fields
|
return fields
|
||||||
except (AttributeError, KeyError) as e:
|
except (AttributeError, KeyError):
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
del fields['image']
|
del fields['image']
|
||||||
@ -104,9 +103,9 @@ class CustomDecimalField(serializers.Field):
|
|||||||
return round(value, 2).normalize()
|
return round(value, 2).normalize()
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if type(data) == int or type(data) == float:
|
if isinstance(data, int) or isinstance(data, float):
|
||||||
return data
|
return data
|
||||||
elif type(data) == str:
|
elif isinstance(data, str):
|
||||||
if data == '':
|
if data == '':
|
||||||
return 0
|
return 0
|
||||||
try:
|
try:
|
||||||
@ -146,11 +145,11 @@ class SpaceFilterSerializer(serializers.ListSerializer):
|
|||||||
def to_representation(self, data):
|
def to_representation(self, data):
|
||||||
if self.context.get('request', None) is None:
|
if self.context.get('request', None) is None:
|
||||||
return
|
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
|
# if query is sliced it came from api request not nested serializer
|
||||||
return super().to_representation(data)
|
return super().to_representation(data)
|
||||||
if self.child.Meta.model == User:
|
if self.child.Meta.model == User:
|
||||||
if type(self.context['request'].user) == AnonymousUser:
|
if isinstance(self.context['request'].user, AnonymousUser):
|
||||||
data = []
|
data = []
|
||||||
else:
|
else:
|
||||||
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
|
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
|
||||||
@ -302,7 +301,7 @@ class SpaceSerializer(WritableNestedModelSerializer):
|
|||||||
model = Space
|
model = Space
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
|
'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',)
|
'image', 'use_plural',)
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
|
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
|
||||||
@ -636,7 +635,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
|||||||
validated_data['recipe'] = Recipe.objects.get(**recipe)
|
validated_data['recipe'] = Recipe.objects.get(**recipe)
|
||||||
|
|
||||||
# assuming if on hand for user also onhand for shopping_share users
|
# 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())
|
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
|
||||||
if self.instance:
|
if self.instance:
|
||||||
onhand_users = self.instance.onhand_users.all()
|
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
|
# assuming if on hand for user also onhand for shopping_share users
|
||||||
onhand = validated_data.get('food_onhand', None)
|
onhand = validated_data.get('food_onhand', None)
|
||||||
reset_inherit = self.initial_data.get('reset_inherit', False)
|
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())
|
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
|
||||||
if onhand:
|
if onhand:
|
||||||
validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users
|
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):
|
def get_step_recipe_data(self, obj):
|
||||||
# check if root type is recipe to prevent infinite recursion
|
# check if root type is recipe to prevent infinite recursion
|
||||||
# can be improved later to allow multi level embedding
|
# 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
|
return StepRecipeSerializer(obj.step_recipe, context={'request': self.context['request']}).data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -956,8 +955,7 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
book = validated_data['book']
|
book = validated_data['book']
|
||||||
recipe = validated_data['recipe']
|
recipe = validated_data['recipe']
|
||||||
if not book.get_owner() == self.context['request'].user and not self.context[
|
if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
|
||||||
'request'].user in book.get_shared():
|
|
||||||
raise NotFound(detail=None, code=None)
|
raise NotFound(detail=None, code=None)
|
||||||
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
|
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
|
||||||
return obj
|
return obj
|
||||||
@ -1023,10 +1021,10 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
|||||||
value = value.quantize(
|
value = value.quantize(
|
||||||
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
||||||
return (
|
return (
|
||||||
obj.name
|
obj.name
|
||||||
or getattr(obj.mealplan, 'title', None)
|
or getattr(obj.mealplan, 'title', None)
|
||||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||||
or obj.recipe.name
|
or obj.recipe.name
|
||||||
) + f' ({value:.2g})'
|
) + f' ({value:.2g})'
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
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 rest_framework.schemas import get_schema_view
|
||||||
|
|
||||||
from cookbook.helper import dal
|
from cookbook.helper import dal
|
||||||
from recipes.settings import DEBUG, PLUGINS
|
|
||||||
from cookbook.version_info import TANDOOR_VERSION
|
from cookbook.version_info import TANDOOR_VERSION
|
||||||
|
from recipes.settings import DEBUG, PLUGINS
|
||||||
|
|
||||||
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe,
|
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan,
|
||||||
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage,
|
PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList,
|
||||||
Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UserFile,
|
Space, Step, Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit,
|
||||||
get_model_name, UserSpace, Space, PropertyType, UnitConversion)
|
UnitConversion, UserFile, UserSpace, get_model_name)
|
||||||
from .views import api, data, delete, edit, import_export, lists, new, telegram, views
|
from .views import api, data, delete, edit, import_export, lists, new, telegram, views
|
||||||
from .views.api import CustomAuthToken, ImportOpenData
|
from .views.api import CustomAuthToken, ImportOpenData
|
||||||
|
|
||||||
|
|
||||||
# extend DRF default router class to allow including additional routers
|
# extend DRF default router class to allow including additional routers
|
||||||
class DefaultRouter(routers.DefaultRouter):
|
class DefaultRouter(routers.DefaultRouter):
|
||||||
def extend(self, r):
|
def extend(self, r):
|
||||||
@ -131,7 +132,6 @@ urlpatterns = [
|
|||||||
path('api/backup/', api.get_backup, name='api_backup'),
|
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/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/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/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/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'),
|
path('api/download-file/<int:file_id>/', api.download_file, name='api_download_file'),
|
||||||
|
@ -3,7 +3,6 @@ import io
|
|||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import pathlib
|
import pathlib
|
||||||
import random
|
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
@ -15,7 +14,6 @@ from zipfile import ZipFile
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
import validators
|
import validators
|
||||||
from PIL import UnidentifiedImageError
|
|
||||||
from annoying.decorators import ajax_request
|
from annoying.decorators import ajax_request
|
||||||
from annoying.functions import get_object_or_None
|
from annoying.functions import get_object_or_None
|
||||||
from django.contrib import messages
|
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.cache import caches
|
||||||
from django.core.exceptions import FieldError, ValidationError
|
from django.core.exceptions import FieldError, ValidationError
|
||||||
from django.core.files import File
|
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.fields.related import ForeignObjectRel
|
||||||
from django.db.models.functions import Coalesce, Lower
|
from django.db.models.functions import Coalesce, Lower
|
||||||
from django.db.models.signals import post_save
|
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 django_scopes import scopes_disabled
|
||||||
from icalendar import Calendar, Event
|
from icalendar import Calendar, Event
|
||||||
from oauth2_provider.models import AccessToken
|
from oauth2_provider.models import AccessToken
|
||||||
|
from PIL import UnidentifiedImageError
|
||||||
from recipe_scrapers import scrape_me
|
from recipe_scrapers import scrape_me
|
||||||
from recipe_scrapers._exceptions import NoSchemaFoundInWildMode
|
from recipe_scrapers._exceptions import NoSchemaFoundInWildMode
|
||||||
from requests.exceptions import MissingSchema
|
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.image_processing import handle_image
|
||||||
from cookbook.helper.ingredient_parser import IngredientParser
|
from cookbook.helper.ingredient_parser import IngredientParser
|
||||||
from cookbook.helper.open_data_importer import OpenDataImporter
|
from cookbook.helper.open_data_importer import OpenDataImporter
|
||||||
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner,
|
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, CustomIsOwnerReadOnly,
|
||||||
CustomIsOwnerReadOnly, CustomIsShared,
|
CustomIsShared, CustomIsSpaceOwner, CustomIsUser,
|
||||||
CustomIsSpaceOwner, CustomIsUser, group_required,
|
CustomRecipePermission, CustomTokenHasReadWriteScope,
|
||||||
is_space_owner, switch_user_active_space, above_space_limit,
|
CustomTokenHasScope, CustomUserPermission,
|
||||||
CustomRecipePermission, CustomUserPermission,
|
IsReadOnlyDRF, above_space_limit, group_required,
|
||||||
CustomTokenHasReadWriteScope, CustomTokenHasScope, has_group_permission, IsReadOnlyDRF)
|
has_group_permission, is_space_owner,
|
||||||
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch
|
switch_user_active_space)
|
||||||
from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup, clean_dict
|
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.scrapers.scrapers import text_scraper
|
||||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
|
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
|
||||||
from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food,
|
from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food,
|
||||||
FoodInheritField, ImportLog, Ingredient, InviteLink, Keyword, MealPlan,
|
FoodInheritField, ImportLog, Ingredient, InviteLink, Keyword, MealPlan,
|
||||||
MealType, Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
|
MealType, Property, PropertyType, Recipe, RecipeBook, RecipeBookEntry,
|
||||||
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
|
||||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
|
Step, Storage, Supermarket, SupermarketCategory,
|
||||||
SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, PropertyType, Property)
|
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
||||||
|
UserFile, UserPreference, UserSpace, ViewLog)
|
||||||
from cookbook.provider.dropbox import Dropbox
|
from cookbook.provider.dropbox import Dropbox
|
||||||
from cookbook.provider.local import Local
|
from cookbook.provider.local import Local
|
||||||
from cookbook.provider.nextcloud import Nextcloud
|
from cookbook.provider.nextcloud import Nextcloud
|
||||||
from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema
|
from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema
|
||||||
from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSerializer,
|
from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
|
||||||
|
AutoMealPlanSerializer, BookmarkletImportListSerializer,
|
||||||
BookmarkletImportSerializer, CookLogSerializer,
|
BookmarkletImportSerializer, CookLogSerializer,
|
||||||
CustomFilterSerializer, ExportLogSerializer,
|
CustomFilterSerializer, ExportLogSerializer,
|
||||||
FoodInheritFieldSerializer, FoodSerializer,
|
FoodInheritFieldSerializer, FoodSerializer,
|
||||||
FoodShoppingUpdateSerializer, GroupSerializer, ImportLogSerializer,
|
FoodShoppingUpdateSerializer, FoodSimpleSerializer,
|
||||||
IngredientSerializer, IngredientSimpleSerializer,
|
GroupSerializer, ImportLogSerializer, IngredientSerializer,
|
||||||
InviteLinkSerializer, KeywordSerializer, MealPlanSerializer,
|
IngredientSimpleSerializer, InviteLinkSerializer,
|
||||||
MealTypeSerializer, RecipeBookEntrySerializer,
|
KeywordSerializer, MealPlanSerializer, MealTypeSerializer,
|
||||||
RecipeBookSerializer, RecipeFromSourceSerializer,
|
PropertySerializer, PropertyTypeSerializer,
|
||||||
|
RecipeBookEntrySerializer, RecipeBookSerializer,
|
||||||
|
RecipeExportSerializer, RecipeFromSourceSerializer,
|
||||||
RecipeImageSerializer, RecipeOverviewSerializer, RecipeSerializer,
|
RecipeImageSerializer, RecipeOverviewSerializer, RecipeSerializer,
|
||||||
RecipeShoppingUpdateSerializer, RecipeSimpleSerializer,
|
RecipeShoppingUpdateSerializer, RecipeSimpleSerializer,
|
||||||
ShoppingListAutoSyncSerializer, ShoppingListEntrySerializer,
|
ShoppingListAutoSyncSerializer, ShoppingListEntrySerializer,
|
||||||
@ -94,11 +99,9 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
|
|||||||
SpaceSerializer, StepSerializer, StorageSerializer,
|
SpaceSerializer, StepSerializer, StorageSerializer,
|
||||||
SupermarketCategoryRelationSerializer,
|
SupermarketCategoryRelationSerializer,
|
||||||
SupermarketCategorySerializer, SupermarketSerializer,
|
SupermarketCategorySerializer, SupermarketSerializer,
|
||||||
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
SyncLogSerializer, SyncSerializer, UnitConversionSerializer,
|
||||||
UserFileSerializer, UserSerializer, UserPreferenceSerializer,
|
UnitSerializer, UserFileSerializer, UserPreferenceSerializer,
|
||||||
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer,
|
UserSerializer, UserSpaceSerializer, ViewLogSerializer)
|
||||||
RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer,
|
|
||||||
PropertySerializer, AutoMealPlanSerializer)
|
|
||||||
from cookbook.views.import_export import get_integration
|
from cookbook.views.import_export import get_integration
|
||||||
from recipes import settings
|
from recipes import settings
|
||||||
|
|
||||||
@ -186,7 +189,8 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
|||||||
if query is not None and query not in ["''", '']:
|
if query is not None and query not in ["''", '']:
|
||||||
if fuzzy and (settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
if fuzzy and (settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
||||||
'django.db.backends.postgresql']):
|
'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))
|
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
|
||||||
else:
|
else:
|
||||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
|
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')
|
child.move(parent, f'{node_location}-child')
|
||||||
content = {'msg': _(f'{child.name} was moved successfully to parent {parent.name}')}
|
content = {'msg': _(f'{child.name} was moved successfully to parent {parent.name}')}
|
||||||
return Response(content, status=status.HTTP_200_OK)
|
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}
|
content = {'error': True, 'msg': _('An error occurred attempting to move ') + child.name}
|
||||||
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -775,8 +779,7 @@ class StepViewSet(viewsets.ModelViewSet):
|
|||||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||||
pagination_class = DefaultPagination
|
pagination_class = DefaultPagination
|
||||||
query_params = [
|
query_params = [
|
||||||
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'),
|
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'), qtype='int'),
|
||||||
qtype='int'),
|
|
||||||
QueryParam(name='query', description=_('Query string matched (fuzzy) against object name.'), qtype='string'),
|
QueryParam(name='query', description=_('Query string matched (fuzzy) against object name.'), qtype='string'),
|
||||||
]
|
]
|
||||||
schema = QueryParamAutoSchema()
|
schema = QueryParamAutoSchema()
|
||||||
@ -799,7 +802,6 @@ class RecipePagination(PageNumberPagination):
|
|||||||
def paginate_queryset(self, queryset, request, view=None):
|
def paginate_queryset(self, queryset, request, view=None):
|
||||||
if queryset is None:
|
if queryset is None:
|
||||||
raise Exception
|
raise Exception
|
||||||
self.facets = RecipeFacet(request, queryset=queryset)
|
|
||||||
return super().paginate_queryset(queryset, request, view)
|
return super().paginate_queryset(queryset, request, view)
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
@ -808,7 +810,6 @@ class RecipePagination(PageNumberPagination):
|
|||||||
('next', self.get_next_link()),
|
('next', self.get_next_link()),
|
||||||
('previous', self.get_previous_link()),
|
('previous', self.get_previous_link()),
|
||||||
('results', data),
|
('results', data),
|
||||||
('facets', self.facets.get_facets(from_cache=True))
|
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
|
||||||
@ -820,63 +821,33 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
pagination_class = RecipePagination
|
pagination_class = RecipePagination
|
||||||
|
|
||||||
query_params = [
|
query_params = [
|
||||||
QueryParam(name='query', description=_(
|
QueryParam(name='query', description=_('Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
|
||||||
'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', description=_(
|
QueryParam(name='keywords_or', description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'), qtype='int'),
|
||||||
'ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'),
|
QueryParam(name='keywords_and', description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'), qtype='int'),
|
||||||
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_or',
|
QueryParam(name='keywords_and_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'), qtype='int'),
|
||||||
description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'),
|
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'), qtype='int'),
|
||||||
qtype='int'),
|
QueryParam(name='foods_or', description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), qtype='int'),
|
||||||
QueryParam(name='keywords_and',
|
QueryParam(name='foods_and', description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), qtype='int'),
|
||||||
description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'),
|
QueryParam(name='foods_or_not', description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), qtype='int'),
|
||||||
qtype='int'),
|
QueryParam(name='foods_and_not', description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), 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='units', description=_('ID of unit a recipe should have.'), qtype='int'),
|
||||||
QueryParam(name='rating', description=_(
|
QueryParam(name='rating', description=_('Rating a recipe should have or greater. [0 - 5] Negative value filters rating less than.'), qtype='int'),
|
||||||
'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', description=_('ID of book a recipe should be in. For multiple repeat parameter.')),
|
||||||
QueryParam(name='books_or',
|
QueryParam(name='books_or', description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), qtype='int'),
|
||||||
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_and',
|
QueryParam(name='books_or_not', description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
|
||||||
description=_('Book IDs, repeat for multiple. Return recipes with all 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='books_or_not',
|
QueryParam(name='internal', description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
||||||
description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
|
QueryParam(name='random', description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||||
QueryParam(name='books_and_not',
|
QueryParam(name='new', description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||||
description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
|
QueryParam(name='timescooked', description=_('Filter recipes cooked X times or more. Negative values returns cooked less than X times'), qtype='int'),
|
||||||
QueryParam(name='internal',
|
QueryParam(name='cookedon', description=_('Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||||
description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
QueryParam(name='createdon', description=_('Filter recipes created on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||||
QueryParam(name='random',
|
QueryParam(name='updatedon', description=_('Filter recipes updated on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||||
description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
QueryParam(name='viewedon', description=_('Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||||
QueryParam(name='new',
|
QueryParam(name='makenow', description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']')),
|
||||||
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()
|
schema = QueryParamAutoSchema()
|
||||||
|
|
||||||
@ -1095,17 +1066,10 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = ShoppingListEntrySerializer
|
serializer_class = ShoppingListEntrySerializer
|
||||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||||
query_params = [
|
query_params = [
|
||||||
QueryParam(name='id',
|
QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='int'),
|
||||||
description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'),
|
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.')
|
||||||
qtype='int'),
|
),
|
||||||
QueryParam(
|
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'),
|
||||||
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()
|
schema = QueryParamAutoSchema()
|
||||||
|
|
||||||
@ -1344,12 +1308,10 @@ def recipe_from_source(request):
|
|||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
elif url and not data:
|
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):
|
if validators.url(url, public=True):
|
||||||
return Response({
|
return Response({
|
||||||
'recipe_json': get_from_youtube_scraper(url, request),
|
'recipe_json': get_from_youtube_scraper(url, request),
|
||||||
# 'recipe_tree': '',
|
|
||||||
# 'recipe_html': '',
|
|
||||||
'recipe_images': [],
|
'recipe_images': [],
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
if re.match(
|
if re.match(
|
||||||
@ -1412,8 +1374,6 @@ def recipe_from_source(request):
|
|||||||
if scrape:
|
if scrape:
|
||||||
return Response({
|
return Response({
|
||||||
'recipe_json': helper.get_from_scraper(scrape, request),
|
'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))),
|
'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))),
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@ -1437,7 +1397,7 @@ def reset_food_inheritance(request):
|
|||||||
try:
|
try:
|
||||||
Food.reset_inheritance(space=request.space)
|
Food.reset_inheritance(space=request.space)
|
||||||
return Response({'message': 'success', }, status=status.HTTP_200_OK)
|
return Response({'message': 'success', }, status=status.HTTP_200_OK)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
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)
|
return Response(UserSpaceSerializer().to_representation(instance=user_space), status=status.HTTP_200_OK)
|
||||||
else:
|
else:
|
||||||
return Response("not found", status=status.HTTP_404_NOT_FOUND)
|
return Response("not found", status=status.HTTP_404_NOT_FOUND)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
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"'
|
response['Content-Disposition'] = 'attachment; filename="' + uf.name + '.zip"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -1710,25 +1670,3 @@ def ingredient_from_string(request):
|
|||||||
},
|
},
|
||||||
status=200
|
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 re
|
||||||
import threading
|
import threading
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.core.cache import cache
|
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.shortcuts import get_object_or_404, render
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from cookbook.forms import ExportForm, ImportExportBase, ImportForm
|
from cookbook.forms import ExportForm, ImportExportBase
|
||||||
from cookbook.helper.permission_helper import group_required, above_space_limit
|
from cookbook.helper.permission_helper import group_required
|
||||||
from cookbook.helper.recipe_search import RecipeSearch
|
from cookbook.helper.recipe_search import RecipeSearch
|
||||||
from cookbook.integration.cheftap import ChefTap
|
from cookbook.integration.cheftap import ChefTap
|
||||||
from cookbook.integration.chowdown import Chowdown
|
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.rezeptsuitede import Rezeptsuitede
|
||||||
from cookbook.integration.rezkonv import RezKonv
|
from cookbook.integration.rezkonv import RezKonv
|
||||||
from cookbook.integration.saffron import Saffron
|
from cookbook.integration.saffron import Saffron
|
||||||
from cookbook.models import ExportLog, ImportLog, Recipe, UserPreference
|
from cookbook.models import ExportLog, Recipe
|
||||||
from recipes import settings
|
from recipes import settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/eslint-parser": "^7.21.3",
|
"@babel/eslint-parser": "^7.21.3",
|
||||||
"@popperjs/core": "^2.11.7",
|
"@popperjs/core": "^2.11.7",
|
||||||
"@riophae/vue-treeselect": "^0.4.0",
|
|
||||||
"@vue/cli": "^5.0.8",
|
"@vue/cli": "^5.0.8",
|
||||||
"@vue/composition-api": "1.7.1",
|
"@vue/composition-api": "1.7.1",
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.2.0",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,6 @@
|
|||||||
"file_upload_disabled": "",
|
"file_upload_disabled": "",
|
||||||
"warning_space_delete": "",
|
"warning_space_delete": "",
|
||||||
"food_inherit_info": "",
|
"food_inherit_info": "",
|
||||||
"facet_count_info": "",
|
|
||||||
"step_time_minutes": "",
|
"step_time_minutes": "",
|
||||||
"confirm_delete": "",
|
"confirm_delete": "",
|
||||||
"import_running": "",
|
"import_running": "",
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
"file_upload_disabled": "Nahrávání souborů není povoleno pro Váš prostor.",
|
"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?",
|
"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.",
|
"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",
|
"step_time_minutes": "Nastavte čas v minutách",
|
||||||
"confirm_delete": "Jste si jisti že chcete odstranit tento {objekt}?",
|
"confirm_delete": "Jste si jisti že chcete odstranit tento {objekt}?",
|
||||||
"import_running": "Probíhá import, čekejte prosím!",
|
"import_running": "Probíhá import, čekejte prosím!",
|
||||||
|
@ -446,7 +446,6 @@
|
|||||||
"Valid Until": "Gyldig indtil",
|
"Valid Until": "Gyldig indtil",
|
||||||
"Private_Recipe_Help": "Opskriften er kun synlig for dig, og dem som den er delt med.",
|
"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.",
|
"food_inherit_info": "Felter på mad som skal nedarves automatisk.",
|
||||||
"facet_count_info": "Vis opskriftsantal på søgeresultater.",
|
|
||||||
"Copy Link": "Kopier link",
|
"Copy Link": "Kopier link",
|
||||||
"Copy Token": "Kopier token",
|
"Copy Token": "Kopier token",
|
||||||
"show_ingredient_overview": "Vis en liste af alle ingredienser i starten af en opskrift.",
|
"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?",
|
"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",
|
"Copy Link": "Link Kopieren",
|
||||||
"Users": "Benutzer",
|
"Users": "Benutzer",
|
||||||
"facet_count_info": "Zeige die Anzahl der Rezepte auf den Suchfiltern.",
|
|
||||||
"Copy Token": "Kopiere Token",
|
"Copy Token": "Kopiere Token",
|
||||||
"Invites": "Einladungen",
|
"Invites": "Einladungen",
|
||||||
"Message": "Nachricht",
|
"Message": "Nachricht",
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
"recipe_property_info": "Μπορείτε επίσης να προσθέσετε ιδιότητες σε φαγητά ώστε να υπολογίζονται αυτόματα βάσει της συνταγής σας!",
|
"recipe_property_info": "Μπορείτε επίσης να προσθέσετε ιδιότητες σε φαγητά ώστε να υπολογίζονται αυτόματα βάσει της συνταγής σας!",
|
||||||
"warning_space_delete": "Μπορείτε να διαγράψετε τον χώρο σας μαζί με όλες τις συνταγές, τις λίστες αγορών, τα προγράμματα γευμάτων και οτιδήποτε άλλο έχετε δημιουργήσει. Η ενέργεια αυτή είναι μη αναστρέψιμη! Θέλετε σίγουρα να το κάνετε;",
|
"warning_space_delete": "Μπορείτε να διαγράψετε τον χώρο σας μαζί με όλες τις συνταγές, τις λίστες αγορών, τα προγράμματα γευμάτων και οτιδήποτε άλλο έχετε δημιουργήσει. Η ενέργεια αυτή είναι μη αναστρέψιμη! Θέλετε σίγουρα να το κάνετε;",
|
||||||
"food_inherit_info": "Πεδία σε φαγητά τα οποία πρέπει να κληρονομούνται αυτόματα.",
|
"food_inherit_info": "Πεδία σε φαγητά τα οποία πρέπει να κληρονομούνται αυτόματα.",
|
||||||
"facet_count_info": "Εμφάνιση του αριθμού των συνταγών στα φίλτρα αναζήτησης.",
|
|
||||||
"step_time_minutes": "Χρόνος βήματος σε λεπτά",
|
"step_time_minutes": "Χρόνος βήματος σε λεπτά",
|
||||||
"confirm_delete": "Θέλετε σίγουρα να διαγράψετε αυτό το {object};",
|
"confirm_delete": "Θέλετε σίγουρα να διαγράψετε αυτό το {object};",
|
||||||
"import_running": "Εισαγωγή σε εξέλιξη, παρακαλώ περιμένετε!",
|
"import_running": "Εισαγωγή σε εξέλιξη, παρακαλώ περιμένετε!",
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
"recipe_property_info": "You can also add properties to foods to calculate them automatically based on your recipe!",
|
"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 ?",
|
"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.",
|
"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",
|
"step_time_minutes": "Step time in minutes",
|
||||||
"confirm_delete": "Are you sure you want to delete this {object}?",
|
"confirm_delete": "Are you sure you want to delete this {object}?",
|
||||||
"import_running": "Import running, please wait!",
|
"import_running": "Import running, please wait!",
|
||||||
|
@ -419,7 +419,6 @@
|
|||||||
"Users": "Usuarios",
|
"Users": "Usuarios",
|
||||||
"Invites": "Invitaciones",
|
"Invites": "Invitaciones",
|
||||||
"food_inherit_info": "Campos que han de ser heredados por defecto.",
|
"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 Link": "Copiar Enlace",
|
||||||
"Copy Token": "Copiar Token",
|
"Copy Token": "Copiar Token",
|
||||||
"Create_New_Shopping_Category": "Añadir nueva Categoría de Compras",
|
"Create_New_Shopping_Category": "Añadir nueva Categoría de Compras",
|
||||||
|
@ -418,7 +418,6 @@
|
|||||||
"Message": "Message",
|
"Message": "Message",
|
||||||
"Sticky_Nav_Help": "Toujours afficher le menu de navigation en haut de l’écran.",
|
"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.",
|
"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",
|
"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.",
|
"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",
|
"Use_Kj": "Utiliser kJ au lieu de kcal",
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
"recipe_property_info": "ניתן גם להוסיף ערכים למאכלים בכדי לחשב אוטומטית בהתאם למתכון שלך!",
|
"recipe_property_info": "ניתן גם להוסיף ערכים למאכלים בכדי לחשב אוטומטית בהתאם למתכון שלך!",
|
||||||
"warning_space_delete": "ניתן למחיק את המרחב כולל כל המתכונים, רשימות קניות, תוכניות אוכל וכל מה שנוצר. פעולה זו הינה בלתי הפיכה! האם אתה בטוח ?",
|
"warning_space_delete": "ניתן למחיק את המרחב כולל כל המתכונים, רשימות קניות, תוכניות אוכל וכל מה שנוצר. פעולה זו הינה בלתי הפיכה! האם אתה בטוח ?",
|
||||||
"food_inherit_info": "ערכים על אוכל שאמורים להיות תורשתיים כברירת מחדל.",
|
"food_inherit_info": "ערכים על אוכל שאמורים להיות תורשתיים כברירת מחדל.",
|
||||||
"facet_count_info": "הצג מספר מתכונים בסנני החיפוש.",
|
|
||||||
"step_time_minutes": "זמן הצעד בדקות",
|
"step_time_minutes": "זמן הצעד בדקות",
|
||||||
"confirm_delete": "האם אתה בטוח רוצה למחק את {object}?",
|
"confirm_delete": "האם אתה בטוח רוצה למחק את {object}?",
|
||||||
"import_running": "ייבוא מתבצע, נא להמתין!",
|
"import_running": "ייבוא מתבצע, נא להמתין!",
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
"file_upload_disabled": "Unggahan file tidak diaktifkan untuk ruang Anda.",
|
"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?",
|
"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.",
|
"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",
|
"step_time_minutes": "Langkah waktu dalam menit",
|
||||||
"confirm_delete": "Anda yakin ingin menghapus {object} ini?",
|
"confirm_delete": "Anda yakin ingin menghapus {object} ini?",
|
||||||
"import_running": "Impor berjalan, harap tunggu!",
|
"import_running": "Impor berjalan, harap tunggu!",
|
||||||
|
@ -304,7 +304,6 @@
|
|||||||
"tree_select": "Usa selezione ad albero",
|
"tree_select": "Usa selezione ad albero",
|
||||||
"sql_debug": "Debug SQL",
|
"sql_debug": "Debug SQL",
|
||||||
"remember_search": "Ricorda ricerca",
|
"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?",
|
"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.",
|
"food_inherit_info": "Campi di alimenti che devono essere ereditati per impostazione predefinita.",
|
||||||
"enable_expert": "Abilita modalità esperto",
|
"enable_expert": "Abilita modalità esperto",
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
"file_upload_disabled": "Opplasting av filer er ikke aktivert i området ditt.",
|
"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?",
|
"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.",
|
"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",
|
"step_time_minutes": "Tid for trinn, i minutter",
|
||||||
"confirm_delete": "Er du sikker på at du vil slette dette {object}?",
|
"confirm_delete": "Er du sikker på at du vil slette dette {object}?",
|
||||||
"import_running": "Importering pågår. Vennligst vent!",
|
"import_running": "Importering pågår. Vennligst vent!",
|
||||||
|
@ -464,7 +464,6 @@
|
|||||||
"Valid Until": "Geldig tot",
|
"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?",
|
"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.",
|
"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.",
|
"Split_All_Steps": "Splits alle rijen in aparte stappen.",
|
||||||
"Combine_All_Steps": "Voeg alle stappen samen tot een veld.",
|
"Combine_All_Steps": "Voeg alle stappen samen tot een veld.",
|
||||||
"Plural": "Meervoud",
|
"Plural": "Meervoud",
|
||||||
|
@ -420,7 +420,6 @@
|
|||||||
"Users": "Użytkownicy",
|
"Users": "Użytkownicy",
|
||||||
"Invites": "Zaprasza",
|
"Invites": "Zaprasza",
|
||||||
"food_inherit_info": "Pola w pożywieniu, które powinny być domyślnie dziedziczone.",
|
"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",
|
"Copy Token": "Kopiuj Token",
|
||||||
"Message": "Wiadomość",
|
"Message": "Wiadomość",
|
||||||
"reset_food_inheritance": "Zresetuj dziedziczenie",
|
"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.",
|
"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.",
|
"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?",
|
"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": "",
|
||||||
"plural_short": "",
|
"plural_short": "",
|
||||||
"Use_Plural_Unit_Always": "",
|
"Use_Plural_Unit_Always": "",
|
||||||
|
@ -387,7 +387,6 @@
|
|||||||
"Copy Token": "Copiar Token",
|
"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?",
|
"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.",
|
"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": "",
|
||||||
"plural_short": "",
|
"plural_short": "",
|
||||||
"Use_Plural_Unit_Always": "",
|
"Use_Plural_Unit_Always": "",
|
||||||
|
@ -343,7 +343,6 @@
|
|||||||
"Undefined": "Nedefinit",
|
"Undefined": "Nedefinit",
|
||||||
"Select": "Selectare",
|
"Select": "Selectare",
|
||||||
"food_inherit_info": "Câmpuri pe alimente care ar trebui să fie moștenite în mod implicit.",
|
"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",
|
"Amount": "Cantitate",
|
||||||
"Auto_Sort_Help": "Mutați toate ingredientele la cel mai potrivit pas.",
|
"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.",
|
"search_create_help_text": "Creați o rețetă nouă direct în Tandoor.",
|
||||||
|
@ -343,7 +343,6 @@
|
|||||||
"DelayFor": "Отложить на {hours} часов",
|
"DelayFor": "Отложить на {hours} часов",
|
||||||
"New_Entry": "Новая запись",
|
"New_Entry": "Новая запись",
|
||||||
"GroupBy": "Сгруппировать по",
|
"GroupBy": "Сгруппировать по",
|
||||||
"facet_count_info": "Показывать количество рецептов в фильтрах поиска.",
|
|
||||||
"food_inherit_info": "Поля для продуктов питания, которые должны наследоваться по умолчанию.",
|
"food_inherit_info": "Поля для продуктов питания, которые должны наследоваться по умолчанию.",
|
||||||
"warning_space_delete": "Вы можете удалить свое пространство, включая все рецепты, списки покупок, планы питания и все остальное, что вы создали. Этого нельзя отменить! Вы уверены, что хотите это сделать?",
|
"warning_space_delete": "Вы можете удалить свое пространство, включая все рецепты, списки покупок, планы питания и все остальное, что вы создали. Этого нельзя отменить! Вы уверены, что хотите это сделать?",
|
||||||
"Description_Replace": "Изменить описание"
|
"Description_Replace": "Изменить описание"
|
||||||
|
@ -302,7 +302,6 @@
|
|||||||
"Description_Replace": "Zamenjaj Opis",
|
"Description_Replace": "Zamenjaj Opis",
|
||||||
"recipe_property_info": "Živilom lahko dodate tudi lastnosti, ki se samodejno izračunajo na podlagi vašega recepta!",
|
"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?",
|
"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",
|
"per_serving": "na porcijo",
|
||||||
"Ingredient Editor": "Urejevalnik Sestavin",
|
"Ingredient Editor": "Urejevalnik Sestavin",
|
||||||
"Instruction_Replace": "Zamenjaj Navodila",
|
"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.",
|
"reusable_help_text": "Bör inbjudningslänken vara användbar för mer än en användare.",
|
||||||
"Ingredient Editor": "Ingrediensredigerare",
|
"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?",
|
"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.",
|
"food_inherit_info": "Fält på mat som ska ärvas som standard.",
|
||||||
"Auto_Sort": "Automatisk Sortering",
|
"Auto_Sort": "Automatisk Sortering",
|
||||||
"Day": "Dag",
|
"Day": "Dag",
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
"file_upload_disabled": "Alanınız için dosya yükleme aktif değil.",
|
"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?",
|
"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": "",
|
"food_inherit_info": "",
|
||||||
"facet_count_info": "",
|
|
||||||
"step_time_minutes": "Dakika olarak adım süresi",
|
"step_time_minutes": "Dakika olarak adım süresi",
|
||||||
"confirm_delete": "",
|
"confirm_delete": "",
|
||||||
"import_running": "",
|
"import_running": "",
|
||||||
|
@ -421,7 +421,6 @@
|
|||||||
"Use_Plural_Food_Simple": "",
|
"Use_Plural_Food_Simple": "",
|
||||||
"plural_usage_info": "",
|
"plural_usage_info": "",
|
||||||
"warning_space_delete": "Ви можете видалити ваш простір разом зі всіма рецептами, списками покупок, планами харчування і всім іншим, що ви створили. Ця дія незворотня! Ви впевнені, що бажаєте це зробити?",
|
"warning_space_delete": "Ви можете видалити ваш простір разом зі всіма рецептами, списками покупок, планами харчування і всім іншим, що ви створили. Ця дія незворотня! Ви впевнені, що бажаєте це зробити?",
|
||||||
"facet_count_info": "Показати кількість рецептів на полі пошуку.",
|
|
||||||
"Amount": "Кількість",
|
"Amount": "Кількість",
|
||||||
"Auto_Sort": "Автоматичне сортування",
|
"Auto_Sort": "Автоматичне сортування",
|
||||||
"Auto_Sort_Help": "Перемістити всі інгредієнти до більш підходящого кроку.",
|
"Auto_Sort_Help": "Перемістити всі інгредієнти до більш підходящого кроку.",
|
||||||
|
@ -414,7 +414,6 @@
|
|||||||
"Warning_Delete_Supermarket_Category": "删除超市类别也会删除与食品的所有关系。 你确定吗?",
|
"Warning_Delete_Supermarket_Category": "删除超市类别也会删除与食品的所有关系。 你确定吗?",
|
||||||
"New_Supermarket": "创建新超市",
|
"New_Supermarket": "创建新超市",
|
||||||
"warning_space_delete": "您可以删除您的空间,包括所有食谱、购物清单、膳食计划以及您创建的任何其他内容。 这不能被撤消! 你确定要这么做吗 ?",
|
"warning_space_delete": "您可以删除您的空间,包括所有食谱、购物清单、膳食计划以及您创建的任何其他内容。 这不能被撤消! 你确定要这么做吗 ?",
|
||||||
"facet_count_info": "在搜索筛选器上显示食谱计数。",
|
|
||||||
"Hide_as_header": "隐藏标题",
|
"Hide_as_header": "隐藏标题",
|
||||||
"food_inherit_info": "默认情况下应继承的食物上的字段。",
|
"food_inherit_info": "默认情况下应继承的食物上的字段。",
|
||||||
"Custom Filter": "自定义筛选器",
|
"Custom Filter": "自定义筛选器",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1165,7 +1165,9 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
|
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
|
||||||
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
|
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
|
||||||
|
|
||||||
|
|
||||||
"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4":
|
"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4":
|
||||||
|
|
||||||
version "7.22.15"
|
version "7.22.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8"
|
||||||
integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==
|
integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==
|
||||||
@ -1537,20 +1539,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||||
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
||||||
|
|
||||||
"@riophae/vue-treeselect@^0.4.0":
|
|
||||||
version "0.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@riophae/vue-treeselect/-/vue-treeselect-0.4.0.tgz#0baed5a794cffc580b63591f35c125e51c0df241"
|
|
||||||
integrity sha512-J4atYmBqXQmiPFK/0B5sXKjtnGc21mBJEiyKIDZwk0Q9XuynVFX6IJ4EpaLmUgL5Tve7HAS7wkiGGSti6Uaxcg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.3.1"
|
|
||||||
babel-helper-vue-jsx-merge-props "^2.0.3"
|
|
||||||
easings-css "^1.0.0"
|
|
||||||
fuzzysearch "^1.0.3"
|
|
||||||
is-promise "^2.1.0"
|
|
||||||
lodash "^4.0.0"
|
|
||||||
material-colors "^1.2.6"
|
|
||||||
watch-size "^2.0.0"
|
|
||||||
|
|
||||||
"@rollup/plugin-babel@^5.2.0":
|
"@rollup/plugin-babel@^5.2.0":
|
||||||
version "5.3.1"
|
version "5.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
|
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
|
||||||
@ -3142,7 +3130,7 @@ asn1.js@^5.2.0:
|
|||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
safer-buffer "^2.1.0"
|
safer-buffer "^2.1.0"
|
||||||
|
|
||||||
assert@^1.1.1:
|
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.1.tgz#038ab248e4ff078e7bc2485ba6e6388466c78f76"
|
resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.1.tgz#038ab248e4ff078e7bc2485ba6e6388466c78f76"
|
||||||
integrity sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==
|
integrity sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==
|
||||||
@ -3304,6 +3292,7 @@ babel-generator@^6.26.0:
|
|||||||
source-map "^0.5.7"
|
source-map "^0.5.7"
|
||||||
trim-right "^1.0.1"
|
trim-right "^1.0.1"
|
||||||
|
|
||||||
|
|
||||||
babel-helper-vue-jsx-merge-props@^2.0.3:
|
babel-helper-vue-jsx-merge-props@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6"
|
resolved "https://registry.yarnpkg.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6"
|
||||||
@ -3915,6 +3904,7 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001520:
|
|||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001529.tgz#c1f2a411e85fdaace4b1560e1bad078b00ac3181"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001529.tgz#c1f2a411e85fdaace4b1560e1bad078b00ac3181"
|
||||||
integrity sha512-n2pUQYGAkrLG4QYj2desAh+NqsJpHbNmVZz87imptDdxLAtjxary7Df/psdfyDGmskJK/9Dt9cPnx5RZ3CU4Og==
|
integrity sha512-n2pUQYGAkrLG4QYj2desAh+NqsJpHbNmVZz87imptDdxLAtjxary7Df/psdfyDGmskJK/9Dt9cPnx5RZ3CU4Og==
|
||||||
|
|
||||||
|
|
||||||
canvg@^3.0.6:
|
canvg@^3.0.6:
|
||||||
version "3.0.10"
|
version "3.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.10.tgz#8e52a2d088b6ffa23ac78970b2a9eebfae0ef4b3"
|
resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.10.tgz#8e52a2d088b6ffa23ac78970b2a9eebfae0ef4b3"
|
||||||
@ -6023,6 +6013,7 @@ functions-have-names@^1.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
||||||
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
|
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
|
||||||
|
|
||||||
|
|
||||||
fuzzysearch@^1.0.3:
|
fuzzysearch@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/fuzzysearch/-/fuzzysearch-1.0.3.tgz#dffc80f6d6b04223f2226aa79dd194231096d008"
|
resolved "https://registry.yarnpkg.com/fuzzysearch/-/fuzzysearch-1.0.3.tgz#dffc80f6d6b04223f2226aa79dd194231096d008"
|
||||||
@ -7769,6 +7760,7 @@ map-visit@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
object-visit "^1.0.0"
|
object-visit "^1.0.0"
|
||||||
|
|
||||||
|
|
||||||
material-colors@^1.2.6:
|
material-colors@^1.2.6:
|
||||||
version "1.2.6"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
|
resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
|
||||||
@ -11089,6 +11081,7 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||||
|
|
||||||
|
|
||||||
util@^0.10.4:
|
util@^0.10.4:
|
||||||
version "0.10.4"
|
version "0.10.4"
|
||||||
resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
|
resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
|
||||||
@ -11351,6 +11344,7 @@ vuedraggable@^2.24.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sortablejs "1.10.2"
|
sortablejs "1.10.2"
|
||||||
|
|
||||||
|
|
||||||
watch-size@^2.0.0:
|
watch-size@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/watch-size/-/watch-size-2.0.0.tgz#096ee28d0365bd7ea03d9c8bf1f2f50a73be1474"
|
resolved "https://registry.yarnpkg.com/watch-size/-/watch-size-2.0.0.tgz#096ee28d0365bd7ea03d9c8bf1f2f50a73be1474"
|
||||||
|
Loading…
Reference in New Issue
Block a user