[Django] 02 - Django REST Framework (DRF)

Ref: https://testdriven.io/courses/tdd-django/django-rest-framework/

DRF is composed of two core components:

  1. Serializers are used to convert Django model instances to JSON (serialization) and vice versa (deserialization).
  2. Views (along with ViewSets), which are similar to traditional Django views, handle HTTP requests and return the serialized data. The view itself contains the necessary logic to return the response.

 

DRF has three types of views:

    1. Views, which subclasses Django's View class, are the most basic (and most explicit) DRF view type. They can be function (implemented via the api_view decorator) or class (implemented via the APIView class) based.
    2. ViewSets provide a layer of abstraction above DRF views. They are often used to combine the create, read, update, and destroy (CRUD) logic into a single view. A ViewSet "helps ensure that URL conventions will be consistent across your API, minimizes the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf". They are perfect for implementing the basic CRUD operations for your API. They can be limiting though if your API goes beyond the basics or the API endpoints don't cleanly map back to the models.
    3. Generic Views typically take the abstraction further by inferring the response format, allowed methods, and payload shape based on the serializer.

 

Ref: Django 3.0 Full Course For Beginners 2020 | Django Step By Step Tutorials【先有了Django的基础,4+hr】

Ref: Django REST Framework Full Course For Beginners | Build REST API With Django【视频教程,重点在本篇过一遍,2+hr】

Ref: Django REST framework 中文教程

Ref: Django REST framework

Ref: 一图看懂Django和DRF【感觉不错,concise】

Templates 没了;Views 变成了 Views ViewSets Serializers。

 

 

 

 

开始学习


访问数据库

一、模型 model

调用模型,对数据库进行操作。

装饰器:You can use pytest marks to tell pytest-django your test needs database access.

# app/tests/movies/test_models.py

import pytest
from movies.models import Movie


@pytest.mark.django_db  # 这个是必须要有的
def test_movie_model():
    movie = Movie(title="Raising Arizona", genre="comedy", year="1987")
    movie.save()
    assert movie.title == "Raising Arizona"
    assert movie.genre == "comedy"
    assert movie.year  == "1987"
    assert movie.created_date
    assert movie.updated_date
    assert str(movie)  == movie.title

 

model 定义。

# app/movies/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser


class CustomUser(AbstractUser):
    pass

class Movie(models.Model): title = models.CharField(max_length=255) genre = models.CharField(max_length=255) year = models.CharField(max_length=4) created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) def __str__(self): return f"{self.title}"

  

数据库操作 物理上实行。然后 pytest。

$ docker-compose exec movies python manage.py migrate
$ docker-compose exec movies python manage.py makemigrations $ docker-compose exec movies pytest
jeffrey@unsw-ThinkPad-T490:django-tdd-docker$ docker-compose exec movies pytest
===================================================================================== test session starts ======================================================================================
platform linux -- Python 3.8.2, pytest-5.4.1, py-1.9.0, pluggy-0.13.1
django: settings: drf_project.settings (from ini)
rootdir: /usr/src/app, inifile: pytest.ini
plugins: django-3.9.0
collected 3 items                                                                                                                                                                              

tests/movies/test_models.py .                                                                                                                                                            [ 33%]
tests/test_foo.py ..                                                                                                                                                                     [100%]

====================================================================================== 3 passed in 0.64s =======================================================================================
测试Log

 

 

二、超级用户 Admin

  • admin page

这么注册了,UI 上才会有对应多出来的表格填写。

# app/movies/admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin

from .models import Movie, CustomUser


@admin.register(CustomUser)
class UserAdmin(DefaultUserAdmin):
    pass


@admin.register(Movie)
class MovieAdmin(admin.ModelAdmin):
    fields = (
        "title", "genre", "year", "created_date", "updated_date",
    )
    list_display = (
        "title", "genre", "year", "created_date", "updated_date",
    )
    readonly_fields = (
        "created_date", "updated_date",
    )

 

  • Create a superuser account

docker-compose exec movies python manage.py createsuperuser

然后,登录并添加若干条数据。

 

 

三、Serializers

在 URL --> view --> Serializers中,这里是直接测试 serializer部分链接数据的接口。

  • 测试本体

定义一些基本的规则。

# app/movies/serializers.py

from rest_framework import serializers
from .models import Movie

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model            = Movie
        fields           = '__all__'
        read_only_fields = ('id', 'created_date', 'updated_date',)

 

  • 测试用例

从 serializer 的角度去测试。

# app/tests/movies/test_serializers.py

