我的网站搭建(第17天) 评论库添加回复功能

  • 发布时间:2016年5月12日 13:50
  • 作者:杨仕航
* 该文是基于Python2.7开发的,最新Python3.x和Django2.x视频教程可以前往 >> Django2.0视频教程

久等了,Django-Comments评论库的回复功能写出来了。这个功能对comments库改动还是有点大的。

首先先找到该评论库的位置。这个一般在python安装目录下的Lib/site-packages文件夹里面。我用的是WebFaction服务器,comments评论库的具体路径是"~/lib/python2.7/django_contrib_comments-1.6.2-py2.7.egg/django_comments/"


目录结构如下,为了更好讲解我是如何修改这个comments评论库,稍微说一下相关文档的作用:


1、目录templatetags是comments库的自定义标签处理的文件夹

2、文件templatetags/comments.py 包含自定义标签的处理方法

3、目录views是comments库的相关响应方法

4、文件views/comments.py 包括评论库的主要处理方法

5、文件models.py 评论库的模型文件

6、文件admin.py 评论库的admin界面显示内容

这个回复功能具体以怎样的形式展现出来,这个我想了好多天。既要友好,又要处理方便,不容易。大概最终界面如下:

1、评论和回复区分来,评论深色显示,回复浅色显示并所有相关回复放置在对应的评论下方;

2、鼠标移动到评论或者回复上面就会显示一个“回复”的链接,点击则在下方出现回复框。


要实现这些功能,需要改动很多东西。我总结一下,免得讲解的时候,出现混乱现象:

1、修改comments库的模型结构,为了区分哪些是评论,哪些是回复,以及回复对应哪个评论

2、修改了模型结构,就需要修改admin界面的显示

3、修改评论和回复提交时对应的处理方法,确保很够正确写入数据库中

4、修改comments库的自定义标签,让回复内容能够在前端页面显示出来

5、前端页面添加提交回复的方法和动作处理等


大概是这5点,内容有点多。接下来一一给大家讲解comments库是如何实现回复功能的。

1、修改comments的模型

打开comments库的models.py文件,在CommentAbstractModel类里面加入3个字段

特别说明!我用的comments库版本是1.6.2。后面的出了新版本,CommentAbstractModel类不在models.py文件中。不过可以从models.py的import语句找到来自哪个文件。

新版本comments库的CommentAbstractModel类放在models.py同个目录下的abstracts.py文件中。

@python_2_unicode_compatible
class CommentAbstractModel(BaseCommentAbstractModel):
    """
    A user comment about some object.
    """

    # Who posted this comment? If ``user`` is set then it was an authenticated
    # user; otherwise at least user_name should have been set and the comment
    # was posted by a non-authenticated user.
    user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'),
                             blank=True, null=True, related_name="%(class)s_comments")
    user_name = models.CharField(_("user's name"), max_length=50, blank=True)
    # Explicit `max_length` to apply both to Django 1.7 and 1.8+.
    user_email = models.EmailField(_("user's email address"), max_length=254,
                                   blank=True)
    user_url = models.URLField(_("user's URL"), blank=True)

    comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH)

    # Metadata about the comment
    submit_date = models.DateTimeField(_('date/time submitted'), default=None, db_index=True)
    ip_address = models.GenericIPAddressField(_('IP address'), unpack_ipv4=True, blank=True, null=True)
    is_public = models.BooleanField(_('is public'), default=True,
                                    help_text=_('Uncheck this box to make the comment effectively '
                                                'disappear from the site.'))
    is_removed = models.BooleanField(_('is removed'), default=False,
                                     help_text=_('Check this box if the comment is inappropriate. '
                                                 'A "This comment has been removed" message will '
                                                 'be displayed instead.'))
    #reply fields
    root_id = models.IntegerField(default=0)
    reply_to = models.IntegerField(default=0)
    reply_name = models.CharField(max_length=50, blank=True)

    # Manager
    objects = CommentManager()

这个类后面内容比较多,我就拿关键的部分。这3个字段分别是root_id, reply_to, reply_name。

root_id:这个字段是标记回复的一开始是哪个评论。因为回复也可能回复其他人的回复,这些回复都会放在一个评论下面,所以就加这个字段标记一下。

reply_to: 这个字段就是直接标记回复哪个评论或者回复了。

reply_name: 回复哪个评论或者回复的帐号名。虽然这个账号名和通过comment对象获取找到对应的名称,但这个需要写比较多的代码,还是直接写到数据库中,方便获取。

修改了模型结构,记得更新一下数据库!


2、修改admin显示

打开comments库的admin.py文件,找到CommentsAdmin类,修改list_display,加入刚刚3个字段即可。

list_display = ('id', 'reply_to', 'name', 'content_type', 'object_pk', 'ip_address', 'submit_date', 'is_public', 'is_removed')


