Python Django 學習紀錄:架設個人部落格(四)
回復功能和樹狀結構
從評論功能到回復功能,涉及到許多前端ajax的處理
各項資料在表單、模型、模板頁面、ajax中傳遞
而這次回復功能更是接續上次評論功能的ajax異步更新
設計出樹狀結構,根據設置的編號來回復不同的評論
最後則是優化用戶體驗,讓用戶在回復時能得到相應的資訊與操作體驗
一.、如何設計回復功能
1.評論可被回復:
用戶點擊評論後的回復連結
對評論進行回復
2.回復可被回復:
用戶點擊回復後的回復連結
對相同評論進行回復
3.樹狀層級:
樹狀總共分為三層
頂層第一層為該文章
第二層則為對應文章的評論
底層第三層則為回復對象的評論或回復
為樹狀結構設置變數
將該變數傳至篩選資料的方法
在前端模板取用該資料時就能依據變數辨別該資料對應的評論或回覆
4.回復功能:
用戶點擊該評論後的回復連結後
透過js跳轉到原評論用富文本編輯,變更為回復用的的路徑
將要回復的評論顯示在上方
並將輸入的文本以ajax的方式添加到該評論下方的回復區域
二.、實現回復功能
1.在評論模型添加回復功能:
首先在 comment 應用的<models.py> 的 Comment模型添加回復功能
這裡使用related_name反向連結模型:
from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User class Comment(models.Model): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) #透過外鍵指向模型 object_id = models.PositiveIntegerField() #紀錄對應模型的主鍵值 content_object = GenericForeignKey('content_type', 'object_id') #集合前兩項成一個通用外鍵 text = models.TextField() comment_time = models.DateTimeField(auto_now_add=True) user = models.ForeignKey(User,related_name='comments', on_delete=models.CASCADE) #反向連結模型 #外鍵連結至模型本身,可以傳送空白資料,設置反向連結變數 #回復的評論,樹狀層級第二層 root =models.ForeignKey('self', null=True,related_name='root_comment', on_delete=models.CASCADE) #回復的對象(評論或回復),樹狀層級,若為0為則為第一層 parent = models.ForeignKey('self', null=True,related_name='parent_comment', on_delete=models.CASCADE) #回復的對象,外鍵模型至用戶模型 reply_to = models.ForeignKey(User, null=True,related_name='replies', on_delete=models.CASCADE) #返回字段 def __str__(self): return self.text class Meta: ordering = ['comment_time']
2.在傳遞資料方法添加層級篩選:
在blog應用<views.py>的blog_detail方法,將樹狀層級參數加入篩選:
comments = Comment.objects.filter(content_type=blog_content_type, object_id=blog.pk, parent=None)
3.在前端頁面顯示回復內容:
在<blog_detail.html>添加回復:
<div class="comment-area"> <h3 class="comment-area_title">評論列表</h3> <!-- 添加評論列表id --> <div id="comment_list"> {% for comment in comments %} <div class="comment"> {{ comment.user }} ({{ comment.comment_time|date:"Y-m-d H:i:s" }}): {{comment.text |safe }} {% for reply in comment.root_comment.all %} <div class="reply"> {{ reply.user.username }} {{ reply.comment_time|date:"Y-m-d H:i:s" }}: 回復 {{ reply.reply_to.username }} {{ reply.text }} </div> {% endfor %} </div> {% empty %} 暫無評論 {% endfor %} </div> </div>
4.調整評論與回復功能CSS樣式:
在<blog.css>調整評論與回復功能的css樣式
將評論區隔、將回復縮排:
div.comment { border-bottom: 1px dashed #ccc; margin-bottom: 0.5em; padding-bottom: 0.5em; } div.reply { margin-left:2em; }
5.回復與評論共用編輯器:
為評論設置編號,並把編號傳送至前端頁面
點下評論後的回復連結時,使畫面跳轉至文本編輯器
並在編輯器上方根據編號顯示要回應的評論對象內容
(1)設置評論編號
在form表單設置評論編號,在blog_detail方法傳遞編號至前端頁面
在 comment 應用的 <forms.py> 添加評論編號:
#獲取回復的評論編號 reply_comment_id = forms.IntegerField(widget=forms.HiddenInput(attrs={'id':'reply_comment_id'}))
在 blog 應用 <views.py> 的 blog_detail 方法將編輯器取得的評論編號加入初始化的步驟:
#使用initial在運行時聲明表單字段的初始值,將文章類型與編號賦值 data={} data['content_type']= blog_content_type.model data['object_id'] = blog_pk data['reply_comment_id'] = 0 #初始化回復對象為頂層(值為0) context['comment_form'] = CommentForm(initial=data)
(2)前端頁面操作
點下評論後的回復連結時,獲取評論編號,同時使畫面跳轉至文本編輯器,並將評論按鈕改成回復按鈕
並在編輯器上方根據編號顯示要回應的評論內容(設置為隱藏,獲取編號與資料後才顯示)
對blog應用的<blog_detail.html>的頁面的評論區域進行修改
註解後為修改的部分:
<div class="row"> <div class="col-xs-10 col-xs-offset-1"> <div class="comment-area"> <h3 class="comment-area_title">提交評論</h3> {% if user.is_authenticated %} <form id="comment_form" action="{% url 'update_comment' %}" method="POST" style="overflow:hidden"> {% csrf_token %} <label>{{ user.username }},歡迎評論</label> <!-- 點擊回復時才顯示回復的評論對象內容 --> <div id="reply_content_container" style="display:none"> <p>回復:</p> <div id="reply_content"></div> </div> <!-- 傳入編輯器表單 --> {% for field in comment_form %} {{ field }} {% endfor %} <span id="comment_error" class="text-danger pull-left"></span> <!-- 加入id,根據js改變值為評論或是回復 --> <input id="comment_btn" type="submit" value="評論" class="btn btn-primary pull-right"> </form> {% else %} 您尚未登錄,登錄之後方可評論 <a class="btn btn-primary" href="{% url 'login' %}?from={{ request.get_full_path }}">登錄</a> <a class="btn btn-primary" href="{% url 'register' %}?from={{ request.get_full_path }}">註冊</a> {% endif %} </div> <div class="comment-area"> <h3 class="comment-area_title">評論列表</h3> <div id="comment_list"> {% for comment in comments %} <div class="comment"> {{ comment.user }} ({{ comment.comment_time|date:"Y-m-d H:i:s" }}): <!-- 設置id,根據傳入的編號改變,並將最終的id的內容顯示到編輯器上方 --> <div id="comment_{{ comment.pk }}"> {{ comment.text |safe }} </div> <!-- 點擊回復的同時取得編號 --> <a href="javascript:reply({{ comment.pk }})">回復</a> {% for reply in comment.root_comment.all %} <div class="reply"> {{ reply.user.username }} {{ reply.comment_time|date:"Y-m-d H:i:s" }}: 回復 {{ reply.reply_to.username }} <!-- 設置id,根據傳入的編號改變,並將最終的id的內容顯示到編輯器上方 --> <div id="comment_{{ reply.pk }}"> {{ reply.text |safe }} </div> <!-- 點擊回復的同時取得編號 --> <a href="javascript:reply({{ reply.pk }})">回復</a> </div> {% endfor %} </div> {% empty %} 暫無評論 {% endfor %} </div> </div> </div> </div>
在<blog_detail.html>下方的js擴充區塊添加function
點擊回復連結的同時,將評論編號傳入function
將該評論編號傳至評論或回復內容標籤的id
獲取評論或回覆內容的id,將該標籤中的內容傳至文本編輯器上方的'reply_content_container'標籤
且將評論按鈕改回回復按鈕
而function下方則是點擊回復連結時,將畫面跳轉至文本編輯器的動畫,並自動進入編輯器:
//回復 function reply(reply_comment_id){ //設置值 $('#reply_comment_id').val(reply_comment_id); var html = $('#comment_' + reply_comment_id).html(); $('#reply_content').html(html); $('#reply_content_container').show(); $('#comment_btn').val('回復') //動畫跳轉至編輯器位置 $('html').animate({scrollTop: $('#comment_form').offset().top - 60}, 300, function(){ //跳轉後自動進入編輯器 CKEDITOR.instances['id_text'].focus(); }); }
5.回復功能實現:
已經設置好了點擊回復時的前端處理
接著則是要實現回復後的方法
- 驗證回復表單
- 傳遞與儲存表單數據
(1)驗證回復表單
判斷回復編號
小於0則代表出錯,因為編號不會小於0
等於0則為樹狀結構第一層,評論為回復該文章
不屬於上述兩者則代表已有評論資料,透過資料庫查找該資料
並將該資料回傳至parent(樹狀結構第三層):
from .models import Comment #驗證回復資料 def clean_reply_comment_id(self): #將編號放入變數 reply_comment_id = self.cleaned_data['reply_comment_id'] #編號不可小於0 if reply_comment_id < 0: raise forms.ValidationError('回復出錯') #編號等於0,表示回復的對象為文章,為樹狀結構第一層的評論 elif reply_comment_id == 0: self.cleaned_data['parent'] = None #如果編號不為0,則根據編號尋找該條評論 elif Comment.objects.filter(pk=reply_comment_id).exists(): #取得該評論編號 self.cleaned_data['parent'] = Comment.objects.get(pk=reply_comment_id) else: raise forms.ValidationError('回復出錯') return reply_comment_id
(2)傳遞與儲存表單數據
a.儲存數據至資料庫
取得parent樹狀層級(None則為第一層)
如果parent有資料,則先判斷底層(第三層)是否有資料,若有就回傳,無則取第二層資料回傳
接著獲取parent資料(第二層),最後獲取評論者的用戶名稱
b.傳遞數據至前台
data['reply_to']若parent有資料,就傳遞該評論用戶名稱,無則傳遞空白
data['pk']傳遞該評論編號
以下為代碼:
from django.shortcuts import render, redirect from .models import Comment from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.urls import reverse from .forms import CommentForm from django.http import JsonResponse import datetime #日期計算方法 # Create your views here. def update_comment(request): referer = request.META.get('HTTP_REFERER', reverse('update_comment')) comment_form = CommentForm(request.POST,user=request.user)#傳送user參數 data={} if comment_form.is_valid(): #如果檢查通過 comment = Comment() comment.user = comment_form.cleaned_data['user'] comment.text = comment_form.cleaned_data['text'] comment.content_object = comment_form.cleaned_data['content_object'] #獲取樹狀層級 parent = comment_form.cleaned_data['parent'] #如果有值(None則為第一層) if not parent is None: #取得樹狀第二層的評論內容,若無則取得樹狀層級第三層的回復內容 comment.root = parent.root if not parent.root is None else parent #取得樹狀層級第三層的回復內容 comment.parent = parent #取得評論的用戶名稱 comment.reply_to = parent.user comment.save() #返回數據 data['status'] = 'SUCCESS' data['username'] = comment.user.username data['comment_time'] = (comment.comment_time + datetime.timedelta(hours=8)).strftime("%Y-%m-%d %H:%M:%S") data['text'] = comment.text #如果有值(None則為第一層) if not parent is None: #取得評論的用戶名稱 data['reply_to'] = comment.reply_to.username #如果為第一層則沒有評論對象,傳回空白 else: data['reply_to'] = '' #回傳評論的編號 data['pk'] = comment.pk data['root_pk'] = comment.root.pk if not comment.root is None else '' else: data['status'] = 'ERROR' #返回表單錯誤訊息 data['message'] = list(comment_form.errors.values())[0] return JsonResponse(data)
6.ajax更新回復與細節處理:
(1)html部分
在comment、reply標籤加入id
將評論與回復所有的html代碼,加上span標籤方便插入
將暫無評論加入id,若新增評論就可以透過js移除
將評論按鈕加入id,若點擊回復評論的連結,透過js將值改為回復
以下為代碼:
<div class="row"> <div class="col-xs-10 col-xs-offset-1"> <div class="comment-area"> <h3 class="comment-area_title">提交評論</h3> {% if user.is_authenticated %} <!-- 添加提交評論id --> <form id="comment_form" action="{% url 'update_comment' %}" method="POST" style="overflow:hidden"> {% csrf_token %} <label>{{ user.username }},歡迎評論</label> <!-- 回復時才顯示回復的評論對象內容 --> <div id="reply_content_container" style="display:none"> <p>回復:</p> <div id="reply_content"></div> </div> <!-- 傳入編輯器表單 --> {% for field in comment_form %} {{ field }} {% endfor %} <!-- 添加錯誤訊息 --> <span id="comment_error" class="text-danger pull-left"></span> <input id="comment_btn" type="submit" value="評論" class="btn btn-primary pull-right"> </form> {% else %} 您尚未登錄,登錄之後方可評論 <a class="btn btn-primary" href="{% url 'login' %}?from={{ request.get_full_path }}">登錄</a> <a class="btn btn-primary" href="{% url 'register' %}?from={{ request.get_full_path }}">註冊</a> {% endif %} </div> <div class="comment-area"> <h3 class="comment-area_title">評論列表</h3> <!-- 添加評論列表id --> <div id="comment_list"> {% for comment in comments %} <div id="root_{{ comment.pk }}" class="comment"> <span>{{ comment.user }}</span> <span>({{ comment.comment_time|date:"Y-m-d H:i:s" }}):</span> <!-- 設置id,根據傳入的編號改變,並將最終的id的內容顯示到編輯器上方 --> <div id="comment_{{ comment.pk }}"> <span>{{ comment.text |safe }}</span> </div> <!-- 點擊回復的同時取得編號 --> <a href="javascript:reply({{ comment.pk }})">回復</a> {% for reply in comment.root_comment.all %} <div id="root_{{ reply.pk }}" class="reply"> <span>{{ reply.user.username }}</span> <span>({{ reply.comment_time|date:"Y-m-d H:i:s" }}):</span> <span>回復</span> <span>{{ reply.reply_to.username }}</span> <!-- 設置id,根據傳入的編號改變,並將最終的id的內容顯示到編輯器上方 --> <div id="comment_{{ reply.pk }}"> <span>{{ reply.text |safe }} </div> <!-- 點擊回復的同時取得編號 --> <a href="javascript:reply({{ reply.pk }})">回復</a> </div> {% endfor %} </div> {% empty %} <span id="no_comment">暫無評論</span> {% endfor %} </div> </div> </div> </div> </div>
(2)js部分
判斷樹狀結構的parent的值是否為0,來決定插入評論還是回復
插入後動畫跳轉至評論或回復區塊
初始化編輯器:
- 將編輯器上方顯示的評論移除
- 將編輯器對應的評論編號改為0(樹狀結構第一層)
- 若有"暫無評論",則在評論後移除
- 將編輯器按鈕的值改為評論(原先有可能被改為回復)
以下為代碼:
{% block script_extends %} <script type="text/javascript"> $("#comment_form").submit(function(){ //選擇(#為id)comment_form,submit為觸發提交事件,會回調一個function //判斷是否為空 $("#comment_error").text('') if(CKEDITOR.instances['id_text'].document.getBody().getText().trim()==''){ $('#comment_error').text('評論不能為空'); return false; } //更新數據到textarea CKEDITOR.instances['id_text'].updateElement(); //異步提交 $.ajax({ url: "{% url 'update_comment' %}", //提交資料的目的路徑 type: 'POST',//提交資料的類型 data: $(this).serialize(), //序列化表單值 cache: false, //提交網址不須緩存,因此關閉緩存 success: function(data){ console.log(data); if(data['status'] == 'SUCCESS'){ if($('#reply_comment_id').val()=='0'){ //插入評論 var comment_html = '<div id="root_'+ data['pk'] +'" class="comment"><span>' + data['username'] + '</span><span>(' + data['comment_time'] + '):</span><div id="comment_' + data['pk'] + '"><span>' + data['text'] + '</span></div><a href="javascript:reply( ' + data['pk'] + ')">回復</a></div>'; $("#comment_list").prepend(comment_html); //動畫跳轉至評論 $('html').animate({scrollTop: $("#comment_list").offset().top - 120}, 300); }else{ //插入回復 var reply_html = '<div id="root_'+ data['pk'] +'" class="reply"><span>'+ data['username'] +'</span><span>('+ data['comment_time'] +'):</span><span> 回復 </span><span>'+ data['reply_to'] +'</span><div id="comment_'+ data['pk'] +'"><span>'+ data['text'] +'</div><a href="javascript:reply('+ data['pk'] +')">回復</a></div>'; $('#root_'+ data['root_pk']).append(reply_html); //動畫跳轉至回復 $('html').animate({scrollTop: $("#root_" + data['pk']).offset().top - 300}, 300); } //清空編輯框內容 CKEDITOR.instances['id_text'].setData(''); $('#reply_content_container').hide(); $('#reply_comment_id').val('0'); $('#no_comment').remove(); $('#comment_error').text('評論成功'); //將回復按鈕改回評論 $('#comment_btn').val('評論'); }else{ //顯示錯誤訊息 $('#comment_error').text(data['message']); } }, error: function(xhr){ console.log(xhr); }, }); return false; }); //回復 function reply(reply_comment_id){ //設置值 $('#reply_comment_id').val(reply_comment_id); var html = $('#comment_' + reply_comment_id).html(); $('#reply_content').html(html); $('#reply_content_container').show(); $('#comment_btn').val('回復') //動畫跳轉至編輯器位置 $('html').animate({scrollTop: $('#comment_form').offset().top - 60}, 300, function(){ //跳轉後自動進入編輯器 CKEDITOR.instances['id_text'].focus(); }); } </script> {% endblock %}
(3)調整排序
評論列表倒序、回復列表正序
原先兩者皆為倒敘,因此要改動兩個地方
comment 應用<models.py>部分:
class Meta: ordering = ['comment_time']
blog應用 blog_detail方法 :
context['comments'] = comments.order_by('-comment_time')
留言列表