引言

前后端不分离

在前后端不分离的应用模式中,前端页面看到的效果都是由后端控制,由后端渲染页面或重定向,也就是后端需要控制前端的展示,前端与后端的耦合度很高

这种应用模式比较适合纯网页应用,但是当后端对接 App 时, App 可能并不需要后端返回一个 HTML 网页,而仅仅是数据本身,所以后端原本返回网页的接口不适用于前端 App 应用,为了对接 App 后端还需再开发一套接口。

91-1.png

前后端分离

在前后端分离的应用模式中,后端仅返回前端所需的数据,不再渲染 HTML 页面,不再控制前端的效果。至于前端用户看到什么效果,从后端请求的数据如何加载到前端中,都由前端自己决定,网页有网页的处理方式, App 有 App 的处理方式,但无论哪种前端,所需的数据基本相同,后端仅需开发一套逻辑对外提供数据即可。

91-2.png

RESTful

基本介绍

RESTful 是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。

REST 全称是 Representational State Transfer,中文意思是表征状态转移。如果一个架构符合 REST 的约束条件和原则,我们就称它为 RESTful 架构。一个简单的点是,相比我们之前编写的 Django 代码,符合 REST 约束的会更规范地设计 url .

理论上 REST 架构风格并不是绑定在 HTTP 上,只不过目前 HTTP 是唯一与 REST 相关的实例。 所以我们这里描述的 REST 也是通过 HTTP 实现的 REST。

RESTful 的核心操作:URL 定位资源,用 HTTP 动词(GET,POST,PUT,DELETE)描述操作。

相关文章:
https://www.ruanyifeng.com/blog/2018/10/restful-api-best-practices.html

错误示范:

1
2
3
/getAllCars
/createNewCar
/deleteAllRedCars

应该改为:

1
2
3
4
GET     /Cars
POST /Cars
PUT /Cars/2 # 这个2仅是示例
DELETE /Cars?color=Red

这种设计极大地减少了 urls 的数量。对于某一个资源的操作,它们的 url 是相同的。每一个资源,对应到后端,就是一个模型类。

安装 DRF

DRF 官网: https://www.django-rest-framework.org/

我这里装在了虚拟环境:

1
2
3
pip install djangorestframework
pip install markdown # Markdown support for the browsable API.
pip install django-filter # Filtering support

快速入门

创建一个 Django 项目 day1031, 创建应用 books.

./day1031/settings.py 中:

1
2
3
4
5
6
7
8
9
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework' # add
]

如果想使用基于浏览器的可视化的 API 目录,并且希望获得一个登录登出功能,那么可以在根路由下添加下面的路由,这个功能类似 Django 自带的 admin 后台:

1
2
3
4
5
6
7
8
# ./day1031/urls.py
from django.contrib import admin
from django.urls import path, include # add

urlpatterns = [
path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls')) # add
]

迁移。runserver.

现在就可以访问 http://127.0.0.1:8000/api-auth/login/ 了。

新增 ./books/urls.py

./day1031/urls.py 中:

1
path('api/', include('books.urls')),

这么做的目的是,访问自己的应用时有一个前缀。其实也可以不这么做,这里只是教学演示。

./books/models.py 中增加下面内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from django.db import models

# Create your models here.


# 定义图书模型类 BookInfo
class BookInfo(models.Model):
# verbose_name 用于在后台管理中,不显示 “btitle”,而是“图书标题”
btitle = models.CharField(max_length=20, verbose_name='图书标题')
bpub_date = models.DateField(verbose_name='出版时间')
bread = models.IntegerField(default=0, verbose_name='阅读量')
bcomment = models.IntegerField(default=0, verbose_name='评论量')
is_delete = models.BooleanField(default=False, verbose_name='逻辑删除')

class Meta:
db_table = 'tb_books' # 指明数据库表名,写不写都行
verbose_name = '图书' # 在admin站点中显示的名称
verbose_name_plural = verbose_name # 显示的复数名称

def __str__(self):
"""定义每个数据对象的显示信息"""
return "图书:《"+self.btitle+"》"


#定义英雄模型类HeroInfo
class HeroInfo(models.Model):
GENDER_CHOICES = (
(0, 'female'),
(1, 'male')
)
hname = models.CharField(max_length=20, verbose_name='名称')
hgender = models.SmallIntegerField(choices=GENDER_CHOICES, default=0, verbose_name='性别')
hcomment = models.CharField(max_length=200, null=True, verbose_name='描述信息')
hbook = models.ForeignKey(BookInfo, on_delete=models.CASCADE, verbose_name='图书') #外键
is_delete = models.BooleanField(default=False, verbose_name='逻辑删除')

class Meta:
db_table = 'tb_heros'
verbose_name = '英雄'
verbose_name_plural = verbose_name

def __str__(self):
return self.hname

新增 ./books/serializers.py

1
2
3
4
5
6
7
8
9
10
from rest_framework import serializers
from books.models import BookInfo

class BookInfoSerializer(serializers.ModelSerializer):
"""专门用于对图书进行进行序列化和反序列化的类: 序列化器类"""
class Meta:
# 当前序列化器在序列化数据的时候,使用哪个模型
model = BookInfo
# fields = ["id","btitle"] # 多个字段可以使用列表声明,如果是所有字段都要转换,则使用 '__all__'
fields = '__all__' # 多个字段可以使用列表声明,如果是所有字段都要转换,则使用 '__all__'

上面的代码之后解释。

./books/urls.py 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from rest_framework.routers import DefaultRouter
# 之前导入的是函数视图,现在改为类视图,参见:
# https://github.com/dropsong/py_webServer/blob/master/day1010/booktest/urls.py
from .views import BookInfoAPIView

urlpatterns = []

# 创建路由对象
routers = DefaultRouter()

# 通过路由对象对视图类进行路由生成
# 通过 restful 设计,注册类视图
routers.register("books",BookInfoAPIView)

urlpatterns+=routers.urls

./books/views.py 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.shortcuts import render

# Create your views here.


from rest_framework.viewsets import ModelViewSet
from books.models import BookInfo
from .serializers import BookInfoSerializer
# Create your views here.
class BookInfoAPIView(ModelViewSet):
# 当前视图类所有方法使用得数据结果集是谁?(从哪一个模型里查数据)
queryset = BookInfo.objects.all()
# 当前视图类使用序列化器类是谁
serializer_class = BookInfoSerializer

上面的 queryset 和 serializer_class 实际上是面向切面编程,传值进去之后,会为框架所用。

./day1031/setting.py 中:

1
2
3
4
5
6
7
8
9
10
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'books', # add
]

迁移,运行。

91-3.jpeg

这个可视化的页面是方便观察、管理的,实际上前端只需要拿到 json 就行了。

我们可以通过 http://127.0.0.1:8000/api/books/1/(这个链接符合 REST 规范)进行修改等操作。

这里的数据修改可以在数据库中看到同步:

91-4.png

简单的原理总结:

91-5.png

之后做项目的时候,前端已经写好了,启动上,这个时候我们就可以专注于写后端。这就是前后端分离。

序列化概述

91-6.png

安装 pygments :

1
pip install pygments --proxy="127.0.0.1:7897"

出于演示目的,创建一个新的名为 snippets 的 app :

1
python3 manage.py startapp snippets

./day1031/settings.py 中增加下面内容:

1
2
3
4
5
6
7
8
9
10
11
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'books',
'snippets', # add
]

./snippets/models.py 中增加下面内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles

LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])


class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)

class Meta:
ordering = ['created']

迁移,这里我们只迁移新的应用:

1
2
python3 manage.py makemigrations snippets
python3 manage.py migrate snippets

新建 ./snippets/serializers.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES


# 在序列化类中没有的字段,查询时得不到,新增也不需要提交这个字段
class SnippetSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True) # readonly GET 时需要,POST 时不需要
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
code = serializers.CharField(style={'base_template': 'textarea.html'}) # style 是为了测试方便
linenos = serializers.BooleanField(required=False)
language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')

# validated 验证后的
def create(self, validated_data):
"""
Create and return a new `Snippet` instance, given the validated data.
"""
# 现在这个函数好像没做什么,但是之后我们会填写自己的逻辑

# 数据已经经过序列化的验证。若验证后想做一些自己的操作,然后再 save ,就可以在这里写代码
return Snippet.objects.create(**validated_data)

# instance 用来帮我们查出实例(想想 books/2 这个链接)
def update(self, instance, validated_data):
"""
Update and return an existing `Snippet` instance, given the validated data.
"""
# 如果前端提交为空,也不会赋空值,而是原来的值
instance.title = validated_data.get('title', instance.title)
instance.code = validated_data.get('code', instance.code)
instance.linenos = validated_data.get('linenos', instance.linenos)
instance.language = validated_data.get('language', instance.language)
instance.style = validated_data.get('style', instance.style)
instance.save()
return instance

这里我们可以看到,相比于直接继承 serializers.ModelSerializer ,继承其父类 serializers.Serializer 会需要更多代码,同时也意味着可以在更大程度上自定义。

序列化器的使用

进入 django shell :

1
python3 manage.py shell
1
2
3
4
5
6
7
8
>>> from snippets.models import Snippet
>>> from snippets.serializers import SnippetSerializer
>>> from rest_framework.renderers import JSONRenderer
>>> from rest_framework.parsers import JSONParser
>>> snippet = Snippet(code='foo = "bar"\n')
>>> snippet.save()
>>> snippet = Snippet(code='print("hello, world")\n')
>>> snippet.save()

