Skip to content
本页目录

上一章我们做好了评论功能,就有了作者和读者沟通的方式。

但有的时候读者和读者同样需要沟通,评论别人的评论,俗称多级评论

本章将实现基础的多级评论功能。

准确的讲是两级评论。

模型

多级评论,也就是让评论模型和自身相关联,使其可以有一个父级。

修改评论模型,新增 parent 字段:

python
# comment/models.py

...
class Comment(models.Model):
    ...
    parent = models.ForeignKey(
        'self',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='children'
    )
  • 一个父评论可以有多个子评论,而一个子评论只能有一个父评论,因此用了一对多外键。
  • 之前的一对多外键,第一个参数直接引用了对应的模型,但是由于语法规则限制,这里显然不能够自己引用自己,因此用了传递字符串 self 的方式,作用都是一样的。

进行迁移后,模型就改好了。

序列化器

在原有的评论序列化器上修改:

python
# comment/serializers.py

...

# 新增这个类
class CommentChildrenSerializer(serializers.ModelSerializer):
    url = serializers.HyperlinkedIdentityField(view_name='comment-detail')
    author = UserDescSerializer(read_only=True)

    class Meta:
        model = Comment
        exclude = [
            'parent',
            'article'
        ]

# 修改这个类
class CommentSerializer(serializers.ModelSerializer):
    # 这是已有代码
    url = serializers.HyperlinkedIdentityField(view_name='comment-detail')
    author = UserDescSerializer(read_only=True)

    # 以下是新增代码
    article = serializers.HyperlinkedRelatedField(view_name='article-detail', read_only=True)
    article_id = serializers.IntegerField(write_only=True, allow_null=False, required=True)

    parent = CommentChildrenSerializer(read_only=True)
    parent_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)

    def update(self, instance, validated_data):
        validated_data.pop('parent_id', None)
        return super().update(instance, validated_data)

    class Meta:
        ...

新增代码大致可以分为三块,让我们来拆解它们:

  • 为了让文章引用更人性化,将 article 改为超链接字段用了 HyperlinkedRelatedField ,它和之前用过的 HyperlinkedIdentityField 差别很小,你可以简化理解为 HyperlinkedRelatedField 用于对外键关系,而 HyperlinkedIdentityField 用于对当前模型自身。(完整的解释看这里
  • parent 为父评论,用了嵌套序列化器 CommentChildrenSerializer 。注意这个序列化器的 Metaexclude 来定义不需要的字段。
  • 由于我们希望父评论只能在创建时被关联,后续不能更改(很合理),因此覆写 def update(...) ,使得在更新评论时忽略掉 parent_id 参数。

这就完成了。接下来测试。

测试

新建一个文章主键为 2 、父评论主键为 17 的评论:

python
...> http -a Obama:admin123456 post http://127.0.0.1:8000/api/comment/ parent_id=17 article_id=2 content='comment to parent comment 17'

HTTP/1.1 201 Created
...

{
    "article": "http://127.0.0.1:8000/api/article/2/",
    "author": {
        "date_joined": "2020-07-02T07:57:06.116144Z",
        "id": 2,
        "last_login": null,
        "username": "Obama"
    },
    "content": "comment to parent comment 17",
    "created": "2020-12-28T08:21:22.636431Z",
    "id": 20,
    "parent": {
        "author": {
            "date_joined": "2020-07-02T07:57:06.116144Z",
            "id": 2,
            "last_login": null,
            "username": "Obama"
        },
        "content": "New comment by Obama",
        "created": "2020-12-28T08:08:05.646643Z",
        "id": 17,
        "url": "http://127.0.0.1:8000/api/comment/17/"
    },
    "url": "http://127.0.0.1:8000/api/comment/20/"
}

注意这里由于将 article 改为了嵌套序列化器(只读),因此用 article_id 进行外键赋值。

如果我想在更新评论内容的同时修改父评论:

python
...> http -a Obama:admin123456 PUT http://127.0.0.1:8000/api/comment/21/ parent_id=99 article_id=2 content='comment to parent comment 10'

HTTP/1.1 200 OK
...
{
    ...
    "content": "comment to parent comment 10",
    "parent": {
        ...
        "id": 17,
    }
}

content 修改成功而 parent_id 无变化,和预想的逻辑表现一致。

其他的请求形式就不赘述了,读者可以自行尝试。