from movies.serializers import MovieSerializer


def test_valid_movie_serializer():
    valid_serializer_data = {
        "title": "Raising Arizona",
        "genre": "comedy",
        "year": "1987"
    }
    serializer = MovieSerializer(data=valid_serializer_data)
assert serializer.is_valid() assert serializer.validated_data == valid_serializer_data assert serializer.data == valid_serializer_data assert serializer.errors == {} def test_invalid_movie_serializer(): invalid_serializer_data = { "title": "Raising Arizona", "genre": "comedy" } serializer = MovieSerializer(data=invalid_serializer_data)
assert not serializer.is_valid() assert serializer.validated_data == {} assert serializer.data == invalid_serializer_data assert serializer.errors == {"year": ["This field is required."]}

 

  • 测试过程

仅测试文件名包含:models 的测试文件。

jeffrey@unsw-ThinkPad-T490:django-tdd-docker$ docker-compose exec movies pytest 
============================= test session starts ============================== platform linux -- Python 3.8.2, pytest-5.4.1, py-1.9.0, pluggy-0.13.1 django: settings: drf_project.settings (from ini) rootdir: /usr/src/app, inifile: pytest.ini plugins: django-3.9.0 collected 5 items tests/movies/test_models.py . [ 20%] tests/test_foo.py .. [ 60%] tests/movies/test_serializers.py .. [100%] ============================== 5 passed in 0.73s ===============================
jeffrey@unsw-ThinkPad-T490:django-tdd-docker$ docker-compose exec movies pytest -k models ============================= test session starts ============================== platform linux -- Python 3.8.2, pytest-5.4.1, py-1.9.0, pluggy-0.13.1 django: settings: drf_project.settings (from ini) rootdir: /usr/src/app, inifile: pytest.ini plugins: django-3.9.0 collected 5 items / 4 deselected / 1 selected tests/movies/test_models.py . [100%] ======================= 1 passed, 4 deselected in 0.73s ========================

 

 

 

 

RESTful Routes


添加一个 REST API

一、添加新的 url

主urls包含子urls的规则。

# app/drf_project/urls.py

from django.contrib import admin
from django.urls import include, path

from .views import ping

urlpatterns = [
    path("admin/", admin.site.urls),
    path("ping/",  ping, name="ping"),
    path("", include("movies.urls")),
]

此处定义好 子urls的规则。

# app/movies/urls.py

from django.urls import path

from .views import MovieList

urlpatterns = [
    path("api/movies/", MovieList.as_view()),
]

urls --> views

# app/movies/views.py

from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status

from .models import Movie
from .serializers import MovieSerializer

class MovieList(APIView):
    def post(self, request, format=None):
        serializer = MovieSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 

有了 rest_framework,也便有了如下的 UI,带来便利。

 

 

二、测试新的 url

两种测试方式,命令行,或者 script mode。

(1) cmd

http --json POST http://localhost:8000/api/movies/ title=Fargo genre=comedy year=1996

(2) script mode

# app/tests/movies/test_views.py

import json
import pytest
from movies.models import Movie

@pytest.mark.django_db
def test_add_movie(client):
    movies = Movie.objects.all()
    assert len(movies) == 0

    resp = client.post(
        "/api/movies/",
        {
            "title": "The Big Lebowski",
            "genre": "comedy",
            "year": "1998",
        },
        content_type="application/json"
    )
    assert resp.status_code == 201
    assert resp.data["title"] == "The Big Lebowski"

    movies = Movie.objects.all()
    assert len(movies) == 1

 

Reminder: 之前是测试serializer,这里是测试url,然后 url --> serializer by view。

 

 

 

GET a Single Movie

一、添加 URL: REST API

路由分配。

# app/movies/urls.py

from django.urls import path
from .views import MovieList, MovieDetail

urlpatterns = [
    path("api/movies/", MovieList.as_view()),
    path("api/movies/<int:pk>/", MovieDetail.as_view()),  # -->
]

GET 某一条内容,并处理。(如果是POST,则没有pk这个参数)

# app/movies/views.py

class
MovieDetail(APIView):
def get_object(self, pk): try: return Movie.objects.get(pk=pk) except Movie.DoesNotExist: raise Http404 def get(self, request, pk, format=None): movie = self.get_object(pk) serializer = MovieSerializer(movie) return Response(serializer.data)

 

 

二、测试 REST API

每一次都要生成 Movie.objects,如果是多次测试,频繁创建就显得比较麻烦,所以,下面将会用到 fixture。

