网站搭建 (第21天) 评论功能设计

一、前言

为什么一直拖着评论功能到现在才开始准备写,确实因为最近较忙,而且评论功能确实也不好写。之前,我上网查了很久,大概的方法总结起来有下面三个。

方法一:第三方社会化评论插件,如友言,多说,畅言,disqus。

方法二:Django评论库

方法三:自己写代码实现

先从第三方社会化评论插件开始,我没有做过多的涉及,而且插件众多,没有必要多花精力,使用专业的配置上就好,如果想使用评论插件,可以看这篇https://www.cnblogs.com/KevinSong/p/4695899.html

至于Django的评论库,可以按照官方文档配置一番即可,https://django-contrib-comments.readthedocs.io/en/latest/

下面开始说重点了,我采用的正好是第三种方法,这种逻辑性比较强,不过难度上确实不低。在这里必须要感谢杨仕航老师的精心讲解,教会了我使用子评论与父评论数据库自关联使用及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文件,将字段显示在后台。

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)

三、服务器端基础

在正式写服务器代码前,先看一下有关评论功能的服务器端基本架构。如果只是简单地在前端页面使用input框或者textarea标签,一般使用POST提交,在服务器端接收一下就行了,比较简单。

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表单来进行验证。

四、编写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默认已经配置好了。

五、前端页面评论框实现

由于在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>

六、服务器返回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)

七、实现页面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://jzfblog.com/detail/142,文章的更新编辑以此链接为准。欢迎关注源站文章!

猜你喜欢

转载自www.cnblogs.com/djcoder/p/10885521.html