Django 评论楼创建
【零】最终效果预览
【一】介绍
(1)情况说明
- 在Django模型层中有这么个字段
parent = models.ForeignKey(to='self', on_delete=models.CASCADE, verbose_name="父评论ID", null=True, blank=True)
- 这个字段是一对多的外键字段
- 其中
to='self'
说明是自关联的 - 当有子评论时,通过添加父评论的ID实现主评论和子评论的相关联
- 其中
- 那么就可以得到这么一个关系图
- 这个是我们理想中应该存在的情况
- 每一个评论都可以有子评论,且可以有多个子评论
- 这就是数据结构中的森林,每一个评论楼都是一个N叉树
- 每一个子评论都可以根据父评论ID获取父评论的所有信息
- 包括父评论用户名等所有信息
- 所以就存在这么一个问题
- 如何将每一个子评论渲染到对应的评论楼中
(2)解决办法思路
-
既然这是个森林那么就可以使用广度优先算法解决
- 难度相对来说有点复杂
-
有没有简单的办法呢
- 只要是个树那么就很难不用到广度优先遍历
- 所以就是破坏这个树
-
这个是新的评论关系
-
模型层部分代码
-
# 自动关联主评论,这个只是主评论楼ID parent = models.ForeignKey(to='self', on_delete=models.CASCADE, verbose_name="主评论楼ID", null=True, blank=True, related_name='children') # 将主评论ID和用户名分开保存 parent_name = models.CharField(max_length=32, verbose_name='父评论名字', null=True, blank=True)
-
-
这是新的评论楼关系图
-
说明
- 每一个评论下都渲染这个评论下的所有子评论
- 现在任然存在子评论的子评论
- 不过子评论的id直接指向评论楼楼主的ID
- 但是这样就少了被回复的那个人的用户名
- 父评论ID智能找到楼主信息
- 所以还有一个字段
- 保存被回复的用户名称
【二】代码说明
(1)模型层代码说明
-
主要说明一下parent字段的related_name
-
这个属性定义了反向关系
-
通过反向关系可以拿到所有的子评论
-
但是还需要定义一个获得子评论的方法
-
def get_children(self): return self.children.all()
-
这样就可以很轻松的拿到所有评论楼下的所有子评论信息
-
class Comment(BaseModel):
content = models.CharField(max_length=255, verbose_name="评论内容")
is_deleted = models.BooleanField(default=False, verbose_name="评论是否哦被删除")
# 自动关联主评论,这个只是主评论楼ID
parent = models.ForeignKey(to='self', on_delete=models.CASCADE, verbose_name="主评论楼ID", null=True, blank=True,
related_name='children')
# 将主评论ID和用户名分开保存
parent_name = models.CharField(max_length=32, verbose_name='父评论名字', null=True, blank=True)
# 关联用户
user = models.ForeignKey(to='user.UserInfo', on_delete=models.CASCADE, verbose_name="用户")
# 关联文章
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, verbose_name='文章')
class Meta:
db_table = 'comment'
verbose_name_plural = '评论表'
def get_children(self):
return self.children.all()
(2)视图层代码说明
(2.1)展示评论楼说明
- 这个很简单
- 首先对过滤掉非本文章的评论
- 然后遍历出所有评论楼
- 就是父评论为空的评论
def article_detail(request, username, article_id):
user_obj = UserInfo.objects.filter(username=username)
if not user_obj:
return render(request, 'error.html', locals())
# 文章详情
article_info_obj = Article.objects.filter(pk=article_id).first()
# 文章评论
all_comment_queryset = Comment.objects.filter(article=article_info_obj)
comment_queryset = all_comment_queryset.filter(parent=None)
return render(request, 'article_detail.html', locals())
(2.2)添加评论说明
- 首先获取前端发送的所有信息,主要的是
- 评论楼ID和父评论名字
- 如果这个有值,那么就是子评论
- 如果这个没有值,那么就是一个新的评论楼
- 这里的创建新评论楼的好处
- 不需要判断评论楼ID和父评论是否有值
- 因为模型层是允许为空的
@login_required
def comment(request):
if request.is_ajax():
# 获取前端信息
article_id = request.POST.get("article_id")
parent_id = request.POST.get("parentId")
content = request.POST.get("content")
username = request.POST.get("username")
# 评论不能为空判断
if not all([article_id, content]):
return json_response(code=2002, error='评论不能为空')
# 创建评论
Comment.objects.create(content=content, parent_id=parent_id, user=request.user, article_id=article_id,
parent_name=username)
# 评论数增加
article_obj = Article.objects.filter(pk=article_id).first()
article_obj.comment_num += 1
article_obj.save()
return json_response()
return json_response(code=2001, error='非ajax请求')
(3)模板层代码说明
(3.1)展示评论
- 首先遍历所有的评论楼
- 然后通过模型层的方法get_children()
- 获得所有的子评论
- 再次循环遍历渲染子评论信息即可
- 需要注意的点是
- 所有的子评论都要保存评论楼的楼主ID
- 这样才是新的模型层关系
<li class="media" style="border: 1px solid #72CDFC; border-top-width: 2px; border-bottom: none;"
id="comment-media-list">
{% for comment_obj in comment_queryset %}
{#评论头部#}
<div class="media-heading" style="margin-bottom: 10px">
<span>#{{ forloop.counter }}楼</span>
<span>{{ comment_obj.create_time|date:"Y-m-d H:i" }}</span>
<span>{{ comment_obj.user.username }}</span>
<span><a href="javascript:;" class="pull-right comment-replay"
username="{{ comment_obj.user.username }}"
comment-id="{{ comment_obj.pk }}">回复@{{ comment_obj.user.username }}</a></span>
</div>
{#评论内容#}
<div class="media-body">
{{ comment_obj.content }}
{% if comment_obj.get_children %}
<a class="pull-right" role="button" data-toggle="collapse"
href="#collapseExample{{ comment_obj.pk }}"
aria-expanded="false" aria-controls="collapseExample">
查看更多
</a>
<div class="collapse" id="collapseExample{{ comment_obj.pk }}">
<div class="well">
{% for children in comment_obj.get_children %}
<p style="margin-bottom: 5px">
{{ children.user.username }}@{{ children.parent_name }}
<span><a href="javascript:;" class="pull-right comment-replay"
username="{{ children.user.username }}"
comment-id="{{ comment_obj.pk }}">回复@{{ children.user.username }}</a></span>
</p>
<div>
{{ children.content }}
</div>
<hr style="border: 1px solid #72CDFC; width: 100%; margin-bottom: 0;">
{% endfor %}
</div>
</div>
{% endif %}
</div>
<hr style="border: 1px solid #72CDFC; width: 100%; margin-bottom: 0;">
{% endfor %}
</li>
(3.2)添加评论Ajax
- 回复评论的处理
- 获取被回复对象的用户名和评论楼的ID
- 添加指定格式
- 渲染到评论框中,并聚焦focus()
- 提交评论给后端Ajax
- 添加到评论框中的内容并不是全都要保存在数据库中比如@admin
- 所以先对这部分进行切分处理
- 然后传递信息给后端对应的路由
- 从后端拿到返回信息
- 错误的信息,进行指定位置的渲染
- 正确的信息,先显示在底部,提供一个刷新按钮
- 点击刷新即可完成评论的添加
<script>
$(document).ready(function () {
// 父评论默认是空的
let parentId = null
// 回复平理论处理
$(".comment-replay").click(function () {
let commentUsername = $(this).attr('username')
parentId = $(this).attr('comment-id')
// 拼接字符到评论框,并且聚焦
$("#comment-text").val("@" + commentUsername + '\n').focus()
})
$("#comment-submit").click(function (e) {
e.preventDefault()
// 获取评论类容
let content = $("#comment-text").val()
// 对次评论进行头部处理
if (parentId) {
let indexNum = content.indexOf('\n') + 1
// 使用trim移除前后的空白名
// 保留回复人的信息
var username = content.slice(1, indexNum).trim();
// 评论内瓤
content = content.slice(indexNum).trim();
}
// 发送ajax请求
$.ajax({
url: "{% url 'comment' %}",
type: 'post',
data: {
"article_id": "{{ article_info_obj.pk }}",
"content": content,
"csrfmiddlewaretoken": "{{ csrf_token }}",
"parentId": parentId,
"username": username,
},
success: function (response) {
if (response.code === 2000) {
// 清空评论框中的内容
$("#comment-text").val('')
// 评论者的名字
let userName = '{{ request.user.username }}'
// 使用模板语法渲染信息
let newComment = `
<div class="media-heading" style="margin-bottom: 10px">
<span>${userName}</span>
<span><a onclick="window.location.reload()" style="color: blue">刷新</a></span>
</div>
<div class="media-body">
${content}
</div>
<hr style="border: 1px solid #72cdfc; width: 100%; margin-bottom: 0;">
`
// 添加到末尾
$("#comment-media-list").append(newComment)
} else {
$("#comment-error").text(response.error)
}
// 请求结束以后需要重置
parentId = null
}
})
})
})
</script>