使用树形结构模型Django-MPTT实现多级评论
编写多级评论思路
多级评论本质是一个树状结构的对象,每个评论是根据他的父级来确定它所在位置。
那么想实现多级评论功能,可以自己实现树状结构逻辑,或者使用第三方库直接调用!
本篇文章选择直接使用第三方库Django-MPTT接入到博客中,用它来管理树状结构的评论数据。
Django-MPTT的使用
django-mptt是一个可复用的django app,旨在让你自己的django项目模型使用MPTT更加简单。它负责将数据库表作为树型结构管理的详细信息,并提供用于处理树型模型实例的工具。
安装和文档
- GitHub地址:https://github.com/django-mptt/django-mptt
- 文档:https://django-mptt.readthedocs.io/
- 安装:
pip install django-mptt -i https://pypi.douban.com/simple
使用方法简述
- 安装并在setting.py中加入django-mptt应用。
修改评论models,继承MPTTModel类,增加parent字段。生成数据库。
parent = TreeForeignKey('self',on_delete=models.CASCADE,null=True,blank=True,related_name='children') # python manage.py makemigrations # python manage.py migrate
- 编写视图,此时通过评论models获取的数据,就是树状结构的,直接返回给前端即可(Comment.objects.all()获取并返回)
- 添加路由,指向视图。
- 前端模板渲染,参考官方给的示例修改即可。
{% load mptt_tags %} <ul> {% recursetree genres %} <li> {{ node.name }} {% if not node.is_leaf_node %} <ul class="children"> {{ children }} </ul> {% endif %} </li> {% endrecursetree %} </ul>
- 新增数据。
from Comments.models import Comments dandy = Comments.objects.create(name='dandy') elina = Comments.objects.create(name='elina') cathy = Comments.objects.create(name='cathy') Comments.objects.create(name='aaa', parent=dandy) Comments.objects.create(name='bbb', parent=elina) Comments.objects.create(name='ccc', parent=cathy)
- 查看效果。
<!--前端展示的html代码--> <ul> <li>dandy <ul class="children"> <li>aaa </li> </ul> </li> <li>elina <ul class="children"> <li>bbb </li> </ul> </li> <li>cathy <ul class="children"> <li>ccc </li> </ul> </li> </ul>
在项目中添加django-mptt应用
- 修改settings.py文件,添加mptt应用。代码示例:
INSTALLED_APPS = [ ...省略... 'mptt', ]
修改已有评论model,parent是必须字段。代码示例:
from django.db import models from mptt.models import MPTTModel,TreeForeignKey class Comments(MPTTModel): article = models.ForeignKey(Blog, on_delete=models.CASCADE, verbose_name="被评论文章") name = models.CharField(max_length=50,verbose_name="用户名称") content = models.TextField(verbose_name="留言内容") parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children') reply_to = models.CharField(null=True,blank=True,max_length=50,verbose_name="被回复人") def __str__(self): return self.name
- 生成数据库
python manage.py makemigrations python manage.py migrate
编写视图、路由和前端页面
编写views.py视图,直接查询所有结果即可(可以通过filter查找出对应文章下的评论,这里省略)代码示例:
from django.shortcuts import render from Comments.models import Comments from django.views.generic.base import View class test_mptt(View): def get(self,request,*args,**kwargs): return render(request, 'genre.html', {'genres': Comments.objects.all()})
编写urls.py路由,代码示例:
from django.urls import path from Comments.views import test_mptt urlpatterns = [ ...省略... path('genres/', test_mptt.as_view(),name='gen'), ]
编写模板渲染后端数据(可以根据{% if not node.reply_to %}来判断是否为回复评论,如果是回复评论则增加回复样式,此处省略)。代码示例:
<!--精简代码,可根据自行需要添加样式--> {% load mptt_tags %} <ul> {% recursetree genres %} <li> {{ node.name }} {% if not node.is_leaf_node %} <ul class="children"> {{ children }} </ul> {% endif %} </li> {% endrecursetree %} </ul>
<!--多级评论代码示例--> {% load mptt_tags %} {% if comment_list %} <ul> {% recursetree comment_list %} <!--如果没有被回复人字段,则添加分割符--> {% if not node.reply_to %}<hr>{% endif %} <div {% if node.reply_to %} style="margin-left:10%"{% endif %}> <li> <!--判断是否存在被回复人字段,如果存在则显示【aaa → @bbb】,否则显示【xxx与】--> {% if not node.reply_to %} <a href="{{ node.website }}" target="_blank"><strong style="color: pink">{{ node.username }}</strong></a> 于 {% else %} <a href="{{ node.website }}" target="_blank"><strong style="color: pink">{{ node.username }}</strong></a> <span class="iconfont" style="font-size: 1.25rem"></span> @{{ node.reply_to }} {% endif %} <span style="color: green"> {{ node.add_time }} </span> 时说:<button style="float:right" value="{{ node.id }}" onclick="myFunction('{{ node.id }}','{{ node.username }}')">回复</button> <div style="font-family: inherit; font-size: 1em;" class="mb-2 mt-2 ml-4"> {{ node.body }}</div> {% if not node.is_leaf_node %} <ul class="children"> {{ children }} </ul> {% endif %} </li> </div> {% endrecursetree %} </ul> {% else %} 暂无评论 {% endif %}
添加测试数据,查看效果
- 添加测试数据,主要是新建一级评论和回复一级评论时传入的parent。代码示例:
from Comments.models import Comments dandy = Comments.objects.create(name='dandy') elina = Comments.objects.create(name='elina') cathy = Comments.objects.create(name='cathy') Comments.objects.create(name='aaa', parent=dandy) Comments.objects.create(name='bbb', parent=elina) Comments.objects.create(name='ccc', parent=cathy)
启动django服务,访问genres/路径查看效果。(第二张图是前面多级评论代码示例的效果图)
评论提交和其他说明
评论提交
评论提交部分,只需要传递后端需要的字段即可(例如博文ID、评论人、被评论人留言ID、被评论人名称、csrf等)。通过ajax传递,或者通过表单(隐藏部分表单内容)提交均可,这里不再做演示。
最大层级
django-mptt并没有限制最大层级,如果无限回复最后一个,可能会导致前端样式异常。这里可以在后端增加判断,如果大于第二层级则默认修改为第二层级(既评论的父级是根节点)。使用get_root()方法,获取当前实例的根节点实例。代码示例:
# reply_to_id为被回复人留言的id
r_id = request.POST.get('reply_to_id')
if r_id:
parent = Comment.objects.all().get(id=r_id)
new_comment.parent = parent.get_root() # 获取当前实例的根节点实例
new_comment.reply_to = request.POST.get('reply_to')
new_comment.save()
Django-MPTT扩展内容
MPTT模型实例方法
get_ancestors(ascending=False, include_self=False) # 返回一个包含所有当前实例祖宗的queryset
get_children() # 返回包换当前实例的直接孩子的queryset(即下一级所有的子节点),按树序排列
get_descendants(include_self=False) # 返回当前实例的所有子节点,按树序排列
get_descendant_count() # 返回当前实例所有子节点的数量
get_family() # 返回从当前实例开始的所有家庭成员节点,用树型结构
get_next_sibling() # 返回当前实例的下一个树型同级节点的实例
get_previous_sibling() # 返回当前实例的上一个树型同级节点的实例
get_root() # 获取当前实例的根节点实例
get_siblings(include_self=False) # 获取所有同级兄弟节点的实例的queryset
insert_at(target, position='first-child', save=False) # 插入作为目标节点的第一个子节点(如果save=True)
is_child_node() # 是否是子节点
is_leaf_node() # 是否是叶节点
is_root_node() # 是否是根节点
move_to(target, position='first-child') # 移动到某个节点的第一个子节点位置,target为空将会被移到根节点,此时不需要position位置参数
position位置参数:
'first-child', 'last-child','left', 'right'
参考资料
【1】https://www.cnblogs.com/wuzdandz/p/10595416.html ,树形结构模型Django-MPTT,2019-03-25,dandyzhang
【2】https://www.dusaiphoto.com/article/detail/67/ ,Django搭建个人博客:用django-mptt实现多级评论功能,2019/05/04,杜赛