登录        注册
本站基于Django开发,源码 Github 欢迎 Fork、Star。由于站点升级导致评论区留言信息丢失,欢迎前来发表新的评论

Django个人博客开发十四 | 评论区

Django stormsha 2060浏览 209喜欢 0评论
本渣渣不专注技术,只专注使用技术,不是一个资深的coder,是一个不折不扣的copier

1、前言

博客中的评论系统其实是个很复杂的东西,但是网上已经有现成的轮子了,比如django-contrib-comments,可以直接拿过来用。咱们的博客主页是抓取别人的,目的是使用 Django 还原实现,达到快速借助 Wordpress 主题,使用 Djano 框架建站。这样你就可以找一个自己喜欢的前端模板,快速搭建自己的小天地了。那么这里就不便使用轮子,这里会根据 崔庆才 博客主页评论功能自己设计后台逻辑。

推荐学习:django-contrib-comments

2、创建评论应用

参考》 Django个人博客开发五 | 创建第一个APP

创建 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 + '&nbsp; <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 }}&nbsp; 
                        <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&amp;&amp;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、效果图

20371

9、总结

这里我这里写的评论区功能其实很独立,只要在需要添加评论功能的页面需要的地方加入

{% include 'comment_list.html' %}

这个评论功能,纯自己写的,没有写太多功能,比如删除评论、过滤垃圾用户、修改评论、评论邮件提醒等

以后有时间再升级吧

【友情提示】——如果发现有表达错误,或者知识点错误,或者搞不懂的地方,请及时留言,可以在评论区互相帮助,让后来者少走弯路是我的初衷。我也是一步步摸着石头走过来的,深知网络上只言片语的图文教程,给初学者带来的深深困扰。

【建议】——在对项目结构不太熟悉时,参照完整源码少走弯路

转载请注明: StormSha » Django个人博客开发十四 | 评论区

发表我的评论

表情
(0)个小伙伴在吐槽|0条评论
暂时没有评论,欢迎来尬聊!