Appearance
一篇文章通常还有标签功能,作为分类的补充。
模型和视图
老规矩,首先把标签的 model 建立好:
python
# 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'
)
一篇文章可以有多个标签,一个标签可以对应多个文章,因此是多对多关系。
写完后记得迁移。
接着把视图集也写好:
python
# article/views.py
...
from article.models import Tag
from article.serializers import TagSerializer
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
permission_classes = [IsAdminUserOrReadOnly]
还是那三板斧,没有新内容。
最后的外围工作,就是注册路由:
python
# drf_vue_blog/urls.py
...
router.register(r'tag', views.TagViewSet)
...
序列化器
接下来就是最重要的 TagSerializer
:
python
# article/serializers.py
...
from article.models import Tag
# 新增的序列化器
class TagSerializer(serializers.HyperlinkedModelSerializer):
"""标签序列化器"""
class Meta:
model = Tag
fields = '__all__'
# 修改已有的文章序列化器
class ArticleSerializer(serializers.HyperlinkedModelSerializer):
...
# tag 字段
tags = serializers.SlugRelatedField(
queryset=Tag.objects.all(),
many=True,
required=False,
slug_field='text'
)
...
通过前面章节已经知道,默认的嵌套序列化器只显示外链的 id,需要改得更友好一些。但似乎又没必要改为超链接或者字段嵌套,因为标签就 text
字段有用。因此就用 SlugRelatedField
直接显示其 text
字段的内容就足够了。
让我们给已有的文章新增一个叫 java
的标签试试:
python
PS C:\...> http -a dusai:admin123456 PATCH http://127.0.0.1:8000/api/article/26/ tags:='[\"java\"]'
...
{
"tags": [
"Object with text=java does not exist."
]
}
指令里 tags 里面带那么多斜杠的写法都是 windows 的老毛病造成的。用 Postman 并不需要。
修改失败了,原因是 java
标签不存在。多对多关系,DRF 默认你必须先得有这个外键对象,才能指定其关系。虽然也合情合理,但我们更希望在创建、更新文章时,程序会自动检查数据库里是否存在当前标签。如果存在则指向它,如果不存在则创建一个并指向它。
要实现这个效果,你可能想到覆写 .validate_{field_name}()
或者 .validate()
还或者 .create()/.update()
方法。但是很遗憾,它们都是不行的。
原因是 DRF 执行默认的字段有效性检查比上述的方法都早,程序还执行不到上述的方法,框架就已经抛出错误了。
正确的解法是覆写 to_internal_value()
方法:
python
# 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()
确定标签数据是列表,才会循环并创建新数据。
再重新请求试试:
python
PS C:\...> http -a dusai:admin123456 PATCH http://127.0.0.1:8000/api/article/26/ tags:='[\"java\", \"python\"]'
...
{
"tags": [
"python",
"java"
],
...
}
这次成功了。可以看到同时赋值多个标签也是可以的,置空也是可以的(给个空列表)。
除此之外,因为标签仅有 text
字段是有用的,两个 id
不同但是 text
相同的标签没有任何意义。更重要的是,SlugRelatedField
是不允许有重复的 slug_field
。因此还需要覆写 TagSerializer
的 create()/update()
方法:
python
# article/serializers.py
...
class TagSerializer(serializers.HyperlinkedModelSerializer):
"""标签序列化器"""
def check_tag_obj_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_obj_exists(validated_data)
return super().create(validated_data)
def update(self, instance, validated_data):
self.check_tag_obj_exists(validated_data)
return super().update(instance, validated_data)
...
这样就防止了重复 text
的标签对象出现。
这两个序列化器的完整形态是下面这样子的:
python
# article/serializers.py
class TagSerializer(serializers.HyperlinkedModelSerializer):
"""标签序列化器"""
def check_tag_obj_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_obj_exists(validated_data)
return super().create(validated_data)
def update(self, instance, validated_data):
self.check_tag_obj_exists(validated_data)
return super().update(instance, validated_data)
class Meta:
model = Tag
fields = '__all__'
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)
# tag 字段
tags = serializers.SlugRelatedField(
queryset=Tag.objects.all(),
many=True,
required=False,
slug_field='text'
)
# 覆写方法,如果输入的标签不存在则创建它
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)
# category_id 字段的验证器
def validate_category_id(self, value):
# 数据存在且传入值不等于None
if not Category.objects.filter(id=value).exists() and value != None:
raise serializers.ValidationError("Category with id {} not exists.".format(value))
return value
class Meta:
model = Article
fields = '__all__'
标签的增删改查,就请读者自行测试吧。
无论是通过文章接口还是标签自己的接口,创建新标签应该都是 OK 的。