关于本站
1、基于Django+Bootstrap开发
2、主要发表本人的技术原创博客
3、本站于 2015-12-01 开始建站
久等了,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指出我博文中的纰漏之处,我已经修正。欢迎给我建议 ^_^
相关专题: Django评论库开发