这个时候就可以在数据库中看到更新的数据。

观察一下序列化:

91-7.png

1
2
3
4
5
6
7
>>> ser1.data
{'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}
>>>
>>> type(ser1)
<class 'snippets.serializers.SnippetSerializer'>
>>> type(ser1.data)
<class 'rest_framework.utils.serializer_helpers.ReturnDict'>

同时可以看到:

1
2
>>> issubclass(type(ser1.data), dict)
True

说明我们可以把它当作字典用。

上面只是中间结果(例如 False 其实是不合规范的,实际上要 false),要完成最终的序列化过程,我们还需要将数据转换成 json 格式。

1
2
3
4
5
>>> content = JSONRenderer().render(ser1.data)
>>> content
b'{"id":2,"title":"","code":"print(\\"hello, world\\")\\n","linenos":false,"language":"python","style":"friendly"}'
>>> type(content)
<class 'bytes'>

顺带一提,实际我们这里只是理解框架做的事情,真正写项目的时候不会用到这些。

反序列化则是上面过程的逆向:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> import io
>>> stream = io.BytesIO(content)
>>> type(stream)
<class '_io.BytesIO'>
>>> data = JSONParser().parse(stream)
>>> data
{'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}
>>> serializer = SnippetSerializer(data=data)
>>> serializer.is_valid()
True
>>> serializer.validated_data
{'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'}
>>> serializer.save()
<Snippet: Snippet object (3)>

在上面的操作之后,就可以在数据库中看到新增了一条数据。

也可以同时序列化多个对象,只需要为 serializer 添加一个 many=True 的标志:

1
>>> ser2 = SnippetSerializer(Snippet.objects.all(), many=True)

ModelSerializers

正如之前的演示一样,ModelSerializers 要方便的多,若想只选择几个字段而不是全部,打开之前代码的注释,运行即可看到效果。

DRF 的序列化类有一个 repr 属性,可以通过打印序列化器类实例的结构(representation)查看它的所有字段。以下操作在 Django Shell 中进行:

1
2
3
>>> from snippets.serializers import SnippetSerializer
>>> serializer = SnippetSerializer()
>>> print(repr(serializer))

It’s important to remember that ModelSerializer classes don’t do anything particularly magical, they are simply a shortcut for creating serializer classes:

  • An automatically determined set of fields.
  • Simple default implementations for the create() and update() methods.

编写常规的Django视图

./snippets/views.py 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from django.shortcuts import render

# Create your views here.

from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer

@csrf_exempt
def snippet_list(request):
"""
List all code snippets, or create a new snippet.
"""
if request.method == 'GET':
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return JsonResponse(serializer.data, safe=False)

elif request.method == 'POST':
data = JSONParser().parse(request)
serializer = SnippetSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
return JsonResponse(serializer.errors, status=400)

Note that because we want to be able to POST to this view from clients that won’t have a CSRF token we need to mark the view as csrf_exempt. This isn’t something that you’d normally want to do, and REST framework views actually use more sensible behavior than this, but it’ll do for our purposes right now.

新增 ./snippets/urls.py

1
2
3
4
5
6
from django.urls import path
from snippets import views

urlpatterns = [
path('snippets/', views.snippet_list),
]

./day1031/urls.py 中新增路由:

1
path('', include('snippets.urls')), 

这时运行 server, 就可以在 http://127.0.0.1:8000/snippets/ 中看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[
{
"id": 1,
"title": "",
"code": "foo = \"bar\"\n",
"linenos": false,
"language": "python",
"style": "friendly"
},
{
"id": 2,
"title": "",
"code": "print(\"hello, world\")\n",
"linenos": false,
"language": "python",
"style": "friendly"
},
{
"id": 3,
"title": "",
"code": "print(\"hello, world\")",
"linenos": false,
"language": "python",
"style": "friendly"
}
]

我们解析一下这次的逻辑:
浏览器接受到访问 http://127.0.0.1:8000/snippets/ 的请求,到 ./day1031/urls.py 中寻找路由,由于 snippets.urls 被包含了,所以实际上也会查询到 ./snippets/urls.py 中的路由,查询到路由信息,转到视图函数 snippet_list ,执行相关逻辑。

./snippets/views.py 中新增:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@csrf_exempt
def snippet_detail(request, pk):
"""
Retrieve, update or delete a code snippet.
"""
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return HttpResponse(status=404)

if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return JsonResponse(serializer.data)

elif request.method == 'PUT':
data = JSONParser().parse(request)
serializer = SnippetSerializer(snippet, data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data)
return JsonResponse(serializer.errors, status=400)

elif request.method == 'DELETE':
snippet.delete()
return HttpResponse(status=204)

./snippets/urls.py 中配置路由:

1
path('snippets/<int:pk>/', views.snippet_detail),

我们就可以在 http://127.0.0.1:8000/snippets/1/ 中查看详情:

1
2
3
4
5
6
7
8
{
"id": 1,
"title": "",
"code": "foo = \"bar\"\n",
"linenos": false,
"language": "python",
"style": "friendly"
}

使用 postman 验证上面的代码逻辑。

在 vscode 中安装 postman 插件,如下操作:

91-8.png

在前端 http://127.0.0.1:8000/snippets/ 中可以看到,第一条数据已经被删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"id": 2,
"title": "",
"code": "print(\"hello, world\")\n",
"linenos": false,
"language": "python",
"style": "friendly"
},
{
"id": 3,
"title": "",
"code": "print(\"hello, world\")",
"linenos": false,
"language": "python",
"style": "friendly"
}
]

同时后端数据库中的对应数据也被删除。

在测试一下 PUT 的逻辑:

91-9.png

我们在前端 http://127.0.0.1:8000/snippets/2/ 中看到,数据已经被修改:

1
2
3
4
5
6
7
8
{
"id": 2,
"title": "abc",
"code": "print(\"hello, world\")",
"linenos": false,
"language": "python",
"style": "friendly"
}

同时后端的数据库中,相应数据也已经被修改。

下面测试 POST(新增):

91-10.png

在前端 http://127.0.0.1:8000/snippets/ 中可以看到新增了数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[
{
"id": 2,
"title": "abc",
"code": "print(\"hello, world\")",
"linenos": false,
"language": "python",
"style": "friendly"
},
{
"id": 3,
"title": "",
"code": "print(\"hello, world\")",
"linenos": false,
"language": "python",
"style": "friendly"
},
{
"id": 4,
"title": "asdf",
"code": "print(\"hello, how are you\")",
"linenos": false,
"language": "python",
"style": "friendly"
}
]

同时在后端数据库中也看到了相应数据。

API 视图

使用 @api_view 装饰器将一个传统的 Django 视图改造成 DRF 的 API 视图。

编写 ./snippets/views_v2.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# DRF 的 API 视图
# views.py 和 views_v2.py 只能有一个能成为 views.py
# 谁成为 views.py 谁就是真正起作用的那个

from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer


@api_view(['GET', 'POST'])
def snippet_list(request):
"""
List all code snippets, or create a new snippet.
"""
if request.method == 'GET':
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data) # 注意这里改成了 Response

elif request.method == 'POST':
serializer = SnippetSerializer(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)


@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk):
"""
Retrieve, update or delete a code snippet.
"""
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)

if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return Response(serializer.data)

elif request.method == 'PUT':
serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

elif request.method == 'DELETE':
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

运行以后,就可以在前端 http://127.0.0.1:8000/snippets/ 看到 DRF 为我们生成的页面(而不是之前的只有 JSON 数据),类似于之前的 books 应用:

91-11.png

补充:
DRF 引入了一个扩展 Django 常规 HttpRequest 对象的 Request 对象,并提供了更灵活的请求解析能力。Request 对象的核心功能是 request.data 属性,它和 request.POST 类似,但对于使用 Web API 更为有用:

1
2
request.POST # 只处理表单数据 只适用于 POST 方法
request.data # 处理任意数据 适用于 POST,PUT 和 PATCH 等方法

但是现在有一个问题,我们无法在前端访问 http://127.0.0.1:8000/snippets.json ,而之前的 books 可以,这怎么解决呢?

./snippets/urls.py 中:

1
2
3
4
5
6
7
8
9
10
from django.urls import path
from snippets import views
from rest_framework.urlpatterns import format_suffix_patterns # add

urlpatterns = [
path('snippets/', views.snippet_list),
path('snippets/<int:pk>/', views.snippet_detail),
]

urlpatterns = format_suffix_patterns(urlpatterns) # add

同时在 ./snippets/views.py 中作修改:

1
2
3
4
5
def snippet_list(request, format=None):
# ...

def snippet_detail(request, pk, format = None):
# ...

现在就可以在 http://127.0.0.1:8000/snippets.json 中看到 json 数据。
访问 http://127.0.0.1:8000/snippets/2.json 也同样可以看到 json 数据。

截至目前的代码可以参见:
https://github.com/dropsong/py_webServer/tree/master/day1031

基于类的视图

我们复制一份上面的代码,在这基础上修改。新的代码会在后面放出。

./snippets/views.py 中(全部代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status


class SnippetList(APIView):
"""
List all snippets, or create a new snippet.
"""
def get(self, request, format=None):
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)

def post(self, request, format=None):
serializer = SnippetSerializer(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)

./snippets/urls.py 中(全部代码):

1
2
3
4
5
6
7
8
9
10
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
path('snippets/', views.SnippetList.as_view()),
# path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

