由于可以回复评论,所以评论是一个层次架构的模型
所以在模型中,评论是自己的外键,可以通过一条评论获取回复的评论
在blog/models.py
中添加Comment
模型
class Comment(models.Model):
user = models.ForeignKey(User)
content = models.TextField()
#parent为该评论的父评论,所以第一个参数为'self',当为空时表示为第一层级的评论
#指定related_name='children',这样可以父评论通过comment.children获取子评论,默认是通过comment.comment_set获取
parent = models.ForeignKey('self', blank=True, null=True, related_name='children', on_delete=models.SET_NULL)
post = models.ForeignKey(Post)
created_time = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.content
class Meta:
ordering = ['-created_time']
为了在后台能管理Comment
模型,在blog/admin.py
注册
from blog.models import Comment
admin.site.register(Comment)
然后就是迁移数据库了
python manage.py makemigrations
python manage.py migrate
评论适合文章关联的,没有单独的视图,是显示在文章下面的,所以我们修改PostView
在blog/views.py
的PostView
里修改get_object
方法
为post
对象,添加一个comments
属性
def get_object(self, queryset=None):
post = super(PostView, self).get_object(queryset)
#格式化markdown文章等其他操作
#...
#由于post是comment的外键,所以可以通过post.comment_set.all()来获取该文章下的所以评论
#同时过滤出parent=None的评论
post.comments = post.comment_set.all().filter(parent=None).order_by('-created_time')
return post
修改blog/templates/blog/detail.html
在合适的文章结束的地方添加
<ul>
{% for c in post.comments %}
<li>
{{c.content}}
<ul>
{% for cc in c.children.all %}
<li>
{{cc.content}}
li>
{% endfor %}
ul>
li>
{% endfor %}
ul>
这就是最简单的模板了,这里只显示两个层级的评论
接下来美化一下评论,为了复用,将单条评论的模板提取成一个模板文件blog/comment.html
并且接受评论对象comment
,层级level
和楼数counter
这三个参数
评论是按时间降序排序的,最新的评论在最前面,所以counter
传入的是forloop.revcounter
forloop
是for
标签里计数器,而forloop.revcounter
是翻转计数,从迭代对象的长度递减到0
只有层级level
为1时才显示评论的楼数,level=2
时不显示
通过include
标签可以包含其他模板文件
这样我们的模板就变成下面这样了
<ul>
{% for c in post.comments %}
<li>
{% include 'blog/comment.html' with comment=c level=1 counter=forloop.revcounter %}
<ul>
{% for cc in c.children.all|dictsort:'created_time' %}
<li>
{% include 'blog/comment.html' with comment=cc level=2 %}
li>
{% endfor %}
ul>
li>
{% endfor %}
ul>
接下来编写comment.html
我使用的是UIKit3前端框架,所以一条评论的HTML
大概是这样的
<article class="uk-comment">
<header class="uk-comment-header">
<img class="uk-comment-avatar" src="" alt="">
<h4 class="uk-comment-title">h4>
<ul class="uk-comment-meta uk-subnav">ul>
header>
<div class="uk-comment-body">div>
article>
新建blog/templates/blog/comment.html
,将相关变量填入,就成下面这样了
<article class="uk-comment uk-comment-primary uk-visible-toggle">
<header class="uk-comment-header uk-position-relative">
<div class="uk-grid-medium uk-flex-middle" uk-grid>
<div class="uk-width-auto">
<img class="uk-comment-avatar" src="https://getuikit.com/docs/images/avatar.jpg" width="80" height="80" alt="">
div>
<div class="uk-width-expand">
<h4 class="uk-comment-title uk-margin-remove">
<a class="uk-link-reset" href="#">{{ comment.user.username }}a>
h4>
<p class="uk-comment-meta uk-margin-remove-top">
<a class="uk-link-reset" href="#">{{ comment.created_time }}a>
p>
div>
div>
{% if level == 1 %}
<div class="uk-position-bottom-right uk-position-small">
<span class="uk-link-muted" href="#">#{{ counter }}span>
div>
<div class="uk-position-top-right uk-position-small uk-hidden-hover">
<a class="uk-link-muted" href="#">Replya>
div>
{% endif %}
header>
<div class="uk-comment-body">
<p>{{ comment.content }}p>
div>
article>
现在的页面还不能发表评论,所以只能在后台界面手动添加评论来查看效果了
其中有个回复按键Reply
,下面来实现类似csdn
的评论功能
要实现评论功能,可以通过提交表单实现,这里使用了django
的模型表单类ModelForm
新建blog/forms.py
文件,用来放置自定义表单类
自定义一个CommentForm
类,用来表示一个评论表单
from django.forms import ModelForm
from blog.models import Comment
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ('content', 'parent', 'user', 'post')
有了表单类,就需要想办法吧表单对象传递给模板
只需要在blog/views.py
对PostView
覆写get_context_data
方法
这个方法在get
方法里调用,用来生成上下文传递给模板
因此直接创建一个CommentForm
,添加到上下文里
from blog.forms import CommentForm
...
def get_context_data(self, **kwargs):
context = super(PostView, self).get_context_data(**kwargs)
form = CommentForm()
context.update({'form': form})
return context
有了表单对象之后,就可以在模板里使用了,注意name
属性要和CommentForm
的fields
对应上
这样提交的时候,表单和表单对象才可以对应上
同时必须有{% csrf_token %}
标签,防止csrf
攻击
<form method="post">
{% csrf_token %}
<textarea name="{{ form.content.name }}" id="replay_comment_content" required>textarea>
<input type="hidden" name="{{ form.parent.name }}" id="replay_comment_id">
<input type="hidden" name="{{ form.user.name }}" value="{{ user.id }}">
<input type="hidden" name="{{ form.post.name }}" value="{{ post.id }}">
<button>submitbutton>
form>
form.content
是评论内容,需要用户填写的
form.parent
是被回复的评论的评论对象
form.user
是当前登录的用户
form.post
是当前的文章
后面三个是对用户不可见的,form.parent
的值是当用户点击回复按键才会填充的,可以为空,而form.user
和form.post
的值是一打开页面就确定了
观察csdn
的回复评论的流程,可以发现,当随意点击其中一个回复按键,就会跳转到评论框,并且会在评论框填充[reply]被回复的用户名[/reply]
跳转到评论框可以通过设置锚点,而评论框填充内容就需要javascript
来实现了
首先修改blog/templates/blog/comment.html
,在回复按键上添加class="reply_button"
,方便在js
获取所有回复按键
将href="#reply"
,点击回复按键时会滚动到id
为reply
的组件上
设置data-id
为评论的id
,data-username
为评论的用户名,用于提交的时候获取被评论的id
和用户名
{% if level == 1 %}
- <div class="uk-position-top-right uk-position-small uk-hidden-hover"><a class="uk-link-muted" href="#">Replya>div>
+ <div class="uk-position-top-right uk-position-small uk-hidden-hover"><a class="uk-link-muted reply_button"
+ href="#reply"
+ data-id="{{ comment.id }}"
+ data-username="{{ comment.user }}">Replya>
+ div>
{% endif %}
再将表单的id
设置为reply
,onsubmit
设置为return onSubmitComment()
,用于在提交的时候做一些处理
接下来编写javascript
代码,因为代码会使用到jquery
,所以必须写在引入jquery
文件的后面
在blog/templates/blog/base.html
里所以js
文件导入后面添加一个block afterbody
<script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js">script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.30/js/uikit.min.js">script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.30/js/uikit-icons.min.js">script>
+{% block afterbody %}{% endblock %}
body>
html>
在blog/templates/blog/detail.html
里添加如下代码
{% block afterbody %}
<script>
$('#reply_button').click(function () {
$('#replay_comment_id').val($(this).data('id'));
$('#replay_comment_content').val('[reply]'+$(this).data('username')+'[/reply]\n');
});
function onSubmitComment() {
var content = $('#replay_comment_content').val();
var newstr = content.replace(/^\[reply\].*?\[\/reply\]\n/, '');
if (newstr) {
if (newstr === content) {
$('#replay_comment_id').val('');
}
$('#replay_comment_content').val(newstr);
return true;
}
return false;
}
script>
{% endblock %}
通过$('#reply_button').click()
为所有回复按键添加点击事件
当点击回复按键时将表单的parent
组件(id
为replay_comment_id
)设置为$(this).data('id')
,也就是对应刚刚设置的data-id
属性
将表单的content
组件(id
为replay_comment_content
)设置为'[reply]'+$(this).data('username')+'[/reply]\n'
,提示回复的是哪个用户
当点击提交按键时,调用onSubmitComment
函数,这里会取出提交的评论内容,去掉我们添加的[reply]用户名[/reply]
如果没有我们添加的内容,说明是直接发表评论,也可能是用户自己删掉提示内容,这时候需要将parent
字段置空
到这里前端的内容都搞完了,由于提交时一个POST
请求,还需要后端处理
又回到PostView
,添加一个def post(self, request, *args, **kwargs)
方法,POST
请求会调用post
方法
用request.POST
构建一个CommentForm
,request.POST
包含了所有表单参数
当CommentForm
有效时直接调用save
方法保存到数据库里面,最后通过redirect
重定向会我们的文章页面,避免重复提交表单
from django.shortcuts import render, redirect
...
def post(self, request, *args, **kwargs):
obj = self.get_object()
form = CommentForm(request.POST)
if form.is_valid():
form.save()
return redirect(obj)
当然,后端也应该验证传入的参数,判断用户权限等,这里就先省略了