個人網站搭建 (第21天) 評論功能設計

1、前言

    爲何一直拖着評論功能到如今纔開始準備寫,確實由於最近較忙,並且評論功能確實也很差寫。以前,我上網查了好久,大概的方法總結起來有下面三個。javascript

    方法一:第三方社會化評論插件,如友言,多說,暢言,disqus。
    方法二:Django評論庫
    方法三:本身寫代碼實現html

    先從第三方社會化評論插件開始,我沒有作過多的涉及,並且插件衆多,沒有必要多花精力,使用專業的配置上就好,若是想使用評論插件,能夠看這篇http://www.javashuo.com/article/p-ppyjpnrj-hn.html前端

    至於Django的評論庫,能夠按照官方文檔配置一番便可,https://django-contrib-comments.readthedocs.io/en/latest/java

    下面開始說重點了,我採用的正好是第三種方法,這種邏輯性比較強,不過難度上確實不低。在這裏必需要感謝楊仕航老師的精心講解,教會了我使用子評論與父評論數據庫自關聯使用及Ajax加載評論。python

2、創建評論模型

    一個健全的評論模型,主要應該包括:評論內容、評論時間、評論人、評論對象,其中評論對象包括文章和評論,也就是說除了能夠對文章進行評論,還能夠對評論進行評論。涉及到對評論進行評論就要區分此時的評論是父評論仍是它下面的子評論,這就是數據庫自關聯的使用。ajax

    我將評論模型創建以下,須要解釋一下,root字段是某篇文章的源評論,parent字段指的是當前評論的父評論,reply_to則表示評論回覆的對象。數據庫

from django.db import models
# 跟蹤安裝在Django驅動項目中的全部模型,爲模型提供高級通用界面。
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
# auth模塊是Django提供的標準權限管理系統,能夠提供用戶身份認證, 用戶組和權限管理。
from django.contrib.auth.models import User
from django.db.models.fields import exceptions
from blog.models import Post


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(verbose_name=u'評論內容')
    # 時間自動建立now
    comment_time = models.DateTimeField(verbose_name=u'評論時間', auto_now_add=True)
    # 建立做者,刪除則級聯刪除
    user = models.ForeignKey(User, verbose_name=u'評論人', related_name='comments',  on_delete=models.CASCADE)

    # 自關聯
    root = models.ForeignKey('self', related_name='root_comment', null=True, on_delete=models.CASCADE)
    parent = models.ForeignKey('self', related_name='parent_comment', null=True, on_delete=models.CASCADE)
    reply_to = models.ForeignKey(User, related_name="replies", null=True, on_delete=models.CASCADE)

    def get_comment(self):
        # 此處的一個異常處理,用來捕獲沒有計數對象的狀況
        # 例如在admin後臺中,沒有計數值會顯示爲‘-’
        try:
            post = Post.objects.get(id=self.object_id)
            return post.title
        # 對象不存在就返回0
        except exceptions.ObjectDoesNotExist:
            return 0

    get_comment.short_description = '文章'

    def __str__(self):
        return self.text

    class Meta:
        ordering = ['comment_time']
        verbose_name = '評論'
        verbose_name_plural = '評論'

    創建好評論模型後,別忘記將Comment應用註冊到INSTALLED_APPS中,還需新建adminx.py文件,將字段顯示在後臺。django

from django.contrib import admin  # admin後臺管理
from .models import Comment   # 從當前應用的模型中導入Comment數據表
import xadmin

class CommentAdmin(object):
    # 後臺顯示文章對象,評論內容,評論時間,評論者
    list_display = ('id', 'get_comment', 'text', 'comment_time', 'user')


xadmin.site.register(Comment, CommentAdmin)

3、服務器端基礎

    在正式寫服務器代碼前,先看一下有關評論功能的服務器端基本架構。若是隻是簡單地在前端頁面使用input框或者textarea標籤,通常使用POST提交,在服務器端接收一下就好了,比較簡單。json

from django.shortcuts import render, redirect
from django.contrib.contenttypes.models import ContentType
from .models import Comment
from django.urls import reverse
from .forms import CommentForm
from django.http import JsonResponse