./snippets/views.py 中新增:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class SnippetDetail(APIView):
"""
Retrieve, update or delete a snippet instance.
"""
def get_object(self, pk):
try:
return Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
raise Http404

def get(self, request, pk, format=None):
snippet = self.get_object(pk)
serializer = SnippetSerializer(snippet)
return Response(serializer.data)

def put(self, request, pk, format=None):
snippet = self.get_object(pk)
# 修改时会传入 snippet 实例
serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

def delete(self, request, pk, format=None):
snippet = self.get_object(pk)
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

同时在 ./snippets/urls.py 中将注释打开。

runserver, 一切正常。

本节的代码参见:
https://github.com/dropsong/py_webServer/tree/master/day1103

使用混合类(mixins)

91-12.png

观察下面的代码:

1
2
3
4
5
6
7
8
9
10
11
def get(self, request, format=None):
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)

def post(self, request, format=None):
serializer = SnippetSerializer(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)

痛点:其实很多时候,这里的写法固定,能否更方便程序员开发呢?

其实这里使用混合类,就是为了寻求不同层次的封装。回忆最开始的时候,我们使用了一个非常强大的 ModelViewSet,就是舍弃了一些自由,换来了方便。

官网资料:
https://www.django-rest-framework.org/tutorial/3-class-based-views/
在其中找到 Using mixins 部分,这里不再赘述。

一些总结:

91-12dot5.png

Authentication & Permissions

我们在之前的代码基础上进行修改。

./snippets/models.py 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from django.db import models

# Create your models here.


from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles

LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])


# Snippet (代码)片段
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE) # add
highlighted = models.TextField() # add

class Meta:
ordering = ['created']

在上面的代码中:

  • related_name='snippets' 是反向关联的名称,定义了从 User 模型访问 Snippet 的方式。设定 related_name='snippets' 后,可以通过 user.snippets.all() 获取某个 User 实例下的所有 Snippet 实例。
  • on_delete=models.CASCADE 决定当关联的 User 被删除时的行为。models.CASCADE 表示删除 User 时,所有关联的 Snippet 也会被删除,以保持数据库的一致性。

迁移(这里会遇到一点小情况,具体在官网可查)。

./snippets/models.py 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from django.db import models

# Create your models here.


from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles

from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight

LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])


# Snippet (代码)片段
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField(default='')

class Meta:
ordering = ['created']

# 保存的时候,需要改变行为,这个时候就要重写 save
# 里面具体的逻辑可以暂时不用关心
def save(self, *args, **kwargs):
"""
Use the `pygments` library to create a highlighted HTML
representation of the code snippet.
"""
lexer = get_lexer_by_name(self.language)
linenos = 'table' if self.linenos else False
options = {'title': self.title} if self.title else {}
formatter = HtmlFormatter(style=self.style, linenos=linenos,
full=True, **options)
self.highlighted = highlight(self.code, lexer, formatter)
super().save(*args, **kwargs)

上面,我们为 Snippet 模型添加了两个字段,其中关于代码高亮的字段,我们在 save 方法里处理了。但是那个 user 字段呢?我们在序列化 Snippet 的时候,这个 User 字段当然也要序列化。在 ./snippets/serializers.py 中新增:

1
2
3
4
5
6
7
8
from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

class Meta:
model = User
fields = ['id', 'username', 'snippets']

./snippets/views.py 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, generics


class SnippetList(APIView):
"""
List all snippets, or create a new snippet.
"""
def get(self, request, format=None):
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)

def post(self, request, format=None):
serializer = SnippetSerializer(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)


class SnippetDetail(APIView):
"""
Retrieve, update or delete a snippet instance.
"""
def get_object(self, pk):
try:
return Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
raise Http404

def get(self, request, pk, format=None):
snippet = self.get_object(pk)
serializer = SnippetSerializer(snippet)
return Response(serializer.data)

def put(self, request, pk, format=None):
snippet = self.get_object(pk)
# 修改时会传入 snippet 实例
serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

def delete(self, request, pk, format=None):
snippet = self.get_object(pk)
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)


from django.contrib.auth.models import User

class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer

class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer

./snippets/urls.py 中新增:

1
2
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),

runserver, 在 127.0.0.1:8000/users/ 中可以看到:

91-13.png

并且详情页可以访问。

这个 User 可以看到之后,再和 Snippets 关联等之后的操作就会比较方便。

回顾一下我们的目标,我们希望代码片段的 owner 是通过登录状态自动获取的。

目前为止的代码:
https://github.com/dropsong/py_webServer/tree/master/day1104

下面为方便起见,我们使用 ModelSerializer 和 ModelViewSet. 我们另起一份代码。

./snippets/serializers.py 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES


class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')


from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

class Meta:
model = User
fields = ['id', 'username', 'snippets']

类似地,我们修改 ./snippets/views.py./snippets/urls.py 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# views.py
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework import generics


class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer


from django.contrib.auth.models import User

class UserAPIView(ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# urls.py
from django.urls import path
from rest_framework.routers import DefaultRouter
from snippets import views
from .views import UserAPIView

urlpatterns = [
path('snippets/', views.SnippetList.as_view()),
path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
]

routers = DefaultRouter()
routers.register("users", UserAPIView)

urlpatterns += routers.urls

运行,尝试在前端提交 Snippets, 报错如下:

1
2
3
4
5
6
7
8
9
10
IntegrityError at /snippets/
NOT NULL constraint failed: snippets_snippet.owner_id
Request Method: POST
Request URL: http://127.0.0.1:8000/snippets/
Django Version: 4.2
Exception Type: IntegrityError
Exception Value:
NOT NULL constraint failed: snippets_snippet.owner_id
Exception Location: /home/zhiyue/myenv/lib/python3.11/site-packages/django/db/backends/sqlite3/base.py, line 328, in execute
Raised during: snippets.views.SnippetsAPIView

此时无法添加新的代码片段,因为它的外键字段 owner 没有定义序列化方法。

修改 ./snippets/views.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework import generics


class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer

# 重写这个函数,就可以改变新增行为,面向切面编程
# 具体细节在继承关系里,需要看源码
def perform_create(self, serializer):
serializer.save(owner=self.request.user)

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer


from django.contrib.auth.models import User

class UserAPIView(ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer

runserver, 访问前端 127.0.0.1:8000/snippets/登录 admin 账户,再次发 POST 请求,可以成功。

新的需求: 我们希望 owner 这个字段新增的时候不需要由前端给后端,但是显示代码片段的时候还是要显示。

修改 ./snippets/serializers.py :

1
2
3
4
5
6
class SnippetSerializer(serializers.ModelSerializer):
# owner 是一个 User 对象
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style', 'owner')

runserver, 可以在前端 127.0.0.1:8000/snippets/ 中看到 owner.

为了可以看到 json 数据,以及之后的方便,我们将代码改为与官网统一。修改 ./snippets/views.py./snippets/urls.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# views.py
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework import generics


class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer

# 重写这个函数,就可以改变新增行为,面向切面编程
# 具体细节在继承关系里,需要看源码
def perform_create(self, serializer):
serializer.save(owner=self.request.user)

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer


from django.contrib.auth.models import User

class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer

class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
1
2
3
4
5
6
7
8
9
10
11
12
13
# urls.py
from django.urls import path
from snippets import views
from rest_framework.urlpatterns import format_suffix_patterns

urlpatterns = [
path('snippets/', views.SnippetList.as_view()),
path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

新的需求: 在前端 127.0.0.1:8000/snippets/ ,只有登录的用户才有下面的 POST 框,未登录的用户没有。

还是面向切面编程,在 ./snippets/views.py 中新增:

1
2
3
4
5
6
7
8
9
10
11
from rest_framework import generics, permissions

class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) # add

# 重写这个函数,就可以改变新增行为,面向切面编程
# 具体细节在继承关系里,需要看源码
def perform_create(self, serializer):
serializer.save(owner=self.request.user)

runserver, 可以看到,登出后,下方无 POST 框。

同理,我们在详情页也做类似的操作:

1
2
3
4
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) # add

效果不再展示。

问题/需求 : 现在登录之后,A 用户可以修改 B 用户的 snippets .

新建 ./snippets/permissions.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""

# 详情页中才能写这个函数,因为只有详情也才有 obj(对象)
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True

# Write permissions are only allowed to the owner of the snippet.
return obj.owner == request.user

./snippets/views.py 作如下修改:

1
2
3
4
5
from snippets.permissions import IsOwnerOrReadOnly
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)

runserver, 用户查看不是自己的代码时,下方无 PUT 框。

Relationships & Hyperlinked APIs

引入

我们在前端 127.0.0.1:8000/ 中,只能看到 books,看不到 snippets. 如下:

91-14.png

仅管我们在 ./day1031/urls.py(全局的 url) 中已经写了:

1
2
3
4
5
6
7
8
9
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('books.urls')), # 使用自己应用内的路由,例如 'api/' ,但为方便我们就留空
path('', include('snippets.urls')),
path('api-auth/', include('rest_framework.urls')), # 有这个,才有 DRF 的登录
]

Relationships

./snippets/views.py 中新增:

1
2
3
4
5
6
7
8
9
10
from rest_framework.reverse import reverse
from rest_framework.response import Response
from rest_framework.decorators import api_view

@api_view(['GET'])
def api_root(request, format=None):
return Response({
'users': reverse('user-list', request=request, format=format),
'snippets': reverse('snippet-list', request=request, format=format)
})

