文章分类
博客文章通常需要分类,方便用户快速识别文章的类型,或者进行某种关联操作。
本章就来实现对文章的分类。
增加分类模型
首先在 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 字段会用到。
别忘了数据迁移。
视图与路由
视图还是用视图集的形式:
# 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 还没写,不慌等一会来搞定它。
将路由也注册好:
# 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 改成下面这样:
# 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:
再来看 ArticleSerializer:
由于我们希望文章接口不仅仅只返回分类的 id 而已,所以需要显式指定 category ,将其变成一个嵌套数据,与之前的 author 类似。
DRF 框架原生没有实现可写的嵌套数据 (因为其操作逻辑没有统一的标准),那我想创建/更新文章和分类的外键关系怎么办?一种方法是自己去实现序列化器的 create()/update() 方法;另一种就是 DRF 框架提供的修改外键的快捷方式,即显式指定 category_id 字段,则此字段会自动链接到 category 外键,以便你更新外键关系。
再看 category_id 内部。 write_only 表示此字段仅需要可写; allow_null 表示允许将其设置为空; required 表示在创建/更新时可以不设置此字段。
经过以上设置,实际上序列化器已经可以正常工作了。但有个小问题是如果用户提交了一个不存在的分类外键,后端会返回外键数据不存在的 500 错误,不太友好。解决方法就是对数据预先进行验证。
验证方式又有如下几种:
validate_category_id 检查了两样东西:
如果没通过上述检查,后端就抛出一个 400 错误(代替之前的 500 错误),并返回错误产生的提示,这就更友好一些了。
这就基本完成了对分类的开发。接下来就是实际的测试了。
测试
打开命令行,首先创建分类:
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/"
}
更新已有的分类:
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/"
}
创建文章时指定分类:
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",
...
}
把已有的分类置空:
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 三种请求方法,它们的区别是啥?
删除以及权限等功能就不试了,读者朋友自行尝试吧。
完善分类详情
上面写的分类接口中,我希望分类的列表页面不显示其链接的文章以保持数据清爽,但是详情页面则展示出链接的所有文章,方便接口的使用。因此就需要同一个视图集用到两个不同的序列化器了,即前面章节讲的覆写 get_serializer_class()。
修改序列化器:
# 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',
]
然后修改视图:
# 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
除此之外没有新的魔法。
Last modified: 06 January 2025