[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
View Code

记得在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)
recipe detail

 

    • 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']
RecipeDetailSerializer

 

  • 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'),
Django Url

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
)
Django View

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
View Code

 

 

创建 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)

 

posted @ 2022-06-20 22:15  郝壹贰叁  阅读(31)  评论(0编辑  收藏  举报