15. Django博客项目4 博客后台管理 文章的增加&编辑&删除

22. 后台管理页面搭建

所有的导航条的后台管理标签都绑定路由.
<!-- 6.9 后台管理链接 -->
<li><a href="/backstage/">后台管理</a></li>
22.1 路由层
    # 16. 后台管理 backstage
    url(r'^backstage/', views.backstage),
22.2 视图层
# 12 后台管理
@login_required
def backstage(request):
    return render(request, 'backstage/backend.html')
22.3 模板层
后台管理中的页面很多, 可以在templates目录下新建一个目录存放, 用于划分管理.
1. 模板
后台管理的页面导航条, 侧边栏, 管理选择是固定的, 需要改变的是对应区域的内容, 将固定区域制作成模板.

2022-04-03_00027

左侧菜单栏使用一个 Accordion example 可折叠栏
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
    <div class="panel panel-default">
        <div class="panel-heading" role="tab" id="headingOne">
            <h4 class="panel-title">
                <a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
                    <!--标题-->
                </a>
            </h4>
        </div>
        <!--折叠关联设置-->
        <div id="collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne">
            <div class="panel-body">
			<!--展示的内容-->
            </div>
        </div>
    </div>
</div>
右侧使用展示一个标签页
<div>

  <!-- Nav tabs -->
  <ul class="nav nav-tabs" role="tablist">
    <li role="presentation" class="active"><a href="#home" aria-controls="home" role="tab" data-toggle="tab">Home</a></li>
    <li role="presentation"><a href="#profile" aria-controls="profile" role="tab" data-toggle="tab">Profile</a></li>
    <li role="presentation"><a href="#messages" aria-controls="messages" role="tab" data-toggle="tab">Messages</a></li>
    <li role="presentation"><a href="#settings" aria-controls="settings" role="tab" data-toggle="tab">Settings</a></li>
  </ul>

  <!-- Tab panes -->
  <div class="tab-content">
    <div role="tabpanel" class="tab-pane active" id="home">...</div>
    <div role="tabpanel" class="tab-pane" id="profile">...</div>
    <div role="tabpanel" class="tab-pane" id="messages">...</div>
    <div role="tabpanel" class="tab-pane" id="settings">...</div>
  </div>

</div>

GIF 2022-4-3 8-42-53

tempates/backstage/back_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/{{ request.user.blog.site_theme }}/">
    <!--替换区域-->

    {% block css %}
    {% endblock %}
</head>
<body>

<div>
    <!--导航条-->
    <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.blog.site_name }}</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>

    <!--布局容器-->
    <div class="container-fluid">
        <!--页面布局 2 - 10 -->
        <div class="row">
            <!--侧边栏 2-->
            <div class="col-md-2">
                <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                    <div class="panel panel-default">
                        <div class="panel-heading" role="tab" id="headingOne">
                            <h4 class="panel-title">
                                <a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne"
                                   aria-expanded="true" aria-controls="collapseOne">
                                    <!--标题-->
                                    更多操作
                                </a>
                            </h4>
                        </div>


                        <!--折叠关联设置-->
                        <div id="collapseOne" class="panel-collapse collapse in" role="tabpanel"
                             aria-labelledby="headingOne">
                            <div class="panel-body">
                                <!--展示的内容-->
                                <p><a href="">添加文章</a></p>
                                <br>
                                <p><a href="">添加随笔</a></p>
                                <br>
                                <p><a href="">草稿箱</a></p>
                                <br>
                                <p><a href="">其他</a></p>

                            </div>
                        </div>

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

            <!--侧边栏 10-->
            <div class="col-md-10">
                <div>

                    <!-- Nav tabs -->
                    <ul class="nav nav-tabs" role="tablist">
                        <li role="presentation" class="active"><a href="#home" aria-controls="home" role="tab"
                                                                  data-toggle="tab">文章</a></li>
                        <li role="presentation"><a href="#profile" aria-controls="profile" role="tab" data-toggle="tab">随笔</a>
                        </li>
                        <li role="presentation"><a href="#messages" aria-controls="messages" role="tab"
                                                   data-toggle="tab">草稿</a></li>
                        <li role="presentation"><a href="#settings" aria-controls="settings" role="tab"
                                                   data-toggle="tab">设置</a></li>
                    </ul>

                    <!-- Tab panes -->
                    <div class="tab-content">
                        <div role="tabpanel" class="tab-pane active" id="home">
                            <!--文章展示页面-->
                            {% block article %}
                            {% endblock %}
                        </div>
                        <div role="tabpanel" class="tab-pane" id="profile">
                            <!--随笔展示页面-->
                            {% block essay %}
                            {% endblock %}
                        </div>
                        <div role="tabpanel" class="tab-pane" id="messages">
                            <!--草稿页面-->
                            {% block draft %}
                            {% endblock %}
                        </div>
                        <div role="tabpanel" class="tab-pane" id="settings">
                            <!--设置页面-->
                            {% block setup %}
                            {% endblock %}
                        </div>
                    </div>

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

    </div>
