[Django] 20 - DRF: Add Recipe API
Recipe, Tag, Ingredients 三者的关系?
背景知识
75. Recipe API design
76. APIView vs Viewsets
从 Test Case 开始
Test Case + Model Implementation
-
Write test for recipe model
Core文件夹写Test model的code: test_models.py
class ModelTests(TestCase): # 继续添加unit test methods:
# test_create_recipe()
-
Implement recipe model
Core文件夹写所有的models。
class Recipe(models.Model): """Recipe object.""" user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, ) title = models.CharField(max_length=255) description = models.TextField() time_minutes = models.IntegerField() price = models.DecimalField(max_digits=5, decimal_places=2) link = models.CharField(max_length=255, blank=True) def __str__(self): return self.title
记得在admin.py中注册。
admin.site.register(models.User, UserAdmin)
admin.site.register(models.Recipe)
再创建 APP
Test Case + Serializers + View Implementation
-
Create APP
docker-compose run -rm app sh -c "python manage.py startapp recipe"
仅保留:
-
开始写 Test Case with setUp
test_recipe_api.py
# # Public. # class PublicRecipeAPITests(TestCase): """Test unauthenticated API requests.""" def setUp(self): self.client = APIClient() def test_auth_required(self): """Test auth is required to call API.""" res = self.client.get(RECIPES_URL) # Not login, so unauthorized. self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) # # Private. # class PrivateRecipeApiTests(TestCase): """Test authenticated API requests.""" def setUp(self): self.client = APIClient()
self.user = get_user_model().objects.create_user( 'user@example.com', 'testpass123', ) self.client.force_authenticate(self.user) # Get one authorized user. def test_retrieve_recipes(self): """Test retrieving a list of recipes.""" create_recipe(user=self.user) # Two same users. ----> 留有伏笔 create_recipe(user=self.user) res = self.client.get(RECIPES_URL) recipes = Recipe.objects.all().order_by('-id') serializer = RecipeSerializer(recipes, many=True) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(res.data, serializer.data) def test_recipe_list_limited_to_user(self): """Test list of recipes is limited to authenticated user.""" other_user = get_user_model().objects.create_user( 'other@example.com', 'password123', ) create_recipe(user=other_user) # Two different users. create_recipe(user=self.user) res = self.client.get(RECIPES_URL) recipes = Recipe.objects.filter(user=self.user) # filter serializer = RecipeSerializer(recipes, many=True) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(res.data, serializer.data)
def test_get_recipe_detail(self): """Test get recipe detail.""" recipe = create_recipe(user=self.user) url = detail_url(recipe.id) res = self.client.get(url) # s1: Next, implement the serializer. serializer = RecipeDetailSerializer(recipe) self.assertEqual(res.data, serializer.data)
-
-
Serializer (DRF)
-
class RecipeSerializer(serializers.ModelSerializer): """Serializer for recipes.""" class Meta: model = Recipe fields = ['id', 'title', 'time_minutes', 'price', 'link'] read_only_fields = ['id']
class RecipeDetailSerializer(RecipeSerializer): """Serializer for recipe detail view.""" class Meta(RecipeSerializer.Meta): # Tricky, only add one more: description fields = RecipeSerializer.Meta.fields + ['description']
-
URL + View
如此,才能 打通 从url端进行 Unit Test 的通路。
[URL]
Here, we are using rest_framework api, but not Django URL:
from django.urls import path from . import views from .views import CategoryList, CategoryDetail # # 04, gallery/photo/add 这样是否可以? # next, 美化 templates/gallery.html # urlpatterns = [ path('', views.gallery, name='gallery'), path('mask/<str:pk>/', views.editMask, name='mask'),
Based on rest_framework:
from django.urls import ( path, include, ) from rest_framework.routers import DefaultRouter from recipe import views router = DefaultRouter() router.register('recipes', views.RecipeViewSet) # <---- create an endpoint here. app_name = 'recipe' urlpatterns = [ path('', include(router.urls)), # 这里继续是使用了include ]
[VIEW]
Here, we are using rest_framework api, but not Django View:
from django.views.generic import ( ListView, DetailView, CreateView, UpdateView, DeleteView )
Based on rest_framework:
from rest_framework import viewsets from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated from core.models import Recipe from recipe import serializers
#
# viewsets: 这里展示了对auth permission的支持!
# class RecipeViewSet(viewsets.ModelViewSet):
"""View for manage recipe APIs.""" serializer_class = serializers.RecipeSerializer queryset = Recipe.objects.all() authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) def get_queryset(self): """Retrieve recipes for authenticated user.""" return self.queryset.filter(user=self.request.user).order_by('-id')
# --------------------------------------------------------------------------
def get_serializer_class(self): """Return the serializer class for request.""" if self.action == 'retrieve': # ----> return serializers.RecipeDetailSerializer return self.serializer_class
-
-
ViewSet Actions
-
Ref: https://www.django-rest-framework.org/api-guide/viewsets/
The default routers included with REST framework will provide routes for a standard set of create/retrieve/update/destroy style actions, as shown below:
class UserViewSet(viewsets.ViewSet): """ Example empty viewset demonstrating the standard actions that will be handled by a router class. If you're using format suffixes, make sure to also include the `format=None` keyword argument for each action. """ def list(self, request): pass def create(self, request): pass def retrieve(self, request, pk=None): pass def update(self, request, pk=None): pass def partial_update(self, request, pk=None): pass def destroy(self, request, pk=None): pass
创建 Recipe
[老方法]
之前采用的固定的方式,如果要加入test,变为灵活的方式,该怎么办?使其写成 Unit Test Case。
def create_recipe(user, **params): """Create and return a sample recipe.""" defaults = { 'title': 'Sample recipe title', 'time_minutes': 22, 'price': Decimal('5.25'), 'description': 'Sample description', 'link': 'http://example.com/recipe.pdf', } defaults.update(params) recipe = Recipe.objects.create(user=user, **defaults) return recipe
[新策略]
def test_create_recipe(self): """Test creating a recipe.""" payload = { 'title': 'Sample recipe', 'time_minutes': 30, 'price': Decimal('5.99'), } res = self.client.post(RECIPES_URL, payload) self.assertEqual(res.status_code, status.HTTP_201_CREATED)
recipe = Recipe.objects.get(id=res.data['id']) for key in payload.keys(): self.assertEqual(payload[key], getattr(recipe, key))
Ref: drf---create方法和perform_create方法有什么区别
def perform_create(self, serializer): """Create a new recipe.""" serializer.save(user=self.request.user)