然后在 ./snippets/urls.py 中,作出修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
from django.urls import path
from snippets import views
from rest_framework.urlpatterns import format_suffix_patterns

urlpatterns = [
path('', views.api_root), # add
path('snippets/', views.SnippetList.as_view(), name = 'snippet-list'), # modify
path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
path('users/', views.UserList.as_view(), name = 'user-list'), # modify
path('users/<int:pk>/', views.UserDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

然后在 ./day1031/urls.py(全局 urls) 中:

1
2
3
4
5
6
7
8
9
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
# path('', include('books.urls')), # 使用自己应用内的路由,例如 'api/' ,但为方便我们就留空
path('', include('snippets.urls')),
path('api-auth/', include('rest_framework.urls')), # 有这个,才有 DRF 的登录
]

将 books 注释掉。这个不是好的解决方案,我们之后再处理。

Hyperlinked APIs

需求:我们想要做一些代码高亮的活。

./snippets/urls.py 中新增路由:

1
2
3
4
5
6
7
8
urlpatterns = [
path('', views.api_root),
path('snippets/', views.SnippetList.as_view(), name = 'snippet-list'),
path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view()), # add
path('users/', views.UserList.as_view(), name = 'user-list'),
path('users/<int:pk>/', views.UserDetail.as_view()),
]

./snippets/views.py 中新增:

1
2
3
4
5
6
7
8
9
from rest_framework import renderers

class SnippetHighlight(generics.GenericAPIView):
queryset = Snippet.objects.all()
renderer_classes = [renderers.StaticHTMLRenderer] # 配置渲染器

def get(self, request, *args, **kwargs):
snippet = self.get_object() # 在详情页中拿到了某个 snippet 对象
return Response(snippet.highlighted)

runserver, 访问 127.0.0.1:8000/snippets/6/highlight/, 效果:

91-15.png

但是实际上,我们在 127.0.0.1:8000/snippets/6/ 看到的效果是这样的:

91-16.png

新的需求: 我们不希望代码直接堆积在 127.0.0.1:8000/snippets/6/ 里,而是 "code": 后面放一个链接,可以跳转到 127.0.0.1:8000/snippets/6/highlight/ .

目前为止的代码可以参见:
https://github.com/dropsong/py_webServer/tree/master/day1107

我们另起一份代码。

./snippets/serializers.py 中:

1
2
3
4
5
6
7
8
class SnippetSerializer(serializers.ModelSerializer):
# owner 是一个 User 对象
owner = serializers.ReadOnlyField(source='owner.username')
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html') # add
class Meta:
model = Snippet
# 顺序不要求
fields = ('id', 'title', 'code', 'linenos', 'language', 'style', 'owner', 'highlight') # modify

同时在 ./snippets/urls.py 中:

1
2
3
4
5
6
7
8
urlpatterns = [
path('', views.api_root),
path('snippets/', views.SnippetList.as_view(), name = 'snippet-list'),
path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view(), name = 'snippet-highlight'), # modify
path('users/', views.UserList.as_view(), name = 'user-list'),
path('users/<int:pk>/', views.UserDetail.as_view()),
]

runserver, 效果:

91-17.png

./snippets/serializers.py 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 之前的不要了
# class SnippetSerializer(serializers.ModelSerializer):
# # owner 是一个 User 对象
# owner = serializers.ReadOnlyField(source='owner.username')
# highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
# class Meta:
# model = Snippet
# # 顺序不要求
# fields = ('id', 'title', 'code', 'linenos', 'language', 'style', 'owner', 'highlight')

class SnippetSerializer(serializers.HyperlinkedModelSerializer): # modify
# owner 是一个 User 对象
owner = serializers.ReadOnlyField(source='owner.username')
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
class Meta:
model = Snippet
# 顺序不要求
fields = ('url', 'id', 'title', 'code', 'linenos', 'language', 'style', 'owner', 'highlight') # modify

同时在 ./snippets/urls.py 中:

1
2
3
4
5
6
7
8
urlpatterns = [
path('', views.api_root),
path('snippets/', views.SnippetList.as_view(), name = 'snippet-list'),
path('snippets/<int:pk>/', views.SnippetDetail.as_view(), name = 'snippet-detail'), # modify
path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view(), name = 'snippet-highlight'),
path('users/', views.UserList.as_view(), name = 'user-list'),
path('users/<int:pk>/', views.UserDetail.as_view(), name = 'user-detail'), # modify
]

runserver, 效果:

91-18.png

Pagination

./day1031/settings.py 中新增:

1
2
3
4
5
# 全局分页功能
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 2 # 演示
}

上面代码的作用是在列表页增加分页功能。

runserver, 成功。

目前为止的代码:
https://github.com/dropsong/py_webServer/tree/master/day1109

ViewSets

目标:和 ./snippets/views.py 相比,./books/views.py 相当简洁,我们如何一步一步将 ./snippets/views.py 简化?

我们另起一份代码。

snippets/views.py 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework import generics, permissions


# we delete this:
# class SnippetList(generics.ListCreateAPIView):
# queryset = Snippet.objects.all()
# serializer_class = SnippetSerializer
# permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
#
# # 重写这个函数,就可以改变新增行为,面向切面编程
# # 具体细节在继承关系里,需要看源码
# def perform_create(self, serializer):
# serializer.save(owner=self.request.user)
#
#
from snippets.permissions import IsOwnerOrReadOnly
# class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
# queryset = Snippet.objects.all()
# serializer_class = SnippetSerializer
# permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)


from rest_framework.decorators import action
from rest_framework import renderers
from rest_framework import viewsets
class SnippetViewSet(viewsets.ModelViewSet):
"""
This ViewSet automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.

Additionally we also provide an extra `highlight` action.
"""
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]

@action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
def highlight(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)

def perform_create(self, serializer):
serializer.save(owner=self.request.user)


from django.contrib.auth.models import User

# we delete this:
# class UserList(generics.ListAPIView):
# queryset = User.objects.all()
# serializer_class = UserSerializer
#
# class UserDetail(generics.RetrieveAPIView):
# queryset = User.objects.all()
# serializer_class = UserSerializer

from rest_framework import viewsets
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
This viewset automatically provides `list` and `retrieve` actions.
"""
queryset = User.objects.all()
serializer_class = UserSerializer


from rest_framework import renderers

class SnippetHighlight(generics.GenericAPIView):
queryset = Snippet.objects.all()
renderer_classes = [renderers.StaticHTMLRenderer] # 配置渲染器

def get(self, request, *args, **kwargs):
snippet = self.get_object() # 在详情页中拿到了某个 snippet 对象
return Response(snippet.highlighted)



from rest_framework.reverse import reverse
from rest_framework.response import Response
from rest_framework.decorators import api_view

@api_view(['GET'])
def api_root(request, format=None):
return Response({
'users': reverse('user-list', request=request, format=format),
'snippets': reverse('snippet-list', request=request, format=format)
})

./snippets/urls.py 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from django.urls import path
from snippets import views
from rest_framework.urlpatterns import format_suffix_patterns

from snippets.views import SnippetViewSet, api_root, UserViewSet
from rest_framework import renderers

snippet_list = SnippetViewSet.as_view({
'get': 'list',
'post': 'create'
})
snippet_detail = SnippetViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
})
snippet_highlight = SnippetViewSet.as_view({
'get': 'highlight'
}, renderer_classes=[renderers.StaticHTMLRenderer])
user_list = UserViewSet.as_view({
'get': 'list'
})
user_detail = UserViewSet.as_view({
'get': 'retrieve'
})

urlpatterns = [
path('', api_root),
path('snippets/', snippet_list, name = 'snippet-list'),
path('snippets/<int:pk>/', snippet_detail, name = 'snippet-detail'),
path('snippets/<int:pk>/highlight/', snippet_highlight, name = 'snippet-highlight'),
path('users/', user_list, name = 'user-list'),
path('users/<int:pk>/', user_detail, name = 'user-detail'),
]

urlpatterns = format_suffix_patterns(urlpatterns)

runserver, 功能一切正常。

目前为止的代码:
https://github.com/dropsong/py_webServer/tree/master/day1110

Routers

目标:简化 ./snippets/urls.py .

我们另起一份代码。

修改 ./snippets/views.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework import generics, permissions


# we delete this:
# class SnippetList(generics.ListCreateAPIView):
# queryset = Snippet.objects.all()
# serializer_class = SnippetSerializer
# permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
#
# # 重写这个函数,就可以改变新增行为,面向切面编程
# # 具体细节在继承关系里,需要看源码
# def perform_create(self, serializer):
# serializer.save(owner=self.request.user)
#
#
from snippets.permissions import IsOwnerOrReadOnly
# class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
# queryset = Snippet.objects.all()
# serializer_class = SnippetSerializer
# permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)


from rest_framework.decorators import action
from rest_framework import renderers
from rest_framework import viewsets
class SnippetViewSet(viewsets.ModelViewSet):
"""
This ViewSet automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.

Additionally we also provide an extra `highlight` action.
"""
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]

@action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
def highlight(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)

def perform_create(self, serializer):
serializer.save(owner=self.request.user)


from django.contrib.auth.models import User

# we delete this:
# class UserList(generics.ListAPIView):
# queryset = User.objects.all()
# serializer_class = UserSerializer
#
# class UserDetail(generics.RetrieveAPIView):
# queryset = User.objects.all()
# serializer_class = UserSerializer

from rest_framework import viewsets
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
This viewset automatically provides `list` and `retrieve` actions.
"""
queryset = User.objects.all()
serializer_class = UserSerializer