</div>
<!--js独立区域-->
{% block js %}

{% endblock %}
</body>
</html>
2. 文章管理页面
新建一个文章管理页面
article_page.html
<!--继承模板-->
{% extends 'backstage/back_base.html' %}
{% block article %}
    <!--文章管理页面内容-->
{% endblock %}
使用分页器展示所有的文章
# 12 后台管理
@login_required
def backstage(request):
    # 12.2 获取所有的文章
    article_list = models.Article.objects.filter(blog=request.user.blog)
    print(article_list)
    # 12.3 分页器
    # 导入分页器模板模块
    from app01.utils.pages import Pagination
    # 12.4 计算总数
    all_queryset_sum = article_list.count()

    # 12.5 获取当前的页码  获取的是字符串
    page_num = request.GET.get('page', 1)

    # 12.6 实例化对象
    page_obj = Pagination(
        current_page=page_num,  # 自动转换类型
        all_count=all_queryset_sum,
        per_page_num=2,
        pager_count=10,

    )
    """ 
    current_page: 当前页              必须的 自动转换类型
    all_count:    数据库中的数据总条数  必须的 
    per_page_num: 每页显示的数据条数    默认10
    pager_count:  最多显示的页码个数    默认10
    """
    # 5.6 切片操作
    page_queryset = article_list[page_obj.start: page_obj.end]

    # 12.1 返回后台管理页面
    return render(request, 'backstage/article_page.html', locals())

<!--继承模板-->
{% extends 'backstage/back_base.html' %}
{% block article %}
    <!--文章管理页面内容-->
    <div>
        <table class="table table-striped table-hover">
            <thead>
            <tr>
                <th>文章</th>
                <th>点赞</th>
                <th>点踩</th>
                <th>评论数</th>
                <th>操作</th>
                <th>操作</th>
            </tr>
            </thead>

            <tbody>
            {% for article_obj in page_queryset %}
                <tr>
                    <td><a href="/{{ request.user.username}}/article/{{ article_obj.pk }}/">{{ article_obj.title }}</a></td>
                    <td>{{ article_obj.up_num }}</td>
                    <td>{{ article_obj.down_num }}</td>
                    <td>{{ article_obj.comment_num }}</td>
                    <td><a href="">编辑</a></td>
                    <td><a href="">删除</a></td>
                </tr>

            {% endfor %}

            </tbody>

        </table>
    </div>
    <!--分页器-->
    <div class="pull-right">
        {{ page_obj.page_html|safe }}
    </div>
{% endblock %}
文章的标签使用a标签包裹. 在点击标题的时候跳转到文章的详细展示页面
/{{ request.user.username}}/article/{{ article_obj.pk }}/
用户/article/文章的主键 

image-20220403095029897

为所有导航条的设置头像a标签设置路由
<li><a href="/backstage/">后台管理</a></li>

23. 添加文章

<!--侧边栏的 添加文章a标签设置路由-->
<p><a href="/add/article/">添加文章</a></p>
23.1 路由层
 	# 17.添加文章
    url(r'^add/article/', views.add_article),
23.2 视图层
# 13. 添加文章
def add_article(request):
    # 13.1 获取当前站点的所有分类
    sort_list = models.Sort.objects.filter(blog=request.user.blog)
    print(sort_list)

    # 13.2 获取当前站点的所有标签
    tag_list = models.Tag.objects.filter(blog=request.user.blog)
    print(tag_list)
    
    # 13.3 返回添加文章页面
    return render(request, 'backstage/add_article.html', locals())
