使用树形结构模型Django-MPTT实现多级评论

学习树形结构第三方库django-mptt,可用于实现类似多级评论的功能!
454阅读 · 2020-7-19 23:08发布

编写多级评论思路

多级评论本质是一个树状结构的对象,每个评论是根据他的父级来确定它所在位置。

那么想实现多级评论功能,可以自己实现树状结构逻辑,或者使用第三方库直接调用!

本篇文章选择直接使用第三方库Django-MPTT接入到博客中,用它来管理树状结构的评论数据。

Django-MPTT的使用

django-mptt是一个可复用的django app,旨在让你自己的django项目模型使用MPTT更加简单。它负责将数据库表作为树型结构管理的详细信息,并提供用于处理树型模型实例的工具。

安装和文档

使用方法简述

  1. 安装并在setting.py中加入django-mptt应用。
  2. 修改评论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
    
  3. 编写视图,此时通过评论models获取的数据,就是树状结构的,直接返回给前端即可(Comment.objects.all()获取并返回)
  4. 添加路由,指向视图。
  5. 前端模板渲染,参考官方给的示例修改即可。
    {% load mptt_tags %}
     <ul>
         {% recursetree genres %}
             <li>
                 {{ node.name }}
                 {% if not node.is_leaf_node %}
                     <ul class="children">
                         {{ children }}
                     </ul>
                 {% endif %}
             </li>
         {% endrecursetree %}
     </ul>
    
  6. 新增数据。
    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)
    
  7. 查看效果。
    <!--前端展示的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应用

  1. 修改settings.py文件,添加mptt应用。代码示例:
    INSTALLED_APPS = [
         ...省略...
         'mptt',
     ]
    
  2. 修改已有评论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
    
  3. 生成数据库
    python manage.py makemigrations
     python manage.py migrate
    

编写视图、路由和前端页面

  1. 编写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()})
    
  2. 编写urls.py路由,代码示例:

    from django.urls import path
     from Comments.views import test_mptt
    
     urlpatterns = [
         ...省略...
         path('genres/', test_mptt.as_view(),name='gen'),
     ]
    
  3. 编写模板渲染后端数据(可以根据{% 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">&#xe60f;</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 %}
    

添加测试数据,查看效果

  1. 添加测试数据,主要是新建一级评论和回复一级评论时传入的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)
    
  2. 启动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,杜赛