我们在前一章节里我们做了博客的文章主页面,最后留了个功能——评论。我们这里详细了说一下。
评论功能:
- 提交根评论
- 显示根评论
- 提交子评论
- 显示子评论
评论可以通过render或者AJAX方法来实现。
然后就是这一章的重点:评论树和评论楼的效果,评论楼就是依次把评论显示出来,评论树有些类似下面的样式
111
222对111的回复
111对222的回复
评论树有些类似于递归的处理,以后可能会用的比较多。重点就是这里
而提交和显示就跟点赞是一样的,区别就是点赞的显示是直接通过简单的DOM实现,而评论的显示就用到ORM了。
提交评论
先看一下ORM里的评论表是怎么定义的
class Comment(models.Model): """ 评论表 """ nid = models.AutoField(primary_key=True) article = models.ForeignKey(to="Article", to_field="nid",on_delete=models.CASCADE) user = models.ForeignKey(to="UserInfo", to_field="nid",on_delete=models.CASCADE) content = models.CharField(max_length=255) # 评论内容 create_time = models.DateTimeField(auto_now_add=True) parent_comment = models.ForeignKey("self", null=True, blank=True,on_delete=models.CASCADE) # blank=True 在django admin里面可以不填 def __str__(self): return self.content class Meta: verbose_name = "评论" verbose_name_plural = verbose_nam
也就是说我们在AJAX里需要发送的数据有article,user,content,parent_comment,创建的时间是可以自动生成的。
看一下整个按钮的事件的JavaScript代码
1 <script> 2 // 提交评论 开始 3 $('.post_comment').click(function(){ 4 var comment_content = $('#comment_text').val(); 5 var pid = ''; //父评论的id,为空的时候为根评论 6 var article_id = $('.info').attr('article_id'); 7 8 $.ajax({ 9 url:'blog/comment/', 10 type:'POST', 11 data:{ 12 csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val(), 13 pid : pid, 14 article_id:article_id, 15 content : comment_content 16 }, 17 success:function(data){ 18 $('#comment_text').val(''); 19 } 20 })//ajax完毕 21 22 }) 23 // 提交评论 结束 24 </script>
而ajax请求对应的视图函数
1 def comment(request): 2 user = request.user 3 print('in comment') 4 article_id = request.POST.get('article_id') 5 content = request.POST.get('content') 6 7 article = models.Article.objects.get(nid = article_id) 8 parent_comment = request.POST.get('parent_comment') 9 10 models.Comment.objects.create(article=article,user=user,content=content,parent_comment=parent_comment) 11 return HttpResponse('comment OK')
最后的返回值应该加一个状态码,ajax的success可以根据视图最后的状态决定是否删除text框里的内容。
提交评论以后的操作
现在有个问题:评论提交以后不刷新页面是无法显示出来的, 所以就要在上面的AJAX里添加一个DOM操作的过程,就是在ul标签里append一个新的li就行了,这个li标签可以按照一个字符串的形式保存给一个变量,然后就把这个字符串修改一下然后再添加就行了。这就是开始说的用AJAX的方法显示评论。但是这个评论显示的是一个比较简单的,只显示出来评论的内容就行了。把前面那个js文件完善一下就是下面的结构。
1 // 提交评论 开始 2 $('.post_comment').click(function(){ 3 var comment_content = $('#comment_text').val(); 4 var article_id = $('.info').attr('article_id'); 5 if(pid){ 6 var index = comment_content.indexOf(' '); //获取回复内容中第一个换行符的索引 7 comment_content = comment_content.slice(index=index+1) 8 } 9 if(comment_content==''){alert('评论不能为空')} 10 else{ 11 $.ajax({ 12 url:'blog/comment/', 13 type:'POST', 14 data:{ 15 csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val(), 16 article_id:article_id, 17 content : comment_content, 18 parent_comment:pid, 19 }, 20 success:function(data){ 21 if(data.state){ 22 $('#comment_text').val(''); 23 var comment_li = '<li class="list-group-item">'+ 24 '<div>'+ 25 '{{comment.create_time|date:"Y-m-d H:i"}} '+ 26 '<div class="con">'+ 27 comment_content 28 '</div>'+ 29 '</li>' 30 $('.list-group').append(comment_li); 31 pid = '' 32 } //if(data.state)收 33 } //success收 34 })//ajax收 35 } 36 }) 37 // 提交评论 结束 38 39 // 回复评论 开始 40 $('.comment_reply').click(function(){ 41 var comment_user = $(this).attr('comment_author'); 42 pid = $(this).attr('comment_id'); 43 $('#comment_text').val('@'+comment_user+' '); 44 $('#comment_text').focus(); 45 46 }) //click事件结束 47 // 回复评论 结束
用render显示评论主要用在文章页面的显示,每次请求文章详情页面的时候应该所有的评论都会显示出来。其实没什么难的,在视图中通过url拿到文章的id,再拿到文章,最后获取文章通过外键关联的comment字段(QuerySet形式)。最后把这个list通过render渲染给上一张的article.html文件。
但是article里就要加一个显示评论的div
1 <!-- 评论列表开始 --> 2 <div class="comment_list"> 3 <h3>评论列表</h3> 4 <ul class="list-group"> 5 {%for comment in comment_list%} 6 <li class="list-group-item"> 7 <div> 8 #{{forloop.counter}}楼 {{comment.create_time|date:"Y-m-d H:i"}} 9 10 <a href="/blog/{{comment.user}}/">{{comment.user}}</a> 11 12 13 {% if comment.user.username == user.username %} 14 15 <a href="/blog/comment/delete/{{comment.nid}}/" class="pull-right">删除</a> 16 17 {% else %} 18 <a class="pull-right comment_reply" comment_author='{{comment.user}}' 19 comment_id= '{{comment.nid}}' >回复</a> 20 {% endif %} 21 22 </div> 23 {% if comment.parent_comment %} 24 <div class="parent_comment well"> 25 <p> 26 回复:{{comment.parent_comment.user.username}} 27 </p> 28 29 <p style="margin-left: 30px;"> 30 {{comment.parent_comment.content}} 31 </p> 32 33 </div> 34 {%endif%} 35 <div class="con"> 36 {{comment.content}} 37 </div> 38 39 </li> 40 {%endfor%} 41 </ul> 42 <!-- 评论列表结束 -->
这里有个点是下面要讲的:子评论的显示。我们先不考虑。先看下整体的显示效果
整个div用了一个Bootstrap的样式——列表组。通过for循环生成了里面的li标签。整个过程都是一些基础知识,添加了一个功能:在当前用户和评论的发起者一致的时候,右侧的标签是删除,否则是回复。在实现这个功能的时候遇到了一个坑
最早的if语句是这样的
{% if comment.user == user %}
这就不行了,调试的时候还试过这样的方法也不行(在评论发起人是123时),一直走的是false的路径。
{% if comment.user == ‘123’%}
最后发现一定要拿到最后的username才行。
根评论解决了,下面就是子评论的提交方式。
看一下前面的model,区分根评论和子评论的区别就是parent_comment字段是否有数据,有数据就是这条数据就是对应parent_comment的子评论,那么我们在点击回复按钮的时候就要拿到这一条回复的id,这就是上面render显示评论时候为什么加了几个属性(18行,19行)
回顾一下我们提交评论时候的代码,第5行里定义了一个变量pid,我们需要在点击回复的时候把这条回复的comment_id给他,怎么操作?就不能把这个pid定义在$('.post_comment').click()这个事件中。应该放在外面做一个全局变量。在点击回复时候触发另一个事件,在这个事件中改变pid的值。
我们看一下博客园里点击评论回复以后的效果,直接在评论框里出现了下面的效果并且评论框获取了焦点
出现了一个@评论人的字符串并且换行。
整个事件也比较简单,几行代码就搞定。注意那个 的换行符,我们后面还要用到。
// 回复评论 开始 $('.comment_reply').click(function(){ var comment_user = $(this).attr('comment_author'); pid = $(this).attr('comment_id'); $('#comment_text').val('@'+comment_user+' '); $('#comment_text').focus(); }) //click事件结束 // 回复评论 结束
下面就要修改一下前面提交评论时候的代码了,我先把整个html里的script标签放出来
1 <script> 2 3 var pid = ""; //全局变量pid,点击回复时会赋值为父评论的id值 4 5 // 提交评论 开始 6 $('.post_comment').click(function(){ 7 var comment_content = $('#comment_text').val(); 8 var article_id = $('.info').attr('article_id'); 9 if(pid){ 10 var index = comment_content.indexOf(' '); //获取回复内容中第一个换行符的索引 11 comment_content = comment_content.slice(index=index+1) 12 } 13 if(comment_content==''){alert('评论不能为空')} 14 else{ 15 $.ajax({ 16 url:'blog/comment/', 17 type:'POST', 18 data:{ 19 csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val(), 20 article_id:article_id, 21 content : comment_content, 22 parent_comment:pid, 23 }, 24 success:function(data){ 25 if(data.state){ 26 $('#comment_text').val(''); 27 var comment_li = '<li class="list-group-item">'+ 28 '<div>'+ 29 '{{comment.create_time|date:"Y-m-d H:i"}} '+ 30 '<div class="con">'+ 31 comment_content 32 '</div>'+ 33 '</li>' 34 $('.list-group').append(comment_li); 35 pid = '' 36 } //if(data.state)收 37 } //success收 38 })//ajax收 39 } 40 }) 41 // 提交评论 结束 42 43 // 回复评论 开始 44 $('.comment_reply').click(function(){ 45 var comment_user = $(this).attr('comment_author'); 46 pid = $(this).attr('comment_id'); 47 $('#comment_text').val('@'+comment_user+' '); 48 $('#comment_text').focus(); 49 50 }) //click事件结束 51 // 回复评论 结束 52 </script>
好像也没什么区别,就是有一个用法要注意一下
var index = comment_content.indexOf(' '); //获取回复内容中第一个换行符的索引 comment_content = comment_content.slice(index=index+1)
indexOf方法有些类似于字符串里的搜索指定字符串,返回了一个int类型的数据,而slice类似于python里的字符串切片,把end=index+1是因为他跟python的切片是一样的:顾头不顾腚。最后获取到删除掉@用户名,取剩下的部分。
1 #评论提交 2 def comment(request): 3 ret = {} 4 5 user = request.user 6 7 article_id = request.POST.get('article_id') 8 content = request.POST.get('content') 9 10 article = models.Article.objects.get(nid = article_id) 11 12 parent_comment_id = request.POST.get('parent_comment') 13 14 15 try: 16 if parent_comment_id: #有父级评论 17 parent_comment = models.Comment.objects.get(nid = parent_comment_id) 18 models.Comment.objects.create(article=article,user=user,content=content,parent_comment=parent_comment) 19 else: 20 models.Comment.objects.create(article=article,user=user,content=content) 21 22 ret['state'] = 1 23 except Exception as e: 24 ret['state'] = 0 25 return JsonResponse(ret)
如果pid有值的时候就用get获取一下父级评论的comment对象,否则直接创建。
子评论的显示我们先做一个简单的效果:盖楼的显示效果,先看看出来的效果
这种显示的效果实现起来比较简单,先看一下被render渲染的对象代码(if结构里的)
1 {% if comment.parent_comment %} 2 <div class="parent_comment well"> 3 <p> 4 回复:{{comment.parent_comment.user.username}} 5 </p> 6 <p style="margin-left: 30px;"> 7 {{comment.parent_comment.content}} 8 </p> 9 {%endif%} 10 </div>
其实也就是在显示评论的时候判断一下这条评论有没有父评论,如果有就通过父评论的外键拿到父评论,然后显示出来文本内容就可以了。
显示包含子评论的视图不用去判定评论对象是否包含子评论或父评论,直接把comment对象发给render就行了,所以不需要任何修改。
评论树的实现方法其实还是很有意思的,以最简单暴力的方法来解决就是使用递归的方法。拿到一个comment去查他的parent_comment,然后再去找parent_comment的parent_comment。直到parent_comment字段为空就行了。那么在做递归的时候需要考虑的逻辑还是比较多的(回顾一下递归的两个条件:自己调用自己,有一个出口)。所以我们今天用另外一种方式来实现。
实现的思路就是在页面直接放一个无事件关联的AJAX请求,那么在文章页面一被访问就发送请求,AJAX请求的返回值是返回这个文章的comment对象,先看一看AJAX请求的前面几个参数
$.ajax({ url:'/blog/comment_tree/'+'{{article.nid}}/', type:'GET', data:{ },
由于文章的ID是从url发过去的,所以data里不需要任何数据,然后对应的视图是下面这样的
1 def comment_tree(response,article_id): 2 print('in comment_tree') 3 ret = models.Comment.objects.filter(article=article_id).values('pk','content','parent_comment') 4 ret = list(ret) 5 return JsonResponse(ret,safe=False)
这里有两个点要注意一下:
1.我们是用JsonResponse返回的数据,但是values方法返回的是一个QuerySet对象(ret),ret不能被JSON序列化。可以试一下下面的代码看看输出值是什么
import json ret = models.Comment.objects.filter(article=1).values('pk','content','parent_comment') json.dumps(ret)
结果会有下面的错误提示
TypeError: Object of type 'QuerySet' is not JSON serializable
所以在视图中直接做了一个list强制转换,把QuerySet转换成list,就可以用JSON序列化了。我们还可以把返回值在浏览器中打印一下(console.log(data))
并且在AJAX里拿到的数据还是个object类型的数据,可以继续对其使用for循环迭代。
2.注意一下在JSONResponse里还加了个参数safe,先看看不加会有什么错误提示
'In order to allow non-dict objects to be serialized set the ' TypeError: In order to allow non-dict objects to be serialized set the safe parameter to False.
再看看Django的源代码里对JsonResponse的参数定义
class JsonResponse(HttpResponse): """ An HTTP response class that consumes data to be serialized to JSON. :param data: Data to be dumped into json. By default only ``dict`` objects are allowed to be passed due to a security flaw before EcmaScript 5. See the ``safe`` parameter for more information. :param encoder: Should be a json encoder class. Defaults to ``django.core.serializers.json.DjangoJSONEncoder``. :param safe: Controls if only ``dict`` objects may be serialized. Defaults to ``True``. :param json_dumps_params: A dictionary of kwargs passed to json.dumps(). """
里面指定了默认情况下我们只能对dict类型的数据进行Json序列化,在传输非dict类型数据时就要指定这个safe的值了。我们在上面的传输用过程中把数据转成了list,就必须对safe赋值为false。
最后看一看ajax里对success中的函数是怎么定义的,在这里实现最基础的效果,细节上先不完善了。
1 //获取评论树的信息 2 $.ajax({ 3 url:'/blog/comment_tree/'+'{{article.nid}}/', 4 type:'GET', 5 data:{ 6 7 }, //data收 8 success:function(data){ 9 console.log(typeof(data)) 10 $.each(data,function(index,comment_dict){ 11 12 var s = "<div class='comment_item' comment_id = "+ 13 comment_dict.pk+ 14 "> <span class= 'content'>"+ 15 comment_dict.content+ 16 "</span></div>" 17 18 if(comment_dict.parent_comment){ 19 20 var pid = comment_dict.parent_comment; 21 $("[comment_id="+pid+"]").append(s) 22 23 24 25 } //if收尾 26 else{ 27 28 29 $('.comment_tree').append(s) 30 } //esle收尾 31 } //each内function收尾 32 )//each 收尾 33 }, //success后function 34 }) //ajax收
首先回顾一下$.each的用法
var json1={key1:'a',key2:'b',key3:'c'}; $.each(json1,function(key,value){ //遍历键值对 console.log(key+'````'+value); })
上面这段js代码的输出值是什么样的?
key1````a
key2````b
key3````c
所以就是对object进行了遍历,因为我们传过去的数据是个二维的object,第10行里的function的参数,index就是每次遍历的索引,后面的comment_item就是我们在视图中通过orm中values()指定的字段列表(视图函数第三行)。
和盖楼的方法一样,我们先定义一个字符串s,s是一个整个的div,我们需要的就是依次把通过comment_item里的参数套在s里的div中。在生成div的时候去查一下parent_comment属性中是否有值。我们在创建div的时候为div添加了一个属性
comment_id,值就是当前评论的id值。如果parent_comment中有值,就去找这个值对应的父评论的div。这里用了jQuery的属性选择器。
//查找属性comment_id 等于1的标签 $("[comment_id=1]")
所以这样就是在拿到一个comment_item以后,如果pid里没有值,就把div添加在主div里,而有值的话就把div放在选择器选中的div中。为了显示的效果,可以把嵌套的div的margin-left 指定一个值
.comment_item{ margin-left: 20px; }
就能显示出来下面的效果(实在是不想优化样式了,反正目的已经达到了。)
最后未经修饰的article.html放出来
{%extends 'blog_base.html'%} {%block page-main %} <div> {%csrf_token%} <h1>{{article.title}}</h1> <h2>{{article.user}}</h2> <p>{{article.articledetail.content|safe}}</p> </div> <!-- 点赞踩灭开始 --> <div class="poll clearfix"> <div id="div_digg"> <div class="diggit action"> <span class="diggnum" id="digg_count">{{article.up_count}}</span> </div> <div class="buryit action"> <span class="burynum" id="bury_count">{{article.down_count}}</span> </div> <div class="clear"></div> <div class="diggword" id="digg_tips"> </div> <div class="clear"></div> <div class="diggword" id="digg_tips" style="color: red;"></div> </div> <div class="info" article_id='{{article.nid}}' >123</div> </div> <!-- 点赞踩灭结束 --> <hr> <!-- 评论开始 --> <!-- 评论树开始 --> <h3>评论树</h3> <div class="comment_tree"> </div> <!-- 评论树结束 --> <hr> <!-- 评论列表开始 --> <div class="comment_list"> <h3>评论列表</h3> <ul class="list-group"> {%for comment in comment_list%} <li class="list-group-item"> <div> #{{forloop.counter}}楼 {{comment.create_time|date:"Y-m-d H:i"}} <a href="/blog/{{comment.user}}/">{{comment.user}}</a> {% if comment.user.username == user.username %} <a href="/blog/comment/delete/{{comment.nid}}/" class="pull-right">删除</a> {% else %} <a class="pull-right comment_reply" comment_author='{{comment.user}}' comment_id= '{{comment.nid}}' >回复</a> {% endif %} </div> {% if comment.parent_comment %} <div class="parent_comment well"> <p> 回复:{{comment.parent_comment.user.username}} </p> <p style="margin-left: 30px;"> {{comment.parent_comment.content}} </p> </div> {%endif%} <div class="con"> {{comment.content}} </div> </li> {%endfor%} </ul> <!-- 评论列表结束 --> <div class="comment"> <span class="glyphicon glyphicon-comment">发表评论</span> <div class="comment_list"> </div> <div> {%if user.username%} 当前用户:<input type="text" disabled value="{{user}}"> <div> <p>评论内容</p> <textarea name="" id="comment_text" cols="30" rows="10"></textarea> </div> <button class="post_comment">提交评论</button> {%else%} <a href="/login/">请登录</a> {%endif%} </div> <!-- 评论结束 --> <script src="/static/js/updown.js"></script> <script> var pid = ""; //全局变量pid,点击回复时会赋值为父评论的id值 // 提交评论 开始 $('.post_comment').click(function(){ var comment_content = $('#comment_text').val(); var article_id = $('.info').attr('article_id'); if(pid){ var index = comment_content.indexOf(' '); //获取回复内容中第一个换行符的索引 comment_content = comment_content.slice(index=index+1) } if(comment_content==''){alert('评论不能为空')} else{ $.ajax({ url:'blog/comment/', type:'POST', data:{ csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val(), article_id:article_id, content : comment_content, parent_comment:pid, }, success:function(data){ if(data.state){ $('#comment_text').val(''); var comment_li = '<li class="list-group-item">'+ '<div>'+ '{{comment.create_time|date:"Y-m-d H:i"}} '+ '<div class="con">'+ comment_content '</div>'+ '</li>' $('.list-group').append(comment_li); pid = '' } //if(data.state)收 } //success收 })//ajax收 } }) // 提交评论 结束 // 回复评论 开始 $('.comment_reply').click(function(){ var comment_user = $(this).attr('comment_author'); pid = $(this).attr('comment_id'); $('#comment_text').val('@'+comment_user+' '); $('#comment_text').focus(); }) //click事件结束 // 回复评论 结束 //获取评论树的信息 $.ajax({ url:'/blog/comment_tree/'+'{{article.nid}}/', type:'GET', data:{ }, //data收 success:function(data){ $.each(data,function(index,comment_dict){ var s = "<div class='comment_item' comment_id = "+ comment_dict.pk+ "> <span class= 'content'>"+ comment_dict.content+ "</span></div>" if(comment_dict.parent_comment){ var pid = comment_dict.parent_comment; $("[comment_id="+pid+"]").append(s) } //if收尾 else{ $('.comment_tree').append(s) } //esle收尾 } //each内function收尾 )//each 收尾 }, //success后function }) //ajax收 </script> {%endblock%}
里面包含了两种评论的显示方式。可以参考一下!
key1````a
key2````b
key3````c