23.3 模板层
<!--继承模板-->
{% extends 'backstage/back_base.html' %}
{% block article %}
    <div>
        <!--使用form表单提交数据-->
        <form action="" method="post">
            <!--csrf-->
            {% csrf_token %}
            <h3 class="bg-info">添加文章</h3>

            <!--标题-->
            <div class="form-group">
                <p>标题</p>
                <input type="text" name="title" class="form-control">
            </div>


            <!--大段文本框-->

            <div class="form-group">
                <p>内容</p>
                <textarea name="content" id="id_content" cols="30" rows="10"></textarea>
            </div>

            <!--分类选择-->
            <div class="form-group">
                <p>分类</p>
                {% for sort_obj in sort_list %}
                    <!--提交是分类的id 展示的是分类的名字-->
                    <label class="radio-inline">
                        <input type="radio" name="sort"
                               value="{{ sort_obj.pk }}">{{ sort_obj.name }}
                    </label>

                {% endfor %}
            </div>


            <!--标签选择-->
            <div class="form-group">
                <p>标签</p>
                {% for tag_obj in tag_list %}
                    <label class="checkbox-inline">
                        <input type="checkbox" name="tag" value="{{ tag_obj.pk }}">{{ tag_obj.name }}
                    </label>


                {% endfor %}
            </div>


            <!--提交按钮-->
            <div class="form-group">
                <input type="submit" class="btn btn-primary btn-block">
            </div>

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

image-20220403204121707

<!--模板层中点提交文章a标签绑定跳转地址.-->
<p><a href="/add/articlr/">添加文章</a></p>
23.4 Kind编辑器
KindEditor 是一套开源的在线HTML编辑器,主要用于让用户在网站上获得所见即所得编辑效果,开发人员可以用 KindEditor 把传统的多行文本输入框(textarea)替换为可视化的富文本输入框。 KindEditor 使用 JavaScript 编写,可以无缝地与 Java、.NET、PHP、ASP 等程序集成,比较适合在 CMS、商城、论坛、博客、Wiki、电子邮件等互联网应用上使用。
下载地址: http://kindeditor.net/down.php

image-20220403104403370

使用方法:
1.在需要显示编辑器的位置添加textarea输入框。
<textarea id="editor_id"> </textarea></textarea>
id在当前页面必须是唯一的值。
2.在该HTML页面添加以下脚本。
<script charset="utf-8" src="/editor/kindeditor.js"></script>  <!--引入文件二选一即可-->
<script charset="utf-8" src="/editor/lang/zh-CN.js"></script>  <!--zh-CN中文-->
<script>
        KindEditor.ready(function(K) {
                window.editor = K.create('#editor_id');
        });
</script>
3.获取HTML数据
// 取得HTML内容
html = editor.html();

// 同步数据后可以直接取得textarea的value
editor.sync();
html = document.getElementById('editor_id').value; // 原生API
html = K('#editor_id').val(); // KindEditor Node API
html = $('#editor_id').val(); // jQuery

// 设置HTML内容
editor.html('HTML内容');
基本参数:
width: 编辑器的宽度,可以设置px或%,比textarea输入框样式表宽度优先度高。
    数据类型: String
    默认值: textarea输入框的宽度
    
height: 编辑器的高度,只能设置px,比textarea输入框样式表高度优先度高。
    数据类型: String
    默认值: textarea输入框的高度

minHeight: 指定编辑器最小高度,单位为px。
    数据类型: Int
    默认值: 100

resizeType:
	数据类型: Int
	0: 不能拖动文本框大小
	1: 只能改变高度
	2: 可以随意改动

items: 配置编辑器的工具栏,其中”/”表示换行,”|”表示分隔符。
    数据类型: Array
    默认不写展示全部的工具.
	更多查看: http://kindeditor.net/docs/option.html
23.5 Kind编辑使用
下载后解压将文件复制到静态文件static目录中

image-20220403114731596

{% block js %}
    {% load static %}
    <script charset="utf-8" src="{% static 'kindeditor/kindeditor-all-min.js' %}"></script>  <!--需要的js文件-->
    <script>
        KindEditor.ready(function (K) {
            window.editor = K.create(
                // 作用textarea标签的id
                '#id_content',
                // 写配置
                {
                    width: '100%',
                    height: '500px',
                    resizeType: 1,
                });
        });
    </script>

{% endblock %}

image-20220403203826326

23.6 将文章添加进数据库
后端获取form表单提交的数据, 提交方式为post, 提交成功后返回到管理页面.

后端需要操作两张表:
1. 文章表 
	字段有 标题title 简介desc 内容content 分类 个人站点(一定不要忘记这个) (点赞点踩评论数默认值)
2. 文章与标签的第三张表
	标签