def update_comment(request):
    """提交評論處理功能"""
    # META獲取源地址,若是獲取不到源地址,就默認轉到主頁顯示
    referer = request.META.get('HTTP_REFERER', reverse('blog:home'))
    # user = request.user
    # 數據檢查,前端頁面的驗證不必定保險,故需在服務器端進行雙重保險
    # 驗證用戶是否處在登陸狀態
    if not request.user.is_authenticated:
        context = {'message': '用戶未登陸',  'redirect_to': referer}
        return render(request, 'blog/error.html', context)
    # 從前端textarea標籤獲取用戶評論內容,strip是python中的字符串處理方法,默認移除字符串頭尾的空格或換行符
    text = request.POST.get('text', '').strip()
    # 驗證用戶輸入是否爲空
    if text == '':
        context = {'message': '評論內容爲空',  'redirect_to': referer}
        return render(request, 'blog/error.html', context)

    try:
        content_type = request.POST.get('content_type', '')
        # 由於get到的是字符串,須要先轉化爲int
        object_id = int(request.POST.get('object_id', ''))

        # 找到post對象
        models_class = ContentType.objects.get(model=content_type).model_class()
        models_obj = models_class.objects.get(pk=object_id)

    except Exception as e:
        context = {'message': '評論對象不存在', 'redirect_to': referer}
        return render(request, 'blog/error.html', context)

    # 檢查經過,保存數據
    comment = Comment()
    comment.user = request.user
    comment.text = text

    # Post.objects.get(pk=object_id)
    comment.content_object = models_obj
    comment.save()

    return redirect(referer)

    缺點就是,保存到數據庫中評論沒有樣式,內容不夠豐富。因此在後來使用了同開源中國同樣的編輯器—ckeditor,能夠用來編輯樣式。在這以前,爲方便校驗提交數據的合法性,還能夠創建一個form表單來進行驗證。api

4、編寫form表單

    在這裏,我採用了ckeditor編輯框,關於ckeditor的後臺編輯使用,能夠查看以前寫的,對於評論框來講,還須要在form表單中加入widget屬性。注意:一樣須要完成相應的配置,這裏就默認你們已經清楚了,下面是CommentForm的評論框表單。

from django import forms
from django.contrib.contenttypes.models import ContentType
from django.db.models import ObjectDoesNotExist
from ckeditor.widgets import CKEditorWidget
from .models import Comment


class CommentForm(forms.Form):
    """
    提交評論表單
    """
    content_type = forms.CharField(widget=forms.HiddenInput)
    object_id = forms.IntegerField(widget=forms.HiddenInput)
    text = forms.CharField(widget=CKEditorWidget(config_name='comment_ckeditor'),
                            error_messages={'required': '您還沒有寫任何評論內容'})

    reply_comment_id = forms.IntegerField(widget=forms.HiddenInput(attrs={'id': 'reply_comment_id'}))

    def __init__(self, *args,  **kwargs):
        if 'user' in kwargs:
            self.user = kwargs.pop('user')
        super(CommentForm, self).__init__(*args, **kwargs)

    def clean(self):
        # 驗證用戶是否處在登陸狀態
        if self.user.is_authenticated:
            self.cleaned_data['user'] = self.user
        else:
            raise forms.ValidationError('您還沒有登陸,請先登陸才能評論')

        # 評論對象驗證
        content_type = self.cleaned_data['content_type']
        object_id = self.cleaned_data['object_id']
        # 找到post對象
        try:
            models_class = ContentType.objects.get(model=content_type).model_class()
            models_obj = models_class.objects.get(pk=object_id)
            self.cleaned_data['content_object'] = models_obj
        except ObjectDoesNotExist:
            raise forms.ValidationError('評論對象不存在')
        return self.cleaned_data

    def clean_reply_comment_id(self):
        reply_comment_id = self.cleaned_data['reply_comment_id']
        if reply_comment_id < 0:
            raise forms.ValidationError('回覆出錯')
        elif reply_comment_id == 0:
            self.cleaned_data['parent'] = None
        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

    其中的表單驗證代碼其實就是以前基礎代碼,提取到CommentForm中clean方法中就會自動將提交過來的數據驗證一遍,驗證成功後能夠從cleaned_data方法中獲取驗證的屬性值。text屬性中的config_name參數就是編輯框的圖標功能,我將其在settings默認已經配置好了。