from rest_framework import renderers

class SnippetHighlight(generics.GenericAPIView):
queryset = Snippet.objects.all()
renderer_classes = [renderers.StaticHTMLRenderer] # 配置渲染器

def get(self, request, *args, **kwargs):
snippet = self.get_object() # 在详情页中拿到了某个 snippet 对象
return Response(snippet.highlighted)



from rest_framework.reverse import reverse
from rest_framework.response import Response
from rest_framework.decorators import api_view

@api_view(['GET'])
def api_root(request, format=None):
return Response({
'users': reverse('user-list', request=request, format=format),
'snippets': reverse('snippet-list', request=request, format=format)
})

修改 ./snippets/urls.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from django.urls import path
from snippets import views
from rest_framework.urlpatterns import format_suffix_patterns

from snippets.views import SnippetViewSet, api_root, UserViewSet
from rest_framework import renderers

# 以下删除:
# snippet_list = SnippetViewSet.as_view({
# 'get': 'list',
# 'post': 'create'
# })
# snippet_detail = SnippetViewSet.as_view({
# 'get': 'retrieve',
# 'put': 'update',
# 'patch': 'partial_update',
# 'delete': 'destroy'
# })
# snippet_highlight = SnippetViewSet.as_view({
# 'get': 'highlight'
# }, renderer_classes=[renderers.StaticHTMLRenderer])
# user_list = UserViewSet.as_view({
# 'get': 'list'
# })
# user_detail = UserViewSet.as_view({
# 'get': 'retrieve'
# })
#
# urlpatterns = [
# path('', api_root),
# path('snippets/', snippet_list, name = 'snippet-list'),
# path('snippets/<int:pk>/', snippet_detail, name = 'snippet-detail'),
# path('snippets/<int:pk>/highlight/', snippet_highlight, name = 'snippet-highlight'),
# path('users/', user_list, name = 'user-list'),
# path('users/<int:pk>/', user_detail, name = 'user-detail'),
# ]
#
# urlpatterns = format_suffix_patterns(urlpatterns)


from rest_framework.routers import DefaultRouter
from django.urls import path, include

# Create a router and register our ViewSets with it.
router = DefaultRouter()
router.register(r'snippets', SnippetViewSet, basename='snippet')
router.register(r'users', UserViewSet, basename='user')

# The API URLs are now determined automatically by the router.
urlpatterns = [
path('', include(router.urls)),
]

修改 ./snippets/serializers.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES


# class SnippetSerializer(serializers.ModelSerializer):
# # owner 是一个 User 对象
# owner = serializers.ReadOnlyField(source='owner.username')
# highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
# class Meta:
# model = Snippet
# # 顺序不要求
# fields = ('id', 'title', 'code', 'linenos', 'language', 'style', 'owner', 'highlight')

class SnippetSerializer(serializers.HyperlinkedModelSerializer):
# owner 是一个 User 对象
owner = serializers.ReadOnlyField(source='owner.username')
# highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
class Meta:
model = Snippet
# 顺序不要求
fields = ('id', 'title', 'code', 'linenos', 'language', 'style', 'owner')


from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

class Meta:
model = User
fields = ['id', 'username', 'snippets']

runserver, 成功。

API Guide

快速了解

参见: 官网 API Guide,可在下拉菜单中选择相应内容。

主要关注内容:

  • Request parsing
    • .data
    • .query_params
    • .parsers
  • Authentication
    • .user

这部分内容可以在调试中学习体会:

91-19.png

request._request :

  • Django 原生的 request 对象
  • RESTframework 的 Request 对象扩展了 Django 的 HttpRequest 对象,所以 Django 原生的标准属性和方法也是可用的。例如 request.METArequest.session 字典正常可用。
  • 由于实现原因,Request 该类不继承自 HttpRequest 类,而是使用 composition 扩展了该类。

后略。

几个值得注意的内容:

解析器是干什么的?
因为前后端分离,可能采用 json、xml、html 等各种不同格式的内容,后端必须要有一个解析器来解析前端发送过来的数据,也就是翻译器。对应地,后端也有一个渲染器 Render ,将后端的数据翻译成前端明白的格式。

下面简单演示一下 FileUploadParser .

新建一个应用 guide.

注册应用 ./day1031/settings.py :

1
2
3
4
5
6
7
8
9
10
11
12
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'books',
'snippets',
'guide',
]

配置全局 ./day1031/urls.py :

1
2
3
4
5
6
7
urlpatterns = [
path('admin/', admin.site.urls),
# path('', include('books.urls')), # 使用自己应用内的路由,例如 'api/' ,但为方便我们就留空
# path('', include('snippets.urls')),
path('', include('guide.urls')),
path('api-auth/', include('rest_framework.urls')), # 有这个,才有 DRF 的登录
]

注意这里将之前的一些代码注释掉了,想要实现之前的功能需要恢复。

新增 ./guide/urls.py :

1
2
3
4
5
6
7
from django.contrib import admin
from django.urls import path, include, re_path
from guide.views import FileUploadView

urlpatterns = [
path('upload/<str:filename>/', FileUploadView.as_view()),
]

./guide/views.py 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.shortcuts import render
from rest_framework import views
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response

# Create your views here.

class FileUploadView(views.APIView):
parser_classes = [FileUploadParser]

def put(self, request, filename, format=None):
file_obj = request.data['file']
# ...
# do some stuff with uploaded file
# ...
return Response(status=204)

runserver, 可以进入 127.0.0.1:8000/upload/aaa,但是不能进入 127.0.0.1:8000 或者 127.0.0.1:8000/upload .

file_obj = request.data['file'] 这行前面打断点,启动调试。

在 postman 中:

91-20.png

在 Body 中选择 binary, 上传一个文件,向 127.0.0.1:8000/upload/test/ 发送 PUT 请求。

91-21.png

调试中点击继续,得到 postman 的结果:

91-22.png

下面简单演示一下 StaticHTMLRenderer .

modify ./guide/views.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from django.shortcuts import render
from rest_framework import views
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response

# Create your views here.

class FileUploadView(views.APIView):
parser_classes = [FileUploadParser]

def put(self, request, filename, format=None):
file_obj = request.data['file']
# ...
# do some stuff with uploaded file
# ...
return Response(status=204)


from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import StaticHTMLRenderer

@api_view(['GET'])
@renderer_classes([StaticHTMLRenderer])
def simple_html_view(request):
data = '<html><body><h1>Hello, world</h1></body></html>'
return Response(data)

modify ./guide/urls.py :

1
2
3
4
5
6
7
8
from django.contrib import admin
from django.urls import path, include, re_path
from guide.views import FileUploadView, simple_html_view

urlpatterns = [
path('upload/<str:filename>/', FileUploadView.as_view()),
path('simple_html_view/', simple_html_view),
]

runserver, 效果:在前端 127.0.0.1:8000/simple_html_view/ 看到渲染好的 Hello,world .

Serializers

修改 ./guide/models.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django.db import models

# Create your models here.

from datetime import datetime
class Comment(models.Model):
email=models.EmailField()
content = models.CharField(max_length=200)
created = models.DateTimeField(auto_now_add=True)

# 酒店预订、入住日期、离开日期
class Event(models.Model):
description = models.CharField(max_length=100)
start = models.DateTimeField()
finish = models.DateTimeField()

# 酒店预订、姓名、房间号、日期
class Event1(models.Model):
name = models.CharField(max_length=50)
room_number = models.IntegerField()
date = models.DateField()

class Account(models.Model):
name = models.CharField(max_length=50)
owner = models.ForeignKey('auth.User',related_name='accounts',on_delete=models.CASCADE,default=None,db_constraint=False)

迁移。

新增 ./guide/serializers.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from rest_framework import serializers
from guide.models import Comment

class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()

def create(self, validated_data):
return Comment.objects.create(**validated_data)

def update(self, instance, validated_data):
instance.email = validated_data.get('email', instance.email)
instance.content = validated_data.get('content', instance.content)
instance.created = validated_data.get('created', instance.created)
instance.save()
return instance

补充一个小细节:
Calling .save() will either create a new instance, or update an existing instance, depending on if an existing instance was passed when instantiating the serializer class:

1
2
3
4
5
# .save() will create a new instance.
serializer = CommentSerializer(data=data)

# .save() will update the existing `comment` instance.
serializer = CommentSerializer(comment, data=data)

这里的保存方式和代码中的 create, update 是对应的:

91-23.png

完善一下 comment 的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# ./guide/views.py
from django.shortcuts import render
from rest_framework import views
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response

# Create your views here.

class FileUploadView(views.APIView):
parser_classes = [FileUploadParser]

def put(self, request, filename, format=None):
file_obj = request.data['file']
# ...
# do some stuff with uploaded file
# ...
return Response(status=204)


from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import StaticHTMLRenderer

@api_view(['GET'])
@renderer_classes([StaticHTMLRenderer])
def simple_html_view(request):
data = '<html><body><h1>Hello, world</h1></body></html>'
return Response(data)


from .models import Comment
from .serializers import CommentSerializer
from rest_framework.viewsets import ModelViewSet

