14. Django博客项目3 点赞点踩&根评论子评论

17.模板页面

文章展示的页面布局和左侧栏使用的个人站点样式, 右侧展示文章详细内容.
0. 新建base.html模板页面
1. 将个人站点页面代码复制到模板页面base.html中
2. 设置替换的区域
3. 个人站点页面继承模板页面
17.1 模板页面
base.html页面代码 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>个人站点</title>
    <!--0. 动态获取静态文件路径 -->
    {% load static %}
    <!--1. 导入 jQuery js 文件-->
    <script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
    <!--2. 导入 bootstrap css 文件-->
    <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
    <!--3. 导入 bootstrap js 文件-->
    <script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script>
    <!--4. 导入 sweetalert css 文件-->
    <link rel="stylesheet" href="{% static 'bootstrap-sweetalert-master/dist/sweetalert.css' %}">
    <!--5. 导入 sweetalert js 文件-->
    <script src="{% static 'bootstrap-sweetalert-master/dist/sweetalert.min.js' %}"></script>
    <!-- 导入个人样式-->
    <link rel="stylesheet" href="/media/css/{{ blog_obj.site_theme }}/">
</head>
<body>
<!--6. 导入 导航条-->
<nav class="navbar navbar-inverse">
    <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
                    data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <!--6.1 图标 展示当前用户-->
            <a class="navbar-brand" href="#">{{ request.user.username }}</a>
        </div>

        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <ul class="nav navbar-nav">
                <!--6.2 导航条链接1 -->
                <li class="active"><a href="#">博客 <span class="sr-only">(current)</span></a></li>
                <!--6.2 导航条链接2 -->
                <li><a href="#">文章</a></li>
                <li class="dropdown">
                    <!--6.3 导航条链接3 -->
                    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
                       aria-expanded="false">更多 <span class="caret"></span></a>
                    <ul class="dropdown-menu">
                        <li><a href="#">Action</a></li>
                        <li><a href="#">Another action</a></li>
                        <li><a href="#">Something else here</a></li>
                        <li role="separator" class="divider"></li>
                        <li><a href="#">Separated link</a></li>
                        <li role="separator" class="divider"></li>
                        <li><a href="#">One more separated link</a></li>
                    </ul>
                </li>
            </ul>
            <form class="navbar-form navbar-left">
                <div class="form-group">
                    <input type="text" class="form-control" placeholder="Search">
                </div>
                <button type="submit" class="btn btn-default">Submit</button>
            </form>

            <ul class="nav navbar-nav navbar-right">
                <!-- 6.4 判断是有用户登入 is_authenticated会自动加括号调用函数 CallableBool(True)/ CallableBool(False)-->
                {% if request.user.is_authenticated %}
                    <!-- 6.5 导航条链接4 显示用户名称 -->
                    <li><a href="#">{{ request.user.username }}</a></li>
                    <li class="dropdown">
                        <!-- 6.6 导航条链接5 -->
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                           aria-haspopup="true" aria-expanded="false">更多 <span class="caret"></span></a>
                        <ul class="dropdown-menu">
                            <!-- 6.7 修改密码链接 -->
                            <li><a href="" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
                            <!-- 6.8 修改头像链接 -->
                            <li><a href="#">修改头像</a></li>
                            <!-- 6.9 后台管理链接 -->
                            <li><a href="#">后台管理</a></li>
                            <li role="separator" class="divider"></li>
                            <!-- 6.10 退出登入链接 -->
                            <li><a href="/logout/">退出登入</a></li>
                        </ul>
                    </li>
                {% else %}
                    <!-- 6.11 注册链接 -->
                    <li><a href="/register/">注册</a></li>
                    <!-- 6.10 登入链接 -->
                    <li><a href="/login/">登入</a></li>
                {% endif %}
            </ul>
        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>

<!--9.页面布局 3 9 -->
<div class="container-fluid">
    <div class="row">
        <!--8.1 右侧菜单栏-->
        <div class="col-md-3">
            <!-- 右侧菜单栏 第一个表单 -->
            <div class="panel panel-primary">
                <div class="panel-heading">分类</div>
                <div class="panel-body">
                    {% for sort_obj in sort_list %}
                        <p>
                            <a href="/{{ site_name }}/sort/{{sort_obj.sort__pk  }}">
                                {{ sort_obj.sort__name }}({{ sort_obj.count_num }})
                            </a>
                        </p>
                    {% endfor %}


                </div>
            </div>

            <!-- 右侧菜单栏 第二个表单 -->
            <div class="panel panel-info">
                <div class="panel-heading">标签</div>
                <div class="panel-body">
                    {% for tag_obj in tag_list %}
                        <p>
                            <a href="/{{ site_name }}/tag/{{ tag_obj.tag__pk }}/">
                                {{ tag_obj.tag__name }}({{ tag_obj.count_num }})
                            </a>
                        </p>
                    {% endfor %}

                </div>
            </div>
            <!-- 右侧菜单栏 第三个表单 -->
            <div class="panel panel-default">
                <div class="panel-heading">时间归档</div>
                <div class="panel-body">
                    {% for year_month_obj in year_month_list %}
                        <!-- 手动拼接格式 年-月 -->
                        <p><a href="/{{ site_name }}/archiving/{{ year_month_obj.month|date:"Y-m" }}/">{{ year_month_obj.month|date:"Y年m月" }}({{ year_month_obj.count_num }})</a></p>
                    {% endfor %}
                </div>
            </div>

        </div>
        
		<!--设置替换的区域-->
        {% block centent %}

        {% endblock %}

    </div>
