BBS-后台管理

后台管理功能是:让用户自己去操作增删改查。新建一个应用(app02),要记得在settings中注册应用。

本篇文章只写文章列表、添加文章、删除文章。自己去写编辑文章,对标签的增删改查、对分类的增删改查。只是表不同,字段不同。逻辑是一样的。

重点功能:

  • 富文本编辑器使用
  • 新增文章(防止XSS攻击)
  • 文章新增时,上传图片

一、添加路由

路由分发,urls.py

from django.conf.urls import url,include

# 路由分发
url(r'^app02/', include('app02.urls')),  # 后面不能加$符号

app02下的urls.py

from django.conf.urls import url
from django.contrib import admin
from app02 import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    # 文章列表路由
    url(r'^article_list/', views.article_list),

    # 添加文章路由
    url(r'^article/add/', views.add),
    # 删除路由
    url(r'^article/delete/', views.delete),
    # 上传图片的路由
    url(r'^upload/', views.upload),

]

二、后台管理前端模板

文章列表展示

前端逻辑:

1.table标签
2.展示文章数据
    文章标题,路由跳转:用户名/article/文章id,target="_blank"表示新打开个标签页
    文章点赞数、点踩数、评论数
    文章分类,文章查分类,正向
    注册时间,格式化
    编辑按钮
    删除按钮,阻止自己的事件
3.删除绑定事件

删除绑定事件

    3.1 获取主键id,因为每一条的主键id都不一样,就给删除添加一个自定义属性来保存循环中的文章id
    3.2 二次确认,layer询问框
    3.3 发起ajax请求
    3.4 删除成功后,删除当前的tr

文章列表展示前端代码:

backend文件夹下的article_list.html页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>展示文章列表</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
    <div class="row">
        <h1 class="text-center">文章列表</h1>
        <!-- 添加文章标签 -->
        <a href="/app02/article/add/" class="btn btn-info">添加文章</a>
        <table class="table table-striped table-hover">
            <thead>
            <tr><!--需要展示的字段-->
                <th>标题</th>
                <th>点赞数</th>
                <th>点踩数</th>
                <th>评论数</th>
                <th>分类</th>
                <th>添加时间</th>
                <th>操作</th>
            </tr>
            </thead>
            <tbody>
            {% for article in article_list %}<!-- 展示文章数据 -->
                <tr>
                    <!-- 文章详情页的路由:用户名/article/文章id,target="_blank"表示新打开个标签页 -->
                    <td><a href="/{{ user_obj.username }}/article/{{ article.pk }}"
                           target="_blank">{{ article.title }}</a></td>
                    <td>{{ article.up_num }}</td>
                    <td>{{ article.down_num }}</td>
                    <td>{{ article.comment_num }}</td>
                    <td>{{ article.category.name }}</td><!-- 文章查分类,正向 -->
                    <td>{{ article.create_time|date:'Y-m-d' }}</td>
                    <td>
                        <a href="" class="btn btn-success">编辑</a>
                        <a href="javascript:;" class="btn btn-danger del" pk="{{ article.pk }}">删除</a>
                    </td>
                </tr>
            {% endfor %}
            </tbody>
        </table>
    </div>
</div>