# 13. 添加文章
def add_article(request):
    # 13.4 判断请求方式:
    if request.method == 'POST':
        # 13.5 获取数据 标签, 内容, 分类id, 标签id,可能存在多个(getlist)
        post_obj = request.POST
        title = post_obj.get('title')
        content = post_obj.get('content')
        sort_id = post_obj.get('sort_id')
        tag_id_list = post_obj.getlist('tag_id_list')

        # 文章简介截取文章的前端128个字符
        desc = content[0:128]
        print(sort_id, tag_id_list)
        # 操作文章表
        article_obj = models.Article.objects.create(
            title=title,
            desc=desc,
            content=content,
            sort_id=sort_id,
            blog=request.user.blog)

        # 批量操作第三张表 (第三张表是手动创建的, 无法使用ORM的add set, remove, clear方法)
        # 手动操作第三张表, 可能操作多次使用 bulk_create 批量插入
        # 定义一个列表
        article_obj_list = []
        for tag_id in tag_id_list:
            article_to_tag_obj = models.ArticleToTag(article=article_obj, tag_id=tag_id)
            article_obj_list.append(article_to_tag_obj)

        # 一次插入
        models.ArticleToTag.objects.bulk_create(article_obj_list)

        # 文件写入成功之后, 跳转页面
        return redirect('/backstage/')

    # 13.1 获取当前站点的所有分类
    sort_list = models.Sort.objects.filter(blog=request.user.blog)
    # print(sort_list)

    # 13.2 获取当前站点的所有标签
    tag_list = models.Tag.objects.filter(blog=request.user.blog)
    # print(tag_list)

    # 13.3 返回添加文章页面
    return render(request, 'backstage/add_article.html', locals())

image-20220403230139882

image-20220403233538521

23.7 添加文章问题
文章添加成功之后还存在两个问题.
1. 文章的简介是截取html格式的文章, 看起来杂乱.
   解决方法: 将文本信息提起出来之后(剔除标签)再截取文本的字符作为文章简介.

image-20220404070016082

2. 文章使用html书写, 用户可以在html中利用script标签写JavaScript代码, js可以写死循环, 
   不停的向后端发送请求, 如果多写些文章, 不停的向服务器发送请求, 造成XSS脚本攻击.
   简介方法:
   将script标签的内容注释
   删除script标签
写一个while简单测试.
<script>
    while (1){
        alert(123)
    }
</script>

image-20220404082241751

Kind编辑器点击html按钮, 之后写html代码, 不点html按钮写的就是段落, 被p标签包裹.

image-20220404081231378

打开文章会卡死, 开发者工具都出卡死.

image-20220404082055165

23.8 beautifulsoup4模块
beautiful soup使用Python得到一个库只要的功能可以从HTML或XML文件中提取数据, 能将上面两个问题解决.

image-20220404075846703

使用方式:
安装     pip install beautifulsoup4
导入模块 from bs4 import BeautifulSoup
方法:
find_all()  获取所有标签
标签.decompose() 删除标签
text  获取所有文本--> str类型
# 使用BeautifulSoup模块 文本内容, 模式
soup = BeautifulSoup(content, 'html.parser')
# 获取所有标签
tags = soup.find_all()

# 删除script标签
for tag in tags:
if tag.name == 'script':
# 删除标签
tag.decompose()

# 截取文章文本128个字符
desc = soup.text[0:150]
再次测试

2022-04-04_00039

文章的简介正常.

image-20220404083532886

文章展示区域的script被删除.

image-20220404083721432

24. 编辑器上传文件

24.1 上传接口
KindEditor默认提供ASP、ASP.NET、PHP、JSP上传程序,这些程序是演示程序,建议不要直接在实际项目中使用。
详细: http://kindeditor.net/docs/upload.html
Kind编辑器框架, 写好了图片上传接口, 但这个接口是开发者的, 需要替换成自己的接口, 不然就会失效.

GIF 2022-4-4 9-25-13

// JSP
KindEditor.ready(function(K) {
        K.create('#textarea_id', {
                uploadJson : '../jsp/upload_json.jsp',
                fileManagerJson : '../jsp/file_manager_json.jsp',
                allowFileManager : true
        });
});
使用form表单提交, 提交方式为POST.
POST参数:
imgFile: 文件form名称
dir: 上传类型,分别为image、flash、media、file

返回格式(JSON)
//成功时
{
        "error" : 0,
        // 图片的路径
        "url" : "http://www.example.com/path/to/file.ext"
}
//失败时
{
        "error" : 1,
        "message" : "错误信息"
}
采用表单就需要进行csrf检验.
编辑器的 extraFileUploadParams:
上传图片、Flash、视音频、文件时,支持添加别的参数一并传到服务器。
{% block js %}
    {% load static %}
    <script charset="utf-8" src="{% static 'kindeditor/kindeditor-all-min.js' %}"></script>  <!--需要的js文件-->
    <script>
        KindEditor.ready(function (K) {
            window.editor = K.create(
                // 作用textarea标签的id
                '#id_content',
                // 写配置
                {
                    width: '100%',
                    height: '500px',
                    resizeType: 1,
                    // 图片上传的地址,  form表单post方式上传文件, 如果是表单就需要csrf校验
                    uploadJson: '/upload_img/',
                    // 提交额外的参数
                    extraFileUploadParams: {
                        // 提交csrf 的数据
                        'csrfmiddlewaretoken': "{{csrf_token }}",
                    }
                });
        });
    </script>
{% endblock %}
24.2 路由层
    # 18. 上传文件
    url(r'upload_img/', views.upload_img),