@pytest.mark.django_db
def test_get_single_movie(client):
    movie = Movie.objects.create(title="The Big Lebowski", genre="comedy", year="1998")
    resp = client.get(f"/api/movies/{movie.id}/")
    assert resp.status_code   == 200
    assert resp.data["title"] == "The Big Lebowski"


def test_get_single_movie_incorrect_id(client):
    resp = client.get(f"/api/movies/foo/")  # 原本就没有的url,自然要返回404
    assert resp.status_code == 404

 

 

 

GET All Movies

一、固件 Fixture

将 Movie.objects 的过程独立出来,并返回movie对象。

# app/tests/movies/conftest.py

import pytest
from movies.models import Movie

@pytest.fixture(scope='function')
def add_movie():
    def _add_movie(title, genre, year):
        movie = Movie.objects.create(title=title, genre=genre, year=year)
        return movie
    return _add_movie

相应的 code 可以改进如下。

这里的add_movie其实后面省略了(),等价于函数的返回结果。故,既然都省略了(),也就默认不会有参数。

[改进后效果]

@pytest.mark.django_db
def test_get_single_movie(client, add_movie):
movie
= add_movie(title="The Big Lebowski", genre="comedy", year="1998")
resp
= client.get(f"/api/movies/{movie.id}/")  # 获取单个detail assert resp.status_code == 200 assert resp.data["title"] == "The Big Lebowski"

 

 

二、ORM --> Serialize --> Response

内部先添加两条信息。

然后再通过URL去获取这两条(全部)内容,之后通过 assert 进行测试。

@pytest.mark.django_db
def test_get_all_movies(client, add_movie):
# 先添加 movie_one = add_movie(title="The Big Lebowski", genre="comedy", year="1998") movie_two = add_movie("No Country for Old Men", "thriller", "2007")

# 再获取 resp
= client.get(f"/api/movies/")  # 获取所有(目录)内容的 “具体过程” 如下。
assert resp.status_code == 200 assert resp.data[0]["title"] == movie_one.title assert resp.data[1]["title"] == movie_two.title

添加URL对应的GET处理方法,获取内容。

# app/movies/views.py

class MovieList(APIView):
    def get(self, request, format=None):
        movies     = Movie.objects.all()  # 获得了数据 ORM
        serializer = MovieSerializer(movies, many=True)  # 序列化后再发送返回值 Serialize
        return Response(serializer.data)  # 返回 Response

    def post(self, request, format=None):
        serializer = MovieSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

 

 

 

Database Seed

一、自动加载数据

[
  {
    "model": "movies.movie",
    "pk": 1,
    "fields": {
      "title": "Fargo",
      "genre": "comedy",
      "year": "1996",
      "created_date": "2020-04-11T23:07:11.686Z",
      "updated_date": "2020-04-11T23:07:11.686Z"
    }
  },
  {
    "model": "movies.movie",
    "pk": 2,
    "fields": {
      "title": "No Country for Old Men",
      "genre": "thriller",
      "year": "2007",
      "created_date": "2020-04-11T23:07:52.070Z",
      "updated_date": "2020-04-11T23:07:52.070Z"
    }
  },
  {
    "model": "movies.movie",
    "pk": 3,
    "fields": {
      "title": "A Serious Man",
      "genre": "comedy",
      "year": "2009",
      "created_date": "2020-04-11T23:08:22.582Z",
      "updated_date": "2020-04-11T23:08:22.582Z"
    }
  }
]
准备数据

 

导入数据并测试。

$ docker-compose exec movies python manage.py flush
$ docker-compose exec movies python manage.py loaddata movies.json
$ http --json http://localhost:8000/api/movies/

 

二、Pytest Commands

$ docker-compose exec movies pytest

# disable warnings
$ docker-compose exec movies pytest -p no:warnings

# run only the last failed tests
$ docker-compose exec movies pytest --lf

# run only the tests with names that match the string expression
$ docker-compose exec movies pytest -k "movie and not all_movies"

# stop the test session after the first failure
$ docker-compose exec movies pytest -x

# enter PDB after first failure then end the test session
$ docker-compose exec movies pytest -x --pdb

# stop the test run after two failures
$ docker-compose exec movies pytest --maxfail=2

# show local variables in tracebacks
$ docker-compose exec movies pytest -l

# list the 2 slowest tests
$ docker-compose exec movies pytest  --durations=2
pytest cmd

 

End.

posted @ 2020-11-14 18:02  郝壹贰叁  阅读(110)  评论(0编辑  收藏  举报