From 54819c6861cd7e9d25fb9f6d7c91420ab74cfb5d Mon Sep 17 00:00:00 2001 From: Ching Date: Sun, 13 Feb 2022 01:18:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(recipe=20model=EF=BC=9Brecipe=20api):=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20ingredient=20=E5=92=8C=20recipe-ingredient?= =?UTF-8?q?=20model;=20=E5=A2=9E=E5=8A=A0=E7=9B=B8=E5=85=B3=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加 ingredient 和 recipe-ingredient model; 增加相关 API Signed-off-by: Ching --- develop_requirements.txt | 1 + dsite/urls.py | 2 +- .../0004_ingredient_recipeingredient.py | 45 ++++++++++++++ recipe/models.py | 33 ++++++++++- recipe/serializers.py | 58 ++++++++++++++++++- recipe/urls.py | 14 +++-- recipe/views.py | 56 ++++++++++++++---- utils/const.py | 3 + 8 files changed, 193 insertions(+), 19 deletions(-) create mode 100644 recipe/migrations/0004_ingredient_recipeingredient.py diff --git a/develop_requirements.txt b/develop_requirements.txt index d7f671b..53f564a 100644 --- a/develop_requirements.txt +++ b/develop_requirements.txt @@ -33,3 +33,4 @@ zipp==3.5.0 redis==4.1.0 black instagram_private_api +drf-nested-routers diff --git a/dsite/urls.py b/dsite/urls.py index 87236df..8d6bd53 100644 --- a/dsite/urls.py +++ b/dsite/urls.py @@ -18,8 +18,8 @@ Including another URLconf from django.urls import include, path from django.conf.urls import url -from rest_framework import routers from rest_framework.authtoken import views +from rest_framework_nested import routers router = routers.DefaultRouter() # Wire up our API using automatic URL routing. diff --git a/recipe/migrations/0004_ingredient_recipeingredient.py b/recipe/migrations/0004_ingredient_recipeingredient.py new file mode 100644 index 0000000..287c3c9 --- /dev/null +++ b/recipe/migrations/0004_ingredient_recipeingredient.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.6 on 2022-02-12 13:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [('recipe', '0003_recipe_status')] + + operations = [ + migrations.CreateModel( + name='Ingredient', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ('unit', models.CharField(max_length=32)), + ('status', models.CharField(default='active', max_length=32)), + ], + ), + migrations.CreateModel( + name='RecipeIngredient', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.FloatField()), + ( + 'ingredient', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='recipe_ingredients', + to='recipe.ingredient', + ), + ), + ( + 'recipe', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='recipe_ingredients', + to='recipe.recipe', + ), + ), + ('status', models.CharField(default='active', max_length=32)), + ], + ), + ] diff --git a/recipe/models.py b/recipe/models.py index 52b566f..0348c7e 100644 --- a/recipe/models.py +++ b/recipe/models.py @@ -6,6 +6,27 @@ from utils import const import utils +class Ingredient(models.Model): + name = models.CharField(max_length=128) + unit = models.CharField(max_length=32) + status = models.CharField(max_length=32, default=const.INGREDIENT_STATUS_ACTIVE) + + +class RecipeIngredient(models.Model): + recipe = models.ForeignKey('Recipe', on_delete=models.CASCADE, related_name='recipe_ingredients') + ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE, related_name='recipe_ingredients') + status = models.CharField(max_length=32, default=const.INGREDIENT_STATUS_ACTIVE) + quantity = models.FloatField() + + def serialize(self): + return { + 'id': self.id, + 'recipe': {'id': self.recipe.id, 'name': self.recipe.name}, + 'ingredient': {'id': self.ingredient.id, 'name': self.ingredient.name, 'unit': self.ingredient.unit}, + 'quantity': self.quantity, + } + + class Recipe(models.Model): name = models.CharField(max_length=128) recipe_type = models.CharField(max_length=32, default=const.RECIPE_TYPE_MEAT) @@ -17,10 +38,20 @@ class Recipe(models.Model): def serialize(self, verbose=False): data = {'id': self.id, 'name': self.name, 'recipe_type': self.recipe_type} if verbose: - data.update({'difficulty': self.difficulty, 'rate': self.rate, 'note': self.note}) + data.update( + { + 'difficulty': self.difficulty, + 'rate': self.rate, + 'note': self.note, + 'recipe_ingredients': self.get_ingredients(), + } + ) return data + def get_ingredients(self): + return [i.serialize() for i in self.recipeingredient_set.order_by('id')] + @classmethod def create_from_str(cls, content): content = content.strip() diff --git a/recipe/serializers.py b/recipe/serializers.py index d5c8050..92d89a2 100644 --- a/recipe/serializers.py +++ b/recipe/serializers.py @@ -1,16 +1,68 @@ -from os import read -from django.contrib.auth.models import User, Group from rest_framework import serializers import recipe.models +from utils import const + + +class IngredientSerializer(serializers.ModelSerializer): + class Meta: + model = recipe.models.Ingredient + fields = '__all__' + + +class RecipeIngredientSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(read_only=True) + ingredient = IngredientSerializer() + + def update(self, instance, validated_data): + if 'ingredient' in validated_data: + validated_data.pop('ingredient') + if 'recipe' in validated_data: + validated_data.pop('recipe') + return super().update(instance, validated_data) + + class Meta: + model = recipe.models.RecipeIngredient + fields = '__all__' class RecipeSerializer(serializers.ModelSerializer): id = serializers.IntegerField(read_only=True) + recipe_ingredients = RecipeIngredientSerializer(many=True) + + def update(self, instance, validated_data): + if 'recipe_ingredients' in validated_data: + recipe_ingredients = validated_data.pop('recipe_ingredients') + if recipe_ingredients: + for recipe_ingredient in recipe_ingredients: + recipe_ingredient['recipe'] = instance + RecipeIngredientSerializer.update(recipe_ingredient) + return super().update(instance, validated_data) + + def create(self, validated_data): + if 'recipe_ingredients' in validated_data: + recipe_ingredients = validated_data.pop('recipe_ingredients') + instance = super().create(validated_data) + if recipe_ingredients: + for recipe_ingredient in recipe_ingredients: + recipe_ingredient['recipe'] = instance + RecipeIngredientSerializer.create(recipe_ingredient) + return instance + + @property + def data(self): + # exclude deleted recipe_ingredients + data_ = super().data + data_['recipe_ingredients'] = [ + ingredient + for ingredient in data_['recipe_ingredients'] + if ingredient['status'] != const.INGREDIENT_STATUS_DELETED + ] + return data_ class Meta: model = recipe.models.Recipe - fields = '__all__' + fields = ('recipe_ingredients', 'id', 'name', 'recipe_type', 'status', 'note', 'rate', 'difficulty') class WeekRecipeSerializer(serializers.ModelSerializer): diff --git a/recipe/urls.py b/recipe/urls.py index 4c3c1b1..6501b7c 100644 --- a/recipe/urls.py +++ b/recipe/urls.py @@ -1,16 +1,22 @@ from django.conf.urls import include, url -# from django.core.urlresolvers import reverse from django.urls import path -from rest_framework import routers from recipe import views # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. +from rest_framework_nested import routers + +router = routers.DefaultRouter() +router.register(r'recipe', views.RecipeAPI) +router.register(r'ingredient', views.IngredientAPI) +recipe_nested_router = routers.NestedSimpleRouter(router, r'recipe', lookup='recipe') +recipe_nested_router.register(r'recipe-ingredient', views.RecipeIngredientAPI) + urlpatterns = [ - url(r'^recipe/(?P\d+)$', views.RecipeAPI.as_view(), name='recipe-detail'), - url(r'^recipe/$', views.RecipeListAPI.as_view()), url(r'^week-recipe/$', views.WeekRecipeListAPI.as_view()), url(r'^daily-recipe/(?P\d+)$', views.DailyRecipeAPI.as_view(), name='dailyrecipe-detail'), + path(r'', include(router.urls)), + path(r'', include(recipe_nested_router.urls)), ] diff --git a/recipe/views.py b/recipe/views.py index 436402b..3739297 100644 --- a/recipe/views.py +++ b/recipe/views.py @@ -12,7 +12,7 @@ import recipe.serializers from utils import const -class RecipeAPI(rest_framework.generics.RetrieveUpdateDestroyAPIView): +class RecipeAPI(viewsets.ModelViewSet): # authentication_classes = (authentication.TokenAuthentication, # authentication.SessionAuthentication, @@ -25,15 +25,10 @@ class RecipeAPI(rest_framework.generics.RetrieveUpdateDestroyAPIView): instance.status = const.RECIPE_STATUS_DELETED instance.save(update_fields=['status']) + def list(self, request, *args, **kwargs): + self.queryset = recipe.models.Recipe.objects.exclude(status=const.RECIPE_STATUS_DELETED) + return super().list(request, *args, **kwargs) -class RecipeListAPI(rest_framework.generics.ListAPIView, rest_framework.generics.CreateAPIView): - - # authentication_classes = (authentication.TokenAuthentication, - # authentication.SessionAuthentication, - # authentication.BasicAuthentication) - # permission_classes = (permissions.IsAuthenticated,) - queryset = recipe.models.Recipe.objects.exclude(status=const.RECIPE_STATUS_DELETED) - serializer_class = recipe.serializers.RecipeSerializer filterset_fields = { 'recipe_type': const.FILTER_EXACT, 'difficulty': const.FILTER_GTE_LTE, @@ -41,7 +36,7 @@ class RecipeListAPI(rest_framework.generics.ListAPIView, rest_framework.generics } -class WeekRecipeListAPI(rest_framework.generics.ListAPIView, rest_framework.generics.CreateAPIView): +class WeekRecipeListAPI(rest_framework.generics.ListCreateAPIView): queryset = recipe.models.DailyRecipe.objects.all() serializer_class = recipe.serializers.WeekRecipeSerializer @@ -78,3 +73,44 @@ class DailyRecipeAPI(rest_framework.generics.RetrieveUpdateAPIView): recipes.extend(request.data.get('soup', [])) daily_recipe.recipes.set(recipe.models.Recipe.objects.filter(id__in=recipes)) return Response(daily_recipe.serialize(), status=status.HTTP_201_CREATED, headers={}) + + +class IngredientAPI(viewsets.ModelViewSet): + + # authentication_classes = (authentication.TokenAuthentication, + # authentication.SessionAuthentication, + # authentication.BasicAuthentication) + # permission_classes = (permissions.IsAuthenticated,) + queryset = recipe.models.Ingredient.objects.all() + serializer_class = recipe.serializers.IngredientSerializer + + def list(self, request, *args, **kwargs): + self.queryset = recipe.models.Ingredient.objects.exclude(status=const.INGREDIENT_STATUS_DELETED) + return super().list(request, *args, **kwargs) + + def perform_destroy(self, instance): + instance.status = const.INGREDIENT_STATUS_DELETED + instance.save(update_fields=['status']) + + +class RecipeIngredientAPI(viewsets.ModelViewSet): + + # authentication_classes = (authentication.TokenAuthentication, + # authentication.SessionAuthentication, + # authentication.BasicAuthentication) + # permission_classes = (permissions.IsAuthenticated,) + queryset = recipe.models.RecipeIngredient.objects.exclude(status=const.INGREDIENT_STATUS_DELETED) + serializer_class = recipe.serializers.RecipeIngredientSerializer + + def get_queryset(self): + return self.queryset.filter(recipe=self.kwargs['recipe_pk']) + + def perform_destroy(self, instance): + instance.status = const.INGREDIENT_STATUS_DELETED + instance.save(update_fields=['status']) + + def create(self, request, *args, **kwargs): + recipe_ingredient = recipe.models.RecipeIngredient.objects.create( + recipe_id=kwargs['recipe_pk'], ingredient_id=request.data['ingredient'], quantity=request.data['quantity'] + ) + return Response(recipe_ingredient.serialize(), status=status.HTTP_201_CREATED, headers={}) diff --git a/utils/const.py b/utils/const.py index ff00587..23c6d0a 100644 --- a/utils/const.py +++ b/utils/const.py @@ -14,6 +14,9 @@ RECIPE_TYPE_SOUP = 'soup' RECIPE_STATUS_ACTIVE = 'active' RECIPE_STATUS_DELETED = 'deleted' +INGREDIENT_STATUS_ACTIVE = 'active' +INGREDIENT_STATUS_DELETED = 'deleted' + RECIPE_TYPE_CHOICE = [RECIPE_TYPE_MEAT, RECIPE_TYPE_VEGETABLE, RECIPE_TYPE_SOUP] FILTER_EXACT = ['exact']