Skip to content

Latest commit

 

History

History
256 lines (183 loc) · 7.53 KB

120-文章标签.md

File metadata and controls

256 lines (183 loc) · 7.53 KB

一篇文章通常还有标签功能,作为分类的补充。

模型和视图

老规矩,首先把标签的 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 。因此还需要覆写 TagSerializercreate()/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 的。