Appearance
博客文章通常需要分类,方便用户快速识别文章的类型,或者进行某种关联操作。
本章就来实现对文章的分类。
增加分类模型
首先在 article/models.py
里增加一个分类的模型,并且将其和博文成为一对多的外键:
python
# 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
字段会用到。
别忘了数据迁移。
教程把分类的 model 放到 article app中了。实际项目应根据情况考虑是否需要另起一个单独的分类 app。
视图与路由
视图还是用视图集的形式:
python
# article/views.py
...
from article.models import Category
from article.serializers import CategorySerializer
class CategoryViewSet(viewsets.ModelViewSet):
"""分类视图集"""
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [IsAdminUserOrReadOnly]
与博文的视图集完全一样,没有新的知识。CategorySerializer
还没写,不慌等一会来搞定它。
将路由也注册好:
python
# drf_vue_blog/urls.py
...
from rest_framework.routers import DefaultRouter
from article import views
...
# 其他都不改,就增加这行
router.register(r'category', views.CategoryViewSet)
urlpatterns = [
...
]
是否感受到了自动注册的方便?
序列化器
接下来把 article/serializers.py
改成下面这样:
python
# article/serializers.py
from rest_framework import serializers
from article.models import Article
from user_info.serializers import UserDescSerializer
from article.models import Category
class CategorySerializer(serializers.ModelSerializer):
"""分类的序列化器"""
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 validate_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
:
HyperlinkedIdentityField
前面章节有讲过,作用是将路由间的表示转换为超链接。view_name
参数是路由名,你必须显示指定。category-detail
是自动注册路由时,Router
默认帮你设置的详情页面的名称,类似的还有category-list
等,更多规则参考文档。- 创建日期不需要后期修改,所以设置为
read_only_fields
。
再来看 ArticleSerializer
:
- 由于我们希望文章接口不仅仅只返回分类的 id 而已,所以需要显式指定
category
,将其变成一个嵌套数据,与之前的author
类似。 - DRF 框架原生没有实现可写的嵌套数据(因为其操作逻辑没有统一的标准),那我想创建/更新文章和分类的外键关系怎么办?一种方法是自己去实现序列化器的
create()/update()
方法;另一种就是 DRF 框架提供的修改外键的快捷方式,即显式指定category_id
字段,则此字段会自动链接到category
外键,以便你更新外键关系。 - 再看
category_id
内部。write_only
表示此字段仅需要可写;allow_null
表示允许将其设置为空;required
表示在创建/更新时可以不设置此字段。
经过以上设置,实际上序列化器已经可以正常工作了。但有个小问题是如果用户提交了一个不存在的分类外键,后端会返回外键数据不存在的 500 错误,不太友好。解决方法就是对数据预先进行验证。
验证方式又有如下几种:
- 覆写序列化器的
.validate(...)
方法。这是个全局的验证器,其接收的唯一参数是所有字段值的字典。当你需要同时对多个字段进行验证时,这是个很好的选择。 - 另一种就是教程用到的,即
.validate_{field_name}(...)
方法,它会只验证某个特定的字段,比如category_id
。
validate_category_id
检查了两样东西:
- 数据库中是否包含了对应 id 值的数据。
- 传入值是否为 None。这是为了能够将已有的外键置空。
如果没通过上述检查,后端就抛出一个 400 错误(代替之前的 500 错误),并返回错误产生的提示,这就更友好一些了。
这就基本完成了对分类的开发。接下来就是实际的测试了。
测试
打开命令行,首先创建分类:
你创建的数据 id 和博主的不一定相同,这是正常的,因为我在写教程时会反复测试,确保正确。
python
C:\...> http -a dusai:admin123456 POST http://127.0.0.1:8000/api/category/ title=Django
...
{
"created": "2020-08-23T08:01:20.113074Z",
"id": 6,
"title": "Django",
"url": "http://127.0.0.1:8000/api/category/6/"
}
更新已有的分类:
python
C:\...> http -a dusai:admin123456 PUT http://127.0.0.1:8000/api/category/6/ title=Flask
...
{
"created": "2020-08-23T08:01:20.113074Z",
"id": 6,
"title": "Flask",
"url": "http://127.0.0.1:8000/api/category/6/"
}
创建文章时指定分类:
python
C:\Users\Dusai>http -a dusai:admin123456 POST http://127.0.0.1:8000/api/article/ category_id=6 title=ILoveDRF body=WishYouToo!
...
{
"author": {
...
},
"body": "WishYouToo!",
"category": {
"created": "2020-08-23T08:01:20.113074Z",
"id": 6,
"title": "Flask",
"url": "http://127.0.0.1:8000/api/category/6/"
},
"title": "ILoveDRF",
...
}
把已有的分类置空:
python
C:\Users\Dusai>http -a dusai:admin123456 PATCH http://127.0.0.1:8000/api/article/20/ category_id:=null
...
{
"category": null,
...
}
这里细心一点的就会发现,在更新资源时用到了 POST
、PUT
、 PATCH
三种请求方法,它们的区别是啥?
POST
:创建新的资源。PUT
: 整体更新特定资源,默认情况下你需要完整给出所有必须的字段。PATCH
: 部分更新特定资源,仅需要给出需要更新的字段,未给出的字段默认不更改。
另一个小问题是更新分类用了
:=
符号,这是 httpie 里输入非字符串数据的方法。详情见文档。
删除以及权限等功能就不试了,读者朋友自行尝试吧。
完善分类详情
上面写的分类接口中,我希望分类的列表页面不显示其链接的文章以保持数据清爽,但是详情页面则展示出链接的所有文章,方便接口的使用。因此就需要同一个视图集用到两个不同的序列化器了,即前面章节讲的覆写 get_serializer_class()
。
修改序列化器:
python
# article/serializers.py
...
class ArticleCategoryDetailSerializer(serializers.ModelSerializer):
"""给分类详情的嵌套序列化器"""
url = serializers.HyperlinkedIdentityField(view_name='article-detail')
class Meta:
model = Article
fields = [
'url',
'title',
]
class CategoryDetailSerializer(serializers.ModelSerializer):
"""分类详情"""
articles = ArticleCategoryDetailSerializer(many=True, read_only=True)
class Meta:
model = Category
fields = [
'id',
'title',
'created',
'articles',
]
然后修改视图:
python
# article.views.py
...
from article.serializers import CategorySerializer, CategoryDetailSerializer
class CategoryViewSet(viewsets.ModelViewSet):
"""分类视图集"""
...
def get_serializer_class(self):
if self.action == 'list':
return CategorySerializer
else:
return CategoryDetailSerializer
除此之外没有新的魔法。