【七】文章详情页搭建

发布时间 2023-07-21 15:57:37作者: Chimengmeng

【一】文章详情页搭建

【路由】

  • <str:username>/article/<int:article_id>/
    • username/article/id
 # 文章详情页
    path('<str:username>/article/<int:article_id>/', views.aricle_detail),

【引言】

  • 在每次定义路由后都要先验证路由是否冲突,是否能正常访问
    • 如果不能正常访问,可以选择尝试将路由放在靠前的位置尝试一下
  • 文章详情页个个人站点基本一致
    • 这里采用模板继承的方式
  • 侧边栏的渲染需要数据进行渲染
    • 但是模板继承没办法渲染另一个页面的数据
    • 这里采用的解决的办法就是制作inclesion_tag标签

【需求】

  • 需要展示的效果
    • 布局采用两侧分 3/9
    • 左侧展示个人站点一样的
      • 文章分类
      • 文章标签
      • 日期归档
    • 展示文章标题
    • 展示文章详细内容
    • 具有的功能
      • 点赞
      • 点踩
    • 评论
      • 根评论
      • 子评论
  • 要求
    • 实时展示点赞数/点踩数
    • 用户可以根据主评论追加子评论

【二】侧边栏制作inclusion_tag

  • 创建指定文件
    • book/templatetags/mytag.py
# -*-coding: Utf-8 -*-
# @File : mytag .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/20

from django import template
from django.db.models import Count
from django.db.models.functions import TruncMonth

from book import models

register = template.Library()


# 自定义inclusion_tag
@register.inclusion_tag('left_menu.html')
def left_menu(username):
    # 构造侧边栏需要的数据
    user_obj = models.UserInfo.objects.filter(username=username).first()
    blog = user_obj.blog

    # 查询当前用户所有的分类及分类下的文章数
    category_list = models.Category.objects.filter(blog=blog).annotate(category_num=Count('article__pk')).values('name',
                                                                                                                 'category_num',
                                                                                                                 'pk')
    # 查询当前用户所有的标签及标签下的文章数
    tag_list = models.CategoryTag.objects.filter(blog=blog).annotate(category_num=Count('article__pk')).values('name',
                                                                                                               'category_num',
                                                                                                               'pk')

    # 按照年月统计所有的文章 - 年月归档
    date_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth("create_time")).values(
        "month").annotate(count_num=Count('pk')).values('month', 'count_num')

    return locals()
  • 解释

    • 固定模板写法
    from django import template
    
    register = template.Library()
    
    
    # 自定义inclusion_tag
    @register.inclusion_tag('left_menu.html')
    def left_menu():
        return locals()
    
    • 将这个 inclusion_tag 注册到静态文件(大概原理是)
  • 侧边栏需要展示的内容

    • 文章分类

    • 文章标签

    • 日期归档

    • 代码参考自个人站点搭建,这里只是对那部分代码进行拆分

  • 因为我们查询数据需要用到 username 参数,但是我们在这里获取不到这个参数

    • 因此将他制作成形参,谁需要这个功能谁就传进来一个username
  • left_menu.html

<div class="panel panel-info">
    <div class="panel-heading">
        <h3 class="panel-title">文章分类</h3>
    </div>
    <div class="panel-body">
        {% for category in category_list %}
            <p><a href="/{{ username }}/category/{{ category.pk }}/">{{ category.name }}
                ({{ category.category_num }})</a></p>
        {% endfor %}
    </div>
</div>
<div class="panel panel-danger">
    <div class="panel-heading">
        <h3 class="panel-title">文章标签</h3>
    </div>
    <div class="panel-body">
        {% for tag in tag_list %}
            <p><a href="/{{ username }}/tag/{{ tag.pk }}/">{{ tag.name }} ({{ tag.category_num }})</a></p>
        {% endfor %}

    </div>
</div>
<div class="panel panel-warning">
    <div class="panel-heading">
        <h3 class="panel-title">日期归档</h3>
    </div>
    <div class="panel-body">
        {% for date in date_list %}
            <p><a href="/{{ username }}/archive/{{ date.month|date:"Y-m" }}/">{{ date.month|date:"Y年m月" }}
                ({{ date.count_num }})</a></p>
        {% endfor %}

    </div>
</div>

【三】基础模板搭建(主页面模板)

  • base.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <!--  本地 链接 引入方法  -->
    <!--  Websource 文件夹 拷贝到当前文件夹下即可使用  -->
    <!--  jQuery 文件(先导入)  -->
    <script src="{% static 'js/jquery.min.js' %}"></script>
    <!--  Bootstrap 的 JS 文件 (动画效果需要jQuery)  -->
    <script src="{% static 'plugins/Bootstrap/js/bootstrap.min.js' %}"></script>
    <!--  Bootstrap 的 CSS 样式文件  -->
    <link rel="stylesheet" href="{% static 'plugins/Bootstrap/css/bootstrap.min.css' %}">
    <!-- bootstrap-sweetalert(弹框) 的 CSS 文件   -->
    <link rel="stylesheet" href="{% static 'plugins/bootstrap-sweetalert/dist/sweetalert.css' %}">
    <!-- bootstrap-sweetalert(弹框) 的 JS 文件 -->
    <script src="{% static 'plugins/bootstrap-sweetalert/dist/sweetalert.js' %}"></script>

    <!--  以下为 css样式书写区  -->
    <link rel="stylesheet" href="/Source/css/{{ blog.site_theme }}">

    <style>
        {% block css %}

        {% endblock %}
    </style>