class CommentViewSet(ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ./guide/urls.py
from django.contrib import admin
from django.urls import path, include, re_path
from guide.views import FileUploadView, simple_html_view, CommentViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()

router.register(r'comment', CommentViewSet)

urlpatterns = [
path('upload/<str:filename>/', FileUploadView.as_view()),
path('simple_html_view/', simple_html_view),
]

urlpatterns += router.urls

runserver, worked.

新的需求: 做一个字段级验证,如果 comment 内容里没有 nihao 就报错。

修改 ./guide/serializers.py 为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from rest_framework import serializers
from guide.models import Comment

class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()

def create(self, validated_data):
return Comment.objects.create(**validated_data)

def update(self, instance, validated_data):
instance.email = validated_data.get('email', instance.email)
instance.content = validated_data.get('content', instance.content)
instance.created = validated_data.get('created', instance.created)
instance.save()
return instance

def validate_content(self, value):
"""
Check that the comment is about nihao.
"""
if 'nihao' not in value.lower():
raise serializers.ValidationError("not about nihao")
return value

runserver, worked.

新的需求: 现在需要做一个对象级别的验证

做如下新增:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ./guide/serializers.py
class EventSerializer(serializers.ModelSerializer):
description = serializers.CharField(max_length=100)
start = serializers.DateTimeField()
finish = serializers.DateTimeField()

class Meta:
model = Event
fields = ['id', 'description', 'start', 'finish']

def validate(self, data):
"""
Check that start is before finish.
"""
if data['start'] > data['finish']:
raise serializers.ValidationError("finish must occur after start")
return data
1
2
3
4
# ./guide/views.py
class EventViewSet(ModelViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer
1
2
# ./guide/urls.py
router.register(r'event', EventViewSet)

runserver, worked.

验证器参数: (我们不再在代码中体现,而只是给出简单的例子), Individual fields on a serializer can include validators, by declaring them on the field instance, for example:

1
2
3
4
5
6
7
def multiple_of_ten(value):
if value % 10 != 0:
raise serializers.ValidationError('Not a multiple of ten')

class GameRecord(serializers.Serializer):
score = serializers.IntegerField(validators=[multiple_of_ten])
...

Serializer classes can also include reusable validators that are applied to the complete set of field data. These validators are included by declaring them on an inner Meta class, like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
class EventSerializer(serializers.Serializer):
name = serializers.CharField()
room_number = serializers.IntegerField(choices=[101, 102, 103, 201])
date = serializers.DateField()

class Meta:
# Each room only has one event per day.
validators = [
UniqueTogetherValidator(
queryset=Event.objects.all(),
fields=['room_number', 'date']
)
]

为演示目的,在全局的 url 配置中注释掉 snippets 的部分。

一些奇妙的改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# ./guide/serializers.py
from rest_framework import serializers
from guide.models import Comment, Event, Event1, Account
from django.contrib.auth.models import User

class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()

def create(self, validated_data):
return Comment.objects.create(**validated_data)

def update(self, instance, validated_data):
instance.email = validated_data.get('email', instance.email)
instance.content = validated_data.get('content', instance.content)
instance.created = validated_data.get('created', instance.created)
instance.save()
return instance

def validate_content(self, value):
"""
Check that the comment is about nihao.
"""
if 'nihao' not in value.lower():
raise serializers.ValidationError("not about nihao")
return value


class EventSerializer(serializers.ModelSerializer):
description = serializers.CharField(max_length=100)
start = serializers.DateTimeField()
finish = serializers.DateTimeField()

class Meta:
model = Event
fields = ['id', 'description', 'start', 'finish']

def validate(self, data):
"""
Check that start is before finish.
"""
if data['start'] > data['finish']:
raise serializers.ValidationError("finish must occur after start")
return data


# add
class CreateUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('email', 'username', 'password')
extra_kwargs = {'password': {'write_only': True}} # 如果没有这句,会露出密码(密文)

def create(self, validated_data):
user = User(
email=validated_data['email'],
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.save()
return user
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# ./guide/views.py
from django.shortcuts import render
from rest_framework import views
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response

# Create your views here.

class FileUploadView(views.APIView):
parser_classes = [FileUploadParser]

def put(self, request, filename, format=None):
file_obj = request.data['file']
# ...
# do some stuff with uploaded file
# ...
return Response(status=204)


from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import StaticHTMLRenderer

@api_view(['GET'])
@renderer_classes([StaticHTMLRenderer])
def simple_html_view(request):
data = '<html><body><h1>Hello, world</h1></body></html>'
return Response(data)


from .models import *
from .serializers import *
from rest_framework.viewsets import ModelViewSet

class CommentViewSet(ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer


class EventViewSet(ModelViewSet):
queryset = Event.objects.all().order_by('id')
serializer_class = EventSerializer


# add
class UserViewSet(ModelViewSet):
queryset = User.objects.all()
serializer_class = CreateUserSerializer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ./guide/urls.py
from django.contrib import admin
from django.urls import path, include, re_path
from guide.views import FileUploadView, UserViewSet, simple_html_view, CommentViewSet, EventViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()

router.register(r'comment', CommentViewSet)
router.register(r'event', EventViewSet)
router.register(r'users', UserViewSet) # add

urlpatterns = [
path('upload/<str:filename>/', FileUploadView.as_view()),
path('simple_html_view/', simple_html_view),
]

urlpatterns += router.urls

runserver, 可以在前端看到一些 User 的改动。

BaseSerializer

这是 DRF 中其它序列化器的基类,一般不直接使用。所以,下面的内容看看就好。

Serializer 类直接继承了 BaseSerializer 类,所以两者具有基本相同的 API:

  • .data 返回传出的原始数据。
  • .is_valid() 反序列化并验证传入的数据。
  • .validated_data 返回经过验证后的传入数据。
  • .errors 返回验证期间的错误。
  • .save() 将验证的数据保留到对象实例中。

它还有可以覆写的四种方法:
.to_representation() 重写此方法来改变读取操作的序列化结果。
.to_internal_value() 重写此方法来改变写入操作的序列化结果。
.create() 和 .update() 重写其中一个或两个来改变保存实例时的动作。

因为此类提供与 Serializer 类相同的接口,所以可以将它与现有的基于类的通用视图一起使用,就像使用常规 Serializer 或 ModelSerializer 一样。区别是 BaseSerializer 类并不会在可浏览的 API 页面中生成 HTML 表单。

Serializer fields

Core arguments

注意这里的标题是和官网对应的。

read_only, write_only 不再赘述。

required :
Normally an error will be raised if a field is not supplied during deserialization. Set to false if this field is not required to be present during deserialization.

例子:
我们在 ./guide/serializers.py 中,修改:

1
2
3
4
5
6
# 前略
class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField(required=False) # modified
# ...

效果:POST 时前端无须提交 Created .

可以在官网查看:allow_null, source, validators, label, help_text, initial .

style 例子:

1
2
3
4
# Use <input type="password"> for the input.
password = serializers.CharField(
style={'input_type': 'password'}
)

String fields

可以上官网查看,有些还是挺有用的。

EmailField, RegexField .

SlugField :
这个可以实现的,正则也能实现,只不过这个更简单一点(代价是功能受限)。

URLField, UUIDField, FilePathField, IPAddressField .

其他

可以在官网查看 Numeric fields, Date and time fields, Choice selection fields 等。

这里演示一下 ChoiceField :

1
2
3
4
5
6
7
# ./guide/serializers.py
class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
# content = serializers.CharField(max_length=200)
content = serializers.ChoiceField(choices=[100, 101])
created = serializers.DateTimeField(required=False)
# ...

效果:

91-24.png

回滚此次改动。

Serializer relations

https://www.django-rest-framework.org/api-guide/relations/

./guide/models.py 中新增:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 唱片和歌曲的模型类
class Album(models.Model): # 唱片
album_name = models.CharField(max_length=100)
artist = models.CharField(max_length=100)

def __str__(self):
return self.album_name

class Track(models.Model): # 歌曲
album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE)
order = models.IntegerField()
title = models.CharField(max_length=100)
duration = models.IntegerField()

class Meta:
unique_together = ['album', 'order']
ordering = ['order']

def __str__(self):
return '%d: %s' % (self.order, self.title)

迁移。

./guide/admin.py 中:

1
2
3
4
5
6
7
8
from django.contrib import admin

# Register your models here.

from .models import *

admin.site.register(Album)
admin.site.register(Track)

一通操作:

1
2
3
4
5
6
7
8
9
10
# ./guide/serializers.py
class AlbumSerializer(serializers.ModelSerializer):
class Meta:
model = Album
fields = '__all__'

class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = '__all__'
1
2
3
4
5
6
7
8
# ./guide/views.py
class AlbumViewSet(ModelViewSet):
queryset = Album.objects.all()
serializer_class = AlbumSerializer

class TrackViewSet(ModelViewSet):
queryset = Track.objects.all()
serializer_class = TrackSerializer
1
2
3
# ./guide/urls.py
router.register(r'albums', AlbumViewSet)
router.register(r'tracks', TrackViewSet)

runserver, worked:

91-25.png

新的需求: 我们想要在唱片下面顺便显示一些歌曲。

作修改如下:

1
2
3
4
5
6
7
# ./guide/serializers.py
class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.StringRelatedField(many=True)

class Meta:
model = Album
fields = ['id', 'album_name', 'artist', 'tracks']

效果,前端得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"album_name": "唱片1",
"artist": "周杰伦",
"tracks": [
"1: 蚂蚁",
"2: 东风破"
]
}
]
}

目前为止的代码:
https://github.com/dropsong/py_webServer/tree/master/day1110_2

