今天对文章分类进行补充,做一期文章标签。
教程来源杜塞-django-vue系列
博客链接 传送门
模型和视图
老规矩,创建标签的模型:
# article/models.py
# 文章标签
class Tag(models.Model):
text = models.CharField(max_length=30)
class Meta:
ordering = ['-id']
def __str__(self):
return self.text
···
# 博客文章
class Article(models.Model):
···
# 标签
tags = models.ManyToManyField(
Tag,
blank=True,
related_name='articles'
)
···
一篇文章不可能只有一个标签,所以我们选用多对多关系。
注意数据迁移
接下来完成视图函数:
# article/views.py
from .models import Tag
from .serializers import TagSerializer
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
permission_classes
视图函数还是如出一辙,同样接下来我们注册路由,序列化器我们放在后面写。
# drf_vue_blog/urls.py
···
router.register(r'tag', views.TagViewSet)
···
序列化器
接下来就是最重要的 TagSerializer
:
# article/serializers.py
# 新增标签序列化器
class TagSerializer(serializers.HyperlinkedModelSerializer):
"""标签序列化器"""
class Meta:
model = Tag
fields='__all__'
# 修改文章序列化器,添加tags字段
class ArticleSerializer(serializers.HyperlinkedModelSerializer):
"""文章序列化器"""
# tag 字段
tags = serializers.SlugRelatedField(
queryset=Tag.objects.all(),
many=True,
required=False,
slug_field='text'
)
通过前面章节已经知道,默认的嵌套序列化器只显示外链的 id,需要改得更友好一些。但似乎又没必要改为超链接或者字段嵌套,因为标签就 text 字段有用。因此就用 SlugRelatedField 直接显示其 text 字段的内容就足够了。
让我们给已有的文章新增一个叫 java
的标签试试:
>http -a xianwei:admin123456 PATCH http://127.0.0.1:8000/api/article/15/ tags:="[\"java\"]"
HTTP/1.1 400 Bad Request
···
{
"tags": [
"Object with text=java does not exist."
]
}
指令中的反斜线时windows的通病,需要注意。
修改失败了,原因是标签java
不存在。多对多的关系中,DRF默认你必须得有这个外键对象,才能指定其关系。虽然合情合理,但我们更希望在创建、更新文章时,程序会自动检查数据库中是否存在此标签。如果存在就指向,不存在就创建并指向。
要实现这个效果,你可能想到覆写.validate_{field_name}()
或者.validate()
还或者.create()/.update()
方法。但很遗憾他们都不行。
原因是DRF执行默认的字段有效性检查比上述的方法都要早,程序还执行不到上述方法,框架就抛出错误了。
正确的是覆写to_internal_value()
方法(新知识点):
# article/serializers.py
class ArticleSerializer(serializers.HyperlinkedModelSerializer):
···
# 覆写方法,如果如数的不存在就自动创建
def to_internal_value(self, data):
tags_data = data.get('tags')
if isinstance(tags_data,list):
for text in tags_data:
if not Tag.objects.filter(text=text).exists():
Tag.objects.create(text=text)
return super().to_internal_value(data)
to_internal_value() 方法原本作用是将请求中的原始 Json 数据转化为 Python 表示形式(期间还会对字段有效性做初步检查)。它的执行时间比默认验证器的字段检查更早,因此有机会在此方法中将需要的数据创建好,然后等待检查的降临。isinstance() 确定标签数据是列表,才会循环并创建新数据。
再请求试试:
>http -a xianwei:admin123456 PATCH http://127.0.0.1:8000/api/article/15/ tags:="[\"java\",\"python\"]"
HTTP/1.1 200 OK
···
{
"author": {
"date_joined": "2021-06-13T14:58:00",
"id": 3,
"last_login": null,
"username": "xianwei"
},
"body": "aaa",
"category": null,
"created": "2021-06-17T20:51:53.437601",
"tags": [
"python",
"java"
],
"title": "category_1",
"updated": "2021-06-18T17:16:01.303651",
"url": "http://127.0.0.1:8000/api/article/15/"
}
这次成功了,当然同时赋值多个标签也是可以的,指控也是可以的(给空列表).
除此之外,因为标签仅有text
字段是有用的,两个id
不同但是text
相同的标签是没有任何意义的。更重要的是,SlugRelatedField
是不允许有重复的slug_field
。
因此我们还要覆写TagSerializer
的create()/update()
方法。、
class TagSerializer(serializers.HyperlinkedModelSerializer):
"""标签序列化器"""
def check_tag_exists(self, validated_data):
text = validated_data.get('text')
if Tag.objects.filter(text=text).exists():
raise serializers.ValidationError('Tag with text {} exists.'.format(text))
def create(self, validated_data):
self.check_tag_exists(validated_data)
return super().create(validated_data)
def update(self, instance, validated_data):
self.check_tag_exists(validated_data)
return super().update(instance, validated_data)
···
注意这里是添加了这几行代码,原有的还在。
这样就防止了重复的text
的标签对象现象。
两个序列化器的完整代码是这个样的:
# article/serializers.py
class ArticleSerializer(serializers.HyperlinkedModelSerializer):
"""文章序列化器"""
author = UserDescSerializer(read_only=True)
# tag 字段
tags = serializers.SlugRelatedField(
queryset=Tag.objects.all(),
many=True,
required=False,
slug_field='text'
)
# 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
# 覆写方法,如果如数的不存在就自动创建
def to_internal_value(self, data):
tags_data = data.get('tags')
if isinstance(tags_data, list):
for text in tags_data:
if not Tag.objects.filter(text=text).exists():
Tag.objects.create(text=text)
return super().to_internal_value(data)
class Meta:
model = Article
fields = '__all__'
class TagSerializer(serializers.HyperlinkedModelSerializer):
"""标签序列化器"""
def check_tag_exists(self, validated_data):
text = validated_data.get('text')
if Tag.objects.filter(text=text).exists():
raise serializers.ValidationError('Tag with text {} exists.'.format(text))
def create(self, validated_data):
self.check_tag_exists(validated_data)
return super().create(validated_data)
def update(self, instance, validated_data):
self.check_tag_exists(validated_data)
return super().update(instance, validated_data)
class Meta:
model = Tag
fields = '__all__'
标签的增删改查,就请读者自行测试吧。
无论是通过文章接口还是标签自己的接口,创建新标签应该都是 OK 的。