24.3 视图层
# 14. 上传图片
def upload_img(request):
    # 14.1 判断请求方式
    if request.method == 'POST':
        # 14.2 获取文件 表单的name=imgFile
        print(request.FILES)  # <MultiValueDict: {'imgFile': [<InMemoryUploadedFile: 头像2.png (image/png)>]}>

    return HttpResponse('OK')

GIF 2022-4-4 9-30-00

# 拼接图片的路径, 图片名需要是唯一标识 file_obj.name是上传文件的名字, 加上一个uuid 随机字符串在后面
import uuid
randon_str = str(uuid.uuid4())  # file_obj.name--> xxx.jpg
name, suffix = file_obj.name.split('.')
file_name = name + '_' + randon_str + '.' + suffix

print(file_name)
# 头像2_6ec03913-27ad-4501-949a-9f95f990d54e.png

image-20220404102648102

上传文件之后有一个固定分返回值格式:
//成功时
{
        "error" : 0,
        // 文件的路径
        "url" : "http://www.example.com/path/to/file.ext"
}
//失败时
{
        "error" : 1,
        "message" : "错误信息"
}
文件的返回:
http://127.0.0.1:8000/
media/article_img/%E5%A4%B4%E5%83%8F1_bc32ac91-06d2-4014-bc22-b7587d39bfc8.png
# 14. 上传图片
def upload_img(request):
    # 14.1 判断请求方式
    if request.method == 'POST':

        try:
            # 14.2 获取文件 表单的name=imgFile
            print(request.FILES)
            # <MultiValueDict: {'imgFile': [<InMemoryUploadedFile: 头像2.png (image/png)>]}>

            # 14.3 获取文件
            file_obj = request.FILES.get('imgFile')

            # 14.4 将文件保存到media的article_img目录下
            # 手动拼接路径
            import os
            from BBS.settings import BASE_DIR
            file_dir = os.path.join(BASE_DIR, 'media', 'article_img')
            # 判断目录是否存在 不存在则则创建 isdir判断是否为一个目录
            if not os.path.isdir(file_dir):
                os.mkdir(file_dir)  # 创建一层目录article_img

            # 拼接图片的路径, 图片名需要是唯一标识 file_obj.name是上传文件的名字, 加上一个uuid 随机字符串在后面
            import uuid
            randon_str = str(uuid.uuid4())  # file_obj.name--> xxx.jpg
            name, suffix = file_obj.name.split('.')
            file_name = name + '_' + randon_str + '.' + suffix

            print(file_name)
            #

            file_path = os.path.join(file_dir, file_name)
            print(file_path)

            # 保存图片
            with open(file_path, mode='wb') as wf:
                for line in file_obj:
                    wf.write(line)

            # 返回值
            back_dict = {'error': 0,
                         'url': '/media/article_img/file_name/'
                         }
        except Exception as e:
            back_dict = {'error': 0,
                         'message': e
                         }

        return JsonResponse(back_dict)
	else:
        return HttpResponse('非法访问!')

GIF 2022-4-4 10-53-00

24. 修改头像

为所有导航条的设置头像a标签设置路由
<li><a href="/set/avatar/">修改头像</a></li>
24.1 路由层
    # 19. 修改头像
    url(r'^set/avatar/', views.set_avatar),
24.2 视图层
# 15. 修改头像
def set_avatar(request):
    # 15.1 返回修改头像页面

    return render(request, 'set_avatar.html')
24.3 模板层
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>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>
</head>
<body>
<div class="container-fluid">
    <form action="" method="post"  enctype="multipart/form-data">
        <!--csrf-->
        {% csrf_token %}

        <div class="row">
            <div class="col-md-4 col-md-offset-2">
                <h3>原头像</h3>
                <img src="/media/{{ request.user.avatar }}" alt="..." width="300px">
            </div>

            <div class="col-md-4 col-md-offset-2">


                <label for="my_avatar"><h3>新头像(点击图片)</h3>
                    {% load static %}
                    <img src="{% static 'img/default.png' %}" alt="" id='my_img' width="300px">
                    <input type="file" name="avatar" id="my_avatar" style="display: none">
                </label>

            </div>
            <div class="row">
                <div class="col-md-4 col-md-offset-4"><input type="submit" class="btn-primary btn-block"></div>

            </div>

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

