Django-Vue搭建个人博客:文章标签
6606 views, 2021/03/12 updated Go to Comments
一篇文章通常还有标签功能,作为分类的补充。
模型和视图
老规矩,首先把标签的 model 建立好:
# 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 article.models import Tag from article.serializers import TagSerializer class TagViewSet(viewsets.ModelViewSet): queryset = Tag.objects.all() serializer_class = TagSerializer permission_classes = [IsAdminUserOrReadOnly]
还是那三板斧,没有新内容。
最后的外围工作,就是注册路由:
# drf_vue_blog/urls.py ... router.register(r'tag', views.TagViewSet) ...
序列化器
接下来就是最重要的 TagSerializer
:
# 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
的标签试试:
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()
方法:
# 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()
确定标签数据是列表,才会循环并创建新数据。
再重新请求试试:
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()
方法:
# 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
的标签对象出现。
这两个序列化器的完整形态是下面这样子的:
# 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 的。