引言 前后端不分离 在前后端不分离的应用模式中,前端页面看到的效果都是由后端控制,由后端渲染页面或重定向,也就是后端需要控制前端的展示,前端与后端的耦合度很高 。
这种应用模式比较适合纯网页应用,但是当后端对接 App 时, App 可能并不需要后端返回一个 HTML 网页,而仅仅是数据本身,所以后端原本返回网页的接口不适用于前端 App 应用,为了对接 App 后端还需再开发一套接口。
前后端分离 在前后端分离的应用模式中,后端仅返回前端所需的数据,不再渲染 HTML 页面,不再控制前端的效果 。至于前端用户看到什么效果,从后端请求的数据如何加载到前端中,都由前端自己决定,网页有网页的处理方式, App 有 App 的处理方式,但无论哪种前端,所需的数据基本相同,后端仅需开发一套逻辑对外提供数据即可。
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 pip install django-filter
快速入门 创建一个 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' ]
如果想使用基于浏览器的可视化的 API 目录,并且希望获得一个登录登出功能,那么可以在根路由下添加下面的路由,这个功能类似 Django 自带的 admin 后台:
1 2 3 4 5 6 7 8 from django.contrib import adminfrom django.urls import path, include urlpatterns = [ path('admin/' , admin.site.urls), path('api-auth/' , include('rest_framework.urls' )) ]
迁移。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 modelsclass BookInfo (models.Model): 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 = '图书' verbose_name_plural = verbose_name def __str__ (self ): """定义每个数据对象的显示信息""" return "图书:《" +self.btitle+"》" 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 serializersfrom books.models import BookInfoclass BookInfoSerializer (serializers.ModelSerializer): """专门用于对图书进行进行序列化和反序列化的类: 序列化器类""" class Meta : model = BookInfo fields = '__all__'
上面的代码之后解释。
在 ./books/urls.py
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from rest_framework.routers import DefaultRouterfrom .views import BookInfoAPIViewurlpatterns = [] routers = DefaultRouter() 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 renderfrom rest_framework.viewsets import ModelViewSetfrom books.models import BookInfofrom .serializers import BookInfoSerializerclass 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' , ]
迁移,运行。
这个可视化的页面是方便观察、管理的,实际上前端只需要拿到 json 就行了。
我们可以通过 http://127.0.0.1:8000/api/books/1/
(这个链接符合 REST 规范)进行修改等操作。
这里的数据修改可以在数据库中看到同步:
简单的原理总结:
之后做项目的时候,前端已经写好了,启动上,这个时候我们就可以专注于写后端。这就是前后端分离。
序列化概述
安装 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' , ]
在 ./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_lexersfrom pygments.styles import get_all_stylesLEXERS = [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 serializersfrom snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICESclass SnippetSerializer (serializers.Serializer): id = serializers.IntegerField(read_only=True ) title = serializers.CharField(required=False , allow_blank=True , max_length=100 ) code = serializers.CharField(style={'base_template' : 'textarea.html' }) linenos = serializers.BooleanField(required=False ) language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python' ) style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly' ) def create (self, validated_data ): """ Create and return a new `Snippet` instance, given the validated data. """ return Snippet.objects.create(**validated_data) 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 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()
这个时候就可以在数据库中看到更新的数据。
观察一下序列化:
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 renderfrom django.http import HttpResponse, JsonResponsefrom django.views.decorators.csrf import csrf_exemptfrom rest_framework.parsers import JSONParserfrom snippets.models import Snippetfrom 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 pathfrom snippets import viewsurlpatterns = [ 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 插件,如下操作:
在前端 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 的逻辑:
我们在前端 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(新增):
在前端 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 from rest_framework import statusfrom rest_framework.decorators import api_viewfrom rest_framework.response import Responsefrom snippets.models import Snippetfrom 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) 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 应用:
补充: 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 pathfrom snippets import viewsfrom rest_framework.urlpatterns import format_suffix_patterns urlpatterns = [ path('snippets/' , views.snippet_list), path('snippets/<int:pk>/' , views.snippet_detail), ] urlpatterns = format_suffix_patterns(urlpatterns)
同时在 ./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 Snippetfrom snippets.serializers import SnippetSerializerfrom django.http import Http404from rest_framework.views import APIViewfrom rest_framework.response import Responsefrom rest_framework import statusclass 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 pathfrom rest_framework.urlpatterns import format_suffix_patternsfrom snippets import viewsurlpatterns = [ path('snippets/' , views.SnippetList.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) 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)
观察下面的代码:
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 部分,这里不再赘述。
一些总结:
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 modelsfrom pygments.lexers import get_all_lexersfrom pygments.styles import get_all_stylesLEXERS = [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 ) owner = models.ForeignKey('auth.User' , related_name='snippets' , on_delete=models.CASCADE) highlighted = models.TextField() 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 modelsfrom pygments.lexers import get_all_lexersfrom pygments.styles import get_all_stylesfrom pygments.lexers import get_lexer_by_namefrom pygments.formatters.html import HtmlFormatterfrom pygments import highlightLEXERS = [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 ) owner = models.ForeignKey('auth.User' , related_name='snippets' , on_delete=models.CASCADE) highlighted = models.TextField(default='' ) class Meta : ordering = ['created' ] 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 Userclass 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 Snippetfrom snippets.serializers import SnippetSerializer, UserSerializerfrom django.http import Http404from rest_framework.views import APIViewfrom rest_framework.response import Responsefrom rest_framework import status, genericsclass 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) 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 Userclass 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/
中可以看到:
并且详情页可以访问。
这个 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 serializersfrom snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICESclass SnippetSerializer (serializers.ModelSerializer): class Meta : model = Snippet fields = ('id' , 'title' , 'code' , 'linenos' , 'language' , 'style' ) from django.contrib.auth.models import Userclass 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 from snippets.models import Snippetfrom snippets.serializers import SnippetSerializer, UserSerializerfrom rest_framework.viewsets import ModelViewSetfrom rest_framework import genericsclass 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 Userclass UserAPIView (ModelViewSet ): queryset = User.objects.all () serializer_class = UserSerializer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from django.urls import pathfrom rest_framework.routers import DefaultRouterfrom snippets import viewsfrom .views import UserAPIViewurlpatterns = [ 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 Snippetfrom snippets.serializers import SnippetSerializer, UserSerializerfrom rest_framework.viewsets import ModelViewSetfrom rest_framework import genericsclass 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 Userclass 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 = 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 from snippets.models import Snippetfrom snippets.serializers import SnippetSerializer, UserSerializerfrom rest_framework.viewsets import ModelViewSetfrom rest_framework import genericsclass 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 from django.urls import pathfrom snippets import viewsfrom rest_framework.urlpatterns import format_suffix_patternsurlpatterns = [ 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, permissionsclass 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)
runserver, 可以看到,登出后,下方无 POST 框。
同理,我们在详情页也做类似的操作:
1 2 3 4 class SnippetDetail (generics.RetrieveUpdateDestroyAPIView): queryset = Snippet.objects.all () serializer_class = SnippetSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
效果不再展示。
问题/需求 : 现在登录之后,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 permissionsclass IsOwnerOrReadOnly (permissions.BasePermission): """ Custom permission to only allow owners of an object to edit it. """ def has_object_permission (self, request, view, obj ): if request.method in permissions.SAFE_METHODS: return True return obj.owner == request.user
对 ./snippets/views.py
作如下修改:
1 2 3 4 5 from snippets.permissions import IsOwnerOrReadOnlyclass 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. 如下:
仅管我们在 ./day1031/urls.py
(全局的 url) 中已经写了:
1 2 3 4 5 6 7 8 9 from django.contrib import adminfrom django.urls import path, includeurlpatterns = [ path('admin/' , admin.site.urls), path('' , include('books.urls' )), path('' , include('snippets.urls' )), path('api-auth/' , include('rest_framework.urls' )), ]
Relationships 在 ./snippets/views.py
中新增:
1 2 3 4 5 6 7 8 9 10 from rest_framework.reverse import reversefrom rest_framework.response import Responsefrom 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 pathfrom snippets import viewsfrom rest_framework.urlpatterns import format_suffix_patternsurlpatterns = [ path('' , views.api_root), path('snippets/' , views.SnippetList.as_view(), name = 'snippet-list' ), path('snippets/<int:pk>/' , views.SnippetDetail.as_view()), path('users/' , views.UserList.as_view(), name = 'user-list' ), 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 adminfrom django.urls import path, includeurlpatterns = [ path('admin/' , admin.site.urls), path('' , include('snippets.urls' )), path('api-auth/' , include('rest_framework.urls' )), ]
将 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()), 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 renderersclass SnippetHighlight (generics.GenericAPIView): queryset = Snippet.objects.all () renderer_classes = [renderers.StaticHTMLRenderer] def get (self, request, *args, **kwargs ): snippet = self.get_object() return Response(snippet.highlighted)
runserver, 访问 127.0.0.1:8000/snippets/6/highlight/
, 效果:
但是实际上,我们在 127.0.0.1:8000/snippets/6/
看到的效果是这样的:
新的需求: 我们不希望代码直接堆积在 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 = 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' )
同时在 ./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' ), path('users/' , views.UserList.as_view(), name = 'user-list' ), path('users/<int:pk>/' , views.UserDetail.as_view()), ]
runserver, 效果:
在 ./snippets/serializers.py
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class SnippetSerializer (serializers.HyperlinkedModelSerializer): 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' )
同时在 ./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' ), 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' ), ]
runserver, 效果:
在 ./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 Snippetfrom snippets.serializers import SnippetSerializer, UserSerializerfrom rest_framework.viewsets import ModelViewSetfrom rest_framework import generics, permissionsfrom snippets.permissions import IsOwnerOrReadOnlyfrom rest_framework.decorators import actionfrom rest_framework import renderersfrom rest_framework import viewsetsclass 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 from rest_framework import viewsetsclass UserViewSet (viewsets.ReadOnlyModelViewSet): """ This viewset automatically provides `list` and `retrieve` actions. """ queryset = User.objects.all () serializer_class = UserSerializer from rest_framework import renderersclass SnippetHighlight (generics.GenericAPIView): queryset = Snippet.objects.all () renderer_classes = [renderers.StaticHTMLRenderer] def get (self, request, *args, **kwargs ): snippet = self.get_object() return Response(snippet.highlighted) from rest_framework.reverse import reversefrom rest_framework.response import Responsefrom 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 pathfrom snippets import viewsfrom rest_framework.urlpatterns import format_suffix_patternsfrom snippets.views import SnippetViewSet, api_root, UserViewSetfrom rest_framework import rendererssnippet_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 Snippetfrom snippets.serializers import SnippetSerializer, UserSerializerfrom rest_framework.viewsets import ModelViewSetfrom rest_framework import generics, permissionsfrom snippets.permissions import IsOwnerOrReadOnlyfrom rest_framework.decorators import actionfrom rest_framework import renderersfrom rest_framework import viewsetsclass 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 from rest_framework import viewsetsclass UserViewSet (viewsets.ReadOnlyModelViewSet): """ This viewset automatically provides `list` and `retrieve` actions. """ queryset = User.objects.all () serializer_class = UserSerializer from rest_framework import renderersclass SnippetHighlight (generics.GenericAPIView): queryset = Snippet.objects.all () renderer_classes = [renderers.StaticHTMLRenderer] def get (self, request, *args, **kwargs ): snippet = self.get_object() return Response(snippet.highlighted) from rest_framework.reverse import reversefrom rest_framework.response import Responsefrom 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 pathfrom snippets import viewsfrom rest_framework.urlpatterns import format_suffix_patternsfrom snippets.views import SnippetViewSet, api_root, UserViewSetfrom rest_framework import renderersfrom rest_framework.routers import DefaultRouterfrom django.urls import path, includerouter = DefaultRouter() router.register(r'snippets' , SnippetViewSet, basename='snippet' ) router.register(r'users' , UserViewSet, basename='user' ) 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 serializersfrom snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICESclass SnippetSerializer (serializers.HyperlinkedModelSerializer): owner = serializers.ReadOnlyField(source='owner.username' ) class Meta : model = Snippet fields = ('id' , 'title' , 'code' , 'linenos' , 'language' , 'style' , 'owner' ) from django.contrib.auth.models import Userclass 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
这部分内容可以在调试中学习体会:
request._request
:
Django 原生的 request 对象
RESTframework 的 Request 对象扩展了 Django 的 HttpRequest 对象,所以 Django 原生的标准属性和方法也是可用的。例如 request.META
和 request.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('guide.urls' )), path('api-auth/' , include('rest_framework.urls' )), ]
注意这里将之前的一些代码注释掉了,想要实现之前的功能需要恢复。
新增 ./guide/urls.py
:
1 2 3 4 5 6 7 from django.contrib import adminfrom django.urls import path, include, re_pathfrom guide.views import FileUploadViewurlpatterns = [ 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 renderfrom rest_framework import viewsfrom rest_framework.parsers import FileUploadParserfrom rest_framework.response import Responseclass FileUploadView (views.APIView): parser_classes = [FileUploadParser] def put (self, request, filename, format =None ): file_obj = request.data['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 中:
在 Body 中选择 binary, 上传一个文件,向 127.0.0.1:8000/upload/test/
发送 PUT 请求。
调试中点击继续,得到 postman 的结果:
下面简单演示一下 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 renderfrom rest_framework import viewsfrom rest_framework.parsers import FileUploadParserfrom rest_framework.response import Responseclass FileUploadView (views.APIView): parser_classes = [FileUploadParser] def put (self, request, filename, format =None ): file_obj = request.data['file' ] return Response(status=204 ) from rest_framework.decorators import api_view, renderer_classesfrom 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 adminfrom django.urls import path, include, re_pathfrom guide.views import FileUploadView, simple_html_viewurlpatterns = [ 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 modelsfrom datetime import datetimeclass 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 serializersfrom guide.models import Commentclass 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 serializer = CommentSerializer(data=data) serializer = CommentSerializer(comment, data=data)
这里的保存方式和代码中的 create, update 是对应的:
完善一下 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 from django.shortcuts import renderfrom rest_framework import viewsfrom rest_framework.parsers import FileUploadParserfrom rest_framework.response import Responseclass FileUploadView (views.APIView): parser_classes = [FileUploadParser] def put (self, request, filename, format =None ): file_obj = request.data['file' ] return Response(status=204 ) from rest_framework.decorators import api_view, renderer_classesfrom 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 Commentfrom .serializers import CommentSerializerfrom rest_framework.viewsets import ModelViewSetclass 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 from django.contrib import adminfrom django.urls import path, include, re_pathfrom guide.views import FileUploadView, simple_html_view, CommentViewSetfrom rest_framework.routers import DefaultRouterrouter = 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 serializersfrom guide.models import Commentclass 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 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 class EventViewSet (ModelViewSet ): queryset = Event.objects.all () serializer_class = EventSerializer
1 2 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 : 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 from rest_framework import serializersfrom guide.models import Comment, Event, Event1, Accountfrom django.contrib.auth.models import Userclass 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 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 from django.shortcuts import renderfrom rest_framework import viewsfrom rest_framework.parsers import FileUploadParserfrom rest_framework.response import Responseclass FileUploadView (views.APIView): parser_classes = [FileUploadParser] def put (self, request, filename, format =None ): file_obj = request.data['file' ] return Response(status=204 ) from rest_framework.decorators import api_view, renderer_classesfrom 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 ModelViewSetclass 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from django.contrib import adminfrom django.urls import path, include, re_pathfrom guide.views import FileUploadView, UserViewSet, simple_html_view, CommentViewSet, EventViewSetfrom rest_framework.routers import DefaultRouterrouter = DefaultRouter() router.register(r'comment' , CommentViewSet) router.register(r'event' , EventViewSet) router.register(r'users' , UserViewSet) 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 )
效果:POST 时前端无须提交 Created .
可以在官网查看:allow_null
, source
, validators
, label
, help_text
, initial
.
style
例子:
1 2 3 4 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 class CommentSerializer (serializers.Serializer): email = serializers.EmailField() content = serializers.ChoiceField(choices=[100 , 101 ]) created = serializers.DateTimeField(required=False )
效果:
回滚此次改动。
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 adminfrom .models import *admin.site.register(Album) admin.site.register(Track)
一通操作:
1 2 3 4 5 6 7 8 9 10 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 class AlbumViewSet (ModelViewSet ): queryset = Album.objects.all () serializer_class = AlbumSerializer class TrackViewSet (ModelViewSet ): queryset = Track.objects.all () serializer_class = TrackSerializer
1 2 3 router.register(r'albums' , AlbumViewSet) router.register(r'tracks' , TrackViewSet)
runserver, worked:
新的需求: 我们想要在唱片下面顺便显示一些歌曲。
作修改如下:
1 2 3 4 5 6 7 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 serializersfrom guide.models import Album, Comment, Event, Event1, Account, Trackfrom django.contrib.auth.models import Userclass CommentSerializer (serializers.Serializer): email = serializers.EmailField() content = serializers.CharField(max_length=200 ) 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__' 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 class TrackSerializer (serializers.ModelSerializer): class Meta : model = Track fields = ['order' , 'title' , 'duration' ] class AlbumSerializer (serializers.ModelSerializer): tracks = TrackSerializer(many=True ) 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 serializersfrom guide.models import Album, Comment, Event, Event1, Account, Trackfrom django.contrib.auth.models import Userclass CommentSerializer (serializers.Serializer): email = serializers.EmailField() content = serializers.CharField(max_length=200 ) 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 timeclass 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' )), path('' , include('guide.urls' )), path('api-auth/' , include('rest_framework.urls' )), ]
我们现在使用 UniqueValidator 防止重复添加同一本书。
1 2 3 4 5 6 7 8 9 10 11 12 13 from rest_framework import serializersfrom rest_framework.validators import UniqueValidatorfrom books.models import BookInfoclass BookInfoSerializer (serializers.ModelSerializer): 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 from rest_framework import serializersfrom rest_framework.validators import UniqueValidatorfrom books.models import BookInfoclass BookInfoSerializer (serializers.ModelSerializer): 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' : [ '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_classesfrom rest_framework.permissions import IsAuthenticatedfrom 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), ]
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 from snippets.models import Snippetfrom snippets.serializers import SnippetSerializer, UserSerializerfrom rest_framework.viewsets import ModelViewSetfrom rest_framework import generics, permissionsfrom snippets.permissions import IsOwnerOrReadOnlyfrom rest_framework.decorators import actionfrom rest_framework import renderersfrom rest_framework import viewsetsfrom django.utils.decorators import method_decoratorfrom django.views.decorators.cache import cache_pagefrom django.views.decorators.vary import vary_on_cookie, vary_on_headersclass 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] @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) from django.contrib.auth.models import Userfrom rest_framework import viewsetsclass UserViewSet (viewsets.ReadOnlyModelViewSet): """ This viewset automatically provides `list` and `retrieve` actions. """ queryset = User.objects.all () serializer_class = UserSerializer from rest_framework import renderersclass SnippetHighlight (generics.GenericAPIView): queryset = Snippet.objects.all () renderer_classes = [renderers.StaticHTMLRenderer] def get (self, request, *args, **kwargs ): snippet = self.get_object() return Response(snippet.highlighted) from rest_framework.reverse import reversefrom rest_framework.response import Responsefrom 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 ) })
没做缓存、刚设置缓存、设置缓存之后取数据是三种不同的速度,平均就时间而言:刚设置缓存 > 没做缓存 > 设置缓存之后。
但是我们目前实现的缓存还比较低级 。例如我们在 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_THROTTLE_CLASSES' : [ 'rest_framework.throttling.AnonRateThrottle' , 'rest_framework.throttling.UserRateThrottle' ], 'DEFAULT_THROTTLE_RATES' : { 'anon' : '1/sec' , 'user' : '500/day' } }
效果:未登录,则一秒内最多访问一次;登录,则每天可以访问 500 次(随便设的)。
下面尝试局部的限流:
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_THROTTLE_RATES' : { 'anon' : '1/hour' , 'user' : '10/min' } }
1 2 3 4 5 6 7 8 9 10 11 12 from rest_framework.throttling import UserRateThrottlefrom rest_framework.views import APIViewclass 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 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()), ]
上面的是用类,下面尝试用视图函数来实现:
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 from django.shortcuts import renderfrom rest_framework import viewsfrom rest_framework.parsers import FileUploadParserfrom rest_framework.response import Responseclass FileUploadView (views.APIView): parser_classes = [FileUploadParser] def put (self, request, filename, format =None ): file_obj = request.data['file' ] return Response(status=204 ) from rest_framework.decorators import api_view, renderer_classesfrom 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 ModelViewSetclass 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 from rest_framework.permissions import IsAuthenticatedfrom rest_framework.throttling import UserRateThrottlefrom rest_framework.response import Response@api_view(['GET' ] ) @permission_classes([IsAuthenticated] ) @throttle_classes([UserRateThrottle] ) def example_view (request, format =None ): content = { 'status' : 'request was permitted' } return Response(content) from rest_framework.views import APIViewclass 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 UserRateThrottleclass BurstRateThrottle (UserRateThrottle ): scope = 'burst'
在 ./guide/views.py
中,我们注释掉之前的,改写:
1 2 3 4 5 6 7 8 9 10 class ExampleView (APIView ): 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_THROTTLE_RATES' : { 'anon' : '1/hour' , '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' ]
在全局 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_THROTTLE_RATES' : { 'anon' : '1/hour' , 'burst' : '5/min' , }, 'DEFAULT_FILTER_BACKENDS' : ['django_filters.rest_framework.DjangoFilterBackend' ], }
方便起见,我们修改一下 ./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 DjangoFilterBackendfrom rest_framework import filtersclass 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
我们另起一份代码。
在 ./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 PageNumberPaginationclass 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
为了防止之前分页设置的影响,我们在全局 settings 中注释掉:
1 2 3 4 5 6 7 8 9 10 11 REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES' : { 'anon' : '1/hour' , '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