</head>
<body>

{#导航条#}
<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>
            <a class="navbar-brand" href="#">{{ blog.site_title }}</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">
                <li class="active"><a href="#">BBS <span class="sr-only">(current)</span></a></li>
                <li><a href="#">链接</a></li>
                <li class="dropdown">
                    <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">提交</button>
            </form>
            <ul class="nav navbar-nav navbar-right">
                {% if request.user.is_authenticated %}
                    <li><a href="#">{{ request.user.username }}</a></li>
                    <li class="dropdown">
                        <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="#" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
                            <li><a href="#">修改头像</a></li>
                            <li><a href="#">后台管理</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="/log_out/">退出登录</a></li>
                        </ul>
                        <!-- Large modal 修改密码模态框 -->

                        <!-- Large modal -->
                        <div class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog"
                             aria-labelledby="myLargeModalLabel">
                            <div class="modal-dialog modal-lg" role="document">
                                <div class="modal-content">
                                    <h1 class="text-center">修改密码</h1>
                                    <div class="row">
                                        <div class="col-md-8 col-md-offset-2">
                                            <div class="form-group">
                                                <label for="">用户名</label>
                                                <input type="text" disabled value="{{ request.user.username }}"
                                                       class="form-control">
                                            </div>
                                            <div class="form-group">
                                                <label for="">原密码</label>
                                                <input type="password" id="id_old_password" class="form-control">
                                            </div>
                                            <div class="form-group">
                                                <label for="">新密码</label>
                                                <input type="password" id="id_new_password" class="form-control">
                                            </div>
                                            <div class="form-group">
                                                <label for="">确认密码</label>
                                                <input type="password" id="id_confirm_password" class="form-control">
                                            </div>
                                            <button type="button" class="btn btn-default pull-right"
                                                    data-dismiss="modal">
                                                取消修改
                                            </button>

                                            <button class="btn btn-danger center-block pull-right"
                                                    style="margin-bottom: 30px;margin-right: 10px" id="id_edit">
                                                确认修改
                                            </button>
                                            <span style="color: red" id="id_pwd_error"></span>

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

                {% else %}
                    <li><a href="/register/">注册</a></li>
                    <li><a href="/login/">登陆</a></li>
                {% endif %}
            </ul>

        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>

{#导航条内部#}
<div class="container-fluid">
    <div class="row">
        <div class="col-md-3">
            {% load mytag %}
            {% left_menu username %}
        </div>
        <div class="col-md-9">
            {% block content %}

            {% endblock %}
        </div>
    </div>
</div>

{#JS代码#}
{% block js %}

{% endblock %}


</body>
</html>
  • 导航条样式没有改变
    • 因为需要模板继承
    • 所以将文章分类这三个部分用一个block块包裹起来
    • 当某一个页面继承了当前页面的时候
    • 只需要将模模板块内的模板进行替换即可
      • 同时也对css/js代码进行了划分版块
      • 当被继承时,可以直接覆写

【四】文章详情页搭建(继承自base.html)

{% extends 'base.html' %}
{% load static %}
<style>
    {% block css %}
        #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.gif' %}') no-repeat;
            text-align: center;
            cursor: pointer;
            margin-top: 2px;
            padding-top: 5px;
        }

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

        .clear {
            clear: both;
        }

        .diggword {
            margin-top: 5px;
            margin-left: 0;
            font-size: 12px;
            color: #808080;
        }

    {% endblock %}
</style>

{% block content %}
    <h1>{{ article_obj.title }}</h1>
    <div class="article_content">
        {{ article_obj.content|safe }}
    </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" style="color: red"></div>
        </div>
    </div>
    {#  点赞功能结束  #}
    {#  评论展示  #}
    <div>
        <ul class="list-group" style="list-style-type: none">
            {% for comment in comment_list %}

                <li class="">
                    <div class="panel panel-info comment_list">
                        <div class="panel-heading">
                            <span># {{ forloop.counter }} 楼</span>
                            <span>{{ comment.comment_time|date:"Y-m-d H:i:s" }}</span>
                            <span>{{ comment.user.username }}</span>
                            <span><a class="pull-right replay"
                                     username="{{ comment.user.username }}"
                                     comment_id="{{ comment.pk }}">回复</a></span>
                        </div>
                        <div class="panel-body">
                            {#        判断当前评论是否是子评论,如果是子评论需要渲染对应的评论的人名                    #}
                            {% if comment.parent_id %}
                                <p>@ {{ comment.parent.user.username }}</p>
                            {% endif %}
                            {{ comment.content }}
                        </div>
                    </div>
                </li>

            {% endfor %}
        </ul>
    </div>

    {#  评论结束  #}
    {#  文章评论开始  #}
    {% if request.user.is_authenticated %}
        <div>
            <p><span class="glyphicon glyphicon-edit"></span>发表评论</p>
            <div>
                <textarea name="comment" id="id_comment" cols="60" rows="10"></textarea>
            </div>
            <button class="btn btn-primary" id="id_submit">提交评论</button>
            <span style="color: red" id="error"></span>
        </div>
    {% else %}
        <li><a href="/register/">注册</a></li>
        <li><a href="/login/">登陆</a></li>
    {% endif %}
    {#  文章评论结束  #}
{% endblock %}



{#js代码#}
{% block js %}
    <script>
        // 给所有的action类绑定事件
        $('.action').click(function () {
            // $(this).hasClass("diggit") // 当标签中有属性 diggit 的标签被点击时返回true 否则返回false
            let isUp = $(this).hasClass("diggit");
            let $div = $(this);
            // 发送Ajax请求
            $.ajax({
                url: "/up_or_down/",
                type: "post",
                data: {
                    "article_id": "{{ article_obj.pk }}",
                    "is_up": isUp,
                    "csrfmiddlewaretoken": "{{ csrf_token }}",
                },
                success: function (args) {
                    console.log(args)
                    if (args.code === 1000) {
                        $('#digg_tips').text(args.message)
                        // 将前端数字 +1
                        // 先获取到之前的数字
                        let oldNum = $div.children().text(); // 文本是字符类型
                        $div.children().text(Number(oldNum) + 1);
                    } else {
                        $('#digg_tips').html(args.message)
                    }
                },

            })

        })
        // 设置全局的 commentId 变量
        let commentId = null
        // 用户点击评论按钮提交评论
        $("#id_submit").click(function () {
            // 获取用户评论的内容
            let comMent = $('#id_comment').val();
            // 判断当前评论是否是子评论 如果是 需要将手动渲染的 @username删除
            if (commentId) {
                // 先找到\n对应的索引值,利用切片,切片顾头不顾尾,需要索引+1
                let indexNum = comMent.indexOf('\n') + 1;
                comMent = comMent.slice(indexNum); // 将 indexNum 之前的数据全部切除,只保留后面的部分
            }
            $.ajax({
                url: "/comment/",
                type: "POST",
                data: {
                    'article_id': '{{ article_obj.pk }}',
                    "comment": comMent,
                    // 如果commentId没有值,后端存储null也没有关系
                    'commentId': commentId,
                    "csrfmiddlewaretoken": '{{ csrf_token }}'
                },
                success: function (args) {
                    if (args) {
                        $('#error').text(args.message)

                        // 将评论框里面的内容清空
                        $('#id_comment').val('');

                        // 临时渲染本次评论
                        let userName = '{{ request.user.username }}';
                        // 模版字符串
                        let tmp = `
                        <li class="">
                        <div class="panel panel-info comment_list">
                            <div class="panel-heading">
                                <span>${userName}</span>
                                <span><a href="" class="pull-right">回复</a></span>
                            </div>
                            <div class="panel-body">
                                ${comMent}
                            </div>
                        </div>
                        </li>
                        `
                        // 将生成好的标签添加到标签内
                        $(".list-group").append(tmp);
                        // 清空全局的 commentId 字段
                        commentId = null;
                    }
                }
            })
        })

        // 给回复按钮绑定点击事件
        $('.replay').click(function () {
            // 评论对应的评论人的姓名 --- 评论人的评论的主键值
            // 自定义属性值
            // 获取用户名
            let commentUserName = $(this).attr('username')
            // 直接获取主键值,覆盖全局主键值
            commentId = $(this).attr('comment_id')
            // 拼接信息 - 塞给评论框
            $("#id_comment").val('@' + commentUserName + '\n').focus()

        })
    </script>
{% endblock %}

【五】功能实现 - 展示文章详情

【1】前端

<h1>{{ article_obj.title }}</h1>
<div class="article_content">
    {{ article_obj.content|safe }}
</div>
  • 后端传进当前所需要的文章是对象
    • 取到文章对象的标题
    • 取到文章对象的详细内容

【2】后端

def aricle_detail(request, username, article_id):
    # 校验当前用户是否存在
    user_obj = models.UserInfo.objects.filter(username=username).first()
    blog = user_obj.blog
    # 用户如果不存在,返回 404 页面
    if not user_obj:
        return render(request, 'error.html', locals())

    # 先获取文章对象 - 获取当前用户的文章
    article_obj = models.Article.objects.filter(pk=article_id, blog__userinfo__username=username).first()
    # 用户文章不存在,返回 404 页面
    if not article_obj:
        return render(request, 'error.html', locals())

    # 获取当前文章的所有评论内容
    comment_list = models.Comment.objects.filter(article=article_obj)

    return render(request, 'article_detail.html', locals())
  • 主逻辑
    • 接收两个形参
      • username
        • 当前登录的用户名
      • article_id
        • 需要查询的详细文章的主键值
    • 先通过用户名判断当前用户是否存在
      • 用户不存在自动跳转到 404 页面
    • 用户存在,根据文章的主键id获取到文章对象
      • 判断当前文章是否存在
      • 不存在则自动跳转 404 页面
    • 根据文章对象获取到该文章下的所有评论
  • 返回文章详情页面和名称空间

【六】功能实现 - 点赞点踩功能

  • 点赞点踩功能逻辑比较多

    • 同时也为了减少一块代码的过度冗余

    • 新开设路由地址

    • up_or_down/

      path("up_or_down/", views.up_or_down),
      

【1】前端页面搭建

逻辑分析

  • 自己手撸代码比较麻烦且还容易达不到自己想要的效果

    • 在这里我拷贝博客园样式代码进行二次修改(html+css)
    <div id="div_digg">
        <div class="diggit" onclick="votePost(17569860,'Digg')">
            <span class="diggnum" id="digg_count">0</span>
        </div>
        <div class="buryit" onclick="votePost(17569860,'Bury')">
            <span class="burynum" id="bury_count">0</span>
        </div>
        <div class="clear"></div>
        <div class="diggword" id="digg_tips">
        </div>
    </div>
    
  • 点赞点踩是两个不同的功能,在页面上有两个不同的图标

    • 那我们该如何分辨到底是点击了哪个呢?
  • 这里我们采用的方法,仅仅只适用于两个图标的时候

    • 给两个图标的共同类添加一个类(让两个标签同属于一个类)

    • 再给这个公共类绑定点击事件

    • 在点击事件内部的 this 就是被我们点击图标的图标对象

    • 这两个图标的内部都有各自独特的类

      • 我们只需要判断其中我们需要的一个图标是否具有相应的类属性即可
      • 具有类属性的(如点赞图标)被我们判定为true
      • 那false一定是点踩图标
    • 给图标绑定事件,发送Ajax请求

    • 后端将数据处理完毕返回前端,需要对数据渲染

      • 前端页面点赞点踩之后数据要增加 1
      • 但是这里需要注意数据类型的问题
        • 我们可以通过标签取到标签内部的值
        • 在标签的内部值的基础上进行+1操作
        • 但是我们取到的这个值是一个字符串,需要转为字符串才能进行运算
          • Number(num) +1

代码实现

css样式

#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.gif' %}') no-repeat;
    text-align: center;
    cursor: pointer;
    margin-top: 2px;
    padding-top: 5px;
}

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

.clear {
    clear: both;
}

.diggword {
    margin-top: 5px;
    margin-left: 0;
    font-size: 12px;
    color: #808080;
}

html样式

{#  点赞功能开始  #}
<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" style="color: red"></div>
    </div>
</div>
{#  点赞功能结束  #}
  • 我们给两个按钮都绑定了一个共同的类属性action
  • 同时每一个按钮都有自己的类属性
    • diggit action
      • 点赞
    • buryit action
      • 点踩

绑定点击事件

// 给所有的action类绑定事件
$('.action').click(function () {
    // $(this).hasClass("diggit") // 当标签中有属性 diggit 的标签被点击时返回true 否则返回false
    let isUp = $(this).hasClass("diggit");
    let $div = $(this);
    // 发送Ajax请求
    $.ajax({
        url: "/up_or_down/",
        type: "post",
        data: {
            "article_id": "{{ article_obj.pk }}",
            "is_up": isUp,
            "csrfmiddlewaretoken": "{{ csrf_token }}",
        },
        success: function (args) {
            console.log(args)
            if (args.code === 1000) {
                $('#digg_tips').text(args.message)
                // 将前端数字 +1
                // 先获取到之前的数字
                let oldNum = $div.children().text(); // 文本是字符类型
                $div.children().text(Number(oldNum) + 1);
            } else {
                $('#digg_tips').html(args.message)
            }
        },

    })

})
  • 主逻辑分析

    • 首先根据公共类属性,取到两个标签

      • $('.action').click(function () {})
    • 然后绑定事件

    • 在事件内部,提前定义变量存储属于每一个标签的判断标志

      • 具有某个属性的为true,否则为false
      • let isUp = $(this).hasClass("diggit");
    • 在事件内部,提前定义变量存储被点击标签的标志

      • let $div = $(this);
    • 发送Ajax请求

      • 提交数据的地址为点赞点踩的接口
        • url: "/up_or_down/"
      • 请求方式
        • type: "post"
      • 构建数据内容
        • 对某个文章进行点赞点踩对应的文章id
          • "article_id": "{{ article_obj.pk }}"
        • 点赞点踩的标志
          • "is_up": isUp
        • csrf 校验的token
          • "csrfmiddlewaretoken": "{{ csrf_token }}"
        • 成功后触发的事件
        • 如果点赞点踩成功
          • 首先在标签下面渲染提示信息
            • 取到展示提示信息的标签,将标签的文本替换成提示信息
            • $('#digg_tips').text(args.message)
          • 同时还要让点赞数/点踩数 +1
            • 首先要获取到当前标签内部的值
              • let oldNum = $div.children().text()
            • 在原值的基础上进行 +1 操作
              • $div这个是点赞/点踩的标签
              • 我们的错误展示标签框在这个下面,所以取他的儿子标签
              • 数据更改后,将数据塞回到标签内
              • $div.children().text(Number(oldNum) + 1)
        • 点赞点踩失败
          • 提示失败信息

【2】后端接口实现

def up_or_down(request):
    '''
    # 校验用户是否登陆
    # 判断当前文章是否是属于自己,自己不能给自己点赞
    # 判断当前用户是否给当前用户点过赞
    # 操作数据库存入点赞数据
    :param request:
    :return:
    '''
    if request.is_ajax():
        back_dict = {"code": 1000, "message": ""}
        # (1)校验用户是否登陆
        if request.user.is_authenticated:
            # 文章主键值
            article_id = request.POST.get('article_id')
            # 是否点赞  is_up ---- 字符串
            is_up = request.POST.get('is_up')
            # json模块自带功能 js 格式数据 ---- loads ---- py格式数据
            is_up = json.loads(is_up)
            # (2)判断当前文章是否是属于自己,自己不能给自己点赞
            # 根据文章id查文章对象,根据文章对象查作者,比对当前request请求中的用户
            article_obj = models.Article.objects.filter(pk=article_id).first()
            # 是否是属于自己 自己不能给自己点赞
            if not article_obj.blog.userinfo == request.user:
                # 判断当前用户是否给当前用户点过赞
                is_click = models.UpAndDown.objects.filter(user=request.user, article=article_obj)
                if not is_click:
                    # 操作数据库记录数据
                    # 同步普通字段
                    # 判断当前用户是点赞还是点踩了
                    if is_up:
                        # 给点赞数 +1
                        models.Article.objects.filter(pk=article_id).update(up_num=F("up_num") + 1)
                        back_dict["message"] = "点赞成功!感谢支持!"
                    else:
                        # 给点踩数 +1
                        models.Article.objects.filter(pk=article_id).update(down_num=F("down_num") + 1)
                        back_dict["message"] = "点踩成功!感谢反馈!"
                    # 操作点赞点踩表,保存数据
                    models.UpAndDown.objects.create(user=request.user, article=article_obj, is_up=is_up)
                else:
                    back_dict["code"] = 2001
                    if is_up:
                        back_dict["message"] = "您已经点过赞了,不能再点了哦!"
                    else:
                        back_dict["message"] = "您已经点过踩了,不能再点了哦!"
            else:
                back_dict["code"] = 2002
                if is_up:
                    back_dict["message"] = "不支持给自己点赞哦!"
                else:
                    back_dict["message"] = "不支持给自己点踩哦!"
        else:
            # 校验用户是否登陆
            back_dict["code"] = 2003
            back_dict["message"] = '请先 <a href="/login/">登录</a> , 谢谢 !'
        return JsonResponse(back_dict)
  • 主逻辑分析

  • 判断当前请求的请求方式

    • ajax请求
  • 构建基础返回信息字典

    • back_dict = {"code": 1000, "message": ""}
  • 校验当前用户是否登陆

    • 未登录

      # 校验用户是否登陆
      back_dict["code"] = 2003
      back_dict["message"] = '请先 <a href="/login/">登录</a> , 谢谢 !'
      
    • 首先修改状态码

    • 其次修改返回的错误信息

      • 注意这里我们想让返回的信息渲染出来后,点击登录能跳转到登陆界面

      • 我们需要给我们的信息添加标签

        • 在后端是文本
      • 在前端需要渲染

        • 渲染方式

        • .html 直接修改标签的值

          • $('#digg_tips').html(args.message)
        • 修改属性

          • |safe
          • 使用案例:数据库存的是文本的标签属性,但是前端对这段文本进行属性的更改,就能渲染出标签样式
          • {{ article_obj.content|safe }}
        • 后端先渲染,再发给前端

          • mark_safe()
  • 确认用户已经登陆

    • 获取文章的主键值

      • article_id = request.POST.get('article_id')
    • 获取点赞标志

      • is_up = request.POST.get('is_up')
      • 注意这里取到的是文本类型的true/false
      • 我么可以利用json函数的loads方法,进行数据类型强转
        • 可以点进json格式内部查看源码
      • is_up = json.loads(is_up)
    • 根据文章主键值查找当前文档所对应的文章

      • models.Article.objects.filter(pk=article_id).first()
    • 通过文章对象跨表查询这篇文章的作者是否和当前登录用户相同

      • 用户相同,不允许点赞

        • 修改状态码
        • 修改提示信息
        # 是否是属于自己 自己不能给自己点赞
        if not article_obj.blog.userinfo == request.user:
            ...
        else:
            back_dict["code"] = 2002
            if is_up:
                back_dict["message"] = "不支持给自己点赞哦!"
            else:
                back_dict["message"] = "不支持给自己点踩哦!"
        
    • 用户不同

      • 根据登录的用户和文章在点赞点踩表中查询到是否已经点过赞/点过踩

      • 如果点过了

        • 修改状态码
        • 修改提示信息
        # 判断当前用户是否给当前用户点过赞
        is_click = models.UpAndDown.objects.filter(user=request.user, article=article_obj)
        if not is_click:             
            ...
        else:
            back_dict["code"] = 2001
            if is_up:
                back_dict["message"] = "您已经点过赞了,不能再点了哦!"
            else:
                back_dict["message"] = "您已经点过踩了,不能再点了哦!"
        
    • 用户不同且没有点过赞

      • 判断当前的标签是点赞还是点踩

        • 根据传过来的is_up值即可判断
      • 点赞

        # 判断当前用户是点赞还是点踩了
        if is_up:
            # 给点赞数 +1
            models.Article.objects.filter(pk=article_id).update(up_num=F("up_num") + 1)
            back_dict["message"] = "点赞成功!感谢支持!"
        
      • 点踩

        else:
            # 给点踩数 +1
            models.Article.objects.filter(pk=article_id).update(down_num=F("down_num") + 1)
            back_dict["message"] = "点踩成功!感谢反馈!"
        
      • 向数据库提交数据,保存数据

        # 操作点赞点踩表,保存数据
        models.UpAndDown.objects.create(user=request.user, article=article_obj, is_up=is_up)
        

【七】功能实现 - 评论功能(根评论/子评论)

【1】思路分析

评论功能先写根评论

  • 前端(提交数据)

    • 用根评论先将整体的逻辑跑通,再去填补其他的空白

    • 前端要有用户输入评论的标签

      • 点赞点踩会给评论框带来浮动影响
        • clearfix 清除浮动
    • 评论完点击按钮发送Ajax请求

  • 后端(处理传过来的数据)

    • 开设专门的路由接口处理
  • 前端(渲染返回的信息)

    • DOM操作临时渲染评论楼
      • 效果就是评论完将评论信息渲染到最底下的那一个
      • 页面刷新评论完全展示
    • 永久渲染
      • 后端直接将所有的评论信息传递给前端页面
      • 前端页面对所有的信息进行渲染
      • 评论楼的样式参考博客园

再写子评论

  • 从回复按钮入手
    • 点击回复按钮都发生了哪些事?
      • 首先评论框聚焦
        • .focus()
      • 其次评论框内提示评论的是谁的评论
        • @username + '\n'
  • 思考
    • 根评论和子评论点击的是同一个按钮,那么根评论和子评论的区别在哪
  • 其实我们只需要给Ajax添加一个父评论的标志即可
  • 点击回复按钮后,应该获取到当前根评论对应的用户名和主键值
    • 针对主键值,要做进一步处理
    • 针对子评论
      • 在前端展示我们想要的数据格式
      • 但是在后端存储我们要将多余的数据清除,只留下评论的内容

【2】功能实现 - 前端评论楼搭建

评论楼渲染

{#  评论展示  #}
    <div>
        <ul class="list-group" style="list-style-type: none">
            {% for comment in comment_list %}

                <li class="">
                    <div class="panel panel-info comment_list">
                        <div class="panel-heading">
                            <span># {{ forloop.counter }} 楼</span>
                            <span>{{ comment.comment_time|date:"Y-m-d H:i:s" }}</span>
                            <span>{{ comment.user.username }}</span>
                            <span><a class="pull-right replay"
                                     username="{{ comment.user.username }}"
                                     comment_id="{{ comment.pk }}">回复</a></span>
                        </div>
                        <div class="panel-body">
                            {#        判断当前评论是否是子评论,如果是子评论需要渲染对应的评论的人名                    #}
                            {% if comment.parent_id %}
                                <p>@ {{ comment.parent.user.username }}</p>
                            {% endif %}
                            {{ comment.content }}
                        </div>
                    </div>
                </li>

            {% endfor %}
        </ul>
    </div>

    {#  评论结束  #}
  • 循环获取每一条评论进行渲染

校验用户是否登陆

{#  文章评论开始  #}
    {% if request.user.is_authenticated %}
        <div>
            <p><span class="glyphicon glyphicon-edit"></span>发表评论</p>
            <div>
                <textarea name="comment" id="id_comment" cols="60" rows="10"></textarea>
            </div>
            <button class="btn btn-primary" id="id_submit">提交评论</button>
            <span style="color: red" id="error"></span>
        </div>
    {% else %}
        <li><a href="/register/">注册</a></li>
        <li><a href="/login/">登陆</a></li>
    {% endif %}
    {#  文章评论结束  #}
  • 判断当前用户是否登陆
    • if request.user.is_authenticated
  • 未登录展示注册登录需要先登录使用
  • 已登录展示评论框

绑定事件 - 主评论 -提交评论

// 设置全局的 commentId 变量
        let commentId = null
        // 用户点击评论按钮提交评论
        $("#id_submit").click(function () {
            // 获取用户评论的内容
            let comMent = $('#id_comment').val();
            // 判断当前评论是否是子评论 如果是 需要将手动渲染的 @username删除
            if (commentId) {
                // 先找到\n对应的索引值,利用切片,切片顾头不顾尾,需要索引+1
                let indexNum = comMent.indexOf('\n') + 1;
                comMent = comMent.slice(indexNum); // 将 indexNum 之前的数据全部切除,只保留后面的部分
            }
            $.ajax({
                url: "/comment/",
                type: "POST",
                data: {
                    'article_id': '{{ article_obj.pk }}',
                    "comment": comMent,
                    // 如果commentId没有值,后端存储null也没有关系
                    'commentId': commentId,
                    "csrfmiddlewaretoken": '{{ csrf_token }}'
                },
                success: function (args) {
                    if (args) {
                        $('#error').text(args.message)

                        // 将评论框里面的内容清空
                        $('#id_comment').val('');

                        // 临时渲染本次评论
                        let userName = '{{ request.user.username }}';
                        // 模版字符串
                        let tmp = `
                        <li class="">
                        <div class="panel panel-info comment_list">
                            <div class="panel-heading">
                                <span>${userName}</span>
                                <span><a href="" class="pull-right">回复</a></span>
                            </div>
                            <div class="panel-body">
                                ${comMent}
                            </div>
                        </div>
                        </li>
                        `
                        // 将生成好的标签添加到标签内
                        $(".list-group").append(tmp);
                        // 清空全局的 commentId 字段
                        commentId = null;
                    }
                }
            })
        })  
  • 主逻辑分析

    • 获取全局标签

      • let commentId = null
      • 全局子评论标签内容
        • 如果没有子评论默认为空
          • 后端允许为空
        • 如果有子评论
          • 在子评论内重新赋值
    • 给提交评论按钮绑定点击事件

      • $("#id_submit").click(function () {}
    • 绑定Ajax请求事件

      • 获取当前用户的评论内容

        • let comMent = $('#id_comment').val();
      • 根据全局子评论变量判断是否是子品论

        • 是子评论,进行子品论文本处理

          • 对子评论内容进行切片 - 删除不需要的内容

          • 找到\n所在的位置,切片顾头不顾尾,所以索引+1

          • let indexNum = comMent.indexOf('\n') + 1;

            切片前
            @username\n
            ...
            
            切片后
            ...
            
          • 对数据进行切片

          • comMent = comMent.slice(indexNum);

        • 不是子评论,进行主评论文本处理

      • 绑定Ajax事件

        • 请求地址

          • url: "/comment/"
        • 请求类型

          • type: "POST"
        • 构建数据

          • 文章主键值
          • 'article_id': '{{ article_obj.pk }}'
          • 根评论内容
          • "comment": comMent
          • 子评论内容
          • 'commentId': commentId
          • csrf的token
          • "csrfmiddlewaretoken": '{{ csrf_token }}'
        • 请求成功,绑定事件

          • 渲染返回的提示信息
            • 找到提示信息的标签框,并将信息放进去
            • $('#error').text(args.message)
            • 评论成功后将评论框清空
            • $('#id_comment').val('');
            • 渲染本次评论的内容
              • 利用模版语法进行渲染
              • 这里渲染内容前端采用.html()方法
            • 操作结束后要将子评论内容清空,否则每次提交的评论都有值,都会渲染到当前评论下
          success: function (args) {
          if (args) {
          $('#error').text(args.message)
          
          // 将评论框里面的内容清空
          $('#id_comment').val('');
          
          // 临时渲染本次评论
          let userName = '{{ request.user.username }}';
          // 模版字符串
          let tmp = `
          <li class="">
              <div class="panel panel-info comment_list">
                  <div class="panel-heading">
                      <span>${userName}</span>
                      <span><a href="" class="pull-right">回复</a></span>
                  </div>
                  <div class="panel-body">
                      ${comMent}
                  </div>
              </div>
          </li>
          `
          // 将生成好的标签添加到标签内
          $(".list-group").append(tmp);
          // 清空全局的 commentId 字段
          commentId = null;
          }
          }
          

绑定事件 - 子评论 - 回复

  • 因为根评论和子评论点击的回复标签都是同一个,所以采用自定义属性值进行标签的区分
    • 根据自定义的属性值取到标签
    • let commentUserName = $(this).attr('username')
    • 或者评论框内容,覆盖全局子评论内容
    • 在回复框内拼接新信息,并聚焦评论框,点击提交评论按钮触发提交评论事件
 // 给回复按钮绑定点击事件
        $('.replay').click(function () {
            // 评论对应的评论人的姓名 --- 评论人的评论的主键值
            // 自定义属性值
            // 获取用户名
            let commentUserName = $(this).attr('username')
            // 直接获取主键值,覆盖全局主键值
            commentId = $(this).attr('comment_id')
            // 拼接信息 - 塞给评论框
            $("#id_comment").val('@' + commentUserName + '\n').focus()

        })

【3】功能实现 - 后端处理评论内容

路由接口

  • comment/

代码实现

def comment(request):
    if request.is_ajax():
        if request.method == 'POST':
            back_dict = {"code": 1000, "message": ""}
            # (1)校验用户是否登陆
            if request.user.is_authenticated:
                article_id = request.POST.get('article_id')
                comment = request.POST.get('comment')
                commentId = request.POST.get('commentId')

                # 直接操作评论表存储数据 ---两张表
                with transaction.atomic():
                    models.Article.objects.filter(pk=article_id).update(comment_num=F("comment_num") + 1)
                    models.Comment.objects.create(user=request.user, article_id=article_id, content=comment,parent_id=commentId)
                back_dict["message"] = "发表评论成功!"
            else:
                back_dict["code"] = 2001
                back_dict["message"] = "请先登录!"

            return JsonResponse(back_dict)
  • 主逻辑分析

  • 校验当前请求方式

    • if request.is_ajax()
    • if request.method == 'POST'
  • 构建基础返回信息字典

    • back_dict = {"code": 1000, "message": ""}
  • 校验当前用户是否登陆

    • if request.user.is_authenticated
  • 读取必要的参数

    • 文章的主键值
      • article_id = request.POST.get('article_id')
    • 根评论内容
      • comment = request.POST.get('comment')
    • 子评论内容
      • 子评论可能为空,但是我们在设计表的时候,允许该字段为空,影响不大
      • commentId = request.POST.get('commentId')
  • 开启事务,在事务内处理数据

    • with transaction.atomic():

    • 文章表的评论数 + 1

      • models.Article.objects.filter(pk=article_id).update(comment_num=F("comment_num") + 1)
    • 文章评论表内的评论内容存储

      • models.Comment.objects.create(user=request.user, article_id=article_id, content=comment,parent_id=commentId)
    • 构建信息

      • 评论成功
        • back_dict["message"] = "发表评论成功!"
      • 评论失败
        • back_dict["code"] = 2001
        • back_dict["message"] = "请先登录!"

未登录用户无法使用评论功能,所以返回请登录的信息

【补充】前端渲染文本属性的标签样式

【1】直接修改标签的值(Directly modifying the value of a tag):

  • 这种方式适用于需要将数据直接插入到HTML标签中的情况。
  • 一种常用的方法是使用jQuery库来选中需要修改的标签
    • 并使用.html()方法来设置新的值。
    • 例如:
$('#digg_tips').html(args.message);
  • 上述代码会将args.message的值直接赋予id为"digg_tips"的标签内部。

【2】修改属性(Modifying attributes):

  • 当需要修改标签的属性而不仅仅是内部内容时
    • 可以使用管道(pipe)中的|safe过滤器来确保属性内容被当做原始HTML进行渲染。
  • 这对于将存储在数据库中的文本作为标签属性
    • 并在前端进行样式渲染非常有用。
    • 例如:
{{ article_obj.content|safe }}
  • 上述代码会将article_obj.content的值作为标签的属性
    • 并在前端渲染时将其作为HTML代码进行解析。

【3】后端先渲染,再传递给前端(Rendering on the backend and then passing it to the frontend):

  • 有时,可以在后端进行模板渲染,并使用mark_safe()函数将生成的HTML标记为安全。
    • 然后,将包含渲染结果的HTML传递给前端进行展示。
    • 这种方式通常用于在后端生成动态内容,并将其作为静态HTML响应发送给前端。
    • 例如:
from django.utils.safestring import mark_safe

def render_article(request, article_data):
    # 后端渲染逻辑
    rendered_content = generate_rendered_content(article_data)
    safe_rendered_content = mark_safe(rendered_content)

    return render(request, 'article.html', {'rendered_content': safe_rendered_content})
  • 上述代码中,generate_rendered_content()函数在后端进行模板渲染
    • 并返回生成的HTML内容。
  • 然后,使用mark_safe()函数将其标记为安全,确保在前端展示时被解析为HTML。

【补充】制作inclusion_tag标签

【一】inclusion_tag标签详解

  • inclusion_tag是Django框架提供的一个有用的标签
    • 它允许开发者在模板中重用一段HTML代码。
  • 通过定义和注册自己的inclusion tag
    • 您可以将常见的显示逻辑封装为一个可重复使用的组件。

【1】详解

  • 首先,在Django的views.py文件或者一个单独的Python模块中
    • 定义您的inclusion_tag。
from django import template
from django.shortcuts import render

register = template.Library()

@register.inclusion_tag('your_template.html')
def your_inclusion_tag(parameter):
    # 添加您的处理逻辑
    # 基于传入的参数,执行相应的操作
    context = {
        'parameter': parameter,
        # 添加其他需要传递给模板的变量
    }
    return context
  • 在您的模板文件(例如,your_template.html)中
    • 使用your_inclusion_tag
{% load your_template %}

<!DOCTYPE html>
<html>
<head>
    <title>Your Page</title>
</head>
<body>
    {% your_inclusion_tag "your_value" %}
</body>
</html>
  • 最后
    • 在视图函数中
    • 将模板渲染为响应并返回。
from django.shortcuts import render

def your_view(request):
    # 添加您的视图逻辑
    # 通常,您会处理一些数据,并将其传递给模板
    return render(request, 'your_template.html')
  • 这是一个基本的示例
    • 展示了如何使用inclusion_tag
    • 当您在模板中调用your_inclusion_tag标签时
    • 将会渲染与该标签关联的模板,并传递给它指定的参数。

这样,您就可以创建可重复使用的HTML组件,并在多个页面中使用它们,从而提高代码的可维护性和重用性。

【二】制作步骤

当我们有重复需要的HTML代码,并且页面中的数据也需要反复使用的时候,就可以考虑制作成inclusion_tag标签

【1】创建templatetags文件夹

  • 在项目的app目录下创建一个 templatetags文件夹

【2】创建自定义文件名的py文件

  • 在上面的 templatetags文件夹下创建一个py文件
    • py文件名可以是任意自定义

【3】在py文件中书写固定语法

  • 在新创建的py文件头部添加两行固定代码
from django import templete
register = templete.Lirary()

【4】书写自定义的代码逻辑

from django import template

register = template.Library()

@register.inclusion_tag(left_menu.html)
def index():
	准备上述页面需要的数据
	return locals()