文章详情页文章评论功能

一、文章评论功能实现流程  

  文章评论包含两种评论,根评论:对文章的评论;子评论:对评论的评论。两者的区别在于是否存在父评论。

  实现流程:1、构建样式;2、提交根评论;3、显示根评论(分为render显示和Ajax显示);4、提交子评论;5、显示子评论(分为render显示和Ajax显示);6、评论树显示(博客园是楼层显示)。

二、构建评论样式

1、article_detail.html:

{# 文章点赞,清除浮动 #}
<div class="clearfix">
    <div id="div_digg">
    {# 推荐 #}
    <div class="diggit action">
        <span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
    </div>
    {# 点灭 #}
    <div class="buryit action">
        <span class="diggnum" id="bury_count">{{ article_obj.down_count }}</span>
    </div>
    <div class="clear"></div>
    <div class="diggword" id="digg_tips" style="color: red;"></div>
</div>
</div>

<div class="comments">
    <p>发表评论</p>
    <p>昵称: <input type="text" id="tbCommentAuthor" class="author" disabled="disabled" size="50" value="{{ request.user.username }}"></p>
    <p>评论内容</p>
    <textarea name="" id="" cols="60" rows="10"></textarea> {# textarea是一个内联标签 #}
    <p><button class="btn btn-default comment_btn">提交评论</button></p>
</div>

  由于id=div_digg中存在浮动,显示时会造成点赞区域和评论区域在同一行,在点赞区外包了一层div标签,添加bootstrap的clearfix清除浮动。

  由于textarea是一个内联标签,下面直接接button标签,文本框和按钮会显示在同一行,同样给按钮包一层p标签,让按钮在文本框下方显示。

2、article_detail.css评论区样式调整

/* 评论 */
input.author {
    background-image: url("/static/font/icon_form.gif");
    background-repeat: no-repeat;
    border: 1px solid #ccc;
    padding: 4px 4px 4px 30px;
    width: 300px;
    font-size: 13px;
    background-position: 3px -3px;
}

3、显示效果如下所示:

  

三、提交根评论

1、创建评论路由

urlpatterns = [
    ...
    path('digg/', views.digg),  # 点赞
    path('comment/', views.comment),  # 评论
    ...
]

2、创建评论视图函数comment

def comment(request):
    print(request.POST)

    article_id = request.POST.get("article_id")
    pid = request.POST.get("pid")
    content = request.POST.get("content")
    user_id = request.user.pk

    # 在数据库生成一条评论对象  父评论为空是根评论,不为空则是子评论
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)

    return HttpResponse("comment")

3、在article_detail.html模板中创建评论事件

<script>
    // 点赞请求
    $('#div_digg .action').click(function () {
    ...

    // 评论请求
    $(".comment_btn").click(function () {
        var content = $('#comment_content').val();    // 拿到评论框的内容
        var pid = "";   // 父评论默认为空

        $.ajax({
            url: "/comment/",
            type: "post",
            data: {
                'csrfmiddlewaretoken': $("[name= 'csrfmiddlewaretoken']").val(),
                'article_id': "{{ article_obj.pk }}",
                'content': content,
                'pid': pid,
            },
            success:function (data) {
                console.log(data);

                // 提交后清空评论框
                $("#comment_content").val("");
            }
        })
    })
</script>

4、提交评论后,在数据库blog_comment表中可以看到评论记录

  

四、显示根评论

1、render显示根评论

(1)要render显示评论,需要修改article_detail视图函数

def article_detail(request, username, article_id):

    user = UserInfo.objects.filter(username=username).first()
    blog = user.blog

    article_obj = models.Article.objects.filter(pk=article_id).first()

    comment_list = models.Comment.objects.filter(article_id=article_id)

    return render(request, "article_detail.html", locals())

  传递comment_list到模板中,根据过滤条件获取的是当前文章的评论。

(2)在article_detail.html中构建评论列表

{# 文章评论列表 #}
<div class="comments">
    <p>评论列表</p>
    <ul class="list-group comment_list">
        {% for comment in comment_list %}
            <li class="list-group-item">
                <div>
                    <a href=""># {{ forloop.counter }}楼</a>    
                    <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>    
                    <a href=""><span><{{ comment.user.username }}/span></a>    
                    <a href="" class="pull-right">回复</a>
                </div>
                <div class="comment_con">
                    {# 评论内容 #}
                    <p>{{ comment.content }}</p>
                </div>
            </li>
        {% endfor %}
    </ul>

    <p>发表评论</p>
    <p>昵称: <input type="text" id="tbCommentAuthor" class="author" disabled="disabled" size="50" value="{{ request.user.username }}"></p>
    <p>评论内容</p>
    <textarea name="" id="comment_content" cols="60" rows="10"></textarea> {# textarea是一个内联标签 #}
    <p><button class="btn btn-default comment_btn">提交评论</button></p>
</div>

  1)利用bootstrap的列表组组件来构建文章列表样式。

  2)修饰评论内容样式article_detail.css:

.comment_con {
    margin-top: 10px;
}

(3)显示效果

  

2、Ajax显示根评论

 (1)更新comment视图函数,准备返回给ajax回调函数处理的数据

def comment(request):
    print(request.POST)

    article_id = request.POST.get("article_id")
    pid = request.POST.get("pid")
    content = request.POST.get("content")
    user_id = request.user.pk

    # 在数据库生成一条评论对象  父评论为空是根评论,不为空则是子评论
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)

    response = {}
    # create_time是一个datetime.datetime对象,在json序列化时不能对对象进行json序列化,必须进行strftime的转换
    response["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")
    # 评论人
    response["username"] = request.user.username
    # 内容
    response["content"] = content

    return JsonResponse(response)

(2)回调函数处理数据,显示新的评论

// 评论请求
$(".comment_btn").click(function () {
    var content = $('#comment_content').val();    // 拿到评论框的内容
    var pid = "";   // 父评论默认为空

    $.ajax({
        url: "/comment/",
        type: "post",
        data: {
            'csrfmiddlewaretoken': $("[name= 'csrfmiddlewaretoken']").val(),
            'article_id': "{{ article_obj.pk }}",
            'content': content,
            'pid': pid,
        },
        success: function (data) {
            console.log(data);

            // 获取视图函数返回的数据
            var create_time = data.create_time;
            var username = data.username;
            var content = data.content;

            // ES6特性:字符串模板。
            // ES6中允许使用反引号 ` 来创建字符串,此种方法创建的字符串里面可以包含由美元符号加花括号包裹的变量${vraible}。
            var s = `
                <li class="list-group-item">
                    <div>
                        <span>${create_time}</span>    
                        <a href=""><span><${username}/span></a>    
                        <a href="" class="pull-right">回复</a>
                    </div>
                    <div class="comment_con">
                        {# 评论内容 #}
                        <p>${content}</p>
                    </div>
                </li>`;
            // DOM操作把标签字符串整个放入ul的标签中去
            $("ul.comment_list").append(s);

            // 提交后清空评论框
            $("#comment_content").val("");
        }
    })
})

注意:1)ES6中允许使用反引号 ` 来创建字符串,此种方法创建的字符串里面可以包含由美元符号加花括号包裹的变量${vraible}。

//产生一个随机数
var num=Math.random();
//将这个数字输出到console
console.log(`your num is ${num}`);

  2)获取视图函数返回的数据:

// 获取视图函数返回的数据
var create_time = data.create_time;
var username = data.username;
var content = data.content;

(3)显示效果如下:

  

 

 五、提交子评论

   通过点击评论后的回复按钮来提交子评论,点击回复按钮事件:光标挪到输出框下;输出框显示 @用户名。

1、修改article_detail.html文章评论列表回复标签

{# 文章评论列表 #}
<div class="comments">
    <p>评论列表</p>
    <ul class="list-group comment_list">
        {% for comment in comment_list %}
            <li class="list-group-item">
                <div>
                    <a href=""># {{ forloop.counter }}楼</a>   
                    <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>   
                    <a href=""><span><{{ comment.user.username }}/span></a>   
                    <a class="pull-right reply_btn" username="{{ comment.user.username }}">回复</a>
                </div>
                <div class="comment_con">
                    {# 评论内容 #}
                    <p>{{ comment.content }}</p>
                </div>
            </li>
        {% endfor %}
    </ul>

    <p>发表评论</p>
    <p>昵称: <input type="text" id="tbCommentAuthor" class="author" disabled="disabled" size="50"
                  value="{{ request.user.username }}"></p>
    <p>评论内容</p>
    <textarea name="" id="comment_content" cols="60" rows="10"></textarea> {# textarea是一个内联标签 #}
    <p>
        <button class="btn btn-default comment_btn">提交评论</button>
    </p>
</div>

  给<a class="pull-right reply_btn" username="{{ comment.user.username }}">回复</a>,添加了属性username,拿到当前行评论的用户。

  注意:回复这个a标签不能添加href=""属性。否则在触发回复事件后,会刷新当前页面

2、在article_detail.html中编辑点击回复按钮事件

// 回复按钮事件
$(".reply_btn").click(function () {
    $('#comment_content').focus();   // 获取焦点
    // 拿到对应的父评论的用户名
    var val = "@" + $(this).attr("username")+"\n";
    // 给输入框赋值
    $('#comment_content').val(val);

});

  显示效果如下所示:

  

 3、提交子评论

article_detail.html做如下处理

<script>
    // 点赞请求
    ...
    
    var pid = "";   // 父评论默认为空
    // 评论请求
    $(".comment_btn").click(function () {
        var content = $('#comment_content').val();    // 拿到评论框的内容
        if (pid) {
            // pid有值,是子评论
            // 处理拿到子评论值方法一:
            var index = content.indexOf("\n");  // 拿到换行符索引值
            content = content.slice(index+1);  // 切片处理,从index+1一直取到最后
        }

        $.ajax({
            url: "/comment/",
            type: "post",
            data: {
                'csrfmiddlewaretoken': $("[name= 'csrfmiddlewaretoken']").val(),
                'article_id': "{{ article_obj.pk }}",
                'content': content,
                'pid': pid,
            },
            success: function (data) {
                console.log(data);

                // 获取视图函数返回的数据
                var create_time = data.create_time;
                var username = data.username;
                var content = data.content;

                // ES6特性:字符串模板。
                // ES6中允许使用反引号 ` 来创建字符串,此种方法创建的字符串里面可以包含由美元符号加花括号包裹的变量${vraible}。
                var s = `
            <li class="list-group-item">
                <div>
                    <span>${create_time}</span>    
                    <a href=""><span><${username}/span></a>    
                    <a href="" class="pull-right">回复</a>
                </div>
                <div class="comment_con">
                    {# 评论内容 #}
                    <p>${content}</p>
                </div>
            </li>`;
                // DOM操作把标签字符串整个放入ul的标签中去
                $("ul.comment_list").append(s);

                // 提交后清空评论框
                $("#comment_content").val("");
                // pid重新赋值
                pid = "";
            }
        })
    });

    // 回复按钮事件
    $(".reply_btn").click(function () {
        $('#comment_content').focus();   // 获取焦点
        // 拿到对应的父评论的用户名
        var val = "@" + $(this).attr("username")+"\n";
        // 给输入框赋值
        $('#comment_content').val(val);
        // 拿到父评论的主键值
        pid = $(this).attr("comment_pk");
    });
