本渣渣不专注技术,只专注使用技术,不是一个资深的coder,是一个不折不扣的copier |
1、前言
博客中的评论系统其实是个很复杂的东西,但是网上已经有现成的轮子了,比如django-contrib-comments,可以直接拿过来用。咱们的博客主页是抓取别人的,目的是使用 Django 还原实现,达到快速借助 Wordpress 主题,使用 Djano 框架建站。这样你就可以找一个自己喜欢的前端模板,快速搭建自己的小天地了。那么这里就不便使用轮子,这里会根据 崔庆才 博客主页评论功能自己设计后台逻辑。
2、创建评论应用
创建 comment 评论应用,放入 apps 应用管理文件中,不要忘记在 settings.py 中注册新添加的 app 应用
下边编写老四样:models、view、urls.py、templatetags
3、添加评论模型
这个评论其实是一个很复杂的功能,但是考虑到博客主要是展示自己学习、生活、成长的地方,并不是论坛,所以就没有考虑很多东西,评论搞的就很简陋
首先根据页面分析一下需要什么表
文章评论、给我留言、关于自己页面都有评论功能,而且崔庆才个人博客的评论功能,还支持游民评论
需要一个游民表(CommentUser)
需要一个评论信息表 (Comment)
有三个页面有评论功能看,在这里我打算把三个评论区分别建表存储
文章评论(ArticleComment)、关于自己(AboutComment)、给我留言(MessageComment)
blog -> comment -> models.py
from django.db import models
from django.conf import settings
from storm.models import Article
import markdown
import emoji
# 游民评论者信息表
class CommentUser(models.Model):
nickname = models.CharField(max_length=20, verbose_name='昵称')
email = models.CharField(max_length=30, verbose_name='邮箱')
address = models.CharField(max_length=200, verbose_name='地址')
# 评论信息表
class Comment(models.Model):
author = models.ForeignKey(CommentUser, related_name='%(class)s_related', verbose_name='评论人')
create_date = models.DateTimeField('创建时间', auto_now_add=True)
content = models.TextField('评论内容')
parent = models.ForeignKey('self', verbose_name='父评论', related_name='%(class)s_child_comments', blank=True, null=True)
rep_to = models.ForeignKey('self', verbose_name='回复', related_name='%(class)s_rep_comments', blank=True, null=True)
class Meta:
"""这是一个元类,用来继承的"""
abstract = True
def __str__(self):
return self.content[:20]
def content_to_markdown(self):
# 先转换成emoji然后转换成markdown,'escape':所有原始HTML将被转义并包含在文档中
to_emoji_content = emoji.emojize(self.content, use_aliases=True)
to_md = markdown.markdown(to_emoji_content,
safe_mode='escape',
extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
])
return to_md
# 文章评论区,据继承评论信息表
class ArticleComment(Comment):
# 记录评论属于哪篇文章
belong = models.ForeignKey(Article, related_name='article_comments', verbose_name='所属文章')
class Meta:
verbose_name = '文章评论'
verbose_name_plural = verbose_name
ordering = ['create_date']
# 关于自己页面评论信息
class AboutComment(Comment):
class Meta:
verbose_name = '关于自己评论'
verbose_name_plural = verbose_name
ordering = ['create_date']
# 给我留言页面评论信息
class MessageComment(Comment):
class Meta:
verbose_name = '给我留言'
verbose_name_plural = verbose_name
ordering = ['create_date']
这里评论信息表其实可以只创建一个表即可,但是这里分开创建
一是可以后期方便扩展,二是可以了解模型内继承的使用
4、分析评论区信息
因为后台逻辑,其实就是对用户需求进行处理反馈的东西,那么首先咱们的页面已经有了,就是 崔庆才 个人博客,所以需要分析一下 崔庆才 个人博客评论区设么怎么个功能,这里怎么分析呢?
首先进入 抓取到的 崔庆才 博客的前端源码,结合前端 HTML 标签对传递的数据进行分析
static -> js -> jquery.js
a.ajax({
url:_deel.url+"/ajax/comment.php",
data:a(this).serialize(),type:a(this).attr("method"),
error:function(w) {
a(".comt-loading").hide();
a(".comt-error").show().html(w.responseText);
setTimeout(function(){$submit.attr("disabled",false).fadeTo("slow",1);
a(".comt-error").fadeOut()},3000)},
success:function(B){
a(".comt-loading").hide();
r.push(a("#comment").val());
a("textarea").each(function(){this.value=""});
var y=addComment,A=y.I("cancel-comment-reply-link"),w=y.I("wp-temp-form-div"),C=y.I(y.respondId),x=y.I("comment_post_ID").value,z=y.I("comment_parent").value;
if(!o&&$comments.length){
n=parseInt($comments.text().match(/\d+/));
$comments.text($comments.text().replace(n,n+1))
}
new_htm='" id="new_comm_'+k+'"></';
new_htm=(z=="0")?('\n<ol style="clear:both;' + '" class="commentlist commentnew'+new_htm+"ol>"):('\n<ul class="children'+new_htm+"ul>");
ok_htm='\n<span id="success_'+k+b;ok_htm+="</span><span></span>\n";
if(z=="0"){
if(a("#postcomments .commentlist").length){a("#postcomments .commentlist").before(new_htm)
}else {
a("#respond").after(new_htm)
}
}else{
a("#respond").after(new_htm)}a("#comment-author-info").slideUp();
console.log(a("#new_comm_"+k));a("#new_comm_"+k).hide().append(B);
a("#new_comm_"+k+" li").append(ok_htm);
a("#new_comm_"+k).fadeIn(4000);
$body.animate({scrollTop:a("#new_comm_"+k).offset().top-200},500);
a(".comt-avatar .avatar").attr("src",a(".commentnew .avatar:last").attr("src"));
l();
k++;
o="";
a("*").remove("#edit_id");A.style.display="none";
A.onclick=null;
y.I("comment_parent").value="0";
if(w&&C){
w.parentNode.insertBefore(C,w);
w.parentNode.removeChild(w)
}
}
});
不要被 JS 源码给吓到,掌握了JS套路,局部分析,经过前端标签属性比如comment_parent、respond等属性值在这个文件内查找,定位到以上 AJAX 代码块
经过调试分析这段 AJAX 代码给后台传递了这些数据
w:评论内容
comment_post_ID:评论所属页面,给我留言、文章留言、关于自己页面
author:评论者
email:评论者邮箱
url:评论者网址
其实前端都是已经写好的只需要把
url:_deel.url+"/ajax/comment.php",
改为:
url: "/comment/add/",
但是由于,这里我多加了一个判断登录用户和游民的功能
success: function(B) {
console.log('success');
a(".comt-loading").hide();
r.push(a("#comment").val());
a("textarea").each(function() {
this.value = ""
});
//在这里添加如下代码
//获取seeion信息,判断用户是否登录
var user_id = "<%=session.getAttribute('uid','')%>";
var nick = $('#author').val();
if (user_id != ''){ //如果用户处于登录状态,展示用户ID
$('#nick').html(nick);
}
else { //如果没有登录,证明是游民,游民可以换马甲
$('#nick').html(nick + ' <a class="switch-author" href="javascript:;" data-type="switch-author" style="font-size:12px;">换个身份</a>');
}
完整源码:Github
5、配置路由
blog -> blog -> urls.py
# 评论
url(r'^comment/', include('comment.urls', namespace='comment')),
blog -> comment -> urls.py
from django.conf.urls import url
from .views import AddcommentView
urlpatterns = [
url(r'^add/$', AddcommentView, name='add_comment'),
]
6、编写视图函数
blog -> comment -> view.py
from storm.models import Article
from .models import ArticleComment, CommentUser, AboutComment, MessageComment
from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
import re
# 获取用户模型
user_model = settings.AUTH_USER_MODEL
# 确定重复是否重复
def confirm(new_content, comment_post_ID, auser):
if comment_post_ID == 'about':
res = AboutComment.objects.filter(content=new_content, author=auser)
elif comment_post_ID == 'message':
res = MessageComment.objects.filter(content=new_content, author=auser)
else:
res = ArticleComment.objects.filter(content=new_content, author=auser, belong_id=comment_post_ID)
if res:
return False
else:
return True
# @login_required
@csrf_exempt
@require_POST
def AddcommentView(request):
if request.is_ajax():
data = request.POST
# 评论内容哦你
new_content = data.get('w')
# 评论对象,指的是页面留言、文章、等
comment_post_ID = data.get('comment_post_ID')
# 评论者
author = data.get('author', '')
# 评论者邮箱
email = data.get('email', '')
# 评论者网址
url = data.get('url', '')
"""
验证信息格式
"""
if not re.match('^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$', email):
return HttpResponse('请输入有效的邮箱格式!', content_type='text/html;charset="utf-8"', status=405)
if not new_content:
return HttpResponse('请写点什么吧!', content_type='text/html;charset="utf-8"', status=405)
if not author or not email:
return HttpResponse('请填写邮箱和昵称', content_type='text/html;charset="utf-8"', status=405)
# 存储评论者信息
CommentUser.objects.get_or_create(nickname=author, email=email, address=url)
# 评论对象,父级对象,就是评论的是谁
comment_parent = data.get('comment_parent')
# 获取用户信息
auser = CommentUser.objects.get(nickname=author, email=email, address=url)
if not confirm(new_content, comment_post_ID, auser):
return HttpResponse('请勿发表重复内容!', content_type='text/html;charset="utf-8"', status=405)
"""
存储评论信息
"""
# 关于自己页面评论
if comment_post_ID == 'about':
# 父级评论
if comment_parent == '0':
new_comment = AboutComment(author=auser, content=new_content, parent=None, rep_to=None)
# 评论他人评论
else:
parent = AboutComment.objects.get(id=comment_parent)
new_comment = AboutComment(author=auser, content=new_content, parent=parent, rep_to=None)
new_comment.save()
# 给我留言页面评论
elif comment_post_ID == 'message':
if comment_parent == '0':
new_comment = MessageComment(MessageComment, auser, new_content)
else:
parent = MessageComment.objects.get(id=comment_parent)
new_comment = MessageComment(author=auser, content=new_content, parent=parent, rep_to=None)
new_comment.save()
# 文章评论
else:
the_article = Article.objects.get(id=comment_post_ID)
if comment_parent == '0':
new_comment = ArticleComment(author=auser, content=new_content, belong=the_article, parent=None, rep_to=None)
else:
parent = ArticleComment.objects.get(id=comment_parent)
new_comment = ArticleComment(author=auser, content=new_content, belong=the_article, parent=parent, rep_to=None)
new_comment.save()
# 获取用什么,分登陆身份和游民身份
request.session['nick'] = new_comment.author.nickname
request.session['tid'] = new_comment.author.id
# 返回当前评论,直接返回HTML内容刚给前端,使用JS在指定位置进行数据展示
return HttpResponse('''<li class="" id="comment-"><div class="c-avatar"><img alt='' src='https://cuiqingcai.com/avatar/.png' class='avatar avatar-54 photo avatar-default' height='54' width='54' /><div class="c-main" id="div-comment-">{0}<div class="c-meta"><span class="c-author">{1}</span></div></div></div>'''.format(new_content, author), content_type='text/html;charset="utf-8"')
return HttpResponse('参数错误', content_type='text/html;charset="utf-8"')
7、编写自定义模板标签
blog -> comment -> templatetags -> comment_tags.py
# 创建了新的tags标签文件后必须重启服务器
from django.utils.safestring import mark_safe
from django import template
from ..models import ArticleComment,AboutComment, MessageComment
register = template.Library()
@register.simple_tag
def get_comment_count(category, entry=0):
"""获取一个文章的评论总数"""
if category == 'about':
lis = AboutComment.objects.all()
elif category == 'message':
lis = MessageComment.objects.all()
else:
lis = ArticleComment.objects.filter(belong_id=entry)
return lis.count()
@register.simple_tag
def get_parent_comments(category, entry=0):
"""获取一个文章的父评论列表"""
if category == 'about':
lis = AboutComment.objects.filter(parent=None)
elif category == 'message':
lis = MessageComment.objects.filter(parent=None)
else:
lis = ArticleComment.objects.filter(belong_id=entry,parent=None)
return lis
@register.simple_tag
def get_child_comments(category,com):
"""获取一个父评论的子评论列表"""
if category == 'about':
lis = AboutComment.objects.filter(parent=com)
elif category == 'message':
lis = MessageComment.objects.filter(parent=com)
else:
lis = ArticleComment.objects.filter(parent=com)
return lis
@register.simple_tag
def get_comment_user_count(category,entry=0):
"""获取评论人总数"""
p = []
if category == 'about':
lis = AboutComment.objects.all()
elif category == 'message':
lis = MessageComment.objects.all()
else:
lis = ArticleComment.objects.filter(belong_id=entry)
for each in lis:
if each.author not in p:
p.append(each.author)
return len(p)
# 递归查找父评论
def find_father(dic, comment_obj):
# 对字典中的每一组元素进行循环操作
for k, v_dic in dic.items():
# 如果k等于comment_obj的父节点,那么表示找到了父亲。
if k == comment_obj.parent:
# 找到了父亲,认祖归宗,把自己归位到父亲下面,并给将来的儿子留个位置
dic[k][comment_obj] = {}
# 找到了父亲,处理完毕,返回
else:
# 刚才没找到,剥一层,接着往下找。
find_father(dic[k], comment_obj)
# 递归生成HTML字符串,展示评论区内容
def generate_comment_html(sub_comment_dic, category, path, s=2):
html = "<ul class='children'>"
# 对传入的字典进行循环操作
for k, v_dic in sub_comment_dic.items():
html += '''
<li class="comment odd alt depth-{0}" id="comment-{1}"><div class="c-avatar"><img alt='' data-original='https://cuiqingcai.com/avatar/ee89e6709c344980b7b82d1a13d496fb.png' class='avatar avatar-54 photo' height='54' width='54' /><div class="c-main" id="div-comment-{1}">{2}<div class="c-meta"><span class="c-author"><a href='http://fsfs' rel='external nofollow' class='url'>{3}</a></span>{4}<a rel='nofollow' class='comment-reply-link' href='{6}?replytocom={1}#respond' onclick='return addComment.moveForm( "div-comment-{1}", "{1}", "respond", "{5}" )' aria-label='回复给{3}'>回复</a></div></div></div>'''.format(s,k.id,k.content,k.author.nickname,k.create_date.strftime('%Y-%m-%d %H:%M:%S'),category,path)
# 有可能v_dic中依然有元素, 递归继续加
if v_dic:
s += 1
html += generate_comment_html(v_dic, category, path, s)
html += "</li>"
# 循环完成最后返回html
html += "</ul>"
return html
# 生成层级评论
@register.simple_tag
def build_comment_tree(category, path, entry=0):
if category == 'about':
comment_list = AboutComment.objects.all()
elif category == 'message':
comment_list = MessageComment.objects.all()
else:
comment_list = ArticleComment.objects.filter(belong_id=entry)
# 定义一个空字典用来保存转换之后的结果
comment_dic = {}
# 对comment_list中的每个元素进行循环
for comment_obj in comment_list:
# 判断comment_obj是否存在父节点。如果没有,这把该评论作为第一个节点
if comment_obj.parent is None:
comment_dic[comment_obj] = {}
else:
# 否则去找该对象的父节点。
find_father(comment_dic, comment_obj)
# 上面执行完毕,comment_dic中会有转换好的结果
# 开始拼接html字符串
html = "<ol class='commentlist'>"
# 规定一个margin left,每次有递归的时候就往右缩进一点。
# 对comment_dic中的每一组元素进行操作
for k, v in comment_dic.items():
# 第一层html
html += '''<li class="comment even thread-even depth-1" id="comment-{0}"><div class="c-avatar"><img alt='' data-original='https://cuiqingcai.com/avatar/5e43cb2c27191170aaece6a30a9d49f4.png' class='avatar avatar-54 photo' height='54' width='54' /><div class="c-main" id="div-comment-{0}">{1}<div class="c-meta"><span class="c-author">{2}</span>{3}<a rel='nofollow' class='comment-reply-link' href='{5}?replytocom={0}#respond' onclick='return addComment.moveForm( "div-comment-{0}", "{0}", "respond", "{4}" )' aria-label='回复给{2}'>回复</a></div></div></div>'''.format(k.id,k.content,k.author.nickname, k.create_date.strftime('%Y-%m-%d %H:%M:%S'), category, path)
# 通过递归把他的儿子加上
html += generate_comment_html(v, category, path)
# 最后把ul关上
html += " </ol>"
# 关掉转义
return mark_safe(html)
blog -> user -> oauth_tags.py
# 获取评论者信息
@register.simple_tag()
def get_tourist_data(uid):
"""返回评论者的信息"""
user = CommentUser.objects.filter(id=uid)
if user:
return user[0]
else:
return ''
8、编写评论区 HTML
blog -> templates -> comment_list.html
{% load staticfiles %}
{% load comment_tags oauth_tags %}
<div id="respond" class="no_webshot">
<!--写评论区域-->
<form action="/comment/add" method="post" id="commentform">
<div class="comt-title">
{% if request.session.username|default:'' != '' %}
{% get_user_data request.session.uid as user %}
<div class="comt-avatar pull-left">
<img alt='' src='/media/{{ user.avatar }}' class='avatar avatar-54 photo avatar-default' height='54' width='54' />
</div>
<div class="comt-author pull-left" id="nick">
{{ request.session.username }}
</div>
{% endif %}
{% if request.session.nick|default:'' != '' %}
{% get_tourist_data request.session.tid as tourist %}
<div class="comt-avatar pull-left">
<img alt='' src='/media/avatar' class='avatar avatar-54 photo avatar-default' height='54' width='54' />
</div>
<div class="comt-author pull-left" id="nick">{{ tourist.nickname }}
<a class="switch-author" href="javascript:;" data-type="switch-author" style="font-size:12px;">换个身份</a>
</div>
{% else %}
<div class="comt-author pull-left" id="nick">
</div>
{% endif %}
<a id="cancel-comment-reply-link" class="pull-right" href="javascript:;">取消评论</a>
</div>
<div class="comt">
<div class="comt-box">
<h3>发表我的评论</h3>
<textarea placeholder="写点什么..." class="input-block-level comt-area" name="w" id="comment" cols="100%" rows="3" tabindex="1" onkeydown="if(event.ctrlKey&&event.keyCode==13){document.getElementById('submit').click();return false};"></textarea>
<textarea name="comment" cols="100%" rows="4" style="display:none"></textarea>
<div class="comt-ctrl">
<button class="btn btn-primary pull-right" type="submit" name="submit" id="submit" tabindex="5">
<i class="fa fa-check-square-o"></i> 提交评论</button>
<div class="comt-tips pull-right">
<input type='hidden' name='comment_post_ID' value='{{ category }}' id='comment_post_ID' />
<input type='hidden' name='comment_parent' id='comment_parent' value='0' />
<p style="display: none;">
<input type="hidden" id="akismet_comment_nonce" name="akismet_comment_nonce" value="da9dc5c77a" /></p><p style="display: none;"><input type="hidden" id="ak_js" name="ak_js" value="50"/>
</p>
</div>
<span data-type="comment-insert-smilie" class="muted comt-smilie">
<i class="fa fa-smile-o"></i> 表情</span>
<span class="muted comt-mailme">
<label for="comment_mail_notify" class="checkbox inline" style="padding-top:0">
<input type="checkbox" name="comment_mail_notify" id="comment_mail_notify" value="comment_mail_notify" checked="checked"/>有人回复时邮件通知我
</label>
</span>
</div>
</div>
<div class="comt-comterinfo" id="comment-author-info" >
<h4>Hi,您需要填写昵称和邮箱!</h4>
<ul>
<li class="form-inline"><label class="hide" for="author">昵称</label><input class="ipt" type="text" name="author" id="author" value="{% if user %}{{ user.username }}{% elif tourist %}{{ tourist.nickname }}{% endif %}" tabindex="2" placeholder="昵称"><span class="help-inline">昵称 (必填)</span></li>
<li class="form-inline"><label class="hide" for="email">邮箱</label><input class="ipt" type="text" name="email" id="email" value="{% if user %}{{ user.email }}{% elif tourist %}{{ tourist.email }}{% endif %}" tabindex="3" placeholder="邮箱"><span class="help-inline">邮箱 (必填)</span></li>
<li class="form-inline"><label class="hide" for="url">网址</label><input class="ipt" type="text" name="url" id="url" value="{% if user %}{{ user.link }}{% elif tourist %}{{ tourist.address }}{% endif %}" tabindex="4" placeholder="网址"><span class="help-inline">网址</span></li>
</ul>
</div>
</div>
</form>
</div>
<!--评论展示区域-->
<div id="postcomments">
{% if category == 'message'%}
{% get_comment_user_count category as user_num%}
{% build_comment_tree category request.path as htm %}
{% elif category == 'about' %}
{% get_comment_user_count category as user_num%}
{% build_comment_tree category request.path as htm %}
{% else %}
{% get_comment_user_count category category as user_num%}
{% build_comment_tree category request.path category as htm %}
{% endif %}
<div id="comments">
<i class="fa fa-comments-o"></i> <b> ({{ user_num }})</b>个小伙伴在吐槽
</div>
{{ htm }}
<div class="commentnav" ></div>
</div>
9、添加评论功能
这里以文章页面为例
blog -> templates -> article.html
把评论区添加到文章详情页底部
</div>
</div>
{% include 'comment_list.html' %}
</div>
</div>
{% endblock body %}
哪个页面需要评论功能,就把下边的代码放在需要得而位置
{% include 'comment_list.html' %}
关于自己、给我留言也在相应位置添加此代码即可
完整源码:Github
10、效果图
9、总结
这里我这里写的评论区功能其实很独立,只要在需要添加评论功能的页面需要的地方加入
{% include 'comment_list.html' %}
这个评论功能,纯自己写的,没有写太多功能,比如删除评论、过滤垃圾用户、修改评论、评论邮件提醒等
以后有时间再升级吧
【友情提示】——如果发现有表达错误,或者知识点错误,或者搞不懂的地方,请及时留言,可以在评论区互相帮助,让后来者少走弯路是我的初衷。我也是一步步摸着石头走过来的,深知网络上只言片语的图文教程,给初学者带来的深深困扰。
【建议】——在对项目结构不太熟悉时,参照完整源码少走弯路
转载请注明: StormSha » Django个人博客开发十四 | 评论区