5、前端頁面評論框實現

    因爲在CommentForm中定義了hidden的input框,因此在建立form表單時,還須要對其中的content_type,object_id進行初始化。這一實現能夠採用模板標籤

from django import template
from ..models import Comment
from django.contrib.contenttypes.models import ContentType
from ..forms import CommentForm

@register.simple_tag
def get_comment_form(obj):
    content_type = ContentType.objects.get_for_model(obj)
    form = CommentForm(initial={
        'content_type': content_type.model,
        'object_id': obj.pk, 'reply_comment_id': 0})
    return form

    在加入了模板標籤後,前端的form表單代碼以下:

<form id="comment_form" action="{% url 'comment:update_comment' %}" onsubmit="return false;" method="POST" style="overflow: hidden">{% csrf_token %}
    {% if user.is_authenticated %}
        <label for="comment-text">{{ user.get_nickname_or_username }},歡迎評論</label>
    {% endif %}
    <div id="reply_content_container" style="display: none">
        <p id="reply_title">回覆:</p>
        <div id="reply_content"></div>
    </div>
    {% get_comment_form post as comment_form %}
    {%  for field in comment_form %}
        {{ field }}
    {% endfor %}
        <input class="btn btn-primary pull-right" type="submit" value="評論">
        <span id="comment_error" class="text-danger pull-right"></span>
</form>

6、服務器返回Json數據

    獲取到前端提交上來的數據之後,自動通過Django後臺的驗證。若是驗證經過,就將評論用戶、評論內容、評論對象以及評論時間保存下來,成功後將數據封裝成Json格式,返回出去。爲了在提交評論時,實現頁面的自動刷新,最好是採用Ajax加載。

from django.shortcuts import render, redirect
from django.contrib.contenttypes.models import ContentType
from .models import Comment
from django.urls import reverse
from .forms import CommentForm
from django.http import JsonResponse


def update_comment(request):
    """提交評論處理功能"""
    # META獲取源地址,若是獲取不到源地址,就默認轉到主頁顯示
    # referer = request.META.get('HTTP_REFERER', reverse('blog:home'))
    # 數據檢查, 前端頁面的驗證不必定保險, 故需在服務器端進行雙重保險
    # 驗證用戶是否處在登陸狀態
    comment_form = CommentForm(request.POST, user=request.user)

    if comment_form.is_valid():
        # 檢查經過,保存數據
        comment = Comment()
        comment.user = comment_form.cleaned_data['user']
        comment.text = comment_form.cleaned_data['text']

        # Post.objects.get(pk=object_id)
        comment.content_object = comment_form.cleaned_data['content_object']
        parent = comment_form.cleaned_data['parent']
        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',
                'username': comment.user.get_nickname_or_username(),
                # strftime並不能正確得出當前時間,會把時區給掩蓋掉,因此使用timestamp時間戳
                # 'comment_time': comment.comment_time.strftime('%Y-%m-%d %H:%M:%S'),
                'comment_time': comment.comment_time.timestamp(),
                'text': comment.text,
                'content_type': ContentType.objects.get_for_model(comment).model # 獲得對應字符串
                }
        if not parent is None:
            data['reply_to'] = comment.reply_to.get_nickname_or_username()
        else:
            data['reply_to'] = ''
        data['pk'] = comment.pk
        data['rook_pk'] = comment.root.pk if not comment.root is None else ''

    else:
        # context = {'message': comment_form.errors, 'redirect_to': referer}
        # return render(request, 'blog/error.html', context)
        data = {'status': 'ERROR', 'message': list(comment_form.errors.values())[0][0]}
    return JsonResponse(data)

7、實現頁面Ajax加載

    1.前期準備

        引用ckeditor的js文件

// <script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
// cdn提供的js,可加速
<script src="https://cdnjs.cloudflare.com/ajax/libs/ckeditor/4.9.2/ckeditor.js"></script>

        編寫一些基礎函數,方便表單submit之後進行的一系列操做。

