diff --git a/cookbook/serializer.py b/cookbook/serializer.py
index 24b39713..4885d59f 100644
--- a/cookbook/serializer.py
+++ b/cookbook/serializer.py
@@ -42,7 +42,8 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
api_serializer = None
# extended values are computationally expensive and not needed in normal circumstances
try:
- if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
+ if str2bool(
+ self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
return fields
except (AttributeError, KeyError) as e:
pass
@@ -96,7 +97,8 @@ class CustomOnHandField(serializers.Field):
shared_users = getattr(request, '_shared_users', None)
if shared_users is None:
try:
- shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
+ shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
+ self.context['request'].user.id]
except AttributeError: # Anonymous users (using share links) don't have shared users
shared_users = []
return obj.onhand_users.filter(id__in=shared_users).exists()
@@ -170,7 +172,8 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer):
class UserPreferenceSerializer(WritableNestedModelSerializer):
- food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True)
+ food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True,
+ required=False, read_only=True)
plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True)
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
@@ -189,9 +192,12 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
class Meta:
model = UserPreference
fields = (
- 'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj', 'search_style', 'show_recent', 'plan_share',
- 'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_inherit_default', 'default_delay',
- 'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix',
+ 'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj', 'search_style',
+ 'show_recent', 'plan_share',
+ 'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
+ 'food_inherit_default', 'default_delay',
+ 'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
+ 'csv_delim', 'csv_prefix',
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'food_children_exist'
)
@@ -393,7 +399,6 @@ class FoodSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Food
fields = ('id', 'name')
- read_only_fields = ['id', 'name']
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin):
@@ -416,7 +421,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
shared_users = getattr(request, '_shared_users', None)
if shared_users is None:
try:
- shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
+ shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
+ self.context['request'].user.id]
except AttributeError:
shared_users = []
filter = Q(id__in=obj.substitute.all())
@@ -487,8 +493,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
-class IngredientSerializer(WritableNestedModelSerializer):
- food = FoodSerializer(allow_null=True)
+class IngredientSimpleSerializer(WritableNestedModelSerializer):
+ food = FoodSimpleSerializer(allow_null=True)
unit = UnitSerializer(allow_null=True)
amount = CustomDecimalField()
@@ -508,6 +514,10 @@ class IngredientSerializer(WritableNestedModelSerializer):
)
+class IngredientSerializer(IngredientSimpleSerializer):
+ food = FoodSerializer(allow_null=True)
+
+
class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
ingredients = IngredientSerializer(many=True)
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
@@ -699,7 +709,8 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
def create(self, validated_data):
book = validated_data['book']
recipe = validated_data['recipe']
- if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
+ if not book.get_owner() == self.context['request'].user and not self.context[
+ 'request'].user in book.get_shared():
raise NotFound(detail=None, code=None)
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
return obj
@@ -752,13 +763,14 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
def get_name(self, obj):
if not isinstance(value := obj.servings, Decimal):
value = Decimal(value)
- value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
+ value = value.quantize(
+ Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
return (
- obj.name
- or getattr(obj.mealplan, 'title', None)
- or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
- or obj.recipe.name
- ) + f' ({value:.2g})'
+ obj.name
+ or getattr(obj.mealplan, 'title', None)
+ or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
+ or obj.recipe.name
+ ) + f' ({value:.2g})'
def update(self, instance, validated_data):
# TODO remove once old shopping list
@@ -829,7 +841,8 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
class Meta:
model = ShoppingListEntry
fields = (
- 'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked', 'recipe_mealplan',
+ 'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked',
+ 'recipe_mealplan',
'created_by', 'created_at', 'completed_at', 'delay_until'
)
read_only_fields = ('id', 'created_by', 'created_at',)
@@ -927,7 +940,10 @@ class ExportLogSerializer(serializers.ModelSerializer):
class Meta:
model = ExportLog
- fields = ('id', 'type', 'msg', 'running', 'total_recipes', 'exported_recipes', 'cache_duration', 'possibly_not_expired', 'created_by', 'created_at')
+ fields = (
+ 'id', 'type', 'msg', 'running', 'total_recipes', 'exported_recipes', 'cache_duration',
+ 'possibly_not_expired',
+ 'created_by', 'created_at')
read_only_fields = ('created_by',)
@@ -1039,10 +1055,12 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
- list_recipe = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("Existing shopping list to update"))
+ list_recipe = serializers.IntegerField(write_only=True, allow_null=True, required=False,
+ help_text=_("Existing shopping list to update"))
ingredients = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_(
"List of ingredient IDs from the recipe to add, if not provided all ingredients will be added."))
- servings = serializers.IntegerField(default=1, write_only=True, allow_null=True, required=False, help_text=_("Providing a list_recipe ID and servings of 0 will delete that shopping list."))
+ servings = serializers.IntegerField(default=1, write_only=True, allow_null=True, required=False, help_text=_(
+ "Providing a list_recipe ID and servings of 0 will delete that shopping list."))
class Meta:
model = Recipe
@@ -1050,9 +1068,12 @@ class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
class FoodShoppingUpdateSerializer(serializers.ModelSerializer):
- amount = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("Amount of food to add to the shopping list"))
- unit = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("ID of unit to use for the shopping list"))
- delete = serializers.ChoiceField(choices=['true'], write_only=True, allow_null=True, allow_blank=True, help_text=_("When set to true will delete all food from active shopping lists."))
+ amount = serializers.IntegerField(write_only=True, allow_null=True, required=False,
+ help_text=_("Amount of food to add to the shopping list"))
+ unit = serializers.IntegerField(write_only=True, allow_null=True, required=False,
+ help_text=_("ID of unit to use for the shopping list"))
+ delete = serializers.ChoiceField(choices=['true'], write_only=True, allow_null=True, allow_blank=True,
+ help_text=_("When set to true will delete all food from active shopping lists."))
class Meta:
model = Recipe
diff --git a/cookbook/templates/ingredient_editor.html b/cookbook/templates/ingredient_editor.html
new file mode 100644
index 00000000..732173a7
--- /dev/null
+++ b/cookbook/templates/ingredient_editor.html
@@ -0,0 +1,31 @@
+{% extends "base.html" %}
+{% load render_bundle from webpack_loader %}
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+
+{% block title %}{% trans 'Ingredient Editor' %}{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+{% endblock %}
+
+
+{% block script %}
+ {% if debug %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+ {% render_bundle 'ingredient_editor_view' %}
+{% endblock %}
\ No newline at end of file
diff --git a/cookbook/urls.py b/cookbook/urls.py
index 7e47ee5e..90c81d1e 100644
--- a/cookbook/urls.py
+++ b/cookbook/urls.py
@@ -47,7 +47,6 @@ router.register(r'user-name', api.UserNameViewSet, basename='username')
router.register(r'user-preference', api.UserPreferenceViewSet)
router.register(r'view-log', api.ViewLogViewSet)
-
urlpatterns = [
path('', views.index, name='index'),
path('setup/', views.setup, name='view_setup'),
@@ -72,6 +71,7 @@ urlpatterns = [
path('settings/', views.user_settings, name='view_settings'),
path('history/', views.history, name='view_history'),
path('supermarket/', views.supermarket, name='view_supermarket'),
+ path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'),
path('abuse/', views.report_share_abuse, name='view_report_share_abuse'),
path('import/', import_export.import_recipe, name='view_import'),
@@ -116,7 +116,8 @@ urlpatterns = [
path('api/share-link/', api.share_link, name='api_share_link'),
path('api/get_facets/', api.get_facets, name='api_get_facets'),
- path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), # TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
+ path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
+ # TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), # TODO is this deprecated?
diff --git a/cookbook/views/api.py b/cookbook/views/api.py
index 87248978..d1e88568 100644
--- a/cookbook/views/api.py
+++ b/cookbook/views/api.py
@@ -68,7 +68,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializ
SupermarketCategorySerializer, SupermarketSerializer,
SyncLogSerializer, SyncSerializer, UnitSerializer,
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
- ViewLogSerializer)
+ ViewLogSerializer, IngredientSimpleSerializer)
from recipes import settings
@@ -119,14 +119,17 @@ class ExtendedRecipeMixin():
# add a recipe count annotation to the query
# explanation on construction https://stackoverflow.com/a/43771738/15762829
- recipe_count = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).values(recipe_filter).annotate(count=Count('pk')).values('count')
+ recipe_count = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).values(
+ recipe_filter).annotate(count=Count('pk')).values('count')
queryset = queryset.annotate(recipe_count=Coalesce(Subquery(recipe_count), 0))
# add a recipe image annotation to the query
- image_subquery = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
+ image_subquery = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).exclude(
+ image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
if tree:
- image_children_subquery = Recipe.objects.filter(**{f"{recipe_filter}__path__startswith": OuterRef('path')},
- space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
+ image_children_subquery = Recipe.objects.filter(
+ **{f"{recipe_filter}__path__startswith": OuterRef('path')},
+ space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
else:
image_children_subquery = None
if images:
@@ -142,11 +145,14 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
query = self.request.query_params.get('query', None)
- fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.trigram.values_list('field', flat=True)])
+ fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in
+ self.request.user.searchpreference.trigram.values_list(
+ 'field', flat=True)])
if query is not None and query not in ["''", '']:
if fuzzy:
- if any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
+ if any([self.model.__name__.lower() in x for x in
+ self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
else:
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
@@ -154,7 +160,8 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
else:
# TODO have this check unaccent search settings or other search preferences?
filter = Q(name__icontains=query)
- if any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
+ if any([self.model.__name__.lower() in x for x in
+ self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
filter |= Q(name__unaccent__icontains=query)
self.queryset = (
@@ -275,10 +282,12 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
except self.model.DoesNotExist:
self.queryset = self.model.objects.none()
else:
- return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True)
+ return self.annotate_recipe(queryset=super().get_queryset(), request=self.request,
+ serializer=self.serializer_class, tree=True)
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
- return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, tree=True)
+ return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class,
+ tree=True)
@decorators.action(detail=True, url_path='move/(?P[^/.]+)', methods=['PUT'], )
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
@@ -454,12 +463,16 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
pagination_class = DefaultPagination
def get_queryset(self):
- self.request._shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [self.request.user.id]
+ self.request._shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [
+ self.request.user.id]
self.queryset = super().get_queryset()
- shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'), checked=False).values('id')
+ shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'),
+ checked=False).values('id')
# onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users]))
- return self.queryset.annotate(shopping_status=Exists(shopping_status)).prefetch_related('onhand_users', 'inherit_fields').select_related('recipe', 'supermarket_category')
+ return self.queryset.annotate(shopping_status=Exists(shopping_status)).prefetch_related('onhand_users',
+ 'inherit_fields').select_related(
+ 'recipe', 'supermarket_category')
@decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer, )
# TODO DRF only allows one action in a decorator action without overriding get_operation_id_base() this should be PUT and DELETE probably
@@ -470,7 +483,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
shared_users = list(self.request.user.get_shopping_share())
shared_users.append(request.user)
if request.data.get('_delete', False) == 'true':
- ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space, created_by__in=shared_users).delete()
+ ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space,
+ created_by__in=shared_users).delete()
content = {'msg': _(f'{obj.name} was removed from the shopping list.')}
return Response(content, status=status.HTTP_204_NO_CONTENT)
@@ -478,7 +492,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
unit = request.data.get('unit', None)
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
- ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space, created_by=request.user)
+ ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space,
+ created_by=request.user)
return Response(content, status=status.HTTP_204_NO_CONTENT)
def destroy(self, *args, **kwargs):
@@ -577,8 +592,22 @@ class IngredientViewSet(viewsets.ModelViewSet):
serializer_class = IngredientSerializer
permission_classes = [CustomIsUser]
+ def get_serializer_class(self):
+ if self.request and self.request.query_params.get('simple', False):
+ return IngredientSimpleSerializer
+ return IngredientSerializer
+
def get_queryset(self):
- return self.queryset.filter(step__recipe__space=self.request.space)
+ queryset = self.queryset.filter(step__recipe__space=self.request.space)
+ food = self.request.query_params.get('food', None)
+ if food and re.match(r'^([1-9])+$', food):
+ queryset = queryset.filter(food_id=food)
+
+ unit = self.request.query_params.get('unit', None)
+ if unit and re.match(r'^([1-9])+$', unit):
+ queryset = queryset.filter(unit_id=unit)
+
+ return queryset
class StepViewSet(viewsets.ModelViewSet):
@@ -587,7 +616,8 @@ class StepViewSet(viewsets.ModelViewSet):
permission_classes = [CustomIsUser]
pagination_class = DefaultPagination
query_params = [
- QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'), qtype='int'),
+ QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'),
+ qtype='int'),
QueryParam(name='query', description=_('Query string matched (fuzzy) against object name.'), qtype='string'),
]
schema = QueryParamAutoSchema()
@@ -631,33 +661,63 @@ class RecipeViewSet(viewsets.ModelViewSet):
pagination_class = RecipePagination
query_params = [
- QueryParam(name='query', description=_('Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
- QueryParam(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'), qtype='int'),
- QueryParam(name='keywords_or', description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'), qtype='int'),
- QueryParam(name='keywords_and', description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'), qtype='int'),
- QueryParam(name='keywords_or_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'), qtype='int'),
- QueryParam(name='keywords_and_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'), qtype='int'),
- QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'), qtype='int'),
- QueryParam(name='foods_or', description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), qtype='int'),
- QueryParam(name='foods_and', description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), qtype='int'),
- QueryParam(name='foods_or_not', description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), qtype='int'),
- QueryParam(name='foods_and_not', description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), qtype='int'),
+ QueryParam(name='query', description=_(
+ 'Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
+ QueryParam(name='keywords', description=_(
+ 'ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'),
+ qtype='int'),
+ QueryParam(name='keywords_or',
+ description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'),
+ qtype='int'),
+ QueryParam(name='keywords_and',
+ description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'),
+ qtype='int'),
+ QueryParam(name='keywords_or_not',
+ description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'),
+ qtype='int'),
+ QueryParam(name='keywords_and_not',
+ description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'),
+ qtype='int'),
+ QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'),
+ qtype='int'),
+ QueryParam(name='foods_or',
+ description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), qtype='int'),
+ QueryParam(name='foods_and',
+ description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), qtype='int'),
+ QueryParam(name='foods_or_not',
+ description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), qtype='int'),
+ QueryParam(name='foods_and_not',
+ description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), qtype='int'),
QueryParam(name='units', description=_('ID of unit a recipe should have.'), qtype='int'),
- QueryParam(name='rating', description=_('Rating a recipe should have or greater. [0 - 5] Negative value filters rating less than.'), qtype='int'),
+ QueryParam(name='rating', description=_(
+ 'Rating a recipe should have or greater. [0 - 5] Negative value filters rating less than.'), qtype='int'),
QueryParam(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.')),
- QueryParam(name='books_or', description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), qtype='int'),
- QueryParam(name='books_and', description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), qtype='int'),
- QueryParam(name='books_or_not', description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
- QueryParam(name='books_and_not', description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
- QueryParam(name='internal', description=_('If only internal recipes should be returned. [''true''/''false'']')),
- QueryParam(name='random', description=_('Returns the results in randomized order. [''true''/''false'']')),
- QueryParam(name='new', description=_('Returns new results first in search results. [''true''/''false'']')),
- 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''/''false'']')),
+ QueryParam(name='books_or',
+ description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), qtype='int'),
+ QueryParam(name='books_and',
+ description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), qtype='int'),
+ QueryParam(name='books_or_not',
+ description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
+ QueryParam(name='books_and_not',
+ description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
+ QueryParam(name='internal',
+ description=_('If only internal recipes should be returned. [''true''/''false'']')),
+ QueryParam(name='random',
+ description=_('Returns the results in randomized order. [''true''/''false'']')),
+ QueryParam(name='new',
+ description=_('Returns new results first in search results. [''true''/''false'']')),
+ 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''/''false'']')),
]
schema = QueryParamAutoSchema()
@@ -672,7 +732,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
if not (share and self.detail):
self.queryset = self.queryset.filter(space=self.request.space)
- params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x in list(self.request.GET)}
+ params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x
+ in list(self.request.GET)}
search = RecipeSearch(self.request, **params)
self.queryset = search.get_queryset(self.queryset).prefetch_related('cooklog_set')
return self.queryset
@@ -770,7 +831,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
levels = int(request.query_params.get('levels', 1))
except (ValueError, TypeError):
levels = 1
- qs = obj.get_related_recipes(levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
+ qs = obj.get_related_recipes(
+ levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
return Response(self.serializer_class(qs, many=True).data)
@@ -780,7 +842,8 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
permission_classes = [CustomIsOwner | CustomIsShared]
def get_queryset(self):
- self.queryset = self.queryset.filter(Q(shoppinglist__space=self.request.space) | Q(entries__space=self.request.space))
+ self.queryset = self.queryset.filter(
+ Q(shoppinglist__space=self.request.space) | Q(entries__space=self.request.space))
return self.queryset.filter(
Q(shoppinglist__created_by=self.request.user)
| Q(shoppinglist__shared=self.request.user)
@@ -794,12 +857,17 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
serializer_class = ShoppingListEntrySerializer
permission_classes = [CustomIsOwner | CustomIsShared]
query_params = [
- QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='int'),
+ QueryParam(name='id',
+ description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'),
+ qtype='int'),
QueryParam(
name='checked',
- description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''recent'']
- ''recent'' includes unchecked items and recently completed items.')
+ description=_(
+ 'Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''recent'']
- ''recent'' includes unchecked items and recently completed items.')
),
- QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'),
+ QueryParam(name='supermarket',
+ description=_('Returns the shopping list entries sorted by supermarket category order.'),
+ qtype='int'),
]
schema = QueryParamAutoSchema()
@@ -926,6 +994,7 @@ class CustomFilterViewSet(viewsets.ModelViewSet, StandardFilterMixin):
space=self.request.space).distinct()
return super().get_queryset()
+
# -------------- non django rest api views --------------------
diff --git a/cookbook/views/views.py b/cookbook/views/views.py
index 26d80f6b..e5bca25f 100644
--- a/cookbook/views/views.py
+++ b/cookbook/views/views.py
@@ -61,7 +61,8 @@ def search(request):
if request.user.userpreference.search_style == UserPreference.NEW:
return search_v2(request)
f = RecipeFilter(request.GET,
- queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(Lower('name').asc()),
+ queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(
+ Lower('name').asc()),
space=request.space)
if request.user.userpreference.search_style == UserPreference.LARGE:
table = RecipeTable(f.qs)
@@ -225,6 +226,11 @@ def supermarket(request):
return render(request, 'supermarket.html', {})
+@group_required('user')
+def ingredient_editor(request):
+ return render(request, 'ingredient_editor.html', {})
+
+
@group_required('user')
def meal_plan_entry(request, pk):
plan = MealPlan.objects.filter(space=request.space).get(pk=pk)
@@ -327,10 +333,10 @@ def user_settings(request):
if not sp:
sp = SearchPreferenceForm(user=request.user)
fields_searched = (
- len(search_form.cleaned_data['icontains'])
- + len(search_form.cleaned_data['istartswith'])
- + len(search_form.cleaned_data['trigram'])
- + len(search_form.cleaned_data['fulltext'])
+ len(search_form.cleaned_data['icontains'])
+ + len(search_form.cleaned_data['istartswith'])
+ + len(search_form.cleaned_data['trigram'])
+ + len(search_form.cleaned_data['fulltext'])
)
if fields_searched == 0:
search_form.add_error(None, _('You must select at least one field to search!'))
diff --git a/vue/src/apps/IngredientEditorView/IngredientEditorView.vue b/vue/src/apps/IngredientEditorView/IngredientEditorView.vue
new file mode 100644
index 00000000..3a3ab2ba
--- /dev/null
+++ b/vue/src/apps/IngredientEditorView/IngredientEditorView.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
diff --git a/vue/src/apps/IngredientEditorView/main.js b/vue/src/apps/IngredientEditorView/main.js
new file mode 100644
index 00000000..c92257cd
--- /dev/null
+++ b/vue/src/apps/IngredientEditorView/main.js
@@ -0,0 +1,18 @@
+import Vue from 'vue'
+import App from './IngredientEditorView.vue'
+import i18n from '@/i18n'
+
+Vue.config.productionTip = false
+
+// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
+let publicPath = localStorage.STATIC_URL + 'vue/'
+if (process.env.NODE_ENV === 'development') {
+ publicPath = 'http://localhost:8080/'
+}
+export default __webpack_public_path__ = publicPath // eslint-disable-line
+
+
+new Vue({
+ i18n,
+ render: h => h(App),
+}).$mount('#app')
diff --git a/vue/src/components/GenericMultiselect.vue b/vue/src/components/GenericMultiselect.vue
index 8698ee91..977a4c01 100644
--- a/vue/src/components/GenericMultiselect.vue
+++ b/vue/src/components/GenericMultiselect.vue
@@ -26,11 +26,11 @@