diff --git a/cookbook/serializer.py b/cookbook/serializer.py index c8350f095..fe33d9667 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -1139,7 +1139,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer): fields = ( 'id', 'title', 'recipe', 'servings', 'note', 'note_markdown', 'from_date', 'to_date', 'meal_type', 'created_by', 'shared', 'recipe_name', - 'meal_type_name', 'shopping','addshopping' + 'meal_type_name', 'shopping', 'addshopping' ) read_only_fields = ('created_by',) @@ -1155,26 +1155,12 @@ class AutoMealPlanSerializer(serializers.Serializer): class ShoppingListRecipeSerializer(serializers.ModelSerializer): - name = serializers.SerializerMethodField('get_name') # should this be done at the front end? recipe_name = serializers.ReadOnlyField(source='recipe.name') mealplan_note = serializers.ReadOnlyField(source='mealplan.note') mealplan_from_date = serializers.ReadOnlyField(source='mealplan.from_date') mealplan_type = serializers.ReadOnlyField(source='mealplan.meal_type.name') servings = CustomDecimalField() - @extend_schema_field(str) - 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 - 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})' - def update(self, instance, validated_data): # TODO remove once old shopping list if 'servings' in validated_data and self.context.get('view', None).__class__.__name__ != 'ShoppingListViewSet': @@ -1252,6 +1238,16 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer): read_only_fields = ('id', 'created_by', 'created_at') +class ShoppingListEntrySimpleCreateSerializer(serializers.Serializer): + amount = CustomDecimalField() + unit_id = serializers.IntegerField(allow_null=True) + food_id = serializers.IntegerField(allow_null=True) + + +class ShoppingListEntryBulkCreateSerializer(serializers.Serializer): + entries = serializers.ListField(child=ShoppingListEntrySimpleCreateSerializer()) + + class ShoppingListEntryBulkSerializer(serializers.Serializer): ids = serializers.ListField() checked = serializers.BooleanField() @@ -1537,8 +1533,8 @@ 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")) - 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.")) + ingredients = serializers.ListField(child=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.")) @@ -1627,9 +1623,10 @@ class SourceImportDuplicateSerializer(serializers.Serializer): id = serializers.IntegerField() name = serializers.CharField() + class RecipeFromSourceResponseSerializer(serializers.Serializer): recipe = SourceImportRecipeSerializer(default=None) - images = serializers.ListField(child=serializers.CharField(),default=[], allow_null=False) + images = serializers.ListField(child=serializers.CharField(), default=[], allow_null=False) error = serializers.BooleanField(default=False) msg = serializers.CharField(max_length=1024, default='') duplicates = serializers.ListField(child=SourceImportDuplicateSerializer(), default=[], allow_null=False) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index a0aed0dd7..163572f10 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -105,7 +105,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, Au SupermarketSerializer, SyncLogSerializer, SyncSerializer, UnitConversionSerializer, UnitSerializer, UserFileSerializer, UserPreferenceSerializer, UserSerializer, UserSpaceSerializer, ViewLogSerializer, ImportImageSerializer, - LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer + LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer ) from cookbook.version_info import TANDOOR_VERSION from cookbook.views.import_export import get_integration @@ -1289,7 +1289,8 @@ class RecipeViewSet(LoggingMixin, viewsets.ModelViewSet): return Response(content, status=http_status) - @decorators.action(detail=True, methods=['GET'], serializer_class=RecipeSimpleSerializer) + @extend_schema(responses=RecipeSimpleSerializer(many=True)) + @decorators.action(detail=True, pagination_class=None, methods=['GET'], serializer_class=RecipeSimpleSerializer) def related(self, request, pk): obj = self.get_object() if obj.get_space() != request.space: @@ -1371,8 +1372,34 @@ class ShoppingListRecipeViewSet(LoggingMixin, viewsets.ModelViewSet): self.queryset = self.queryset.filter(Q(entries__space=self.request.space) | Q(recipe__space=self.request.space)) return self.queryset.filter(Q(entries__isnull=True) | Q(entries__created_by=self.request.user) - | Q( - entries__created_by__in=list(self.request.user.get_shopping_share()))).distinct().all() + | Q(entries__created_by__in=list(self.request.user.get_shopping_share()))).distinct().all() + + @decorators.action(detail=True, methods=['POST'], serializer_class=ShoppingListEntryBulkCreateSerializer, permission_classes=[CustomIsUser]) + def bulk_create_entries(self, request, pk): + obj = self.get_object() + if obj.get_space() != request.space: + raise PermissionDenied(detail='You do not have the required permission to perform this action', code=403) + + serializer = self.serializer_class(data=request.data) + + if serializer.is_valid(): + entries = [] + for e in serializer.validated_data['entries']: + entries.append( + ShoppingListEntry( + list_recipe_id=obj.pk, + amount=e['amount'], + unit_id=e['unit_id'], + food_id=e['food_id'], + created_by_id=request.user.id, + space_id=request.space.id, + ) + ) + + ShoppingListEntry.objects.bulk_create(entries) + return Response(serializer.validated_data) + else: + return Response(serializer.errors, 400) @extend_schema_view(list=extend_schema(parameters=[ diff --git a/vue3/src/components/dialogs/AddToShoppingDialog.vue b/vue3/src/components/dialogs/AddToShoppingDialog.vue index b802c0a96..adccce471 100644 --- a/vue3/src/components/dialogs/AddToShoppingDialog.vue +++ b/vue3/src/components/dialogs/AddToShoppingDialog.vue @@ -1,9 +1,133 @@