summaryrefslogtreecommitdiff
path: root/kitchen
diff options
context:
space:
mode:
Diffstat (limited to 'kitchen')
-rw-r--r--kitchen/serializers.py41
-rw-r--r--kitchen/urls.py1
-rw-r--r--kitchen/views.py159
3 files changed, 201 insertions, 0 deletions
diff --git a/kitchen/serializers.py b/kitchen/serializers.py
index 43eb9a7..0de5220 100644
--- a/kitchen/serializers.py
+++ b/kitchen/serializers.py
@@ -75,6 +75,47 @@ class MetaRecipeSerializer(serializers.ModelSerializer):
fields = "__all__"
+# --- Writable nested meta-recipe ---
+
+
+class SlotOptionWriteSerializer(serializers.Serializer):
+ ingredient_name = serializers.CharField()
+ ingredient_id = serializers.IntegerField(required=False)
+ quantity_per_serving = serializers.DecimalField(max_digits=8, decimal_places=2)
+ unit = serializers.CharField()
+ notes = serializers.CharField(required=False, default="", allow_blank=True)
+ # For auto-creating ingredients
+ default_unit = serializers.CharField(required=False)
+ tags = serializers.ListField(child=serializers.CharField(), required=False, default=list)
+ shelf_life_days = serializers.IntegerField(required=False, allow_null=True, default=None)
+
+
+class SlotWriteSerializer(serializers.Serializer):
+ name = serializers.CharField()
+ required = serializers.BooleanField(default=True)
+ max_choices = serializers.IntegerField(default=1)
+ options = SlotOptionWriteSerializer(many=True)
+
+
+class BaseIngredientWriteSerializer(serializers.Serializer):
+ ingredient_name = serializers.CharField()
+ ingredient_id = serializers.IntegerField(required=False)
+ quantity_per_serving = serializers.DecimalField(max_digits=8, decimal_places=2)
+ unit = serializers.CharField()
+
+
+class MetaRecipeWriteSerializer(serializers.Serializer):
+ name = serializers.CharField()
+ method = serializers.CharField()
+ prep_time_mins = serializers.IntegerField(required=False, allow_null=True, default=None)
+ cook_time_mins = serializers.IntegerField(required=False, allow_null=True, default=None)
+ default_servings = serializers.IntegerField(default=2)
+ gear_needed = serializers.CharField(required=False, default="", allow_blank=True)
+ tags = serializers.ListField(child=serializers.CharField(), required=False, default=list)
+ slots = SlotWriteSerializer(many=True, required=False, default=list)
+ base_ingredients = BaseIngredientWriteSerializer(many=True, required=False, default=list)
+
+
# --- Fixed Recipe nested serializers ---
diff --git a/kitchen/urls.py b/kitchen/urls.py
index 2eabc81..ff3745f 100644
--- a/kitchen/urls.py
+++ b/kitchen/urls.py
@@ -22,4 +22,5 @@ urlpatterns = [
path("bulk-pantry-add/", views.bulk_pantry_add, name="bulk-pantry-add"),
path("generate-shopping-list/", views.generate_shopping_list, name="generate-shopping-list"),
path("import-recipe/", views.import_recipe_url, name="import-recipe"),
+ path("create-meta-recipe/", views.create_meta_recipe, name="create-meta-recipe"),
]
diff --git a/kitchen/views.py b/kitchen/views.py
index 6f34333..3dfbf4b 100644
--- a/kitchen/views.py
+++ b/kitchen/views.py
@@ -24,6 +24,7 @@ from .serializers import (
IngredientSerializer,
PantryItemSerializer,
MetaRecipeSerializer,
+ MetaRecipeWriteSerializer,
SlotSerializer,
SlotOptionSerializer,
MetaRecipeBaseSerializer,
@@ -129,6 +130,164 @@ class ShoppingListItemViewSet(viewsets.ModelViewSet):
filterset_fields = ["checked"]
+# --- Create/Update Meta-Recipe (nested) ---
+
+
+@api_view(["POST", "PUT"])
+@permission_classes([IsAuthenticated])
+def create_meta_recipe(request):
+ """
+ Create or update a full meta-recipe with slots, options, and base ingredients in one call.
+
+ POST creates new. PUT updates (requires "id" in body).
+
+ Body:
+ {
+ "name": "Baked Pasta",
+ "method": "1. Boil pasta\\n2. Mix in tin with veg + sauce\\n3. Oven 25-30 mins",
+ "prep_time_mins": 10,
+ "cook_time_mins": 30,
+ "default_servings": 2,
+ "gear_needed": "baking tray",
+ "tags": ["easy", "one-tray"],
+ "slots": [
+ {
+ "name": "pasta",
+ "required": true,
+ "max_choices": 1,
+ "options": [
+ {"ingredient_name": "gnocchi", "quantity_per_serving": "250", "unit": "g"},
+ {"ingredient_name": "orzo", "quantity_per_serving": "100", "unit": "g"}
+ ]
+ }
+ ],
+ "base_ingredients": [
+ {"ingredient_name": "olive oil", "quantity_per_serving": "1", "unit": "splash"}
+ ]
+ }
+
+ Ingredients are looked up by name (case-insensitive, checks aliases).
+ If not found, they are auto-created. Pass "tags" in an option to tag new ingredients.
+ """
+ serializer = MetaRecipeWriteSerializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ data = serializer.validated_data
+
+ if request.method == "PUT":
+ meta_id = request.data.get("id")
+ if not meta_id:
+ return Response({"error": "PUT requires 'id' field"}, status=status.HTTP_400_BAD_REQUEST)
+ try:
+ meta = MetaRecipe.objects.get(id=meta_id)
+ except MetaRecipe.DoesNotExist:
+ return Response({"error": f"MetaRecipe {meta_id} not found"}, status=status.HTTP_404_NOT_FOUND)
+ # Update fields
+ meta.name = data["name"]
+ meta.method = data["method"]
+ meta.prep_time_mins = data.get("prep_time_mins")
+ meta.cook_time_mins = data.get("cook_time_mins")
+ meta.default_servings = data.get("default_servings", 2)
+ meta.gear_needed = data.get("gear_needed", "")
+ meta.tags = data.get("tags", [])
+ meta.save()
+ # Clear old slots and bases for rebuild
+ meta.slots.all().delete()
+ meta.base_ingredients.all().delete()
+ else:
+ meta = MetaRecipe.objects.create(
+ name=data["name"],
+ method=data["method"],
+ prep_time_mins=data.get("prep_time_mins"),
+ cook_time_mins=data.get("cook_time_mins"),
+ default_servings=data.get("default_servings", 2),
+ gear_needed=data.get("gear_needed", ""),
+ tags=data.get("tags", []),
+ )
+
+ created_ingredients = []
+
+ # Create slots + options
+ for slot_data in data.get("slots", []):
+ slot = Slot.objects.create(
+ meta_recipe=meta,
+ name=slot_data["name"],
+ required=slot_data.get("required", True),
+ max_choices=slot_data.get("max_choices", 1),
+ )
+ for opt_data in slot_data.get("options", []):
+ ingredient, was_created = _resolve_ingredient(
+ opt_data.get("ingredient_id"),
+ opt_data["ingredient_name"],
+ opt_data.get("default_unit", opt_data["unit"]),
+ opt_data.get("tags", []),
+ opt_data.get("shelf_life_days"),
+ )
+ if was_created:
+ created_ingredients.append(ingredient.name)
+ SlotOption.objects.create(
+ slot=slot,
+ ingredient=ingredient,
+ quantity_per_serving=opt_data["quantity_per_serving"],
+ unit=opt_data["unit"],
+ notes=opt_data.get("notes", ""),
+ )
+
+ # Create base ingredients
+ for base_data in data.get("base_ingredients", []):
+ ingredient, _ = _resolve_ingredient(
+ base_data.get("ingredient_id"),
+ base_data["ingredient_name"],
+ base_data.get("default_unit", base_data["unit"]),
+ )
+ MetaRecipeBase.objects.create(
+ meta_recipe=meta,
+ ingredient=ingredient,
+ quantity_per_serving=base_data["quantity_per_serving"],
+ unit=base_data["unit"],
+ )
+
+ # Return the full created recipe
+ meta.refresh_from_db()
+ result = MetaRecipeSerializer(
+ MetaRecipe.objects.prefetch_related(
+ "slots__options__ingredient", "base_ingredients__ingredient"
+ ).get(id=meta.id)
+ ).data
+
+ result["_created_ingredients"] = created_ingredients
+
+ return Response(result, status=status.HTTP_201_CREATED if request.method == "POST" else status.HTTP_200_OK)
+
+
+def _resolve_ingredient(ingredient_id, name, default_unit="items", tag_names=None, shelf_life_days=None):
+ """Find or create an ingredient by ID or name. Returns (ingredient, was_created)."""
+ if ingredient_id:
+ try:
+ return Ingredient.objects.get(id=ingredient_id), False
+ except Ingredient.DoesNotExist:
+ pass
+
+ # Look up by name/alias
+ found = _find_ingredient(name)
+ if found:
+ return found, False
+
+ # Create new
+ ingredient = Ingredient.objects.create(
+ name=name.lower(),
+ default_unit=default_unit,
+ shelf_life_days=shelf_life_days,
+ )
+
+ # Add tags if provided
+ if tag_names:
+ for tag_name in tag_names:
+ tag, _ = Tag.objects.get_or_create(name=tag_name.lower())
+ ingredient.tags.add(tag)
+
+ return ingredient, True
+
+
# --- What Can I Cook? ---