Nested relationships

嵌套关联。

我们另起一份代码。

./guide/serializers.py 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from rest_framework import serializers
from guide.models import Album, Comment, Event, Event1, Account, Track
from django.contrib.auth.models import User

class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
# content = serializers.ChoiceField(choices=[100, 101])
created = serializers.DateTimeField(required=False)

def create(self, validated_data):
return Comment.objects.create(**validated_data)

def update(self, instance, validated_data):
instance.email = validated_data.get('email', instance.email)
instance.content = validated_data.get('content', instance.content)
instance.created = validated_data.get('created', instance.created)
instance.save()
return instance

def validate_content(self, value):
"""
Check that the comment is about nihao.
"""
if 'nihao' not in value.lower():
raise serializers.ValidationError("not about nihao")
return value


class EventSerializer(serializers.ModelSerializer):
description = serializers.CharField(max_length=100)
start = serializers.DateTimeField()
finish = serializers.DateTimeField()

class Meta:
model = Event
fields = ['id', 'description', 'start', 'finish']

def validate(self, data):
"""
Check that start is before finish.
"""
if data['start'] > data['finish']:
raise serializers.ValidationError("finish must occur after start")
return data



class CreateUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('email', 'username', 'password')
extra_kwargs = {'password': {'write_only': True}} # 如果没有这句,会露出密码(密文)

def create(self, validated_data):
user = User(
email=validated_data['email'],
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.save()
return user

class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = '__all__'

# 嵌套关联
# we deleted sth and add this:
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True, read_only=True)

class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']

runserver, on front page 127.0.0.1:8000/albums/ we see:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"album_name": "唱片1",
"artist": "周杰伦",
"tracks": [
{
"id": 1,
"order": 1,
"title": "蚂蚁",
"duration": 300,
"album": 1
},
{
"id": 2,
"order": 2,
"title": "东风破",
"duration": 400,
"album": 1
}
]
},
{
"album_name": "唱片2",
"artist": "朴树",
"tracks": []
}
]
}

新的需求: 能否在 Album 页面 POST 的时候(就是那个按钮),一并提交歌曲的信息呢?

作修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ./guide/serializers.py
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = ['order', 'title', 'duration']

# 嵌套关联

class AlbumSerializer(serializers.ModelSerializer):
# tracks = TrackSerializer(many=True, read_only=True)
tracks = TrackSerializer(many=True) # 修改时,需要去掉 read_only 属性

class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']

# 接近于固定写法
def create(self, validated_data):
tracks_data = validated_data.pop('tracks')
album = Album.objects.create(**validated_data) # 保存一类
# 保存多类
for track_data in tracks_data:
Track.objects.create(album=album, **track_data)
return album

runserver, on front page 127.0.0.1:8000/albums/ we POST this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"album_name": "唱片3",
"artist": "好妹妹",
"tracks": [
{
"order": 1,
"title": "你飞到城市另一边",
"duration": 300
},
{
"order": 2,
"title": "冬",
"duration": 400
}
]
}

worked.

目前为止的代码:
https://github.com/dropsong/py_webServer/tree/master/day1125

自定义关系类型字段

Custom relational fields.

我们另起一份代码。

修改 ./guide/serializers.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from rest_framework import serializers
from guide.models import Album, Comment, Event, Event1, Account, Track
from django.contrib.auth.models import User

class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
# content = serializers.ChoiceField(choices=[100, 101])
created = serializers.DateTimeField(required=False)

def create(self, validated_data):
return Comment.objects.create(**validated_data)

def update(self, instance, validated_data):
instance.email = validated_data.get('email', instance.email)
instance.content = validated_data.get('content', instance.content)
instance.created = validated_data.get('created', instance.created)
instance.save()
return instance

def validate_content(self, value):
"""
Check that the comment is about nihao.
"""
if 'nihao' not in value.lower():
raise serializers.ValidationError("not about nihao")
return value


class EventSerializer(serializers.ModelSerializer):
description = serializers.CharField(max_length=100)
start = serializers.DateTimeField()
finish = serializers.DateTimeField()

class Meta:
model = Event
fields = ['id', 'description', 'start', 'finish']

def validate(self, data):
"""
Check that start is before finish.
"""
if data['start'] > data['finish']:
raise serializers.ValidationError("finish must occur after start")
return data



class CreateUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('email', 'username', 'password')
extra_kwargs = {'password': {'write_only': True}} # 如果没有这句,会露出密码(密文)

def create(self, validated_data):
user = User(
email=validated_data['email'],
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.save()
return user

class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = ['order', 'title', 'duration']


import time
# 自定义关系字段,之前都是 CharField 之类的,这里我们自定义
class TrackListingField(serializers.RelatedField):
# 改变查询时显示的样式
def to_representation(self, value):
# 时长格式: 秒 -> 几分几秒
duration = time.strftime('%M:%S', time.gmtime(value.duration))
return 'Track %d: %s (%s)' % (value.order, value.title, duration)

class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackListingField(many=True, read_only=True)

class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']

runserver, on front page 127.0.0.1:8000/albums/ we see:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"count": 3,
"next": "http://127.0.0.1:8000/albums/?page=2",
"previous": null,
"results": [
{
"album_name": "唱片1",
"artist": "周杰伦",
"tracks": [
"Track 1: 蚂蚁 (05:00)",
"Track 2: 东风破 (06:40)"
]
},
{
"album_name": "唱片2",
"artist": "朴树",
"tracks": []
}
]
}

worked.

Validators

Validators

在全局 url 配置中打开注释,修改:

1
2
3
4
5
6
7
urlpatterns = [
path('admin/', admin.site.urls),
path('api2/', include('books.urls')), # 使用自己应用内的路由,例如 'api/' ,但为方便我们就留空
# path('', include('snippets.urls')),
path('', include('guide.urls')),
path('api-auth/', include('rest_framework.urls')), # 有这个,才有 DRF 的登录
]

我们现在使用 UniqueValidator 防止重复添加同一本书。

1
2
3
4
5
6
7
8
9
10
11
12
13
# ./books/serializers.py
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from books.models import BookInfo

class BookInfoSerializer(serializers.ModelSerializer):
# 和 models.py 不能冲突
btitle = serializers.CharField(max_length=20, validators=[UniqueValidator(queryset=BookInfo.objects.all())])

class Meta:
# 当前序列化器在序列化数据的时候,使用哪个模型
model = BookInfo
fields = ['id', 'btitle', 'bpub_date', 'bread', 'bcomment']

runserver, 效果:不能再添加一本书名重复的书籍。

另外可以关注:

  • UniqueForDateValidator
  • UniqueForMonthValidator
  • UniqueForYearValidator

Advanced field defaults

CurrentUserDefault:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ./books/serializers.py
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from books.models import BookInfo

class BookInfoSerializer(serializers.ModelSerializer):
# 和 models.py 不能冲突
btitle = serializers.CharField(max_length=20, validators=[UniqueValidator(queryset=BookInfo.objects.all())])

# 显示登录的用户(仅展示功能,这个效果意味不明)
owner = serializers.CharField(
default=serializers.CurrentUserDefault()
)

class Meta:
# 当前序列化器在序列化数据的时候,使用哪个模型
model = BookInfo
fields = ['id', 'btitle', 'bpub_date', 'bread', 'bcomment', 'owner']

效果:展示已经登录的用户。

Permissions

在全局 settings 中新增:

1
2
3
4
5
6
7
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 2,
'DEFAULT_PERMISSION_CLASSES': [ # add
'rest_framework.permissions.IsAuthenticated',
]
}

效果:访问网站的任何资源,若未登录,都会提示 Authentication credentials were not provided.

为了之后的演示,我们将上面的新增代码注释掉。

./guide/views.py 中新增:

1
2
3
4
5
6
7
8
9
10
11
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def example_view(request, format=None):
content = {
'status': 'request was permitted' # 注意,文本显示允许
}
return Response(content)

./guide/urls.py 中新增:

1
2
3
4
5
urlpatterns = [
path('upload/<str:filename>/', FileUploadView.as_view()),
path('simple_html_view/', simple_html_view),
path('example_view/', example_view), # add
]

runserver, 在未登录状态下访问 127.0.0.1:8000/example_view/ 提示:

1
Authentication credentials were not provided.

若登录,则提示:

1
2
3
{
"status": "request was permitted"
}

另外可以关注 API Reference 里面的:

  • IsAuthenticated
  • IsAdminUser
  • IsAuthenticatedOrReadOnly

Caching

做缓存只能做查询的缓存,新增、修改、删除的缓存是做不了的。

我们对 snippet 页面做一个缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# ./sinppets/views.py
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework import generics, permissions
from snippets.permissions import IsOwnerOrReadOnly
from rest_framework.decorators import action
from rest_framework import renderers
from rest_framework import viewsets

from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie, vary_on_headers

class SnippetViewSet(viewsets.ModelViewSet):
"""
This ViewSet automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.

Additionally we also provide an extra `highlight` action.
"""
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]

# 重写列表页
# With cookie: cache requested url for each user for 2 hours
# add ------------------
@method_decorator(cache_page(60 * 60 * 2))
@method_decorator(vary_on_cookie)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())

page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)

serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

@action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
def highlight(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)

def perform_create(self, serializer):
serializer.save(owner=self.request.user)
# ----------------- add

from django.contrib.auth.models import User