</div>
</body>
</html>
17.2 继承模板页面
个人站点继承模板页面
{% extends 'base.html' %}
{% block centent %}
    <!--8.2 文章内容  使用媒体对象展示-->
    <div class="col-md-9">

        <!--分页器-->
        {% for queryset_obj in  article_query_set %}

            <!--媒体对象列表-->
            <div class="media">

                <h4 class="media-heading"><a href="">{{ queryset_obj.title }}</a></h4>
                <div class="media-left media-middle">
                    <a href="#">
                        <img class="media-object" src="/media/{{ queryset_obj.blog.userinfo.avatar }}" alt="..."
                             width="80">
                    </a>
                </div>
                <div class="media-body">
                    {{ queryset_obj.desc }}
                </div>
                <br>
                <div class="pull-right">
                    <p>
                        <!--博客表 反向解析两次拿到用户名-->
                        <span>pasted&nbsp;&nbsp;@&nbsp;</span>
                        <span><a href="">{{ queryset_obj.blog.userinfo.username }}&nbsp;</a></span>
                        <!--文章发布时间-->
                        <span>{{ queryset_obj.create_time|date:'Y-m-d' }}&nbsp;</span>
                        <span class="glyphicon glyphicon-comment">评论({{ queryset_obj.comment_num }})&nbsp;</span>
                        <span class="glyphicon glyphicon-thumbs-up">点赞({{ queryset_obj.up_num }})&nbsp;</span>
                        <span class="glyphicon glyphicon-thumbs-down">点踩({{ queryset_obj.down_num }})&nbsp;</span>
                        <span><a href="">编辑</a>&nbsp;</span>
                    </p>
                </div>
                <br>
                <hr>
            </div>
            <nav aria-label="Page navigation"></nav>
        {% endfor %}
        {# 利用自动分页器 #}
        {{ page_obj.page_html|safe }}

    </div>
{% endblock %}

18. 文章详细展示

在点击文件的标题之后, 展示文章的详细内容.
url的设计格式
127.0.0.1:8000/用户/article/文章的主键    
18.1 路由层
# 9. 文章详细展示
def article_desc(request, site_name, article_id):
    # 9.1 通过文章id去获取文章
    article_obj = models.Article.objects.filter(pk=article_id).first()
    
    # 9.2 判断访问的文章是否存在
    if not article_obj:
        # .1 文章不存在返回404页面
        return render(request, 'error.html')
    
    # 9.3 文章展示页面
    return render(request, 'article.html', locals())
18.2 文章展示页面
文章展示页面继承 base.html 模板页面.
# 9. 文章详细展示
def article_desc(request, site_name, article_id):

    # 9.1 通过文章id去获取文章
    article_obj = models.Article.objects.filter(pk=article_id).first()

    # 9.2 站点样式
    blog_obj = models.Blog.objects.filter(site_name=site_name).first()

    # 9.3 判断访问的文章是否存在
    if not article_obj:
        # .1 文章不存在返回404页面
        return render(request, 'error.html')

    # 9.4 文章展示页面
    return render(request, 'article.html', locals())

2022-03-30_00003

18.3 绑定文章id
主页  个人站点 的页面中点击文章标题展示文章的内容, 为文章标题的a标签设置路由.
<!--主页 文章标题 a标签绑定路由   主页没有站点名称的 通过反向解析新获取文章的绑定的站点名称-->
<a href="/{{ queryset_obj.blog.site_name }}/article/{{ queryset_obj.pk }}/">
    {{ queryset_obj.title }}
</a>

2022-03-31_00004

<!--个人站点 文章标题 a标签绑定路由-->
<h4 class="media-heading"><a href="/{{ site_name }}/article/{{ queryset_obj.pk }}/">{{ queryset_obj.title }}</a></h4>

2022-03-31_00006

GIF 2022-3-31 1-29-06

19. 制作包含标签

文章详细左侧表单需要数据才能展示出来.
获取数据的方法:
1. 可以将个人站点的分类归档代码复制一份. (不推荐)
2. 将左侧栏制作成inclusion_tag包含标签, base模板页面调用inclusion_tag包含标签即可.
制作inclusion_tag包含标签步骤:
1. 在app应用下创建一个templatetags目录
2. 在该文件内创建一个menu.py文件(命名无特殊要求)
3. 在tempales中创建一个left_menu.html文件, 包含标签生成页面的文件.
4. 将需要数据的三个表单html代码复制到 left_menu.html文件中.
3. 在py文件中写上固定代码导入需要的模块, 写业务逻辑, 最后将数据传递给指定的页面.
19.1 自定义模板语法
将个人站点的归档代码复制到menu.py文件中, 导入需要的模块.
将个人站点归档代码可以删除掉.
主表.objects.条件.annotate(别名=聚合函数('从表的字段')).valeu(需要展示的字段)
Count 操作的字段针对的是从表的主键, 主键有唯一标识, 只要涉及统计什么的数量就直接使用 '从表__id'.
Max, Min, Sum, Avg 操作它的字段.
# 0. 导入模板
from django import template

# 1. 生成一个模板对象
register = template.Library()

# 3. 导入需要的模块
from app01 import models
# 8.3.2 时导入
from django.db.models import Max, Min, Sum, Count, Avg

#  8.3.5 时导入 获取时间格式 年月 工具
from django.db.models.functions import TruncMonth


# 2. 装饰器中指定作用的页面, 在该页面中生成一个模板
@register.inclusion_tag('left_menu.html')
def left_menu(site_name):  # 定义包含标签 左侧菜单
    # 2.1 将 个人站点的归档代码复制到这里 构造模板需要的数据
    # 8.2 判断访问的站点是否存在
    blog_obj = models.Blog.objects.filter(site_name=site_name).first()

    # 8.3 存在返回个人站点的数据对象
    if blog_obj:
        # 8.3.1 用户存在 获取该用户的所有文章封装的 query_set对象
        article_query_set = models.Article.objects.filter(blog=blog_obj)


        # 8.3.2  导入OMR聚合函数
        # 8.3.4  查询当前用户的所有分类, 和分类下的文章数 使用聚合函数以类名分组 获取每个分组下文章的总数 统计主键 __id可以省略

        sort_list = models.Sort.objects.filter(blog=blog_obj).annotate(
            count_num=Count('article__pk')).values('pk', 'name', 'count_num')
        print(article_query_set)
        print(sort_list)

        # 8.3.5  查询当前用户站点的所有标签及标签下的文章总数
        tag_list = models.Tag.objects.filter(blog=blog_obj).annotate(
            count_num=Count('article__pk')).values('pk', 'name', 'count_num')

        # 8.3.6 导入 模块
        # 8.3.7 按年月分类, 统计分类下的文章总数
        year_month_list = models.Article.objects.filter(blog=blog_obj).annotate(month=TruncMonth('create_time')).values(
            'month').annotate(count_num=Count('pk')).values('month', 'count_num')

        # 将数据放回给模板页面

        return locals()
19.2 获取数据生成模板
将左侧栏表单复制到这里left_menu.html页面中
menu.py生成的数据会被传递到这个页面中, 并渲染出信息.
<!--将左侧栏表单复制到这里-->

<!--个人站点样式-->

<link rel="stylesheet" href="/media/css/{{ blog_obj.site_theme }}/">




<!-- 右侧菜单栏 第一个表单 -->
<div class="panel panel-primary">
    <div class="panel-heading">分类</div>
    <div class="panel-body">
        {% for sort_obj in sort_list %}
            <p>
                <a href="/{{ site_name }}/sort/{{sort_obj.pk  }}">
                    {{ sort_obj.name }}({{ sort_obj.count_num }})
                </a>
            </p>
        {% endfor %}


    </div>
</div>

<!-- 右侧菜单栏 第二个表单 -->
<div class="panel panel-info">
    <div class="panel-heading">标签</div>
    <div class="panel-body">
        {% for tag_obj in tag_list %}
            <p>
                <a href="/{{ site_name }}/tag/{{ tag_obj.pk }}/">
                    {{ tag_obj.name }}({{ tag_obj.count_num }})
                </a>
            </p>
        {% endfor %}

    </div>
</div>
<!-- 右侧菜单栏 第三个表单 -->
<div class="panel panel-default">
    <div class="panel-heading">时间归档</div>
    <div class="panel-body">
        {% for year_month_obj in year_month_list %}
            <!-- 手动拼接格式 年-月 -->
            <p><a href="/{{ site_name }}/archiving/{{ year_month_obj.month|date:"Y-m" }}/">{{ year_month_obj.month|date:"Y年m月" }}({{ year_month_obj.count_num }})</a></p>
        {% endfor %}
    </div>
</div>

19.3 使用包含标签
在模板页面base.html 中调用包含标签.
<!-- 先导入创建的自动模板语法的文件 py文件的名字-->
{% load menu %}
<!--调用 函数的名字 后面跟参数 需要当前站点的名称 -->
{% left_menu site_name %}

GIF 2022-3-31 13-05-16

19.4 文章展示问题
现在文章就是一个段落p标签展示, 没有任何的样式, 如果想让文章变的好看就因该使用html来写.
使用Typora写好之后到处HTML格式文件.

image-20220331132308155

打开导出的文件, 开发开发者工具复制html写的博客代码.

image-20220331132523671

进入后台管理, 将HTML代码文章写入数据库中.

image-20220331134723636

文章页面中 使用safe过滤器将渲染成HTML页面.
<p>{{ article_obj.content|safe }}</p>
复制博客园的文章 复制div id="cnblogs_post_body" 的标签.

image-20220331134318941

image-20220331134509500

将所有的文章内容都替换成html格式的文章, 图片有防盗技术就无法展示.

GIF 2022-3-31 13-51-06

20. 文章点赞点踩

2.1 点赞点踩按钮
复制博客园的点赞, 点赞的HTML代码.

image-20220331141013420

1. 复制HTML标签
<div id="div_digg">
    <div class="diggit" onclick="votePost(16081428,'Digg')">
        <span class="diggnum" id="digg_count">0</span>
    </div>
    <div class="buryit" onclick="votePost(16081428,'Bury')">
        <span class="burynum" id="bury_count">0</span>
    </div>
    <div class="clear"></div>
    <div class="diggword" id="digg_tips">
    </div>
</div>

image-20220331141231045

2. 将点击事件删除掉后, 复制到article.html页面中
3. 复制  div_digg diggit diggnum buryit burynum clear diggword 的样式, 一个一个复制下来.

2022-03-31_00015

#div_digg {
    float: right;
    margin-bottom: 10px;
    margin-right: 30px;
    font-size: 12px;
    width: 125px;
    text-align: center;
    margin-top: 10px;
}