<script>
    // 10. 绑定文本域变化事件  文本阅读器对象, 将上传的头像文件用阅读器获取出来
    $('#my_avatar').change(function () {
        // 10.1 生成一个文本器阅读对象
        let FileReaderObj = new FileReader()

        // 10.2 获取用户上传的文件  (this)[0]对象转为  DOM对象  files[0]取文件值
        let UpFileObj = $(this)[0].files[0];

        // 10.3 将文件对象交给阅读器对象读取
        FileReaderObj.readAsDataURL(UpFileObj)  // 异步操作, IO操作

        // 10.4 绑定加载事件 等待文件阅读完毕之后再执行事假 修改img标签的值
        FileReaderObj.onload = function () {
            // 10.5 attr标签属性值 属性   值(人本阅读器的结果)
            $('#my_img').attr('src', FileReaderObj.result)
        }

    })
</script>
</body>
</html>
24.4 业务逻辑
# 15. 修改头像
def set_avatar(request):
    # 15.2 判断请求方式
    if request.method == 'POST':
        # 15.3 获取头像
        file_obj = request.FILES.get('avatar')
        # userinfo 表更新头像数据
        models.UserInfo.objects.filter(pk=request.user.pk).update(avatar=file_obj)

        """
        models.UserInfo.objects.update(avatar=file_obj)
        直接更新, 头像的路径就变成了文件名, 少了 media目录开头.
        解决方法:
        1. 手动拼接 media
        2. 先创建对象在使用save保存.
        """
        user_obj = request.user
        user_obj.avatar = file_obj
        user_obj.save()

        # 修改成功之后返回到主页
        return redirect('/home/')

    # 15.1 返回修改头像页面
    return render(request, 'set_avatar.html')
models.UserInfo.objects.update(avatar=file_obj)

image-20220404135024548

GIF 2022-4-4 13-57-24

先创建数据对象在修改值, 再保存则会以media开头.
user_obj = request.user  # 当前的用户数据对象
user_obj.avatar = file_obj
user_obj.save()

image-20220404140344228
GIF 2022-4-4 14-02-10

25. 编辑文章

管理页面编辑标签绑定编辑文章的路由.
25.1 路由层
后端需要文字的数据(包含当前文章的类di), 站点的所有分类, 站点的所有标签, 标签与文章第三张表的数据.
    # 20. 编辑文章
    url(r'^edit_article/', views.edit_article),
25.2 视图层
# 编辑文章
def edit_article(request):
    # 获取文件的id
    article_id = request.GET.get('article_id')
    # 判断GET请求是否有值
    print(article_id)
    if article_id:
        #  通过文字id获取文章信息
        article_obj = models.Article.objects.filter(pk=article_id).first()
        # 判断文章是否存在
        if article_obj:
            # 将文章的数据 该站点的所有分类, 该站点的所有标签 传给后端
            sort_list = models.Sort.objects.filter(blog=request.user.blog)

            # 站点下所有的标签
            tag_list = models.Tag.objects.filter(blog=request.user.blog)

            # 文章绑定的标签
            article_tag = models.ArticleToTag.objects.filter(article_id=article_id)
            all_tag = []
            for obj in article_tag:
                print(obj.tag)
                all_tag.append(obj.tag)
            return render(request, 'backstage/edit_article.html', locals())

    return render(request, 'error.html')
默认选中
# 1.站点下所有的标签
blog_obj = models.Blog.objects.filter(site_name='kid')
tag_obj = models.Tag.objects.filter(blog=blog_obj)
print(tag_obj)  # <QuerySet [<Tag: kid站点第一个标签>, <Tag: kid站点第二个标签>]>
for obj in tag_obj:
    print(obj)
    """
    kid站点第一个标签
    kid站点第二个标签
    """

# 2. 获取文章对应的所有标签 第三张表查出的数据是数据对象, 手动将数据取出来放一个列表中.
ArticleToTag_obj = models.ArticleToTag.objects.filter(article_id=1)
print(ArticleToTag_obj)

ArticleToTag_list = []
for obj in ArticleToTag_obj:
    print(obj.tag)
    ArticleToTag_list.append(obj.tag)

for obj in tag_obj:
    if obj in ArticleToTag_list:
        print(f'默认选中{obj}')
        """
        默认选中kid站点第一个标签
        默认选中kid站点第二个标签
        """