from rest_framework import viewsets
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
This viewset automatically provides `list` and `retrieve` actions.
"""
queryset = User.objects.all()
serializer_class = UserSerializer


from rest_framework import renderers

class SnippetHighlight(generics.GenericAPIView):
queryset = Snippet.objects.all()
renderer_classes = [renderers.StaticHTMLRenderer] # 配置渲染器

def get(self, request, *args, **kwargs):
snippet = self.get_object() # 在详情页中拿到了某个 snippet 对象
return Response(snippet.highlighted)



from rest_framework.reverse import reverse
from rest_framework.response import Response
from rest_framework.decorators import api_view

@api_view(['GET'])
def api_root(request, format=None):
return Response({
'users': reverse('user-list', request=request, format=format),
'snippets': reverse('snippet-list', request=request, format=format)
})

没做缓存、刚设置缓存、设置缓存之后取数据是三种不同的速度,平均就时间而言:刚设置缓存 > 没做缓存 > 设置缓存之后。

91-26.jpeg

但是我们目前实现的缓存还比较低级。例如我们在 127.0.0.1:8000/snippets/2/ 中删除一条数据,再次访问 127.0.0.1:8000/snippets/,会发现数据仍然存在,因为缓存没有失效(在后端的数据库中也可以印证,数据其实已经删除了)。

缓存需要在数据发生变更的时候失效,自己写,在 perform 里处理。不用过于担心,或者这个在 redis 中实现也比较方便。

Throttling 限流

限流是为了防止爬虫。

在全局 setting 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 2,
# 'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.IsAuthenticated',
# ]
'DEFAULT_THROTTLE_CLASSES': [ # ------------------------ add
'rest_framework.throttling.AnonRateThrottle', # 匿名的
'rest_framework.throttling.UserRateThrottle' # 已经登录的
],
'DEFAULT_THROTTLE_RATES': {
'anon': '1/sec',
'user': '500/day'
} # ------------------------- add
}

效果:未登录,则一秒内最多访问一次;登录,则每天可以访问 500 次(随便设的)。

下面尝试局部的限流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 全局 settings
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 2,
# 'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.IsAuthenticated',
# ]
# 'DEFAULT_THROTTLE_CLASSES': [
# 'rest_framework.throttling.AnonRateThrottle', # 匿名的
# 'rest_framework.throttling.UserRateThrottle' # 已经登录的
# ],
'DEFAULT_THROTTLE_RATES': {
'anon': '1/hour',
'user': '10/min'
}
}
1
2
3
4
5
6
7
8
9
10
11
12
# ./guide/views.py
from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView

class ExampleView(APIView):
throttle_classes = [UserRateThrottle]

def get(self, request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
1
2
3
4
5
6
7
# ./guide/urls.py
urlpatterns = [
path('upload/<str:filename>/', FileUploadView.as_view()),
path('simple_html_view/', simple_html_view),
path('example_view/', example_view),
path('example_view1/', ExampleView.as_view()), # add
]

上面的是用类,下面尝试用视图函数来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# ./guide/views.py
from django.shortcuts import render
from rest_framework import views
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response

# Create your views here.

class FileUploadView(views.APIView):
parser_classes = [FileUploadParser]

def put(self, request, filename, format=None):
file_obj = request.data['file']
# ...
# do some stuff with uploaded file
# ...
return Response(status=204)


from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import StaticHTMLRenderer

@api_view(['GET'])
@renderer_classes([StaticHTMLRenderer])
def simple_html_view(request):
data = '<html><body><h1>Hello, world</h1></body></html>'
return Response(data)


from .models import *
from .serializers import *
from rest_framework.viewsets import ModelViewSet

class CommentViewSet(ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer


class EventViewSet(ModelViewSet):
queryset = Event.objects.all().order_by('id')
serializer_class = EventSerializer


class UserViewSet(ModelViewSet):
queryset = User.objects.all()
serializer_class = CreateUserSerializer



class AlbumViewSet(ModelViewSet):
queryset = Album.objects.all()
serializer_class = AlbumSerializer

class TrackViewSet(ModelViewSet):
queryset = Track.objects.all()
serializer_class = TrackSerializer


from rest_framework.decorators import api_view, permission_classes, throttle_classes # add
from rest_framework.permissions import IsAuthenticated
from rest_framework.throttling import UserRateThrottle
from rest_framework.response import Response

@api_view(['GET'])
@permission_classes([IsAuthenticated])
@throttle_classes([UserRateThrottle]) # add
def example_view(request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)


from rest_framework.views import APIView

class ExampleView(APIView):
throttle_classes = [UserRateThrottle]

def get(self, request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)

runserver, 在 127.0.0.1:8000/example_view/ 实现了类似的效果。

如何识别客户端?

要限流,就必须识别客户端。DRF 如何判断、区分当前客户端的身份呢?
DRF 利用 HTTP 报头的 'x-forwarded-for' 或 WSGI 中的 'remote-addr' 变量来唯一标识客户端的 IP 地址。如果存在 'x-forwarded-for' 头部属性,则使用它,否则将使用 WSGI 中 'remote-addr' 变量的值。
在代理的情况下,如果想严格标识唯一的客户端 IP 地址,需要首先设置 NUM_PROXIES 来配置 API 后面运行的应用程序代理的数量。此设置应为大于等于 0 的整数。如果设置为非零,则一旦排除了任何应用程序代理 IP 地址,客户端 IP 将被标识为 'x-forwarded-for' 头中的最后一个 IP 地址。如果设置为零,则 'remote-addr' 的值将始终用作标识 IP 地址。重要的是要清楚,如果配置了 NUM_PROXIES, 那么 NAT(网络地址转换) 网关后面的所有客户机都将被视为单个客户机。

新的需求: 我们发现,上面的限流都是读的全局 settings, 因此限流策略是一样的。但是我们想在不同的页面实现不同的限流策略。

新建文件 ./guide/throttles.py:

1
2
3
4
from rest_framework.throttling import UserRateThrottle

class BurstRateThrottle(UserRateThrottle):
scope = 'burst'

./guide/views.py 中,我们注释掉之前的,改写:

1
2
3
4
5
6
7
8
9
10
# 前略
class ExampleView(APIView):
# throttle_classes = [UserRateThrottle]
throttle_classes = [BurstRateThrottle]

def get(self, request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)

在全家 settings 中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 2,
# 'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.IsAuthenticated',
# ]
# 'DEFAULT_THROTTLE_CLASSES': [
# 'rest_framework.throttling.AnonRateThrottle', # 匿名的
# 'rest_framework.throttling.UserRateThrottle' # 已经登录的
# ],
'DEFAULT_THROTTLE_RATES': {
'anon': '1/hour',
# 'user': '10/min',
'burst': '5/min',
}
}

一种更简单的方法: 去官网查找内容 ScopedRateThrottle .

Filtering

之前已经装过:

1
pip install django-filter

在全局 settings 中注册应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'books',
'snippets',
'guide',
'django_filters' # add
]

在全局 settings 中新增:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 2,
# 'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.IsAuthenticated',
# ]
# 'DEFAULT_THROTTLE_CLASSES': [
# 'rest_framework.throttling.AnonRateThrottle', # 匿名的
# 'rest_framework.throttling.UserRateThrottle' # 已经登录的
# ],
'DEFAULT_THROTTLE_RATES': {
'anon': '1/hour',
# 'user': '10/min',
'burst': '5/min',
},
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], # add
}

方便起见,我们修改一下 ./guide/serializers.py :

1
2
3
4
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = ['album', 'order', 'title', 'duration']

现在我们具体地实现一下过滤功能:

1
2
3
4
5
6
7
8
9
10
11
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters

class TrackViewSet(ModelViewSet):
filter_backends = [DjangoFilterBackend,filters.SearchFilter,filters.OrderingFilter]
filterset_fields = '__all__'
search_fields = ['order','title','album__album_name'] #外键加入两个下划线
ordering_fields = '__all__'

queryset = Track.objects.all()
serializer_class = TrackSerializer

runserver, easy to play around. worked.

目前为止的代码:
https://github.com/dropsong/py_webServer/tree/master/day1128

Pagination

我们另起一份代码。

./guide/views.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from rest_framework.pagination import PageNumberPagination

# 分页类 add
class StandardResultsSetPagination(PageNumberPagination):
page_size = 2 # 数字小是仅作演示
page_size_query_param = 'page_size' # 这个名字不能改
max_page_size = 1000

# ...

class TrackViewSet(ModelViewSet):
filter_backends = [DjangoFilterBackend,filters.SearchFilter,filters.OrderingFilter]
filterset_fields = '__all__'
search_fields = ['order','title','album__album_name'] #外键加入两个下划线
ordering_fields = '__all__'

queryset = Track.objects.all()
serializer_class = TrackSerializer
pagination_class = StandardResultsSetPagination # add

为了防止之前分页设置的影响,我们在全局 settings 中注释掉:

1
2
3
4
5
6
7
8
9
10
11
REST_FRAMEWORK = {
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
# 'PAGE_SIZE': 2,

'DEFAULT_THROTTLE_RATES': {
'anon': '1/hour',
# 'user': '10/min',
'burst': '5/min',
},
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
}

runserver, on front page 127.0.0.1:8000/tracks/ we see it worked.

但是这和之前的有什么区别呢?

我们可以手动地调整请求 url 为: 127.0.0.1:8000/tracks/?page_size=3, 可以看到很好地实现了效果。

最后的代码版本:
https://github.com/dropsong/py_webServer/tree/master/day1201