教程来源杜塞-django-vue系列
博客链接 传送门
博客文章通常需要分类,方便用户快速识别文章的类型,或者进行某种关联操作。
本章就来学习实现对文章的分类。
增加分类模型
首先在article/models.py
里增加一个分类的模型,并将其和文章进行建立一对一的外键:
# article/models.py
# 文章分类
class Category(models.Model):
"""文章分类"""
title = models.CharField(max_length=100)
created = models.DateTimeField(default=timezone.now)
class Meta:
ordering = ['-created']
def __str__(self):
return self.title
# 博客文章
class Article(models.Model):
···
# 分类
category = models.ForeignKey(
Category,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='articles'
)
···
字段简单,大体使用title
字段就可以。
注意:
需要进行数据迁移,另外Category类要放在Article上面,因为python执行顺序是自上而下。另外分类也可以新建一个app,但是我感觉没必要
视图与路由
试图还是用视图集的形式,因为简单:
from .models import Category
# article/views.py
class CategoryViewSet(viewsets.ModelViewSet):
"""分类视图集"""
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [IsAdminUserOrReadOnly]
同样我们创建分类的序列化器(没有新知识与之前都是一样的),我们一会在写他
将路由注册好:
# drf_vue_blog/urls.py
# 其他都不改,就增加这行
router.register(r'category', views.CategoryViewSet)
这就是自动注册的魅力。
序列化器
接下来我们把 article/serializers.py
修改一下:
from rest_framework import serializers
from .models import Article, Category
from user_info.serializers import UserDescSerializer
class CategorySerializer(serializers.HyperlinkedModelSerializer):
"""分类的序列化器"""
url = serializers.HyperlinkedIdentityField(view_name='category-detail')
class Meta:
model = Category
fields = '__all__'
read_only_fields = ['created']
class ArticleSerializer(serializers.HyperlinkedModelSerializer):
"""文章序列化器"""
author = UserDescSerializer(read_only=True)
# category 的嵌套序列化字段
category = CategorySerializer(read_only=True)
# category 的id字段,用于创建、更新 category 外键
category_id = serializers.IntegerField(write_only=True,allow_null=True,required=False)
# category_id 字段的验证器
def vaildate_category_id(self, value):
if not Category.objects.filter(id=value).exists() and value is not None:
raise serializers.ValidationError("Category with id {} not exists.".format(value))
return value
class Meta:
model = Article
fields = '__all__'
稍微开始有点复杂了,让我们来拆分一下代码。
CategorySerializer
还是原来的那些东西,不再赘述。
我们看ArticleSerializer
:
- 由于我们希望文章接口不仅仅只返回费雷id,所以我们要显示指定
Category
,将其变成一个嵌套数据,与之前author
一样. - DRF 框架原生没有实现可写的嵌套数据(是因为其逻辑操作没有统一化标准),那么我们想创建/更新文章和文章分类的外键关系怎么办?一种方式
就是自己去实现序列化器的create()/update()
方法;另一种就是DRF框架提供的修改外键的快捷方式,即显示指定的category_id
字段,
则此字段会自动连接到category
外键,以便你更新外键关系。 - 再看
category
内部。writr_only
表示字段仅可以写;allow_null
表示允许为空;required
表示在创建/更新时可以不设置此字段。
讲过以上设置,实际上序列化器已经可以正常工作了。但有个小问题时如果用户提交了一个不存在的分类外键,后端返回外键不存在的500错判五,不太友好。解决方法就是先对数据进行预先验证。
验证的方法有如下几种:
- 覆写序列化器的
.validate(...)
方法。这个是全局的验证器,其接受的唯一参数是所有字段值的字典。当你需要同时对多个字段进行验证时,这是个很好的选择。 - 另一种就是教程用到的,即
.validate_{field_name}(...)
方法,它会将验证某个特定的字段,比如category_id
。
.validate_category_id
检查了两样东西:
- 数据库中是否包含了对应的id数值。
- 传入的值是否为None。他是为了能够将已有的外键置空。
如果没通过上述检查,后端抛出400错误(代替之前的500错误),并且返回产生的提示,这就友好一些了。
这就基本完成了对分类的开发。接下来就是实际的测试了。
测试
打开命令行,首先创建分类:
>http -a xianwei:admin123456 http://127.0.0.1:8000/api/category/ title=Django
HTTP/1.1 201 Created
···
{
"created": "2021-06-17T20:46:25.667557",
"title": "Django",
"url": "http://127.0.0.1:8000/api/category/1/"
}
更新已有的分类:
>http -a xianwei:admin123456 PUT http://127.0.0.1:8000/api/category/1/ title=Flask
HTTP/1.1 200 OK
···
{
"created": "2021-06-17T20:46:25.667557",
"title": "Flask",
"url": "http://127.0.0.1:8000/api/category/1/"
}
细心的网友会发现,他并没有返回id,而是返回的url中我们看到了id,这就是我们前面学习使用的超链接起的作用发。
创建文章并指定分类:
>http -a xianwei:admin123456 POST http://127.0.0.1:8000/api/article/ title=category_11 body=bbb category_id=1
HTTP/1.1 201 Created
···
{
"author": {
"date_joined": "2021-06-13T14:58:00",
"id": 3,
"last_login": null,
"username": "xianwei"
},
"body": "bbb",
"category": {
"created": "2021-06-17T20:46:25.667557",
"title": "Flask",
"url": "http://127.0.0.1:8000/api/category/1/"
},
"created": "2021-06-17T20:52:34.325383",
"title": "category_11",
"updated": "2021-06-17T20:52:34.325383",
"url": "http://127.0.0.1:8000/api/article/16/"
}
注意里时category_id=1,而不是category=1。
置空现有的文章的分类:
>http -a xianwei:admin123456 PATCH http://127.0.0.1:8000/api/article/16/ category_id:=null
HTTP/1.1 200 OK
{
"author": {
"date_joined": "2021-06-13T14:58:00",
"id": 3,
"last_login": null,
"username": "xianwei"
},
"body": "bbb",
"category": null,
"created": "2021-06-17T20:52:34.325383",
"title": "category_11",
"updated": "2021-06-17T20:54:35.888087",
"url": "http://127.0.0.1:8000/api/article/16/"
}
这里需要注意一下,在更新资源时用到了POST
,PUT
,PATCH
三种请求方法,它们的区别时:
POST
:创建新的资源。PUT
:整体高峰新特定资源,默认情况下,要完整给出所有必须的字段。PATCH
:部分字段更新,进更闹心需要的字段,非给出的字段默认不更改。
另外一个小问题就是更新分类用了:=符号,这是httpie非字符串数据的方法,详情见文档。
删除以及权限等功能就不试了,读者们可以自行尝试。
完善分类详情
上面写的分类接口,我们希望分类的泪飙页面不再显示其链接的文章以及保持数据清爽,但是详情页则展示出分类的所有文章,方便接口使用。因此就需要同一个视图集用到
两个不同的序列化器,及前面文章末尾提到的覆写get_serializer_class
。
修改序列化器:
# article/models.py
class ArticleCategoryDetailSerializer(serializers.ModelSerializer):
"""给分类详情的嵌套序列化器"""
url = serializers.HyperlinkedIdentityField(view_name='article-detail')
class Meta:
model = Article
fields=[
'url',
'title',
]
class CategoryDetailSerializer(serializers.ModelSerializer):
"""分类详情"""
article = ArticleCategoryDetailSerializer(many=True, read_only=True)
class Meta:
model = Category
fields=[
'id',
'title',
'created',
'articles'
]
然后修改试图:
# article/views.py
from .serializers import ArticleSerializer,CategorySerializer,CategoryDetailSerializer
class CategoryViewSet(viewsets.ModelViewSet):
"""分类视图集"""
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [IsAdminUserOrReadOnly]
def get_serializer_class(self):
if self.action == 'list':
return CategorySerializer
else:
return CategoryDetailSerializer
除此之外没有新的魔法。