3、修改评论和回复提交的处理方法

这个改动就比较多了,因为我还把是否登录用户和用户是否注册的判断加入进来。然后处理结果用json数据结构返回。

打开comments库的views/comments.py文件,主要修改post_comment方法。我把这个文件添加和修改的东西写出来。

#coding:utf-8
#添加这两个引用,为了返回json数据
from django.http import HttpResponse
import json

#添加返回json的方法,json结构有3个参数(code:返回码,is_success:是否处理成功,message:消息内容)
def ResponseJson(code, is_success, message):
    data = {'code':code, 'success':is_success, 'message':message} 
    return HttpResponse(json.dumps(data), content_type="application/json")

@csrf_protect
@require_POST
def post_comment(request, next=None, using=None):
    """
    Post a comment.

    HTTP POST is required. If ``POST['submit'] == "preview"`` or if there are
    errors a preview template, ``comments/preview.html``, will be rendered.
    """
    # Fill out some initial data fields from an authenticated user, if present
    data = request.POST.copy()
    if request.user.is_authenticated():
        if not data.get('name', ''):
            data["name"] = request.user.get_full_name() or request.user.get_username()
        if not data.get('email', ''):
            data["email"] = request.user.email
    #change 2016-04-11
    else:
        return ResponseJson(501, False, 'No Login')

    #change 2016-05-11
    if not request.user.is_active:
        response_data = {'code':502, 'success':False, 'message':'No Active'} 
        return ResponseJson(502, False, 'No Active')

    # Look up the object we're trying to comment about
    ctype = data.get("content_type")
    object_pk = data.get("object_pk")
    if ctype is None or object_pk is None:
        #return CommentPostBadRequest("Missing content_type or object_pk field.")
        return ResponseJson(503, False, 'Missing content_type or object_pk field.')
    try:
        model = apps.get_model(*ctype.split(".", 1))
        target = model._default_manager.using(using).get(pk=object_pk)
    except TypeError:
        #return CommentPostBadRequest(
        #    "Invalid content_type value: %r" % escape(ctype))
        return ResponseJson(504, False, "Invalid content_type value: %r" % escape(ctype))
    except AttributeError:
        #return CommentPostBadRequest(
        #    "The given content-type %r does not resolve to a valid model." % escape(ctype))
        return ResponseJson(505, False, 
            "The given content-type %r does not resolve to a valid model." % escape(ctype))
    except ObjectDoesNotExist:
        #return CommentPostBadRequest(
        #    "No object matching content-type %r and object PK %r exists." % (
        #        escape(ctype), escape(object_pk)))
        return ResponseJson(506, False, 
            "No object matching content-type %r and object PK %r exists." % (escape(ctype), 
                escape(object_pk)))
    except (ValueError, ValidationError) as e:
        #return CommentPostBadRequest(
        #    "Attempting go get content-type %r and object PK %r exists raised %s" % (
        #        escape(ctype), escape(object_pk), e.__class__.__name__))
        return ResponseJson(507, False, 
            "Attempting go get content-type %r and object PK %r exists raised %s" % (escape(ctype), 
                escape(object_pk), e.__class__.__name__))

    # Do we want to preview the comment?
    preview = "preview" in data

    # Construct the comment form
    form = django_comments.get_form()(target, data=data)

    # Check security information
    if form.security_errors():
        #return CommentPostBadRequest(
        #    "The comment form failed security verification: %s" % escape(str(form.security_errors())))
        return ResponseJson(508, False, "The comment form failed security verification: %s" % escape(str(form.security_errors())))

    # If there are errors or if we requested a preview show the comment
    if form.errors or preview:
        template_list = [
            # These first two exist for purely historical reasons.
            # Django v1.0 and v1.1 allowed the underscore format for
            # preview templates, so we have to preserve that format.
            "comments/%s_%s_preview.html" % (model._meta.app_label, model._meta.model_name),
            "comments/%s_preview.html" % model._meta.app_label,
            # Now the usual directory based template hierarchy.
            "comments/%s/%s/preview.html" % (model._meta.app_label, model._meta.model_name),
            "comments/%s/preview.html" % model._meta.app_label,
            "comments/preview.html",
        ]
        #return render(request, template_list, {
        #        "comment": form.data.get("comment", ""),
        #        "form": form,
        #        "next": data.get("next", next),
        #    },
        #)
        return ResponseJson(509, False, form.data.get("comment", ""))

    # Otherwise create the comment
    comment = form.get_comment_object()
    comment.ip_address = request.META.get("REMOTE_ADDR", None)
    
    #change 2016-05-12
    comment.root_id = data.get('root_id',0)
    comment.reply_to = data.get('reply_to',0)
    comment.reply_name = data.get('reply_name','')

    if request.user.is_authenticated():
        comment.user = request.user

    # Signal that the comment is about to be saved
    responses = signals.comment_will_be_posted.send(
        sender=comment.__class__,
        comment=comment,
        request=request
    )

    for (receiver, response) in responses:
        if response is False:
            #return CommentPostBadRequest(
            #    "comment_will_be_posted receiver %r killed the comment" % receiver.__name__)
            return ResponseJson(510, False, 
                "comment_will_be_posted receiver %r killed the comment" % receiver.__name__)

    # Save the comment and signal that it was saved
    comment.save()
    signals.comment_was_posted.send(
        sender=comment.__class__,
        comment=comment,
        request=request
    )

    #change 2016-05-12
    #return next_redirect(request, fallback=next or 'comments-comment-done',
    #                     c=comment._get_pk_val())
    return ResponseJson(200, True, 'comment success')