"""

# 7.4 打印query_set self.外键  子查询, 查到的一个数据对象, 打印的触发该类的__str__

def __str__(self):
    return f'{self.article_id} -- {self.tag_id}'
    如果__str__方法中返回的值id值那么在获取数据的时候拿的就是 获取到的就是 id值
    models.ArticleToTag.objects.filter(article_id=1) 
    # <QuerySet [<ArticleToTag: 1 -- 1>, <ArticleToTag: 1 -- 2>]>
    
    return f'{self.article} -- {self.tag}'
    如果__str__方法中返回的值数据对象值那么在获取数据的时候拿的就是 获取到的就是 数据对象
    # <QuerySet [<ArticleToTag: kid发布的第一篇文章 Python介绍 -- kid站点第一个标签>
    
    在前端需要默认选中标签的时候, 就会出现 
    for i in [1,2] 
    i in [[1,2], [2,4]] 这样的格式
    把需要的值先取出来拿一个新的列表存储 [[1,2], [2,4]] ==> [2, 4]
    i in [2, 4] 这样就正常了
"""
25.3 模板层
类别默认选中 {% if sort_obj.pk == article_obj.sort_id %}
标签默认选中 {% if tag_obj in all_tag %}
<!--继承模板-->
{% extends 'backstage/back_base.html' %}
{% block article %}
    <div>
        <!--使用form表单提交数据 提交地址为add_article-->
        <form action="" method="post">
            <!--csrf-->
            {% csrf_token %}
            <h3 class="bg-info">添加文章</h3>

            <!--标题-->
            <div class="form-group">
                <p>标题</p>
                <input type="text" name="title" class="form-control" value="{{ article_obj.title }}">
            </div>


            <!--大段文本框-->

            <div class="form-group">
                <p>内容</p>
                <textarea name="content" id="id_content" cols="30" rows="10">
                {{ article_obj.content }}
            </textarea>
            </div>

            <!--分类选择-->
            <div class="form-group">
                <p>分类</p>
                {% for sort_obj in sort_list %}
                    {% if sort_obj.pk == article_obj.sort_id %}
                        <!--提交是分类的id 展示的是分类的名字-->
                        <label class="radio-inline">
                            <input type="radio" name="sort_id"
                                   value="{{ sort_obj.pk }}" checked="checked">{{ sort_obj.name }}
                        </label>
                    {% else %}
                        <!--提交是分类的id 展示的是分类的名字-->
                        <label class="radio-inline">
                            <input type="radio" name="sort_id"
                                   value="{{ sort_obj.pk }}">{{ sort_obj.name }}
                        </label>
                    {% endif %}


                {% endfor %}
            </div>


            <!--标签选择-->
            <div class="form-group">
                <p>标签</p>
                {% for tag_obj in tag_list %}
                    {% if tag_obj in all_tag %}

                        <label class="checkbox-inline">
                            <input type="checkbox" name="tag_id_list" value="{{ tag_obj.pk }}"
                                   checked="checked">{{ tag_obj.name }}
                        </label>
                    {% else %}
                        <label class="checkbox-inline">
                            <input type="checkbox" name="tag_id_list" value="{{ tag_obj.pk }}">{{ tag_obj.name }}
                        </label>
                    {% endif %}


                {% endfor %}
            </div>


            <!--提交按钮-->
            <div class="form-group">
                <input type="submit" class="btn btn-primary btn-block">
            </div>

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

{% block js %}
    {% load static %}
    <script charset="utf-8" src="{% static 'kindeditor/kindeditor-all-min.js' %}"></script>  <!--需要的js文件-->
    <script>
        KindEditor.ready(function (K) {
            window.editor = K.create(
                // 作用textarea标签的id
                '#id_content',
                // 写配置
                {
                    width: '100%',
                    height: '500px',
                    resizeType: 1,
                    // 图片上传的地址,  form表单post方式上传文件, 如果是表单就需要csrf校验
                    uploadJson: '/upload_img/',
                    // 提交额外的参数
                    extraFileUploadParams: {
                        // 提交csrf 的数据
                        'csrfmiddlewaretoken': "{{ csrf_token }}",
                    }

                });
        });
    </script>

{% endblock %}

