Serializer案例
案例: 开发一个博客系统.
功能包含 - 博客列表、博客详细、登陆、注册、评论、点赞、发布博客 (最后三个功能必须登陆成功后才能使用.)
约定俗成的规范
这是大家后端编码时默认约定俗成的规范.该规范是符合restful的.
1. api/v1/user GET请求 --> 获取数据列表 models.UserInfo.objects.all()
2. api/v1/user/2/ GET请求 --> 获取单条数据 models.UserInfo.objects.filter(id=2).first()
3. api/v1/user POST请求 --> 新增数据
4. api/v1/user/3/ PUT请求 --> 更新数据
1,3其实可以放到同一个视图类里;2,4同理.
2
3
4
5
6
7
# 表结构的设计
# 表结构分析
一共创建了4张表
UserInfo 用户表
username password token
- 希望用户登陆成功后,将token信息存储到数据库中
- username和token字段都加上db_index=True的索引,加快查询速度.
Blog 博客表
category image title summary text ctime creator comment_count favor_count
- 谁写的这篇博文,创建者通过FK外键字段creator关联到UserInfo用户表
- 评论数和赞数
当有人发布评论,肯定要通过Comment评论表记录下来,并且将Blog博客表的comment_count字段的值加1
当有人点赞,肯定要通过Favor点赞表记录下来,并且将Blog博客表的favor_count字段的值加1
这样在做首页的展示时,需显示评论数和点赞数的地方,直接读Blog这张单表中的评论数和赞数即可,不用再联表查询.
Favor 点赞表
blog user create_datetime
- 哪个用户给哪篇博客点了个赞,得记录下来
- 当该用户已经给该篇博文点赞后,就不能再赞啦.
在数据库Model表中,使用了blog+user字段的联合索引,进行了约束.
Comment 评论表
blog user content create_datetime
- 哪个用户给哪篇博客做了什么评论,得记录下来. 注:此处没有加多级评论,只是一级评论
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 代码实现
记得进行数据库迁移.
from django.db import models
class UserInfo(models.Model):
username = models.CharField(verbose_name="用户名", max_length=32, db_index=True)
password = models.CharField(verbose_name="密码", max_length=64)
token = models.CharField(verbose_name="TOKEN", max_length=64, null=True, blank=True, db_index=True)
class Blog(models.Model):
category_choices = ((1, "云计算"), (2, "Python全栈"), (3, "Go开发"))
category = models.IntegerField(verbose_name="分类", choices=category_choices)
image = models.CharField(verbose_name="封面", max_length=255)
title = models.CharField(verbose_name="标题", max_length=32)
summary = models.CharField(verbose_name="简介", max_length=256)
text = models.TextField(verbose_name="博文")
ctime = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
creator = models.ForeignKey(verbose_name="创建者", to="UserInfo", on_delete=models.CASCADE)
comment_count = models.PositiveIntegerField(verbose_name="评论数", default=0)
favor_count = models.PositiveIntegerField(verbose_name="赞数", default=0)
class Favor(models.Model):
""" 赞 """
blog = models.ForeignKey(verbose_name="博客", to="Blog", on_delete=models.CASCADE)
user = models.ForeignKey(verbose_name="用户", to="UserInfo", on_delete=models.CASCADE)
create_datetime = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
class Meta:
constraints = [
models.UniqueConstraint(fields=['blog', 'user'], name='uni_favor_blog_user')
]
class Comment(models.Model):
""" 评论表 """
blog = models.ForeignKey(verbose_name="博客", to="Blog", on_delete=models.CASCADE)
user = models.ForeignKey(verbose_name="用户", to="UserInfo", on_delete=models.CASCADE)
content = models.CharField(verbose_name="内容", max_length=150)
create_datetime = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
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
离线脚本进行往数据库里添加数据.
import os
import sys
import django
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(base_dir)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings')
django.setup()
if __name__ == '__main__':
from api import models
v1 = models.UserInfo.objects.create(username="wpq", password="wpq123")
v2 = models.UserInfo.objects.create(username="zk", password="zk123")
models.Blog.objects.create(
category=1,
image="xxx/xxx.jpg",
title="wpq-1",
summary="简介:这是wpq写的第一篇博客",
text="博文: Hello world...",
creator=v1,
)
models.Blog.objects.create(
category=2,
image="xxx/xxx.jpg",
title="zk-1",
summary="简介:这是zk写的第一篇博客",
text="博文: WawAaa...",
creator=v2,
)
models.Comment.objects.create(
blog_id=1,
user_id=1,
content="太棒了!"
)
models.Comment.objects.create(
blog_id=1,
user_id=2,
content="无敌!"
)
models.Comment.objects.create(
blog_id=1,
user_id=2,
content="666!"
)
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
# 博客列表
思考: 查看博客列表的信息, 就是GET请求查询数据库, 将查询到的多条记录进行序列化.. 就是单纯的序列化, 不涉及到校验..
若想实现序列化信息的自定制展示.
“该功能只涉及序列化, 这是前提.. 这样的话,无论是额外字段还是自定义方法的字段都可命名为跟数据库中的字段名一样”
1> category字段读取choice在内存对应的值; 时间字段ctime的格式以“年-月-日”的形式展示. (额外字段、自定义方法) 2> FK外键字段creator的详细信息.. (自定义方法、嵌套) ; 跨表后某一个字段信息(额外字段、自定义方法).
路由信息
from django.urls import path
from api import views
urlpatterns = [
path("api/<str:version>/blog", views.BlogView.as_view()) # 博客列表
]
2
3
4
5
6
注:
以往我忽略的一点, category字段在数据库里是IntegerField存储的, 通过get_category_display读取出来的是字符串
所以在序列化器类中定义额外字段category应该为CharField..
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import serializers
from api import models
class UserInfoModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.UserInfo
fields = ["id", "username"]
class BlogModelSerializer(serializers.ModelSerializer):
category = serializers.CharField(source="get_category_display")
ctime = serializers.DateTimeField(format="%Y-%m-%d")
creator = UserInfoModelSerializer()
class Meta:
model = models.Blog
fields = ["category", "image", "title", "summary",
"ctime", "creator", "comment_count", "favor_count"]
class BlogView(APIView):
def get(self, *args, **kwargs):
queryset = models.Blog.objects.all().order_by("-id") # 倒叙排列
ser = BlogModelSerializer(instance=queryset, many=True)
context = {
"code": 1000,
"data": ser.data
}
return Response(context)
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
# 博客详细
思考: 以博客园为例, 在博客列表页面, 点击某篇博客后, 会跳转到该博文的详细页面, 观察url, 发现url中携带着该博客的ID信息.
So, 我们在设置 “博客详细” 的路由时, 应该给路由设置一个动态的值, 用于接受前端url中传递过来的博客ID.查看博客详细, 就是根据博客ID查询数据库, 将这一条记录进行序列化!
同样的, 与查看博客列表一样, 都是单纯的序列化, 不涉及到校验.. (博客列表是序列化多条数据,博客详细是序列化单条数据).
路由信息
from django.urls import path
from api import views
urlpatterns = [
path("api/<str:version>/blog/", views.BlogView.as_view()), # 博客列表
path("api/<str:version>/blog/<int:pk>/", views.BlogDetailView.as_view()) # 博客详细
]
2
3
4
5
6
7
相关的视图代码
注:
博客详细没有使用博客列表的序列器类, 是因为博客详细需要Blog表的所有字段, 而博客列表有些字段不需要,比如博客内容..
所以博客详细用的__all__
, 博客列表指定的字段.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import serializers
from api import models
class BlogDetailModelSerializer(serializers.ModelSerializer):
category = serializers.CharField(source="get_category_display")
ctime = serializers.DateTimeField(format="%Y-%m-%d")
creator = serializers.SerializerMethodField() # - 当然也可以跟博客列表一样,使用嵌套.
def get_creator(self, obj):
return {
"id": obj.creator.pk,
"username": obj.creator.username,
}
class Meta:
model = models.Blog
fields = "__all__"
class BlogDetailView(APIView):
def get(self, *args, **kwargs):
pk = kwargs["pk"]
instance = models.Blog.objects.filter(pk=pk).first()
if not instance:
context = {
"code": 1001,
"error": "博文不存在!请联系管理员."
}
return Response(context)
ser = BlogDetailModelSerializer(instance=instance)
context = {
"code": 1000,
"data": ser.data
}
return Response(context)
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
# 获取评论
思考: 以博客园为例, 任意点开一篇博文, 你会发现, 该页面下方展示了对该篇博文的一堆评论..
那问题来了, 该处的评论信息在哪里进行序列化呢?
# 方案一(不好)
在查看博客详细的时, 能不能将它相关的评论信息也一并返还给前端?
◎ 可以. 具体怎么做呢?
在博客详细的BlogDetailModelSerializer系列化器类里添加一个自定义的字段comments.
该字段默认绑定的方法get_comments中返回当前博客的所有评论. 大体思路是这样的.
继续分析. get_comments方法中obj参数是当前博客在Blog表的那条记录/db对象, 但在Blog表里没有评论这个字段..
别灰心, 别忘了我们还有Comment评论表, 在Comment评论表中有Fk字段blog.
So, 通过models.Comment.objects.filter(blog=obj)
可以拿到当前博客的所有评论.
接着需要新创建一个CommentModelSerializer的序列化器类对拿到后的所有评论做序列化.
◎ 弊端
这么操作, 意味着, 将它们嵌套死了.
举个栗子, 评论一多, 需要对其进行分页, 需要翻页一点点的拿, 而在 get_comments方法里实现分页功能是不太方便..
So. 方案一这种操作只适合评论不多, 一次性拿出来的情况!! 分页的情况使用方案二能很方便的解决.
class CommentModelSerializer(serializers.ModelSerializer):
user = serializers.CharField(source="user.username")
class Meta:
model = models.Comment
fields = ["user", "content", "create_datetime"]
class BlogDetailModelSerializer(serializers.ModelSerializer):
category = serializers.CharField(source="get_category_display")
ctime = serializers.DateTimeField(format="%Y-%m-%d")
creator = serializers.SerializerMethodField()
comments = serializers.SerializerMethodField() # -- !
def get_creator(self, obj):
return {
"id": obj.creator.pk,
"username": obj.creator.username,
}
def get_comments(self, obj):
# - db里FK键 字段名_id 所以这两条语句的查询效果一样
# queryset = models.Comment.objects.filter(blog_id=obj.pk)
queryset = models.Comment.objects.filter(blog=obj)
# "拿到当前博客的所有评论"后,对其进行序列化
ser = CommentModelSerializer(instance=queryset, many=True)
return ser.data
class Meta:
model = models.Blog
fields = "__all__"
class BlogDetailView(APIView):
def get(self, request, *args, **kwargs):
pk = kwargs["pk"]
instance = models.Blog.objects.filter(pk=pk).first()
if not instance:
context = {
"code": 1001,
"error": "博文不存在!请联系管理员."
}
return Response(context)
ser = BlogDetailModelSerializer(instance=instance)
context = {
"code": 1000,
"data": ser.data
}
return Response(context)
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
POSTMAN中 GET请求 http://127.0.0.1:8000/api/2/blog/1/
{
"code": 1000,
"data": {
"id": 1,
"category": "云计算",
"ctime": "2023-08-29",
"creator": {
"id": 1,
"username": "wpq"
},
"comments": [
{
"user": "wpq",
"content": "太棒了!",
"create_datetime": "2023-08-29T07:02:52.939132Z"
},
{
"user": "zk",
"content": "无敌!",
"create_datetime": "2023-08-29T07:02:52.952468Z"
},
{
"user": "zk",
"content": "666!",
"create_datetime": "2023-08-29T07:10:19.287447Z"
}
],
"image": "xxx/xxx.jpg",
"title": "wpq-1",
"summary": "简介:这是wpq写的第一篇博客",
"text": "博文: Hello world...",
"comment_count": 0,
"favor_count": 0
}
}
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
# 方案二(可以)
通过两个api接口实现, 一个接口写博客详细, 一个接口写当前这篇博客的评论信息.. 这样比较灵活!!
思考: 获取当前这篇博客的评论信息的接口如何在路由中设计呢? 有两种方式都可以. 这里选用方式一.
-- 方式一
path("api/comment/<int:blog_id>/", views.CommentView.as_view()),
-- 方式二
# 获取评论,传递ID的话,在url中通过 ?blog_id=1 的形式传递.
# 筛选系列的功能建议通过这种方式来实现"暂且不明白为啥这么建议." 但url是实打实的不太美观.哈哈.
path("api/comment/", views.CommentView.as_view()),
Ps:
方式一 ID在后端视图类的get方法中通过 kwargs["blog_id"] 获取
方式二 ID在后端视图类的get方法中通过 request.GET.get("blog_id") 或者 request.query_params.get("blog_id") 来获取.
2
3
4
5
6
7
8
9
10
11
路由信息
from django.urls import path
from api import views
urlpatterns = [
path("api/<str:version>/blog/", views.BlogView.as_view()), # 博客列表
path("api/<str:version>/blog/<int:pk>/", views.BlogDetailView.as_view()), # 博客详细
path("api/<str:version>/comment/<int:blog_id>/", views.CommentView.as_view()), # 获取当前博客所有评论
]
2
3
4
5
6
7
8
相关的视图代码
class CommentModelSerializer(serializers.ModelSerializer):
user = serializers.CharField(source="user.username")
class Meta:
model = models.Comment
fields = ["user", "content", "create_datetime"]
class CommentView(APIView):
def get(self, request, version, blog_id):
# 注: 无论是blog_id在Blog表中不存在,或者 该篇博文下没有评论. queryset的值皆为<QuerySet []>
# 那么返回前端的结果是 {"code": 1000,"data": []}
queryset = models.Comment.objects.filter(blog_id=blog_id)
ser = CommentModelSerializer(instance=queryset, many=True)
context = {
"code": 1000,
"data": ser.data
}
return Response(context)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
再说一点, 该处是纯序列化,用的也是get方法, 若想添加评论, post方法里需要验证+save+序列化,
而针对FK外键字段user, 想要自定制展示内容,
在序列化器类里, 额外字段和自定义方法就不顶用啦, 需要用到 前面我们写的那个 NbHookSerializer..
# ★注册
提交数据,进行校验. -- POST方式
注册功能需要重复密码, 很经典的场景. 我们借此再回顾下大体流程. 很重要!
1> Meta.fields列表里写的字段, 就是我们能获取到的所有字段对象..
2> 当执行ser.is_valid的时候, 会剔除掉read_only=True的字段, 剩下的字段对象需要进行验证..
只要该字段对象没有设置默认值没有设置可以不为空以及自动生成的时间, 那么该字段对象前端都必须得传!
3> 验证过程中, confirm_password的钩子函数在password字段的验证之后, 是由Meta.fields列表里字段的先后顺序决定的.
其中的 self.initial_data 是前端传入的所有数据.
4> 校验通过后, 执行ser.save(), 因为UserInfo表中没有confirm_password字段 , 所以不需要该校验通过的confirm_password字段.
需要在存入UserInfo表之前将该字段pop掉.
5> 保存数据库成功后, 执行ser.data 会剔除掉write_only=True的字段, 将剩下的字段对象进行序列化..
若confirm_password没有设置write_only=True,那么该字段序列化会失败.为什么?
因为,save后的ser.data使用的是成功存入数据库的instance对象, 该instance中没有confirm_password这个成员..
(其实不设置write_only=True,需要它展示的话,在ser.data之前,执行语句 ser.instance.confirm_password = pop出去的值即可)
路由信息
from django.urls import path
from api import views
urlpatterns = [
path("api/<str:version>/blog/", views.BlogView.as_view()), # 博客列表
path("api/<str:version>/blog/<int:pk>/", views.BlogDetailView.as_view()), # 博客详细
path("api/<str:version>/comment/<int:blog_id>/", views.CommentView.as_view()), # 获取当前博客所有评论
path("api/<str:version>/register/", views.RegisterView.as_view()), # 注册
]
2
3
4
5
6
7
8
9
相关的视图代码
from rest_framework import exceptions
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from api import models
class RegisterModelSerializer(serializers.ModelSerializer):
confirm_password = serializers.CharField(write_only=True)
class Meta:
model = models.UserInfo
fields = ["id", "username", "password", "confirm_password"]
extra_kwargs = {
"id": {"read_only": True},
"password": {"write_only": True}
}
def validate_confirm_password(self, value):
password = self.initial_data.get("password")
if password != value:
raise exceptions.ValidationError("密码不一致.")
return value
class RegisterView(APIView):
def post(self, request, *args, **kwargs):
ser = RegisterModelSerializer(data=request.data)
if ser.is_valid():
ser.validated_data.pop("confirm_password")
ser.save()
context = {
"code": 1000,
"data": ser.data,
}
return Response(context)
context = {
"code": 1001,
"error": "注册失败",
"detail": ser.errors,
}
return Response(context)
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
POST请求 发json数据 {"username":"bjx","password":"bjx1234","confirm_password":"bjx1234"}
{
"code": 1000,
"data": {
"id": 7,
"username": "bjx"
}
}
2
3
4
5
6
7
若重复密码不一致, 返回前端信息.
{
"code": 1001,
"error": "注册失败",
"detail": {
"confirm_password": [
"密码不一致."
]
}
}
2
3
4
5
6
7
8
9
# 登陆
post请求 校验成功后,验证用户名密码是否正确, 正确添加token,简单的返回了一个token. 登陆成功.
路由信息
from django.urls import path
from api import views
urlpatterns = [
path("api/<str:version>/blog/", views.BlogView.as_view()), # 博客列表
path("api/<str:version>/blog/<int:pk>/", views.BlogDetailView.as_view()), # 博客详细
path("api/<str:version>/comment/<int:blog_id>/", views.CommentView.as_view()), # 获取当前博客所有评论
path("api/<str:version>/register/", views.RegisterView.as_view()), # 注册
path("api/<str:version>/login/", views.LoginView.as_view()), # 登陆
]
2
3
4
5
6
7
8
9
10
相关的视图代码
class LoginModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.UserInfo
fields = ["username", "password"]
class LoginView(APIView):
def post(self, request, *args, **kwargs):
ser = LoginModelSerializer(data=request.data)
if not ser.is_valid():
return Response({"code": 1001, "error": "校验失败", "detail": ser.errors})
instance = models.UserInfo.objects.filter(**ser.validated_data).first()
if not instance:
return Response({"code": 1001, "error": "用户名或密码错误"})
token = uuid.uuid4()
instance.token = token
instance.save()
return Response({"code": 1000, "token": token})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST请求 发json数据 {"username":"bjx","password":"bjx1234"}
{
"code": 1000,
"token": "a963288b-3b33-498a-a139-7b6bbca50a16"
}
2
3
4
# ★进行评论
思考: 第一点, 进行评论, 需要登陆成功后才能评论,认证组件咋写? 第二点, 进行评论的序列化器类写在哪呢?
To do.评论数加1. 用F.
在前面, 我们为了获取当前博客的评论.
编写了CommentView类,在其get函数中使用了序列化器类CommentModelSerializer..完成了该功能.
现在的需求是要为当前博客进行评论, 有两个方案:
方案1> 再写一个视图类, 认证组件 + 类中编写post方法 + post方法用同一个CommentModelSerializer
方案2> 就在CommentView类写post方法 + post方法用同一个CommentModelSerializer + 认证组件.
获取评论是该类的get方法实现的,进行评论是该类的post方法实现的,两个功能在同一个类里
认证组件的编写逻辑, [一般] 是token验证失败抛出异常,验证成功返回元祖..
代入方案1,ok,因为是两个类,获取评论的类无需认证,类中不写认证组件; 进行评论的类需要认证,类中写认证组件.
代入方案2,no. why?
首先,无关认证组件的逻辑,只要类中写了认证组件,类里面的方法get、post在执行之前都要进行认证.
根据一般的认证组件编写逻辑,只要认证失败了,类中的方法就执行不了.
但方案2中, get方法里实现的是获取评论的功能,该功能是无需登陆就能访问的.认证失败就不让其访问,不行的呀!
若想用方案2, 如何是好呢? 需要更改认证组件一般的编写逻辑.
正解: 认证失败不抛出异常,而是返回None值! 返回None值,其一是不抛异常,其二是视图函数中request.user==None
其一保证了,类中get方法虽然进行了认证,哪怕认证失败,也不影响该功能的执行/该方法也会执行.
其二保证了,虽然认证失败了,类中post方法也会执行,但在执行的一开始,我们就判断,当request.user==None,抛出认证失败的异常!!
(相当于,抛出异常的地方从认证类里转换到了视图函数里 根据request.user是否为空在业务函数里做判定)
“根据不同的应用场景编写不同的认证组件逻辑”!!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
路由信息
from django.urls import path
from api import views
urlpatterns = [
path("api/<str:version>/blog/", views.BlogView.as_view()), # 博客列表 get
path("api/<str:version>/blog/<int:pk>/", views.BlogDetailView.as_view()), # 博客详细 get
# get获取当前博客所有评论;post给当前博客添加评论
path("api/<str:version>/comment/<int:blog_id>/", views.CommentView.as_view()),
path("api/<str:version>/register/", views.RegisterView.as_view()), # 注册 post
path("api/<str:version>/login/", views.LoginView.as_view()), # 登陆 post
]
2
3
4
5
6
7
8
9
10
11
认证组件
在项目根目录下创建ext/auth.py,写入以下内容.
from rest_framework.authentication import BaseAuthentication
from api import models
class BlogAuthentication(BaseAuthentication):
def authenticate(self, request):
# - token可放在请求头、请求体等地方,这里简单点,放在url中.
token = request.query_params.get("token")
if not token:
return
instance = models.UserInfo.objects.filter(token=token).first()
if not instance:
return
return instance, token
def authenticate_header(self, request):
return "API"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
**自己编写的Hook组件 **
在项目根目录下创建ext/hook.py,写入以下内容.
from collections import OrderedDict
from rest_framework.fields import SkipField
from rest_framework.relations import PKOnlyObject
class NbHookSerializer(object):
def to_representation(self, instance):
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
"""也就在执行源码本来的代码之前,判断了下nb开头的钩子是否存在. 一共多写了4行代码."""
if hasattr(self, 'nb_%s' % field.field_name):
value = getattr(self, 'nb_%s' % field.field_name)(instance)
ret[field.field_name] = value
else:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
return ret
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
相关的视图代码
import uuid
from rest_framework import exceptions
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from api import models
from ext.auth import BlogAuthentication
from ext.hook import NbHookSerializer
class CommentModelSerializer(NbHookSerializer, serializers.ModelSerializer):
class Meta:
model = models.Comment
fields = ["id", "user", "content", "create_datetime"]
extra_kwargs = {
"id": {"read_only": True},
"user": {"read_only": True},
}
def nb_user(self, obj):
return obj.user.username
class CommentView(APIView):
authentication_classes = [BlogAuthentication, ]
def get(self, request, version, blog_id):
# 注: 无论是blog_id在Blog表中不存在,或者 该篇博文下没有评论. queryset的值皆为<QuerySet []>
# 那么返回前端的结果是 {"code": 1000,"data": []}
queryset = models.Comment.objects.filter(blog_id=blog_id)
ser = CommentModelSerializer(instance=queryset, many=True)
context = {
"code": 1000,
"data": ser.data
}
return Response(context)
def post(self, request, *args, **kwargs):
if not request.user:
return Response({"code": 3000, "error": "认证失败"})
blog_object = models.Blog.objects.filter(id=kwargs["blog_id"]).first()
if not blog_object:
return Response({"code": 2000, "error": "博客不存在"})
ser = CommentModelSerializer(data=request.data)
if not ser.is_valid():
return Response({"code": 1002, "error": "验证失败", "detail": ser.errors})
ser.save(user=request.user, blog=blog_object)
return Response({"code": 1000, "data": ser.data})
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
POSTMAN模拟前端发送请求
http://127.0.0.1:8000/api/v1/comment/1/?token=6a0ce41b-42e3-44e3-a4c7-77f7a60f9ce1
POST 发送Json数据 {"content":"终于可以添加评论了!"}
{
"code": 1000,
"data": {
"id": 5,
"user": "bjx",
"content": "终于可以添加评论了!",
"create_datetime": "2023-08-30T10:13:39.142853Z"
}
}
2
3
4
5
6
7
8
9
10
11
12
# 点赞
谁给哪篇博客点赞.
因为点赞必须登陆后才能使用, 所以谁可以从request.user中取到.. 哪篇博客,即需要传博客ID. 该示栗中通过请求体传递. 还要注意, 在save添加点赞之前需要查询一次, 点赞过了/该数据已经存在, 就不能再添加了.. 若已点赞就不能点赞了.
(更通俗的逻辑应该是 已点赞,再点赞的话,删除该条记录 略.)To do.赞数量更新. 用F.
点赞, 成功的话是需要新增数据的, 根据restful规范, 就应该是用POST请求, 且post请求有请求体, get请求没有..
路由信息
from django.urls import path
from api import views
urlpatterns = [
path("api/<str:version>/blog/", views.BlogView.as_view()), # 博客列表 get
path("api/<str:version>/blog/<int:pk>/", views.BlogDetailView.as_view()), # 博客详细 get
# get获取当前博客所有评论;post给当前博客添加评论
path("api/<str:version>/comment/<int:blog_id>/", views.CommentView.as_view()),
path("api/<str:version>/register/", views.RegisterView.as_view()), # 注册 post
path("api/<str:version>/login/", views.LoginView.as_view()), # 登陆 post
path("api/<str:version>/favor/", views.FavorView.as_view()), # 点赞 post
]
2
3
4
5
6
7
8
9
10
11
12
认证类相关代码
from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptions
from api import models
class BlogAuthentication(BaseAuthentication):
def authenticate(self, request):
# - token可放在请求头、请求体等地方,这里简单点,放在url中.
token = request.query_params.get("token")
if not token:
return
instance = models.UserInfo.objects.filter(token=token).first()
if not instance:
return
return instance, token
def authenticate_header(self, request):
return "API"
class NoAuthentication(BaseAuthentication):
def authenticate(self, request):
raise exceptions.AuthenticationFailed({"code": 2000, "error": "认证失败"})
def authenticate_header(self, request):
return "API"
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
相关的视图代码
import uuid
from rest_framework import exceptions
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from api import models
from ext.auth import BlogAuthentication, NoAuthentication
from ext.hook import NbHookSerializer
class FavorModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Favor
fields = ["id", "blog"] # 其实id字段默认就是read_only=True
# extra_kwargs = {
# "id": {"read_only": True},
# }
class FavorView(APIView):
authentication_classes = [BlogAuthentication, NoAuthentication]
def post(self, request, *args, **kwargs):
ser = FavorModelSerializer(data=request.data)
if not ser.is_valid():
return Response({"code": 1001, "error": "校验失败", "detail": ser.errors})
# 已经点赞了就不能再点了.
# - 这里请求体里就只有 {"blog":1} 它在db中是FK字段,校验成功是会自动转换为对象的.
# So. ser.validated_data["blog"] 其实取到的是一个对象.
# - 需要校验的只有blog字段
# So,这里就可以写成 **ser.validated_data
exists = models.Favor.objects.filter(user=request.user, **ser.validated_data).exists()
if exists:
return Response({"code": 1005, "error": "不能重复点赞"})
# 未点赞存入数据库
ser.save(user=request.user)
return Response({"code": 1000, "data": ser.data})
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
POSTMAN模拟前端发送请求
http://127.0.0.1:8000/api/v1/favor/?token=6a0ce41b-42e3-44e3-a4c7-77f7a60f9ce1
POST请求 发送Json数据 {"blog":1}
{
"code": 1000,
"data": {
"id": 1,
"blog": 1
}
}
再次发送请求
{
"code": 1005,
"error": "不能重复点赞"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 新建博文
Post请求
路由信息
from django.urls import path
from api import views
urlpatterns = [
# get博客列表;post新建博客
path("api/<str:version>/blog/", views.BlogView.as_view()),
path("api/<str:version>/blog/<int:pk>/", views.BlogDetailView.as_view()), # 博客详细 get
# get获取当前博客所有评论;post给当前博客添加评论
path("api/<str:version>/comment/<int:blog_id>/", views.CommentView.as_view()),
path("api/<str:version>/register/", views.RegisterView.as_view()), # 注册 post
path("api/<str:version>/login/", views.LoginView.as_view()), # 登陆 post
path("api/<str:version>/favor/", views.FavorView.as_view()), # 点赞 post
]
2
3
4
5
6
7
8
9
10
11
12
13
相关的视图代码
class BlogModelSerializer(NbHookSerializer, serializers.ModelSerializer):
ctime = serializers.DateTimeField(format="%Y-%m-%d", read_only=True)
creator = UserInfoModelSerializer(read_only=True)
def nb_category(self, obj):
return obj.get_category_display()
class Meta:
model = models.Blog
fields = ["id", "category", "image", "title", "summary", "text",
"ctime", "creator", "comment_count", "favor_count"]
extra_kwargs = {
"comment_count": {"read_only": True},
"favor_count": {"read_only": True},
"text": {"write_only": True},
}
class BlogView(APIView):
authentication_classes = [BlogAuthentication, ]
def get(self, request, *args, **kwargs):
queryset = models.Blog.objects.all().order_by("-id")
ser = BlogModelSerializer(instance=queryset, many=True)
context = {
"code": 1000,
"data": ser.data
}
return Response(context)
def post(self, request, *args, **kwargs):
if not request.user:
return Response({"code": 3000, "error": "认证失败"})
ser = BlogModelSerializer(data=request.data)
if not ser.is_valid():
return Response({"code": 1002, "error": "验证失败", "detail": ser.errors})
ser.save(creator=request.user)
return Response({"code": 1000, "data": ser.data})
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
POSTMAN模拟前端发送请求
POST 请求 http://127.0.0.1:8000/api/v1/blog/?token=6a0ce41b-42e3-44e3-a4c7-77f7a60f9ce1
发送json数据
{
"category": 3,
"image": "xxx/yyy.jpg",
"title": "bjx-1",
"summary": "简介:这是bjx写的第三篇博客",
"text":"哇哇哇哇哇哇啦."
}
前端页面展示
{
"code": 1000,
"data": {
"id": 5,
"category": "Go开发",
"image": "xxx/yyy.jpg",
"title": "bjx-1",
"summary": "简介:这是bjx写的第三篇博客",
"ctime": "2023-08-30",
"creator": {
"id": 4,
"username": "bjx"
},
"comment_count": 0,
"favor_count": 0
}
}
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
待完善的地方:
1> 博客列表、评论列表 - 分页.
2> 视图函数里有很多重复的.. 想办法将视图中的代码进行复用 - 视图.