其中加入了登录检查判断和激活判断,修改原本的出错信息用json返回。基本修改的部分用#change注释标记,原有的代码也注释起来。我用的django版本是1.6,你可能是新版本,有所不同。

也许代码太多了,也不要忽略 comment.root_id = data.get('root_id',0),这个是把我们前面加的字段写入数据。


4、修改自定义标签,获取内容

仔细研究了一下,前端页面获取评论内容是用自定义标签获取,简单示例如下:

{% get_comment_list for blog as comments %}
{% for comment in comments %}
    {#评论内容#}
    {{comment.submit_date|date:"Y-m-d H:i"}} @ {{comment.user_name}} 评论
    {{ comment.comment }}
{% empty %}
    暂无评论
{% endfor %}

用get_comment_list标签获取,再循环处理。

打开comments库的templatetags/comments.py文件。找到BaseCommentNode类的get_queryset方法。该方法就是get_comment_list自定义标签获取所有评论的方法。

我把这个方法修改如下,同样用#change标记我修改的地方:

def get_queryset(self, context):
    ctype, object_pk = self.get_target_ctype_pk(context)
    if not object_pk:
        return self.comment_model.objects.none()

    qs = self.comment_model.objects.filter(
        content_type=ctype,
        object_pk=smart_text(object_pk),
        site__pk=settings.SITE_ID,
        root_id=0, #change
    )

    # The is_public and is_removed fields are implementation details of the
    # built-in comment model's spam filtering system, so they might not
    # be present on a custom comment model subclass. If they exist, we
    # should filter on them.
    field_names = [f.name for f in self.comment_model._meta.fields]
    if 'is_public' in field_names:
        qs = qs.filter(is_public=True)
    if getattr(settings, 'COMMENTS_HIDE_REMOVED', True) and 'is_removed' in field_names:
        qs = qs.filter(is_removed=False)
    if 'user' in field_names:
        qs = qs.select_related('user')

    #change : get sub comments
    for q in qs:
        q.replies=self.comment_model.objects.filter(
            content_type=ctype,
            object_pk=smart_text(object_pk),
            site__pk=settings.SITE_ID,
            root_id=q.id,
            is_public=True,
            is_removed=False,
        ).order_by('submit_date')
    return qs

其中,self.comment_model是评论模型的对象,通过这个对象获取内容。第1个修改的地方是给第1次获取评论的地方加上条件 root_id=0,这样获取到的记录都是评论。

第2个修改的地方在该方法末尾。循环遍历所有相关评论,获取对应的回复。这里利用python的语音特性,动态加了replies属性,该属性用于获取该评论的所有回复。


修改完成之后,就可以修改前端页面了。

{% get_comment_list for blog as comments %}
{% for comment in comments %}
    <div class="blog_comment" name="F{{comment.id}}">
        <p class="comment_title">
            {{comment.submit_date|date:"Y-m-d H:i"}} @ {{comment.user_name}} 评论
        </p>
        <p class="comment_content"
           root='{{comment.id}}'
           role='{{comment.id}}'
           base='{{comment.user_name}}'>
           {{ comment.comment }}
        </p>

        <ul class="comment_reply">
            {% for reply in comment.replies %}
                <li root='{{reply.root_id}}'
                    role='{{reply.id}}'
                    base='{{reply.user_name}}'>
                    {{reply.user_name}} 回复 {{reply.reply_name}} ({{ reply.submit_date|date:"Y-m-d H:i"}}):{{ reply.comment }}
                </li>
            {% endfor %}
        </ul>
    </div>
    
{% empty %}
    暂无评论
{% endfor %}

遍历两次,第1次获取评论,第2次获取对应的回复。


5、前端页面加上回复功能

后台的代码都写好了,现在只需要再修改前端页面的显示和处理即可。

首先,修改显示的样式,这个我只是简单区分评论和回复而已。

/*评论*/
.blog_comment{
    border-bottom: 1px solid #ddd;
}
.blog_comment .comment_title{
    margin-top: 0.5em;
}
.blog_comment .comment_content{
    padding: 0.5em;
    border-radius: 6px;
    background-color: #efefef;
}
/*回复*/
.blog_comment .comment_reply{
    text-indent: 0.5em;
}


回复需要一个地方写内容,所以需要加一个回复框:

<div id="reply_form" style="display:none;text-indent:0;">
    <!--这里需要get_comment_form 这句话,若前面评论部分你有定义就结合一下修改-->
    {% get_comment_form for blog as blog_form %} 
    <form action="#" id="reply_update_form">
        {% csrf_token %}
        {{ blog_form.object_pk }}
        {{ blog_form.content_type }}
        {{ blog_form.timestamp }}
        {{ blog_form.site }}
        {{ blog_form.submit_date }}
        {{ blog_form.security_hash }}
        <input type="hidden" name="next" value="{%url 'detailblog' blog.id%}"/>
        <input id="reply_to" type="hidden" name="reply_to" value="0" />
        <input id="root_id" type="hidden" name="root_id" value="0" />
        <input id="reply_name" type="hidden" name="reply_name" value="">

        <div class="row">
            <div class="col-md-12">
                <textarea class="input-xlarge comment_text" id="id_comment_reply" name="comment" placeholder="请输入回复内容"></textarea>

                <!--如果你在该字段中输入任何内容,你的评论就会被视为垃圾评论-->
                <input type="text" style="display:none;" id="id_honeypot_reply" name="honeypot">
            </div>
        </div>

        <div class="row">
              <div class="form-actions comment_button">
                <input class="btn btn-info" id="submit_reply" type="submit" name="submit" value="回复"/>
                <input class="btn" id="reset_btn" type="reset" name="submit" value="清空"/>
              </div>
        </div>
    </form>
</div>


最后,再写js代码(引用了jQuery库)。用ajax提交回复内容。

$(document).ready(function() {
//绑定回复提交事件
$('#reply_update_form').submit(function() {
    if ($("#id_honeypot_reply").val().length!=0) {alert("Stop!垃圾评论");return false;};
    if ($("#id_comment_reply").val().length==0) {alert("Error:请输入您的回复内容");$("#id_comment").focus();return false;};

    $("#id_timestamp").val(event.timeStamp);

    $.ajax({
        type: "POST",
        data: $('#reply_update_form').serialize(),
        url: "{% comment_form_target %}",
        cache: false,
        dataType: "json",
        success: function(json, textStatus) {
            if(json['success']){
                window.location.reload();
            }else{
                if(json['code']==501){
                    alert('您尚未登录,请先登录才能评论。');
                }else if(json['code']==502){
                    alert('您尚未激活,请先激活您的账户才能评论。');
                }else{
                    alert('评论出错,请刷新重试\n'+json['message']);
                }
            }
        },
        error: function (XMLHttpRequest, textStatus, errorThrown) {
            alert("评论出错\n请检查是否登录了或者刷新试试\n" + errorThrown);
        }
    });
    return false;
});

//绑定回复按钮的鼠标经过事件
$(".comment_content,.comment_reply li").each(function(){
    $(this).hover(function(){
        $(this).append("<span class='reply_button'> <a href='javascript:void(0);' onclick='reply_click(this);'>回复</a></span>");
    },function(){
        $(this).children(".reply_button").remove();
    });
});
});

//回复按钮点击触发的方法
function reply_click(obj){
    //获取回复按钮对应的评论或回复(DOM转成jQuery对象)
    var comment=obj.parentElement.parentElement;
    var $c=$(comment);
    //获取相关信息
    var root=$c.attr("root");
    var role=$c.attr("role");
    var base=$c.attr("base");

    //显示回复面板
    $("#reply_form").hide();
    $c.after($("#reply_form"));
    $("#reply_form").slideDown(200);

    //设置回复表单相关值
    $("#reply_to").val(role);
    $("#root_id").val(root);
    $("#reply_name").val(base);
    return false;
}


之前,写的评论ajax提交也需要修改一下,因为现在返回的内容是json数据格式。参照回复处理返回结果即可,一样的代码。这样就为Django的comments评论库添加回复功能了。


Django开发网站的博文越来越多了,我有个录制视频的想法。等我后面有时间了,再尝试一下 ^_^。

多谢Jackson指出我博文中的纰漏之处,我已经修正。欢迎给我建议 ^_^

上一篇:我的网站搭建(第18天) 评论或回复发送邮件通知

下一篇:Excel筛选公式详解

相关专题: Django评论库开发   

评论列表

智慧如你,不想发表一下意见吗?

新的评论

清空