Django-Vue搭建个人博客:文章分类
7806 views, 2021/03/11 updated Go to Comments
博客文章通常需要分类,方便用户快速识别文章的类型,或者进行某种关联操作。
本章就来实现对文章的分类。
增加分类模型
首先在 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
字段会用到。
别忘了数据迁移。
教程把分类的 model 放到 article app中了。实际项目应根据情况考虑是否需要另起一个单独的分类 app。
视图与路由
视图还是用视图集的形式:
# 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
:
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 和博主的不一定相同,这是正常的,因为我在写教程时会反复测试,确保正确。
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
三种请求方法,它们的区别是啥?
POST
:创建新的资源。PUT
: 整体更新特定资源,默认情况下你需要完整给出所有必须的字段。PATCH
: 部分更新特定资源,仅需要给出需要更新的字段,未给出的字段默认不更改。
另一个小问题是更新分类用了
:=
符号,这是 httpie 里输入非字符串数据的方法。详情见文档。
删除以及权限等功能就不试了,读者朋友自行尝试吧。
完善分类详情
上面写的分类接口中,我希望分类的列表页面不显示其链接的文章以保持数据清爽,但是详情页面则展示出链接的所有文章,方便接口的使用。因此就需要同一个视图集用到两个不同的序列化器了,即前面章节讲的覆写 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
除此之外没有新的魔法。