</script>

注意:

(1)将var pid="";  改为全局变量,拿到事件外。根据pid是否有值,处理评论框内容:

  pid没有值的时候:

var content = $('#comment_content').val();

  pid有值的时候:

if (pid) {
    // pid有值,是子评论
    // 处理拿到子评论值方法一:
    var index = content.indexOf("\n");  // 拿到换行符索引值
    content = content.slice(index+1);  // 切片处理,从index+1一直取到最后
}

(2)给回复按钮这个a标签添加一个新的自定义属性:comment_pk。

<a class="pull-right reply_btn" username="{{ comment.user.username }}" comment_pk="{{ comment.pk }}">回复</a>

  点击回复按钮拿到父评论的主键值

// 拿到父评论的主键值
pid = $(this).attr("comment_pk");

(3)每次提交评论后,都要清空pid。这样发布子评论后,不刷新页面紧接着又发布评论,这个新评论就不会有父评论了,

(4)查看数据库中的blog_comment表,子评论的父评论id:

  

六、显示子评论

1、render显示子评论

{# 文章评论列表 #}
<div class="comments">
    <p>评论列表</p>
    <ul class="list-group comment_list">
        {% for comment in comment_list %}
            <li class="list-group-item">
                <div>
                    <a href=""># {{ forloop.counter }}楼</a>   
                    <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>   
                    <a href=""><span><{{ comment.user.username }}/span></a>   
                    <a class="pull-right reply_btn" username="{{ comment.user.username }}" comment_pk="{{ comment.pk }}">回复</a>
                </div>

                {% if comment.parent_comment_id %}
                    <div class="pid_info well">
                        <p>
                            {# 拿到父评论对象评论人和评论内容 #}
                            {{ comment.parent_comment.user.username }}: {{ comment.parent_comment.content }}
                        </p>
                    </div>
                {% endif %}

                <div class="comment_con">
                    {# 评论内容 #}
                    <p>{{ comment.content }}</p>
                </div>
            </li>
        {% endfor %}
    </ul>

    <p>发表评论</p>
    <p>昵称: <input type="text" id="tbCommentAuthor" class="author" disabled="disabled" size="50"
                  value="{{ request.user.username }}"></p>
    <p>评论内容</p>
    <textarea name="" id="comment_content" cols="60" rows="10"></textarea> {# textarea是一个内联标签 #}
    <p>
        <button class="btn btn-default comment_btn">提交评论</button>
    </p>
</div>

(1)在模板中判断评论对象是否有父评论,如果有显示父评论人和评论对象:

{% if comment.parent_comment_id %}
    <div class="pid_info well">
        <p>
            {# 拿到父评论对象评论人和评论内容 #}
            {{ comment.parent_comment.user.username }}: {{ comment.parent_comment.content }}
        </p>
    </div>
{% endif %}

(2)bootstrap的well用在元素上,就能有嵌入(inset)的简单效果。

(3)显示效果:

  

2、Ajax显示子评论

 (1)改写article.detail.html评论请求事件回调函数事件处理:

var pid = "";   // 父评论默认为空
// 评论请求
$(".comment_btn").click(function () {
    var content = $('#comment_content').val();    // 拿到评论框的内容
    if (pid) {
        // pid有值,是子评论
        // 处理拿到子评论值方法一:
        var index = content.indexOf("\n");  // 拿到换行符索引值
        content = content.slice(index + 1);  // 切片处理,从index+1一直取到最后
    }

    $.ajax({
        url: "/comment/",
        type: "post",
        data: {
            'csrfmiddlewaretoken': $("[name= 'csrfmiddlewaretoken']").val(),
            'article_id': "{{ article_obj.pk }}",
            'content': content,
            'pid': pid,
        },
        success: function (data) {
            console.log(data);

            // 获取视图函数返回的数据
            var create_time = data.create_time;
            var username = data.username;
            var content = data.content;

            var parent_username = data.parent_username;
            var parent_content = data.parent_content;

            if (pid) {
                // ES6特性:字符串模板。
                // ES6中允许使用反引号 ` 来创建字符串,此种方法创建的字符串里面可以包含由美元符号加花括号包裹的变量${vraible}。
                var s = `
                    <li class="list-group-item">
                        <div>
                            <span>${create_time}</span>    
                            <a href=""><span><${username}/span></a>    
                            <a href="" class="pull-right">回复</a>
                        </div>
                        <div class="pid_info well">
                            <p>
                                {# 拿到父评论对象评论人和评论内容 #}
                                ${ parent_username }: ${ parent_content }
                            </p>
                        </div>
                        <div class="comment_con">
                            {# 评论内容 #}
                            <p>${content}</p>
                        </div>
                    </li>`;
            } else {
                var s = `
                    <li class="list-group-item">
                        <div>
                            <span>${create_time}</span>    
                            <a href=""><span><${username}/span></a>    
                            <a href="" class="pull-right">回复</a>
                        </div>
                        <div class="comment_con">
                            {# 评论内容 #}
                            <p>${content}</p>
                        </div>
                    </li>`;
            }
            // DOM操作把标签字符串整个放入ul的标签中去
            $("ul.comment_list").append(s);

            // 提交后清空评论框
            $("#comment_content").val("");
            // pid重新赋值
            pid = "";
        }
    })
});

  注意:根据pid是否有值对var s的标签字符串构建不同的结构和样式。也就是针对根评论和子评论构建不同的结构。由于构建子评论需要显示父评论的用户名和评论内容。因此需要在视图函数中返回响应的数据。

(2)在comment视图函数中添加父评论对象用户名和评论内容

def comment(request):
    print(request.POST)

    article_id = request.POST.get("article_id")
    pid = request.POST.get("pid")
    content = request.POST.get("content")
    user_id = request.user.pk

    # 在数据库生成一条评论对象  父评论为空是根评论,不为空则是子评论
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)

    response = {}
    # create_time是一个datetime.datetime对象,在json序列化时不能对对象进行json序列化,必须进行strftime的转换
    response["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")
    # 评论人
    response["username"] = request.user.username
    # 内容
    response["content"] = content

    # 父评论对象评论人和评论内容
    response["parent_username"] = comment_obj.parent_comment.user.username
    response["parent_content"] = comment_obj.parent_comment.content

    return JsonResponse(response)

(3)显示效果:

  

七、评论树

  前面都是用的评论楼来展示评论列表,但是这种方式结构不够清晰。因此也需要学会用评论树来展示评论列表。

1、评论树的请求数据

(1)构建article_detail.html中评论树标签和点击事件

{# 文章评论列表 #}
<div class="comments list-group">
    <p class="tree_btn">评论树</p>
    <div class="comment_tree">

    </div>
    <script>
        $(".tree_btn").click(function () {
            $.ajax({
                url: "/get_comment_tree/",
                type: "get",
                data: {
                    article_id: "{{ article_obj.pk }}",

                },
                success: function (data) {
                    console.log(data);
                }
            })
        })
    </script>
    <p>评论列表</p>
    ...
</div>

(2)根据ajax的请求url建立对应的url

urlpatterns = [
    path('admin/', admin.site.urls),
    ...
    path('comment/', views.comment),  # 评论
    path("get_comment_tree/", views.get_comment_tree),   # 评论树
    ...
]

(3)建立对应的视图函数get_comment_tree

def get_comment_tree(request):
    article_id = request.GET.get("article_id")
    # 过滤出文章对应的评论,挑出主键值、评论内容、父评论id,拿到的是一个queryset,结构类似一个列表里面装着一个个字典
    # 但是queryset并不是一个列表,可以用list()函数将其转换为列表
    ret = list(models.Comment.objects.filter(article_id=article_id).values("pk", "content", "parent_comment_id"))

    # JsonResponse对非字典的数据进行序列化,必须设置一个参数safe=False
    return JsonResponse(ret, safe=False)

  注意:QuerySet虽然结构类似一个列表里面装着一个个字典但并不是一个列表。可以用list()函数转换为列表。

  其次一般JsonResponse都是用来对字典进行序列化,如果要对一个非字典的数据序列化,必须设置一个参数safe=False,否则会报错。

(4)访问页面点击页面中评论树时,页面控制台输出了一条数组数据:

  

2、展开评论树

{# 文章评论列表 #}
    <div class="comments list-group">
        <p class="tree_btn">评论树</p>
        <div class="comment_tree">

        </div>
        <script>
            $(".tree_btn").click(function () {
                $.ajax({
                    url: "/get_comment_tree/",
                    type: "get",
                    data: {
                        article_id: "{{ article_obj.pk }}",

                    },
                    success: function (data) {
                        console.log(data);  // data是一个列表,列表中包含一个个字典
                        $.each(data, function (index, comment_object) {
                            var pk = comment_object.pk;
                            var content = comment_object.content;
                            var parent_comment_id = comment_object.parent_comment_id;

                            var s = '<div class="comment_item" comment_id='+pk+' style="margin-left: 20px"><span>'+content+'</span></div>';

                            // 判断评论是根评论还是子评论
                            if (!parent_comment_id) {    // 感叹号取反
                                // 根评论
                                $(".comment_tree").append(s);
                            } else {
                                // 子评论
                                // 放入父评论的div标签
                                // 属性选择器,找到comment_id属性值对应的div
                                $("[comment_id="+parent_comment_id+"]").append(s);
                            }

                        })
                    }
                })
            })
        </script>
    ...
</div>

  注意:

(1)变量s是将要插入模板中的标签字符串,标签字符串内定义的评论的内容。

<div class="comment_item" comment_id='pk' style="margin-left: 20px">
    <span>评论内容</span>
</div>

  每个标签字符串都定义了comment_id,对应评论对象的主键值。定义样式,则是为了子评论相对父评论向左移动20像素,样式得到错开。

(2)在ajax回调函数循环get_comment_tree视图函数返回列表

success: function (data) {
    console.log(data);  // data是一个列表,列表中包含一个个字典
    $.each(data, function (index, comment_object) {
        var pk = comment_object.pk;
        var content = comment_object.content;
        var parent_comment_id = comment_object.parent_comment_id;   
     ...
   })
}    

(3)如果判断是子评论的话,需要利用属性选择器,找到属性comment_id=父评论的id,这样筛选到对应的评论将标签字符串放如对应的div中。

var s = '<div class="comment_item" comment_id='+pk+' style="margin-left: 20px"><span>'+content+'</span></div>';

// 判断评论是根评论还是子评论
if (!parent_comment_id) {    // 感叹号取反
    // 根评论
    $(".comment_tree").append(s);
} else {
    // 子评论
    // 放入父评论的div标签
    // 属性选择器,找到comment_id属性值对应的div
    $("[comment_id="+parent_comment_id+"]").append(s);
}

(4)显示效果:

  

3、评论树回顾和优化

(1)在视图中获取的评论列表,会不会有子评论在前,父评论在后,导致在回调函数中循环列表时,发现有子评论无法找到父评论无法插入?

答:这个问题是不会发生的,因为评论生成是按主键进行排序的,根评论一定在前,与它关联的子评论一定在后。

  为了保险在获取评论对象时还是order_by做一下排序:

def get_comment_tree(request):
    article_id = request.GET.get("article_id")
    ret = list(models.Comment.objects.filter(article_id=article_id).order_by("pk").values("pk", "content", "parent_comment_id"))

    return JsonResponse(ret, safe=False)

(2)不再通过点击评论树标签,触发事件来显示评论树。直接访问文章页面就显示评论树?

答:不再把ajax事件内嵌在click事件中,浏览器加载标签字符串时就会直接执行ajax,立刻发新的请求拿到响应结果,由于速度非常快,立刻就将评论树显示在页面上。

<script>
    // $(".tree_btn").click(function () {
    $.ajax({
        url: "/get_comment_tree/",
        type: "get",
        data: {
            article_id: "{{ article_obj.pk }}",

        },
        success: function (data) {
            console.log(data);  // data是一个列表,列表中包含一个个字典
            $.each(data, function (index, comment_object) {
                var pk = comment_object.pk;
                var content = comment_object.content;
                var parent_comment_id = comment_object.parent_comment_id;

                var s = '<div class="comment_item" comment_id='+pk+' style="margin-left: 20px"><span>'+content+'</span></div>';

                // 判断评论是根评论还是子评论
                if (!parent_comment_id) {    // 感叹号取反
                    // 根评论
                    $(".comment_tree").append(s);
                } else {
                    // 子评论
                    // 放入父评论的div标签
                    // 属性选择器,找到comment_id属性值对应的div
                    $("[comment_id="+parent_comment_id+"]").append(s);
                }

            })
        }
    })
    // })
</script>

八、事务操作

  比如在数据库生成一条评论对象,同时将文章的评论数进行更新。这两步操作需要设计为同进同退,即如果有一步没完成,完成的操作需要在数据库回退。

def comment(request):
    print(request.POST)

    article_id = request.POST.get("article_id")
    pid = request.POST.get("pid")
    content = request.POST.get("content")
    user_id = request.user.pk

    # 事务操作:生成记录和评论数更新同进同退
    with transaction.atomic():
        # 在数据库生成一条评论对象  父评论为空是根评论,不为空则是子评论
        comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)
        # 文章评论数更新
        models.Article.objects.filter(pk=article_id).update(comment_count=F("comment_count")+1)

    response = {}
    # create_time是一个datetime.datetime对象,在json序列化时不能对对象进行json序列化,必须进行strftime的转换
    response["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")
    # 评论人
    response["username"] = request.user.username
    # 内容
    response["content"] = content

    # 父评论对象评论人和评论内容
    response["parent_username"] = comment_obj.parent_comment.user.username
    response["parent_content"] = comment_obj.parent_comment.content

    return JsonResponse(response)

1、引入以下模块来实现事务操作:

from django.db import transaction

2、将几个操作作为一个事务的方法:

# 事务操作:生成记录和评论数更新同进同退
with transaction.atomic():
    # 在数据库生成一条评论对象  父评论为空是根评论,不为空则是子评论
    comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)
    # 文章评论数更新          
    models.Article.objects.filter(pk=article_id).update(comment_count=F("comment_count")+1)

九、发送邮件

  文章被评论了,给作者发一份站内信通知他多了一条评论。这里使用django提供的模块来实现发送邮件:

from django.core.mail import send_mail

1、send_mail函数的源码分析

def send_mail(subject, message, from_email, recipient_list,
              fail_silently=False, auth_user=None, auth_password=None,
              connection=None, html_message=None):
    """

    :param subject: 标题
    :param message: 邮件内容
    :param from_email: 发件邮箱
    :param recipient_list: 收件邮箱
    :param fail_silently:
    :param auth_user:
    :param auth_password:
    :param connection:
    :param html_message:
    :return:
    """
    connection = connection or get_connection(
        username=auth_user,
        password=auth_password,
        fail_silently=fail_silently,
    )
    mail = EmailMultiAlternatives(subject, message, from_email, recipient_list, connection=connection)
    if html_message:
        mail.attach_alternative(html_message, 'text/html')

    return mail.send()

2、settings配置官方邮箱

# 邮箱配置
EMAIL_HOST = 'smtp.163.com'  # 如果是 qq 改成 smtp.exmail.qq.com
EMAIL_PORT = 465  # qqs是465
EMAIL_HOST_USER = 'xxx@163.com'           # 帐号
EMAIL_HOST_PASSWORD = 'xxxxxx'  # 密码(授权码)
# DEFAULT_FROM_EMAIL = EMAIL_HOST_USER    # 默认使用当前配置的user
EMAIL_USE_SSL = True  # 是否使用SSL证书, 网易邮箱关闭SSL后SMTP应该为25

3、在comment视图中配置邮件发送

def comment(request):
    ...
    # 为了发送邮件拿到文章对象
    article_obj = models.Article.objects.filter(pk=article_id).first()
    ...

    # 发送邮件
    from django.core.mail import send_mail
    from cnblog import settings
    send_mail(
        "您的文章%s新增了一条评论内容" % article_obj.title,
        content,
        settings.EMAIL_HOST_USER,   # 发送方
        ["44xxxx@qq.com"]   # 接收方
    )
    ...

  接收的邮箱,应该是用户注册时填写的邮箱信息。

  评论后,我的qq邮箱收到邮件:

  

4、多线程解决邮件发送网络延迟引起的网页卡顿

  点击评论会发现,页面需要卡很久,严重影响了用户体验。

# 发送邮件
from django.core.mail import send_mail
from cnblog import settings
# send_mail(
#     "您的文章%s新增了一条评论内容" % article_obj.title,
#     content,
#     settings.EMAIL_HOST_USER,   # 发送方
#     ["44xxxx@qq.com"]   # 接收方
# )

import threading

t = threading.Thread(target=send_mail, args=(
    "您的文章%s新增了一条评论内容" % article_obj.title,
    content,
    settings.EMAIL_HOST_USER,  # 发送方
    ["443xxxx@qq.com"]  # 接收方
))
t.start()

  这样用一个线程去跑send_email,就不会影响响应结果反馈了。

5、改完后完整的comment视图函数如下所示

def comment(request):
    print(request.POST)

    article_id = request.POST.get("article_id")
    pid = request.POST.get("pid")
    content = request.POST.get("content")
    user_id = request.user.pk

    # 为了发送邮件拿到文章对象
    article_obj = models.Article.objects.filter(pk=article_id).first()

    # 事务操作:生成记录和评论数更新同进同退
    with transaction.atomic():
        # 在数据库生成一条评论对象  父评论为空是根评论,不为空则是子评论
        comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content, parent_comment_id=pid)
        # 文章评论数更新
        models.Article.objects.filter(pk=article_id).update(comment_count=F("comment_count")+1)

    response = {}
    # create_time是一个datetime.datetime对象,在json序列化时不能对对象进行json序列化,必须进行strftime的转换
    response["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")
    # 评论人
    response["username"] = request.user.username
    # 内容
    response["content"] = content

    # 发送邮件
    from django.core.mail import send_mail
    from cnblog import settings
    # send_mail(
    #     "您的文章%s新增了一条评论内容" % article_obj.title,
    #     content,
    #     settings.EMAIL_HOST_USER,   # 发送方
    #     ["443614404@qq.com"]   # 接收方
    # )

    import threading

    t = threading.Thread(target=send_mail, args=(
        "您的文章%s新增了一条评论内容" % article_obj.title,
        content,
        settings.EMAIL_HOST_USER,  # 发送方
        ["443614404@qq.com"]  # 接收方
    ))
    t.start()


    # 父评论对象评论人和评论内容
    response["parent_username"] = comment_obj.parent_comment.user.username
    response["parent_content"] = comment_obj.parent_comment.content

    return JsonResponse(response)
comment视图函数

 

posted @ 2018-08-06 12:00  休耕  阅读(2050)  评论(0编辑  收藏  举报