String.prototype.format = function(){
    {#由於javascript中沒有佔位或其餘能夠方法能夠補充,因此須要本身建立一個函數將其實現相似於佔位的方式#}
    var str = this;
    for (var i=0;i<arguments.length; i++){
        {#正則全替換#}
        var str = str.replace(new RegExp('\\{'+ i +'\\}', 'g'), arguments[i])
    };
    return str;
};

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();
    $('html').animate({scrollTop: $('#comment_form').offset().top - 60}, 300, function(){
        CKEDITOR.instances['id_text'].focus();
    });
}

function numFormat(num){
    return ('00' + num).substr(-2);
}

function timeFormat(timestamp){
    {#由於js是的時間戳是以毫秒爲單位的,而python是以秒爲單位,因此要乘以1000#}
    var datetime = new Date(timestamp * 1000);
    var year = datetime.getFullYear();
    var month = numFormat(datetime.getMonth() + 1);
    var day = numFormat(datetime.getDate());
    var hour = numFormat(datetime.getHours());
    var minute = numFormat(datetime.getMinutes());
    var second = numFormat(datetime.getSeconds());
    return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
}

    2.評論表單submit編寫

    評論表單提交之後,將會使用ajax請求後臺數據,拿到後臺返回的json數據之後,經過自定義的format、reply、numFormat、timeFormat等函數以及Ckeditor編輯器的API操做完成ajax提交。有管ckeditor的API可查看:http://docs-old.ckeditor.com/ckeditor_api/

$("#comment_form").submit(function(){
    // 判斷是否爲空

    if(CKEDITOR.instances["id_text"].document.getBody().getText().trim()==''){
        $("#comment_error").text('您還沒有寫任何評論內容');

        return false;
    }

    // 更新數據到textarea
    CKEDITOR.instances['id_text'].updateElement();
    // 異步提交
    $.ajax({
        url: "{% url 'comment: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_{0}" class="comment">' +
                        '<span>{1}</span>' +
                        '<span>({2}):</span>' +
                        '<div id="comment_{0}">{3}</div>' +
                        '<div class="like" onclick="likeChange(this, \'{4}\', {0})">' +
                            '<span class="glyphicon glyphicon-thumbs-up"></span> ' +
                            '<span class="liked-num">0</span>' +
                        '</div>' +
                        '<a href="javascript:reply({0});">回覆</a>' +
                        '</div>';
                    comment_html = comment_html.format(data['pk'], data['username'], timeFormat(data['comment_time']), data['text'], data['content_type']);
                    $("#comment_list").prepend(comment_html);
                }else{
                    // 插入回覆
                    var reply_html = '<div class="reply">' +
                                '<span>{1}</span>' +
                                '<span>({2})</span>' +
                                '<span>回覆</span>' +
                                '<span>{3}:</span>' +
                                '<div id="comment_{0}">{4}</div>' +
                                '<div class="like" onclick="likeChange(this, \'{5}\', {0})">' +
                                    '<span class="glyphicon glyphicon-thumbs-up\"></span> ' +
                                    '<span class="liked-num">0</span>' +
                                '</div>' +
                                '<a href="javascript:reply({0});">回覆</a>' +
                            '</div>';
                    reply_html = reply_html.format(data['pk'], data['username'], timeFormat(data['comment_time']), data['reply_to'], data['text'], data['content_type']);
                    $("#root_" + data['root_pk']).append(reply_html);
                }

                // 清空編輯框的內容
                CKEDITOR.instances['id_text'].setData('');
                $('#reply_content_container').hide();
                $('#reply_comment_id').val('0');
                $('#no_comment').remove();
                window.location.reload();
                $("#comment_error").text('評論成功');
            }else{
                // 顯示錯誤信息
                $("#comment_error").text(data['message']);
            }
        },
        error: function(xhr){
            console.log(xhr);
        }
    });
    return false;
});

原文出處:https://www.jzfblog.com/detail/142,文章的更新編輯以此連接爲準。歡迎關注源站文章!

相關文章
相關標籤/搜索