<script src="/static/layer/layer.js"></script>
<script>
    {# 删除绑定点击事件开始 #}
    $(".del").click(function () {
        var _this = $(this)

        // 1. 主键id怎么获取?
        // 因为每一条的主键id都不一样,就给删除添加一个自定义属性来保存循环中的文章id
        var id = $(this).attr('pk');

        // 2. 二次确认,询问框
        layer.confirm('您确认要删除这条记录吗?', {
            btn: ['确认'] //按钮
        }, function () {
            // 3. 发起ajax请求
            $.ajax({
                url: '/app02/article/delete/',  // 删除路由
                type: 'post',
                data: {
                    id: id,
                    csrfmiddlewaretoken: '{{ csrf_token }}'
                },
                success: function (res) {
                    // 删除当前的tr,DOM操作
                    // 只要一成功,就先删除数据,再返回信息
                    _this.parent().parent().remove();
                    if (res.code == 200) {
                        layer.msg(res.msg, {},function(){
                            {#location.reload();刷新页面#}
                            // _this.parent().parent().remove();  这个语句写在函数中,反应比较慢,为什么?function是回调函数,需要后端处理完逻辑并返回数据之后,才会调用,所以速度会慢一些。
                        })
                    } else {
                        layer.msg(res.msg)
                    }
                }
            })
        });
    })
    {# 删除绑定点击事件结束 #}
</script>
</body>
</html>

添加文章

前端逻辑:

1.搭建样式
2.文章内容使用textarea文本框,使用富文本编辑器
3.文章分类使用下拉框,循环分类列表
4.文章标签使用复选框,定义name、value属性
5.富文本编辑器,直接看对应文档使用
6.提交按钮绑定点击事件
    6.1 获取参数
        文章标题
        文章内容,看文档学习使用方法
        分类
        标签的复选框,获取数据(input框[属性选择器]:选中的值),each循环取值,添加到数组中,数组要用js转成字符串
    6.2 验证参数
    6.3 发起ajax请求
    6.4 添加成功后,跳转到文章列表页

富文本编辑器

文章内容使用富文本编辑器

富文本编辑器介绍:https://blog.csdn.net/q5926167/article/details/127445887)
kindeditor文本编辑器官网:http://kindeditor.net/demo.php

文本编辑器如何使用

1.看文档
2.下载,官方下载
3.编辑器的使用方法
    部署编辑器:把下载的所有文件直接复制到项目中
    修改HTML页面:先有textarea文本框,引入kindeditor.js
    获取HTML数据
4.编辑器初始化参数
	K.create()中的第二个参数,是对象形式
    width
    hright
    items:[],工具栏
    resizeType: 1,
5.上传文件
uploadJson
额外参数,添加csrf_token参数
返回格式json

具体使用:

<textarea name="" id="editor_id" cols="30" rows="10" class="form-control"></textarea>

<script charset="utf-8" src="/static/kindeditor/kindeditor-all.js"></script>  // 改成自己的路径
<script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js"></script>

<script>
    KindEditor.ready(function (K) {
        window.editor = K.create('#editor_id',   // 警号后的名字要跟taxtarea中的id属性值一致
            {width: '900px',
             height: '500px',
             items: ['source', '|',  'image', 'multiimage'],
             resizeType: 1,
             uploadJson: '/app02/upload/',  // 上传图片的路由地址
             extraFileUploadParams: {
             csrfmiddlewaretoken: '{{ csrf_token }}',
            }
        });
    });
</script>    

前端add.html页面代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>添加文章页面</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
    <div class="row">
        <h1 class="text-center">添加文章页面</h1>
        <div class="form-group">
            <label for="title">文章标题</label>
            <input type="text" class="form-control" id="title">
        </div>
        <div class="form-group">
            <label for="username">文章内容</label><!-- 文章内容用文本框 -->
            <textarea name="" id="editor_id" cols="30" rows="10" class="form-control"></textarea>
        </div>
        <div class="form-group">
            <label for="category">文章分类</label><!-- 文章分类使用下拉框 -->
            <select name="" id="category" class="form-control">
                {% for category in category_list %}<!-- option中写分类名称,value值写分类id -->
                    <option value="{{ category.pk }}">{{ category.name }}</option>
                {% endfor %}
            </select>
        </div>
        <div class="form-group">
            <label for="">文章标签</label><!-- 文章标签使用复选框 -->
            {% for tag in tag_list %}<!-- 复选框不能使用id,一循环id会重复 -->
                <input type="checkbox" name="tag" value="{{ tag.pk }}"> {{ tag.name }}
            {% endfor %}
        </div>
        <input type="button" class="btn btn-success commit" value="提交">
    </div>
</div>
<script charset="utf-8" src="/static/kindeditor/kindeditor-all.js"></script>  // 改成自己的路径
<script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js"></script>

<script src="/static/layer/layer.js"></script>
<script>
    {# 富文本编辑器使用开始 #}
    KindEditor.ready(function (K) {
        window.editor = K.create('#editor_id',   // 警号后的名字要跟taxtarea中的id属性值一致
            {
            width: '900px',
            height: '500px',
            items: [
                'source', '|', '|', 'preview', 'print',
                'copy', '|', 'image', 'multiimage',],  // 工具栏
            resizeType: 1,
            uploadJson: '/app02/upload/',  // 上传图片的路由地址
            extraFileUploadParams: {  // 上传图片额外参数,csrf_token参数
                csrfmiddlewaretoken: '{{ csrf_token }}',
            }
        });
    });
    {# 富文本编辑器使用结束 #}

    {# 给提交按钮绑定点击事件开始 #}
    $(".commit").click(function () {
        editor.sync();
        // 1. 获取参数
        var title = $("#title").val();
        // 文章内容,看文档使用它的获取数据的方法
        var content = $('#editor_id').val(); // 取得HTML内容
        {#html = editor.html();#}

        var category = $("#category").val();  // 分类

        // 标签,复选框,获取数据的特殊方式:input框[属性选择器]:选中的值
        // var tags = $("input[name='tag']:checked") // tags是jquery对象

        // each循环取值
        var tag_arr = []  // 定义一个数组,把每个值加到数组中
        $.each($("input[name='tag']:checked"), function (index, value) {
            // $(this)是每一个选中的标签对象(回调函数前面的对象)
            // $(this).val()jquery对象获取value值
            // push()数组尾部追加元素
            tag_arr.push($(this).val());
        });

        // console.log(tag_arr)  // (3) ['1', '2', '3']

        // 2. 验证参数

        // js数组转为字符串 [1,2,3],数组提交不方便
        var tag_str = tag_arr.join(',');
        // console.log(tag_str)

        // 3. 发起ajax请求
        $.ajax({
            url: '',
            type: 'post',
            data: {
                title: title,
                content: content,
                category: category,
                tag_str: tag_str,  // 字符串形式
                csrfmiddlewaretoken: '{{ csrf_token }}'
            },
            success: function (res) {
                if (res.code == 200) {
                    layer.msg(res.msg, {}, function () {
                        // 添加成功,跳转到文章列表页
                        location.href = '/app02/article_list/'
                    })
                } else {
                    layer.msg(res.msg)
                }
            }
        })
    })
    {# 给提交按钮绑定点击事件结束 #}
</script>
</body>
</html>

二、视图函数

文章列表展示

app02下的views.py:

from app01 import models  # 导入app01中的模型类


# from django.contrib.auth.decorators import login_required
# @login_required(login_url='/login/')  # 使用django的auth模块,但是这个不能使用,我们重新写的加密方式改变了。密码不可能一致


def article_list(request):
    if not request.session.get('username'):  # 不登陆不能访问,也可以加个装饰器
        return redirect('/login/')
    # 1. 根据用户id查询出用户对象
    user_obj = models.UserInfo.objects.filter(pk=request.session.get('id')).first()

    # 2.查询当前站点下的文章列表
    # filter(blog(外键)=站点对象)
    # filter(blog_id(外键)=站点对象点字段)
    article_list = models.Article.objects.filter(blog=user_obj.blog).all()

    return render(request, 'backend/article_list.html', locals())

添加文章

后端逻辑

# 前端需要展示的数据(1-3步)
1. 先查询出当前站点,根据用户表中查询外键blog_id
2. 查询当前站点下的所有文章分类
3. 查询当前站点下的所有标签
# 接收前端的ajax请求
1. 定义返回数据的格式
2. 接受参数
    标签字符串需要转成列表形式
    区分文本中的标签和中文文本内容,要借助于一个模块bs4
        删除script标签(tag.name)
        摘要,截取中文文本soup.text[0:100]
3. 验证参数
4. 处理正常的业务逻辑,操作两张表:1.文章表,2.文章和标签的第三章表
    文章表是插入数据
    第三章表,插入数据,标签可能是多个需要循环
5.返回给前端数据


"""
    容易出错的两个方面:
        1.对于摘要,要识别标签,只截取文本
        2.把script标签去除(XSS攻击)
"""

BeautifulSoup4

BeautifulSoup也是一个HTML/XML的解析器,主要的功能也是如何解析和提取HTML/XML数据。

中文官网:https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/

英文官网:
https://www.crummy.com/software/BeautifulSoup/bs4/doc/

具体使用:https://baijiahao.baidu.com/s?id=1663904567728406423&wfr=spider&for=pc

添加文章后端代码

from django.http import JsonResponse

def add(request):
    """添加文章"""
    """
    容易出错的两个方面:
        1.对于摘要,要识别标签,只截取文本
        2.把script标签去除(XSS攻击)
    """
    if not request.session.get('username'):  # 不登陆不能够访问。先做简单的判断
        return redirect('/login/')

    # 1. 先查询出当前站点,根据用户表中查询外键blog_id
    user_obj = models.UserInfo.objects.filter(pk=request.session.get('id')).first()
    blog = user_obj.blog

    if request.method == 'POST':  # 接收前端的ajax请求
        # 1. 定义返回数据的格式
        back_dic = {'code': 200, 'msg': '添加成功'}
        # 2. 接受参数
        title = request.POST.get("title")
        content = request.POST.get("content")
        category_id = request.POST.get("category")
        tag_str = request.POST.get("tag_str")  # string----->1,2,3

        # 把字符串转成列表形式
        tags_arr = tag_str.split(',')
        print(tags_arr)

        '''区分文本中的标签和中文文本内容,要借助于一个模块bs4:
        必须安装:pip install beautifulsoup4
        '''
        from bs4 import BeautifulSoup
        # BeautifulSoup(文本数据变量, '解析器')
        soup = BeautifulSoup(content, 'html.parser')
        # BeautifulSoup(content, 'lxml') # 必须安装lxml  pip install lxml
        print(soup, type(soup))
        # 把script标签去除(XSS攻击)
        # 方法:soup.find_all('')  # 查找出所有标签  # 支持for循环的
        for tag in soup.find_all():
            # print(tag.name)  # 标签名称
            '''xss攻击的原理:前端提交的带有script标签的js代码,我们只需要把提交过来的script标签注释掉或者删除掉'''
            if tag.name == 'script':
                # 删除script标签
                tag.decompose()
        # 方式二:
        # for tag in soup.find_all('script'):  # 只找script标签,就不用判断了
        #     tag.decompose()

        # 摘要,截取中文字符
        # desc = content[0:100]  # 截取的是文章内容的前100个字符。会截到标签
        desc = soup.text[0:100]  # soup.text是中文文本,然后直接截取就行

        # 3. 参数验证省略

        # 4. 处理正常的业务逻辑
        # 操作两张表:1.文章表,2.文章和标签的第三章表

        # 文章表是插入数据
        # article_obj:当前插入数据成功的对象
        article_obj = models.Article.objects.create(title=title,
                                                    desc=desc,
                                                    content=str(soup),  # 需要转成字符串形式
                                                    category_id=category_id,
                                                    blog=blog
                                                    )

        # 操作第三章表,插入数据,标签可能是多个需要循环
        for i in tags_arr:
            # 这种方式操作数据库次数比较多
            models.Article2Tag.objects.create(article_id=article_obj.pk, tag_id=i)

        # 优化一下:批量插入
        # tag_obj = []
        # for i in tags_arr:
        #     obj = models.Article2Tag(article_id=article_obj.pk, tag_id=i)
        #     tag_obj.append(obj)
        # models.Article2Tag.objects.bulk_create(tag_obj)

        # 5.返回给前端数据
        return JsonResponse(back_dic)

    # 2. 查询当前站点下的所有文章分类
    category_list = models.Category.objects.filter(blog=blog).all()

    # 3. 查询当前站点下的所有标签
    tag_list = models.Tag.objects.filter(blog=blog).all()
    return render(request, 'backend/add.html', locals())

删除文章

代码:

def delete(request):
    """删除文章记录"""
    # 接收id
    id = request.POST.get('id')
    back_dic = {'code': 200, 'msg': '删除成功'}
    # 删除文章表,分类id也在文章表中
    models.Article.objects.filter(pk=id).delete()
    # 删除文章与标签的第三张表中的有关与这篇文章的数据
    models.Article2Tag.objects.filter(article_id=id).delete()

    return JsonResponse(back_dic)

富文本编辑器上传图片

图片上传不上去,需要自己更改接口,图片返回格式必须是json格式。

上传图片处理步骤:

1. 接收文件对象。不清除图片的key是什么,可以先打印request.FILES,或直接查看文档
2. 拼接图片上传文件夹的路径
3. 拼接图片的文件名
	生成随机数
    去除随机数中的'-'
    拼接图片后缀
4. 拼接上传图片路径,文件夹路径,文件名
5. with open上传文件,二进制循环写入数据
6. 返回json格式数据

后端代码:

def upload(request):
    """上传图片,需要自己处理"""
    """
    图片返回格式必须是json
    //成功时
    {
            "error" : 0,
            "url" : "http://www.example.com/path/to/file.ext"  # 图片地址
    }
    //失败时
    {
            "error" : 1,
            "message" : "错误信息"
    }

    """
    if request.method == 'POST':
        # 1.接收文件对象
        file_obj = request.FILES.get('imgFile')
        print(request.FILES)  # 不清除图片的key是什么,可以先打印
        import os
        from django.conf import settings
        # 2.拼接图片上传文件夹的路径
        article_img_path = os.path.join(settings.BASE_DIR, 'media', 'article_img')
        if not os.path.exists(article_img_path):  # 路径不存在创建文件夹
            os.mkdir(article_img_path)

        # 3. 拼接图片的文件名
        # 小问题:直接使用文件名作为保存图片的名称,文件名重复时会覆盖情况,生成随机数
        import uuid
        s = str(uuid.uuid4())  # 形式是:dada-ddsa-dasdas-dasdas
        new_s = s.replace('-', '')  # 去除字符串中的'-'
        file_name = new_s + '.' + file_obj.name.rsplit('.')[-1]  # 新随机数.图片后缀

        # 4. 拼接上传图片路径
        path_file = os.path.join(article_img_path, file_name)  # /media/article_img/123.png

        # 5.上传文件,二进制写入数据
        with open(path_file, 'wb') as f:
            for line in file_obj:
                f.write(line)

        # 6.返回json格式数据
        return JsonResponse({
            "error": 0,
            # "url": "/media/article_img/%s" % file_obj.name
            "url": "%s" % path_file
        })

使用别人的前端模板

混合项目,使用别人的页面样式:

使用别人的页面样式:
1.复制html粘贴到templates
2.复制css到static中
3.更改引入的路径,头跟尾部都查看一下
4.更改图片路径
5.然后自己加功能的路由,地址,视图函数
6.自己加js逻辑


ctrl+r 替换
posted @ 2023-05-15 21:31  星空看海  阅读(48)  评论(0编辑  收藏  举报