25.4 保存文件
第三张表是手动创建的, 无法使用ORM的add set, remove, clear方法
需要将原来的数全部删除再重新写数据.
# 16.编辑文章
@login_required
def edit_article(request):
    # 获取文件的id
    article_id = request.GET.get('article_id')
    # 判断GET请求是否有值
    # print(article_id)
    # 13.4 判断请求方式:
    if request.method == 'POST':
        # 13.5 获取数据 标签, 内容, 分类id, 标签id,可能存在多个(getlist)
        post_obj = request.POST
        title = post_obj.get('title')
        content = post_obj.get('content')
        sort_id = post_obj.get('sort_id')
        tag_id_list = post_obj.getlist('tag_id_list')

        # 使用BeautifulSoup模块 文本内容, 模式
        soup = BeautifulSoup(content, 'html.parser')
        # 获取所有标签
        tags = soup.find_all()

        # 删除script标签
        for tag in tags:
            if tag.name == 'script':
                # 删除标签
                tag.decompose()

        # 文章简介截取文章的前端128个字符
        # desc = content[0:128]

        # 截取文章文本128个字符
        desc = soup.text[0:150]

        # print(sort_id, tag_id_list)
        # 操作文章表
        article_obj = models.Article.objects.filter(pk=article_id).update(
            title=title,
            desc=desc,
            # 将处理后的对象使用str转换下类型
            content=str(soup),
            sort_id=sort_id,
            blog=request.user.blog)

        # 批量操作第三张表 (第三张表是手动创建的, 无法使用ORM的add set, remove, clear方法)
        # 需要将原来的数全部删除再重新写数据
        models.ArticleToTag.objects.filter(article_id=article_id).delete()
        # 手动操作第三张表, 可能操作多次使用 bulk_create 批量插入
        # 定义一个列表
        article_obj_list = []
        for tag_id in tag_id_list:
            article_to_tag_obj = models.ArticleToTag(article_id=article_id, tag_id=tag_id)
            article_obj_list.append(article_to_tag_obj)

        # 一次插入
        models.ArticleToTag.objects.bulk_create(article_obj_list)

        # 文件写入成功之后, 跳转页面
        return redirect('/backstage/')

    if article_id:
        #  通过文字id获取文章信息
        article_obj = models.Article.objects.filter(pk=article_id).first()
        # 判断文章是否存在
        if article_obj:
            # 将文章的数据 该站点的所有分类, 该站点的所有标签 传给后端
            sort_list = models.Sort.objects.filter(blog=request.user.blog)

            # 站点下所有的标签
            tag_list = models.Tag.objects.filter(blog=request.user.blog)

            # 文章绑定的标签
            article_tag = models.ArticleToTag.objects.filter(article_id=article_id)
            all_tag = []
            for obj in article_tag:
                print(obj.tag)
                all_tag.append(obj.tag)

            return render(request, 'backstage/edit_article.html', locals())

    return render(request, 'error.html')

26. 删除文件
删除一篇文章, 需要先删除文章表与标签表第三张表的数据, 再删除文章.
26.1 路由层
    # 21. 删除文章
    url(r'^del_article/', views.del_article),
26.2 数图层
# 17.删除文章
@login_required
def del_article(request):
    # 17.1 判断请求方式
    if request.is_ajax():
        # 17.2获取书籍的id
        article_id = request.POST.get('article_id')
        print(article_id)
        try:
            # 17.3 删除文章表与标签表的第三张表中的数据
            models.ArticleToTag.objects.filter(article_id=article_id).delete()

            # 17.4 删除文章表
            models.Article.objects.filter(pk=article_id).delete()
            back_dict = {'code': 200}
        except Exception as e:
            print(e)
            back_dict = {'code': 400}

        return JsonResponse(back_dict)

    else:
        return render(request, 'error.html')
26.3模板层
{% block js %}
    <script>
        $('.del_article').on('click', function () {
            let article_id = $(this).attr('article_id')
            console.log(article_id)

            swal({
                    title: '你确定删除吗?',
                    text: '删除之后清除所有数据!',
                    type: 'warning',
                    showCancelButton: true,
                    confirmButtonText: '确定删除!',
                    cancelButtonText: '取消操作!',
                    // 需要点 ok 关闭弹框
                    closeOnConfirm: false,
                    closeOnCancel: false
                },
                function (isConfirm) {
                    if (isConfirm) {

                        $.ajax({
                            url: '/del_article/',
                            type: 'post',
                            data: {'article_id': article_id , 'csrfmiddlewaretoken': '{{ csrf_token }}'},
                            success: function (args) {
                                if (args.code === 200){
                                    swal("删除成功", "想要找回请在3小时内联系管理员!", "success");
                                    // 刷新页面
                                    window.location.reload()
                                }

                            }
                        })
                    } else {
                        swal('取消成功!', "已经撤销你的操作", 'error');
                    }
                }
            )

        })
    </script>
{% endblock %}
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值