.diggit {
    float: left;
    width: 46px;
    height: 52px;
    background: url(//static.cnblogs.com/images/upup.gif) no-repeat;
    text-align: center;
    cursor: pointer;
    margin-top: 2px;
    padding-top: 5px;
}
.diggnum {
    font-size: 14px;
    color: #075db3;
}

.buryit {
    float: right;
    margin-left: 20px;
    width: 46px;
    height: 52px;
    background: url(//static.cnblogs.com/images/downdown.gif) no-repeat;
    text-align: center;
    cursor: pointer;
    margin-top: 2px;
    padding-top: 5px;
}


.burynum {
    font-size: 14px;
    color: #075db3;
}

.clear {
    clear: both;
}

.diggword {
    margin-top: 5px;
    margin-left: 0;
    font-size: 12px;
    color: #808080;
}
4. 博客园的图片有防盗技术, 将图片下载到本地.
   https://static.cnblogs.com/images/upup.gif
   https://static.cnblogs.com/images/downdown.gif

在这里插入图片描述

image-20220331144052683

image-20220331144255817

/* 修改图片的url */
background: url(/static/img/upup.jpg) no-repeat;
background: url(/static/img/downdown.jpg) no-repeat;
5. 在base.html的<heah>标签中设置css的替换区域.
<!--替换区域-->
{% block css %}
{% endblock %}
{% extends 'base.html' %}
{% block css %}
<style>
    #div_digg {
        float: right;
        margin-bottom: 10px;
        margin-right: 30px;
        font-size: 12px;
        width: 125px;
        text-align: center;
        margin-top: 10px;
    }


    .diggit {
        float: left;
        width: 46px;
        height: 52px;
        background: url(/static/img/upup.jpg) no-repeat;
        text-align: center;
        cursor: pointer;
        margin-top: 2px;
        padding-top: 5px;
    }

    .diggnum {
        font-size: 14px;
        color: #075db3;
    }

    .buryit {
        float: right;
        margin-left: 20px;
        width: 46px;
        height: 52px;
        background: url(/static/img/downdown.jpg) no-repeat;
        text-align: center;
        cursor: pointer;
        margin-top: 2px;
        padding-top: 5px;
    }


    .burynum {
        font-size: 14px;
        color: #075db3;
    }

    .clear {
        clear: both;
    }

    .diggword {
        margin-top: 5px;
        margin-left: 0;
        font-size: 12px;
        color: #808080;
    }
</style>
{% endblock %}

{% block centent %}
<div class="col-md-9">

    <!--文章展示-->
    <div>
        <h1 class="text-center">{{ article_obj.title }}</h1>
        <p>{{ article_obj.content|safe }}</p>
    </div>

    <!--点赞点踩区域-->
    <div>
        <div id="div_digg">

            <!--点赞-->
            <div class="diggit action ">
                <span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
            </div>

            <!--点踩-->
            <div class="buryit action">
                <span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
            </div>
            <!--清除浮动-->
            <div class="clear"></div>
            <div class="diggword" id="digg_tips"><span id="msg" style="color: red"></span></div>

        </div>
    </div>
</div>

{% endblock %}

image-20220401164120743

<!--清除浮动-->
<div class="clear"></div>

image-20220401164213308

2022-03-31_00016

2.2 绑定事件
在base.html设置js独立的区域.
{% block js %}
    <script>
        // 给点赞点踩绑div标签添加一个 同一个属性action  然后绑定点击事件
        $(".action").on('click', function () {
        // 判断按下的按键是否具备diggit属性, 如果是就说明按下的按键是点赞, 否则就点踩
        let is_Up = $(this).hasClass('diggit')
        // console.log(is_Up, typeof is_Up)  // true 'boolean'

        // 发送ajax请求
        $.ajax({
            // 数据提交地址
            url: '/up_or_down/',
            // 请求方式
            type: 'post',
            // 提交的数据
            data: {
                // csrf检验, 值是一个input标签, 需要转为字符串格式 使用''包裹
                "csrfmiddlewaretoken": '{{ csrf_token }}',
                // 文章的主值
                'article_id': '{{ article_obj.pk }}',
                // 点赞还是点踩
                is_Up: is_Up
            },
            // 回调函数
            success: function (args) {
                // 判断响应状态码
                if (args.code === 200) {
                    // 展示返回信息
                    // 点成功的信息
                    $('#msg').text(args.right_msg)
                    // 刷新计数
                    $('#digg_count').text(args.up_num)
                    $('#bury_count').text(args.down_num)
                }else{
                    // 操作失败的信息, 在没有登入的时候会返回一个a标签, 跳转到登入页面.
                    $('#msg').html(args.error_msg)
                }

            }
        })
    })

 </script>

 {% endblock %}
2.3 获取ajax数据
1. 判断请求方式
2. 判断用户是否登入, (文章可以是不登入的用户看, 点赞点踩则需要登入)
3. 判断用户是否给自己的文章点赞
4. 判断用户是否点击过这篇文件
5. 判断点赞还是点踩
逻辑有点先, 先写正确的逻辑在写错误的逻辑.
操作两张表:
Article 中使用F查询, 多点击的数据进行操作
UpAndDown 表中记录 那个用户给哪篇文章点赞还是点踩. 
user_id  article_id is_up (True为点赞/ False为点踩)
  1          3        True
判断某个用户时候给某篇文章是否点击过. 点赞点踩表对象.filter (user_id and article_id) 满足这两个条件
说明点过了.
# 10. 点赞点赞
def up_or_down(request):
    # 10.1 判断请求方式
    if request.is_ajax():
        # 组织返回的数据
        back_dict = {'code': 200}
        # 10.2 判断用户是否登入, (文章可以是不登入的用户看, 点赞点踩则需要登入)
        if request.user.is_authenticated():
            # 有用户登入执行的语句

            # 10.3 获取请求中的数据
            post_obj = request.POST
            article_id = post_obj.get('article_id')
            is_up = post_obj.get('is_Up')

            # 布尔值问题, 前端js的True是字符串
            # print(is_up, type(is_up))  # true <class 'str'>

            # 可以使用json模块转为布尔值
            import json
            is_up = json.loads(is_up)
            # print(is_up, type(is_up))  # True <class 'bool'>

            # 10.4 判断用户是否给自己的文章点赞
            # 通过文章id得到文章对象 文章对象跨表查询用户名
            article_obj = models.Article.objects.filter(pk=article_id).first()

            # 两个数据对象的用户值做比较
            # 不是自己的文章
            if article_obj.blog.userinfo.username != request.user.username:

                # 10.5 判断用户是否点击过这篇文件
                up_or_down_obj = models.UpAndDown.objects.filter(user_id=request.user.pk, article_id=article_obj)
                # 没有点过
                if not up_or_down_obj:

                    # 10.6 判断 这次是 点赞 还是 点踩
                    # 点赞操作
                    if is_up:
                        # 10.7 导入F模块
                        # 10.8 使用F查询更新文章表中 点击的数量
                        models.Article.objects.filter(pk=article_id).update(up_num=F('up_num') + 1)
                        back_dict['right_msg'] = '点赞成功!'
                    # 点踩操作
                    else:
                        models.Article.objects.filter(pk=article_id).update(down_num=F('down_num') - 1)
                        back_dict['right_msg'] = '点踩成功!'

                    # 10.9 将点击操作写入数据库
                    models.UpAndDown.objects.create(user=request.user, article=article_obj, is_up=is_up)	   # 从新获取文章对象 , 传给后端, 刷新计数显示
                    article_obj = models.Article.objects.filter(pk=article_id).first()
                    back_dict['up_num'] = article_obj.up_num
                    back_dict['down_num'] = article_obj.down_num
                # 点过就不能在点了
                else:
                    # 组则报错信息
                    back_dict = {'code': 401, 'error_msg': '已经评论过不能在评论!'}
            # 自己不能给自己点击
            else:
                # 组织错误信息
                back_dict = {'code': 402, 'error_msg': '自己的文章此功能不可操作!'}
        # 如果没有则在点赞点踩下面提示先登入用户
        else:
            # 组织返回的提示信息
            back_dict = {'code': 403, 'error_msg': '<a href="/login/">请先登入账户!</a>'}

        # 10.10 返回数据给回调函数
        print(back_dict)
        return JsonResponse(back_dict)
    # 非法访问
    else:
        return HttpResponse('非法访问')
# 获取请求中的数据
is_up = request.POST.get('is_Up')

# 布尔值问题, 前端js的True是字符串
# print(is_up, type(is_up))  # true <class 'str'>

# 可以使用json模块转为布尔值
import json
is_up = json.loads(is_up)
# print(is_up, type(is_up))  # True <class 'bool'>
点击别人的文章

GIF 2022-4-1 3-45-08

点击自己的文章

GIF 2022-4-1 3-47-25

没有登入时点击
back_dict = {'code': 403, 'error_msg': '<a href="/login/">请先登入账户!</a>'}

GIF 2022-4-1 10-06-38

2.4 点赞点踩计数
两种方法:
1. 后端写入数据之后, 将文章对象发给前端刷新计数.
2. 后端获取包含计数标签的值, 做加减计算, 再刷新显示.
采用方式1, 在后端已经计算过, 就没必要在后端计算了.
back_dict['up_num'] = article_obj.up_num
back_dict['down_num'] = article_obj.down_num
$('#digg_count').text(args.up_num)
$('#bury_count').text(args.down_num)

GIF 2022-4-1 10-50-40

21. 评论

21.1 根评论
在文章展示页面article.html部署一个评论框.
<!--评论功能-->
<div>
    <p><span class="glyphicon glyphicon-comment">发表评论</span></p>
    <textarea name="comment" id="id_comment" cols="45" rows="10"></textarea>
    <p>
        <button id="id_btn">提交</button>
        <span id="id_error"></span>
    </p>
</div>

2022-04-01_00019

设置浮动让评论和点赞点赞不在同一行当中.
<!--点赞点踩区域 设置清除浮动-->
<div class="clearfix">
    <div id="div_digg">
	...
 </div>div>

image-20220401165554968

21.2 提交评论
ajax提交评论的信息, 那个用户给哪篇文章评论了什么.
// 给提交按钮绑定点击事件
$('#id_btn').on('click', function () {
    // 获取大段文本款内的数据
    let comment = $("[name='comment']").val()

    // 提交数据
    $.ajax({
        // 提交地址
        url: '/submit_comment/',
        // 提交方式
        type: 'post',
        // 提交的数据
        data: {
            // 那个用户给哪篇文章评论了什么 (用户在auth中)
            // 文章的id
            article_id: '{{ article_obj.pk }}',
            // 评论的内容
            comment: comment,
            // csrf
            "csrfmiddlewaretoken": '{{ csrf_token }}',
        },
        success: function (args) {
            alert(args)
        }
    })
})
21.3 将提交信息写入表中
先写正确的逻辑.
# 11. 提交评论
def submit_comment(request):
    # 11.1 判断提交请求
    if request.is_ajax():
        # 11.2 判断用户是否登入
        if request.user.is_authenticated():
            # 11.3 获取提交的数据
            post_obj = request.POST
            article_id = post_obj.get('article_id')
            comment = post_obj.get('comment')
            print(article_id, comment)

            # 11.4 将文章评论数写进文章表 将文章评论评论内容添加到评论表
            models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num') + 1)

            # 获取文章对象 前端需要评论数据
            article_obj = models.Article.objects.filter(pk=article_id).first()

            models.Comment.objects.create(user=request.user, article=article_obj, content=comment)

            # 组织返回的信息
            back_dict = {'code': 200, 'right_msg': '评论成功'}
            # 用户没有登入 提示用户登入
            else:
                back_dict = {'code': '400', 'error_msg': '<a href="/login/">请先登入在评论</a>'}

		return JsonResponse(back_dict)

    else:
        return HttpResponse('非法访问')

image-20220402062113947

21.4 展示根评论
文章详细展示中, 获取评论的内容, 并渲染到前端.
# 9.5 评论表信息
comment_obj = models.Comment.objects.filter(article_id=article_id)
<!--评论展示-->
<div>
    <!--标题面板-->
    {% for obj in comment_obj %}

    <div class="panel panel-default">
        <!--评论的其他信息 几楼, 评论时间 评论者 回复a标签-->

        <div class="panel-heading">
            <p>
                <span>{{ forloop.counter }}楼</span>
                <span>{{ obj.comment_time|date:'Y-m-d h:i:s' }}</span>
                <!--评论表通过外键获取用户名 -->
                <span>{{ obj.user.username }}</span>
                <span class="pull-right"><a href="#">回复</a></span>
            </p>
        </div>

        <div class="panel-body">
            <!--评论的信息-->
            <div>
                {{ obj.content }}
            </div>
        </div>
    </div>
    {% endfor %}
</div>

image-20220402131207800

21.5 提交评论时渲染
1. 点击提交评论按键后清空评论框内的内容,
2. 渲染自己的评论
   方式1: DOM临时渲染
   方式2: 页面刷新render渲染
`模板代码${替换的内容}...` 
success:function (args) {
    // 判断响应状态
    if (args.code === 200) {
        // 评论成功, 将评论款内信息清空
        $('#id_comment').val('')
        // 将评论的内容临时展示到评论区
        // 获取当前用户名
        let username = '{{ request.user.username }}'
        // 制作一个模板
        let tem = `
            <div class="panel panel-default">

                <div class="panel-heading">
                    <p>
                    	 <span class='glyphicon glyphicon-comment'>&nbsp;${username}</span>
                    </p>
                </div>

                <div class="panel-body">
                    <div>
                        ${comment}
                    </div>
                </div>
            </div>
`
        // 将模板添加进展示区
        $('#id_exhibit').append(tem)
    }
}else{
    
}

GIF 2022-4-2 13-34-07

没有用户登入时渲染提示登入信息.
if (args.code === 200) {
    ...
}else{
    // 错误的提示信息 只有一种错误用户没有登入
    $('#id_error').html(args.error_msg)

GIF 2022-4-3 7-39-52

21.6 子评论
点击回复书写字评论.
点击回复的时候, 鼠标聚焦到评论框中.
格式:
@用户名(换行)
|(<--光标在这)
* 为回复按钮绑定点击事件, 这个回复是在for循环内不能使用id值来标记, 可以使用类来标记. 之后查找该标签.
需要获取的信息, 评论用户的名字, 根评论的主键id, 可以在for循环中为标签自定义两个属性接收需要的值.
注意点 a标签不要有href, href='#'会跳转到页面头部.
<!-- 评论表通过user外键 获取到用户名, -->
<span class="pull-right"><a class="reply" username="{{ obj.user.username }}" parent_id="{{ obj.pk }}">回复</a></span>

image-20220402153951379

// 给回复所在标签绑定事件
$('.reply').on('click', function (){
    // 获取评论根评论的用户名
    let username = $(this).attr('username')
    // 获取根评论的主键id
    let comment_id = $(this).attr('parent_id')
    // 拼接@用户名 + 换行 + 聚集focus()
    $('#id_comment').val('@' + username + '\n').focus()
})

image-20220402151043325

自评与根评论都是同一个按钮提交数据.
区分根评论和子评论: 子评论的parent_id有值. parent_id指向评论表中的id.

image-20220402153555309

{% block js %}
    <script>

        // 给点赞点踩绑div标签添加一个 同一个属性action  然后绑定点击事件
        $(".action").on('click', function () {
        // 判断按下的按键是否具备diggit属性, 如果是就说明按下的按键是点赞, 否则就点踩
        let is_Up = $(this).hasClass('diggit')
        // console.log(is_Up, typeof is_Up)  // true 'boolean'

        // 发送ajax请求
        $.ajax({
            // 数据提交地址
            url: '/up_or_down/',
            // 请求方式
            type: 'post',
            // 提交的数据
            data: {
                // csrf检验, 值是一个input标签, 需要转为字符串格式 使用''包裹
                "csrfmiddlewaretoken": '{{ csrf_token }}',
                // 文章的主值
                'article_id': '{{ article_obj.pk }}',
                // 点赞还是点踩
                'is_Up': is_Up
            },
            // 回调函数
            success: function (args) {
                // 判断响应状态码
                if (args.code === 200) {
                    // 展示返回信息

                    $('#msg').text(args.right_msg)

                    // 刷新计数
                    $('#digg_count').text(args.up_num)
                    $('#bury_count').text(args.down_num)
                } else {

                    $('#msg').html(args.error_msg)
                }

            }
        })
    })

 // 设置一个全局的变量 初始值值为 null
 var parent_id = null
 // 给提交按钮绑定点击事件
 $('#id_btn').on('click', function () {
     // 获取大段文本款内的数据
     let comment = $("[name='comment']").val()

     // 提交数据
     $.ajax({
         // 提交地址
         url: '/submit_comment/',
         // 提交方式
         type: 'post',
         // 提交的数据
         data: {
             // 那个用户给哪篇文章评论了什么 (用户在auth中)
             // 文章的id
             'article_id': '{{ article_obj.pk }}',
             // 评论的内容
             'comment': comment,
             // 根评论的id,  parent_id 要么为Null 要么有值, 如果有值则是一个子评论
             'parent_id': parent_id,
             // csrf
             "csrfmiddlewaretoken": '{{ csrf_token }}',
         },
         success: function (args) {
             // 判断响应状态
             if (args.code === 200) {
                 // 评论成功, 将评论款内信息清空
                 $('#id_comment').val('')
                 // 将评论的内容临时展示到评论区
                 // 获取当前用户名
                 let username = '{{ request.user.username }}'
                 // 制作一个模板
                 let tem = `
                    <div class="panel panel-default">
                        <div class="panel-heading">
                            <p>
                            	<span class='glyphicon glyphicon-comment'>&nbsp;${username}</span>
                            </p>
                        </div>
                        <div class="panel-body">
                            <div>
                                ${comment}
                            </div>
                        </div>
                    </div>
`
                 // 将模板添加进展示区
                 $('#id_exhibit').append(tem)
             }
         }
     })
 })

 // 给回复所在标签绑定事件
 $('.reply').on('click', function () {
     // 获取评论根评论的用户名
     let username = $(this).attr('username')
     // 获取根评论的主键id 修改全局变量parent_id的值
     parent_id = $(this).attr('parent_id')
     // 拼接@用户名 + 换行 + 聚集focus()
     $('#id_comment').val('@' + username + '\n').focus()
 })
 </script>

image-20220402155449349

# 11. 提交评论
def submit_comment(request):
    # 11.1 判断提交请求
    if request.is_ajax():
        # 11.2 判断用户是否登入
        if request.user.is_authenticated():
            # 11.3 获取提交的数据
            post_obj = request.POST
            article_id = post_obj.get('article_id')
            comment = post_obj.get('comment')
            parent_id = post_obj.get('parent_id')
            print(article_id, comment)

            # 11.4 将文章评论数写进文章表 将文章评论评论内容添加到评论表
            models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num') + 1)

            # 获取文章对象 前端需要评论数据
            article_obj = models.Article.objects.filter(pk=article_id).first()

            models.Comment.objects.create(user=request.user, article=article_obj, content=comment, parent_id=parent_id)

            # 组织返回的信息
            back_dict = {'code': 200, 'right_msg': '评论成功'}
        # 用户没有登入 提示用户登入
        else:
            back_dict = {'code': '400', 'error_msg': '<a href="/login/">请先登入在评论</a>'}

        return JsonResponse(back_dict)

    else:
        return HttpResponse('非法访问')

image-20220402154512652

# 全端传null的值后端获取到是空字符串 ''
print(parent_id, type(parent_id))  # (空的)  <class 'str'>

image-20220402160934242

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4pevzSlH-1649082690806)(https://s2.loli.net/2022/04/02/Emg7GxoijZlkR15.gif)]

image-20220402160140105

子评论中@用户 不是评论的内容不因该写进数据中, 将这部分进行切除.

image-20220402161445629

// 给提交按钮绑定点击事件
$('#id_btn').on('click', function () {
    // 获取大段文本款内的数据
    let comment = $("[name='comment']").val()

    // 判断是否为子评论
    if (parent_id) {
        // 是子评论, 将@username 与 \n 切除
        // 获取\n后一位字符的索引 需要+1
        let IndexNum = comment.indexOf('\n') + 1
        // 切除操作
        comment = comment.slice(IndexNum)  // 将索引值IndexNum之前的都切除, 保留之后的内容
    }
    ...

image-20220402162630398

21.7 子评论的渲染
格式:
@用户
信息
<div class="panel-body">
    <!--评论的信息-->
    <div>
        <!--判断是否为子评论 判断parent_id的值-->
        {% if obj.parent_id %}
        <div>
            <!--展示根评论的用户名-->
            <p><span style="color: #aaff00">@{{ obj.parent.user.username  }}</span></p>

        </div>
        {% endif %}
        {{ obj.content }}
    </div>
</div>
obj.parent.user.username 
评论的数据对象.自关联的外键, 拿到根评论的数据对象.外键.username拿到根评论的用户名.
21.8 重置子评论全局变量
第一次写是子评论
第二次写是根评论

GIF 2022-4-2 17-00-39

这时会发现都是子评论, 因为parent_id获取到值之后就一直有值, 
不重置为null的话, 获取到了值, 之后永远都是子评论.

image-20220402170253559

在回调函数的末尾重置parent_id为null

image-20220402170743921

再次测试, 先写一个子评论, 在写一个根评论.

GIF 2022-4-2 17-10-00

正常

image-20220402171111136

文章展示代码
{% extends 'base.html' %}
{% block css %}
    <style>
        #div_digg {
            float: right;
            margin-bottom: 10px;
            margin-right: 30px;
            font-size: 12px;
            width: 125px;
            text-align: center;
            margin-top: 10px;
        }


        .diggit {
            float: left;
            width: 46px;
            height: 52px;
            background: url(/static/img/upup.jpg) no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }

        .diggnum {
            font-size: 14px;
            color: #075db3;
        }

        .buryit {
            float: right;
            margin-left: 20px;
            width: 46px;
            height: 52px;
            background: url(/static/img/downdown.jpg) no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }


        .burynum {
            font-size: 14px;
            color: #075db3;
        }

        .clear {
            clear: both;
        }

        .diggword {
            margin-top: 5px;
            margin-left: 0;
            font-size: 12px;
            color: #808080;
        }
    </style>
{% endblock %}

{% block centent %}

    <div class="col-md-9">

        <!--文章展示-->
        <div>
            <h1 class="text-center">{{ article_obj.title }}</h1>
            <p>{{ article_obj.content|safe }}</p>
        </div>

        <!--点赞点踩区域-->
        <div class="clearfix">
            <div id="div_digg">

                <!--点赞-->
                <div class="diggit action ">
                    <span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
                </div>

                <!--点踩-->
                <div class="buryit action">
                    <span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
                </div>
                <!--清除浮动-->
                <div class="clear"></div>
                <div class="diggword" id="digg_tips"><span id="msg" style="color: red"></span></div>

            </div>
        </div>


        <!--评论展示-->
        <div id="id_exhibit">
            <!--标题面板-->
            {% for obj in comment_obj %}
                <div class="panel panel-default">
                    <!--评论的其他信息 几楼, 评论时间 评论者 回复a标签-->
                    <div class="panel-heading">
                        <p>
                            <span>{{ forloop.counter }}楼</span>
                            <span>{{ obj.comment_time|date:'Y-m-d h:i:s' }}</span>
                            <!--评论表通过外键获取用户名 -->
                            <span>{{ obj.user.username }}</span>
                            <!-- 评论表通过user外键 获取到用户名, -->
                            <span class="pull-right"><a class="reply" username="{{ obj.user.username }}"
                                                        parent_id="{{ obj.pk }}">回复</a></span>
                        </p>
                    </div>

                    <div class="panel-body">
                        <!--评论的信息-->
                        <div>
                            <!--判断是否为子评论 判断parent_id的值-->
                            {% if obj.parent_id %}
                                <div>
                                    <!--展示根评论的用户名-->
                                    <p><span style="color: #aaff00">@{{ obj.parent.user.username }}</span></p>

                                </div>
                            {% endif %}
                            {{ obj.content }}
                        </div>
                    </div>
                </div>
            {% endfor %}
        </div>

        <!--评论功能-->
        <div>
            <p><span class="glyphicon glyphicon-comment">发表评论</span></p>
            <textarea name="comment" id="id_comment" cols="45" rows="10"></textarea>
            <p>
                <button id="id_btn">提交</button>
                <span id="id_error"></span>
            </p>
        </div>
    </div>




{% endblock %}

{% block js %}
    <script>

        // 给点赞点踩绑div标签添加一个 同一个属性action  然后绑定点击事件
        $(".action").on('click', function () {
            // 判断按下的按键是否具备diggit属性, 如果是就说明按下的按键是点赞, 否则就点踩
            let is_Up = $(this).hasClass('diggit')
            // console.log(is_Up, typeof is_Up)  // true 'boolean'

            // 发送ajax请求
            $.ajax({
                // 数据提交地址
                url: '/up_or_down/',
                // 请求方式
                type: 'post',
                // 提交的数据
                data: {
                    // csrf检验, 值是一个input标签, 需要转为字符串格式 使用''包裹
                    "csrfmiddlewaretoken": '{{ csrf_token }}',
                    // 文章的主值
                    'article_id': '{{ article_obj.pk }}',
                    // 点赞还是点踩
                    'is_Up': is_Up
                },
                // 回调函数
                success: function (args) {
                    // 判断响应状态码
                    if (args.code === 200) {
                        // 展示返回信息

                        $('#msg').text(args.right_msg)

                        // 刷新计数
                        $('#digg_count').text(args.up_num)
                        $('#bury_count').text(args.down_num)
                    } else {

                        $('#msg').html(args.error_msg)
                    }

                }
            })
        })

        // 设置一个全局的变量 初始值值为 null
        var parent_id = null
        // 给提交按钮绑定点击事件
        $('#id_btn').on('click', function () {
            // 获取大段文本款内的数据
            let comment = $("[name='comment']").val()

            // 判断是否为子评论
            if (parent_id) {
                // 是子评论, 将@username 与 \n 切除
                // 获取\n后一位字符的索引 需要+1
                let IndexNum = comment.indexOf('\n') + 1
                // 切除操作
                comment = comment.slice(IndexNum)  // 将索引值IndexNum之前的都切除, 保留之后的内容
            }

            // 提交数据
            $.ajax({
                // 提交地址
                url: '/submit_comment/',
                // 提交方式
                type: 'post',
                // 提交的数据
                data: {
                    // 那个用户给哪篇文章评论了什么 (用户在auth中)
                    // 文章的id
                    'article_id': '{{ article_obj.pk }}',
                    // 评论的内容
                    'comment': comment,
                    // 根评论的id,  parent_id 要么为Null 要么有值, 如果有值则是一个子评论
                    'parent_id': parent_id,
                    // csrf
                    "csrfmiddlewaretoken": '{{ csrf_token }}',
                },
                success: function (args) {
                    // 判断响应状态
                    if (args.code === 200) {
                        // 评论成功, 将评论款内信息清空
                        $('#id_comment').val('')
                        // 将评论的内容临时展示到评论区
                        // 获取当前用户名
                        let username = '{{ request.user.username }}'
                        // 制作一个模板
                        let tem = `
                            <div class="panel panel-default">

                                <div class="panel-heading">
                                    <p>
                                        <span class='glyphicon glyphicon-comment'>&nbsp;${username}</span>
                                    </p>
                                </div>

                                <div class="panel-body">
                                    <div>
                                        ${comment}
                                    </div>
                                </div>
                            </div>
                        `
                        // 将模板添加进展示区
                        $('#id_exhibit').append(tem)

                    }else{
                        // 错误的提示信息 只有一种错误用户没有登入
                        $('#id_error').html(args.error_msg)

                    }
                    // 重置 parent_id的值
                    parent_id = null
                }
            })
        })

        // 给回复所在标签绑定事件
        $('.reply').on('click', function () {
            // 获取评论根评论的用户名
            let username = $(this).attr('username')
            // 获取根评论的主键id 修改全局变量parent_id的值
            parent_id = $(this).attr('parent_id')
            // 拼接@用户名 + 换行 + 聚集focus()
            $('#id_comment').val('@' + username + '\n').focus()

        })
    </script>

{% endblock %}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值