26、回复功能设计和树结构
本节课想要实现如下效果:
1、如何设计回复功能
- 评论可被回复
- 回复可被回复
在comment/models.py中,如果新添加一个回复的类Reply,外键关联到Comment这个类,这样可以实现“评论可被回复”,但“回复可被回复”这个功能,又得新建一个类,然后外键关联到Reply,这样关联下去,显然是不可行的。
实际上,回复也是一种评论的行为,说明回复的本质也是评论,既然这样的话,那我们回复功能可不可以在Comment这个模型里面实现呢?
'content_object’这个类型可以指向任意的对象,可以指向评论的博客,那么也可以指向评论这个模型,评论这个评论,当然这种做法是可以的,不过比较麻烦,就单单我们一条评论要找下面多条回复的时候,就要反复的递归才能得到结果,这样就不是很合理。
那么我们这里怎么去实现它呢?这里就涉及到一种叫做树结构的东西。
我们可以新建一个字段,记录评论的上一级相关信息。这样就可以只用一个字段就可以关联到上一级
同步数据库,重启服务,刷新后台页面
这里parent_id选择回复的是哪一条评论(如果是一条评论的话, 就选0)。所以如果我们想要回复某一条评论的话,我们需要知道它具体的主键值是什么。
我们先让每条评论显示出来自己的主键值,需要修改comment/admin.py,加一个“id”字段:
添加一条评论,选择parent_id为2(意思就是这条评论是回复的上面id为2的那条评论,parent_id已经记录了它所回复的那条评论的主键信息)
这里有点麻烦,我们每次都要查看记住回复的评论的id值。
我们可以用一个外键,指向自己self,null=True表示允许它为空(因为如果这条评论是顶级评论,而不是回复的话,这个字段值就允许为空)
刷新,就可以看到这里可以直接选择回复哪条评论了,但是这里还不是很清楚,我们还需要再改一个地方
新加一个__str__方法:
【补充】
在python中方法名如果是__xxxx__()的,那么就有特殊的功能,因此叫做“魔法”方法;
当使用print输出对象的时候,只要自己定义了__str__(self)方法,那么就会打印从在这个方法中return的数据;
__str__方法需要返回一个字符串,当做这个对象的描写
可以看到,这里就可以选择具体评论内容了
这里只是初步的实验,要用到前端页面,我们还需要进一步优化
修改models.py如下:
新增reply_to = models.ForeignKey(User, null=True, on_delete=models.DO_NOTHING)
,上面也有一条ForeignKey指向User会报错显示有冲突。原因是:外键关联是双向的。我们可以通过评论Comment找到对应的user,在用户User那里也可以反向找到对应的那些评论Comment。如果我们第二条reply_to不写,我们是怎么样通过user得到对应的有哪些评论呢?这里我们可以用shell演示下
这里user有一个comment_set
,通过comment_set.all()
就可以反向得到相关评论的集合。这就是通过user反向得到评论Comment。这里默认命名就是模型的小写comment加一个_set。
而如果我们加上reply_to这一行代码的话,就有两个外键偶读指向User,那么就不知道应该是哪一条与Comment对应了。那么这里我们就可以设置一个属性related_name
,
可以看到user里面就有comments和replies(因为是刚刚创建的,所以没有数据)。这样,通过写related_name
,就能解决外键冲突问题,明确对应的关系
另外,我们还要加一个字段,我想要获取一条评论下面的所有回复,那么就需要记录每一条回复是基于哪一条评论开始的,也就是该条回复的最顶级是哪一条评论。这样我们就可以直接通过一个筛选条件filter直接得到这条评论下面相关的回复。
修改models.py如下:
更改模型之后,接下来一步,我们要看对应的处理方法要不要改动。
像blog/views.py里面的blog_detail,打开某一篇具体博客的时候,里面的comments,要稍作修改
那么具体的评论回复要怎么显示呢?我们可以在模板页面处理
blog_detail.html
在后台页面添加一条回复
然后刷新前端页面,可以看到顶级评论后面跟着刚刚添加的那条回复了
我们先来调整一下前端页面,
修改blog_detail.html如下:
修改blog/static/blog/blog.css如下:
刷新页面
接下来我们加一个回复的按钮
回到comment/forms.py,再多添加一个提交的字段 reply_comment_id
另外,这个字段有个初始化的事情要做,在blog/views.py里面的blog_detail,reply_comment_id一般默认为0 的话就是顶级评论:
可以看到reply_comment_id
这个id标签值value为0 ,说明默认是顶级评论,如果是回复评论,那么我们将是这个标签值改为对应回复的评论的主键值就可以
再修改blog_detail.html,在评论列表里添加“回复”的链接 a标签
另外在下面拓展方法的位置,我们再多写一个function如下:
那么我们要怎么调用这个方法呢?在上面的a标签那里
然后我们在页面,点击某条评论下面的“回复”按钮,那么value值就会变成这条评论对应的主键值
接下来,我们想要点击回复之后,让那条评论显示在评论框上面
可以通过ajax代码实现
修改blog_detail.html如下:
刷新页面,点击回复,可以看到这个评论就会显示在评论框上方
然后我们写入回复的内容,点击“评论”,我们的数据就会提交到表单的CommentForm的处理方法views,views接收到数据 进行判断,那我们先进行数据验证,在forms里面写一个clean方法
修改forms.py如下:
这里我们验证处理完成之后,我们怎么去更新数据?现在我们要加一下回复的数据进去,我们就判断是不是回复。修改comment/views.py如下:
这样,我们后端处理逻辑就完成了,我们刷新页面
完善:如果是评论,我们就写到第一条,如果是回复,我们就写到对应的位置下面。需要打开前端代码进行修改。
首先在comment/views.py返回的数据里面多加一个root_pk字段:
前端代码里,插入数据部分,需要分两种情况:一种是评论、一种是回复。以下代码做一个判断
修改blog_detail.html如下(先给一些内容加一些span标签):
刷新页面
这样,我们就完成了回复评论的功能
还剩一些细节的处理:
原本的页面:
提交回复后,清理掉评论框上方显示的内容。这里我们只需要将它设置隐藏就可以,修改blog_detail.html如下:
刷新页面
还有一个细节,评论列表的排序
回复评论我们改为正序,让评论由小到大的日期排序,修改models.py如下。但是回复和评论的排序是反过来的,评论是倒序的(最新评论在最上面),回复是正序的(第一条回复在最上面)。我们有两个地方需要处理,另外一个地方是blog/views.py,让它倒序显示
刷新页面: