Flask项目实战——8—(首页渲染、首页模板继承、轮播图后台管理、轮播图删改、本地文件上传七牛云)

1、首页渲染

绑定首页界面

前台蓝图文件:apps/front/views.py

# -*- encoding: utf-8 -*-
"""
@File    : views.py
@Time    : 2020/5/11 9:59
@Author  : chen
前台蓝图文件:apps/front/views.py
"""
# 前台的蓝图文件  类视图函数写在这里
from flask import Blueprint, render_template, views, make_response, request, session  # make_response生成response对象,用于返回前端模板

# 导入图像验证码生成文件
from utils.captcha import Captcha

# 图形验证码image是二进制数据,需要转换成字节流才能使用
from io import BytesIO

# 将图形验证码保存到Redis         restful输出信息弹窗
from utils import redis_captcha, restful

# 验证码表单信息验证   登录、注册的Form表单信息收集
from .forms import SignupForm
from .forms import SigninForm

# 导入前台用户模型
from .models import Front_User

# 导入数据库连接 db
from exts import db

# 确保URL安全的文件:utils/safe_url.py
from utils import safe_url

front_bp = Blueprint("front", __name__)          # 前端不用前缀,直接在首页显示,front是蓝图,在front_signup.html调用生成图形验证码时候需要用


# BBS的首页界面路由
@front_bp.route("/")
def index():
    return render_template("front/front_index.html")      # 渲染到首页界面


# 图形验证码路由
@front_bp.route("/captcha/")
def graph_captcha():
    try:                                                 # 异常处理
        # 图像验证码生成文件中返回两个参数   text, image
        text, image = Captcha.gene_graph_captcha()      # 生成图形验证码,image是二进制数据,需要转换成字节流才能使用
        print("发送的图形验证码是:{}".format(text))
        
        # 将图形验证码保存到Redis数据库中
        redis_captcha.redis_set(text.lower(), text.lower())  # redis_set中需要传参key和value,text没有唯一对应的key,只能都传参text
        
        # BytesIO是生成的字节流
        out = BytesIO()
        image.save(out, 'png')                          # 把图片image保存在字节流中,并指定为png格式
        # 文件流指针
        out.seek(0)                                     # 从字节流最初开始读取
        # 生成response对象,用于返回前端模板中
        resp = make_response(out.read())
        resp.content_type = 'image/png'                 # 指定数据类型
    except:
        return graph_captcha()                          # 没有生成验证码就再调用一次
        
    return resp                                         # 返回对象


# 测试referrer的跳转
@front_bp.route("/test/")
def test():
    return render_template("front/front_test.html")


# 用户注册类视图
class SingupView(views.MethodView):
    def get(self):
        # 图像验证码生成文件中返回两个参数   text, image
        # text, image = Captcha.gene_graph_captcha()
        # print(text)                      # 验证码
        # print(image)                     # 图形文件,图形类<PIL.Image.Image image mode=RGBA size=100x30 at 0x1EFC9000C88>

        # 从当前页面跳转过来就是None   从其他页面跳转过来输出就是上一个页面信息     referrer是页面的跳转
        # print(request.referrer)                           # http://127.0.0.1:9999/test/
        
        return_to = request.referrer
        # 确保URL安全的文件:utils/safe_url.py
        print(safe_url.is_safe_url(return_to))              # 判断return_to是否来自站内,是否是安全url,防爬虫
        
        if return_to and return_to != request.url and safe_url.is_safe_url(return_to):       # 跳转的url不能是当前页面,否则没意义
            return render_template("front/front_signup.html", return_to=return_to)           # return_to渲染到前端界面
        else:
            return render_template("front/front_signup.html")                                # 如果没获取url,直接渲染注册界面
        
    # 验证码的form表单信息提交验证
    def post(self):
        form = SignupForm(request.form)                       # 收集表单信息
        
        # 表单验证通过
        if form.validate():
            # 保存到数据库
            telephone = form.telephone.data
            username = form.username.data
            password = form.password1.data                    # forms表单信息
            
            # 前台用户模型数据添加到数据库
            user = Front_User(telephone=telephone, username=username, password=password)
            db.session.add(user)
            db.session.commit()                                                   # 提交到数据库
            
            # 表单验证通过,提交到数据库成功
            return restful.success()
        else:
            return restful.params_error(message=form.get_error())                  # 表单信息验证出错


# 用户登录的类视图
class SinginView(views.MethodView):
    def get(self):
        return_to = request.referrer                                                    # referrer是上一个url
    
        if return_to and return_to != request.url and safe_url.is_safe_url(return_to):  # 跳转的url不能是当前页面,判断url是否安全
            return render_template("front/front_signin.html", return_to=return_to)      # return_to渲染到前端界面
        else:
            return render_template("front/front_signin.html")                           # 如果没获取url,直接渲染注册界面
    
    def post(self):
        form = SigninForm(request.form)                                            # 登录界面的Form表单信息
        
        if form.validate():                                                        # 表单信息存在
            # 收集form表单信息
            telephone = form.telephone.data
            password = form.password.data
            remember = form.remember.data
            
            user = Front_User.query.filter_by(telephone=telephone).first()         # 通过手机号验证该用户是否存在数据库
            if user and user.check_password(password):                             # 判断密码和用户是否正确
                session['user_id'] = user.id                                       # 用户的id存储到session中,用于登录验证
                if remember:                                                       # 如果remember状态是1
                    # session持久化
                    session.permanent = True
                return restful.success()                                           # 成功
            else:
                return restful.params_error(message="手机号或者密码错误")           # 密码是、用户不正确
        else:
            return restful.params_error(message=form.get_error())                  # 表单信息不存在,输出异常信息
        

# 绑定类视图的路由
front_bp.add_url_rule("/signup/", view_func=SingupView.as_view("/signup/"))
front_bp.add_url_rule("/signin/", view_func=SinginView.as_view("/signin/"))

轮播图渲染

需要用到bootstrap的工具进行渲染前端界面
bootstrap导航条链接
bootstrap轮播图链接

创建论坛首页界面:templates/front/front_index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>BBS论坛</title>

    <!--  bootstrap支持css,js的样式  -->
    <script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
    <link href="http://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

    <!--  关联front_index.html的css样式  -->
    <link href="{{ url_for('static', filename='front/css/front_index.css') }}" rel="stylesheet">
    <link href="{{ url_for('static', filename='front/css/front_base.css') }}" rel="stylesheet">

</head>
<body>
<!--  bootstrop中复制来的导航条  -->
<nav class="navbar navbar-default">
  <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="#">BBS论坛</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="/">首页 <span class="sr-only">(current)</span></a></li>
        <!--        <li><a href="#">Link</a></li>     -->
<!--        <li class="dropdown">-->
<!--          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <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">
        <!--    关联登录注册的url,front.signin是类视图中路由决定     -->
        <li><a href="{{ url_for('front.signin') }}">登陆</a></li>
        <li><a href="{{ url_for('front.signup') }}">注册</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>-->
<!--          </ul>-->
<!--        </li>-->
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>
<!--   bootstrop中复制的导航条代码结束 -->

<!--   居中样式  -->
<div class="main-container">
    <div class="lg-container">
        <!-- bootstrop中复制来的轮播图  -->
        <div id="carousel-example-generic" class="carousel slide" data-ride="carousel">
  <!-- 指令 -->
  <ol class="carousel-indicators">
    <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
    <li data-target="#carousel-example-generic" data-slide-to="1"></li>
    <li data-target="#carousel-example-generic" data-slide-to="2"></li>
  </ol>

  <!-- 轮播图 -->
  <div class="carousel-inner" role="listbox">
    <div class="item active">
<!--    轮播图路径    -->
      <img src=".\static\common\images\logo.png" alt="...">
      <div class="carousel-caption">
        ...
      </div>
    </div>
    <div class="item">
<!--   轮播图路径      -->
      <img src=".\static\common\images\1.png" alt="...">
      <div class="carousel-caption">
        ...
      </div>
    </div>
<!--  不同轮播图    -->
      <div class="item">
<!--   轮播图路径      -->
      <img src=".\static\common\images\2.png" alt="...">
      <div class="carousel-caption">
        ...
      </div>
    </div>
    ...
  </div>

  <!-- 轮播图左右切换按钮 -->
  <a class="left carousel-control" href="#carousel-example-generic" role="button" data-slide="prev">
    <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
    <span class="sr-only">Previous</span>
  </a>
  <a class="right carousel-control" href="#carousel-example-generic" role="button" data-slide="next">
    <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
    <span class="sr-only">Next</span>
  </a>
</div>
        <!-- bootstrop中复制来的轮播图 代码结束   -->

    </div>
</div>
<!--  居中样式  -->

</body>
</html>

创建论坛首页界面关联的样式:static/front/front_index.css

.index-banner{
    border-radius: 10px;
    overflow: hidden;
    height: 200px;
}

.index-banner img{
    height: 200px;
}

.post-group{
    border: 1px solid #ddd;
    margin-top: 20px;
    overflow: hidden;
    border-radius: 5px;
    padding: 10px;
}

.post-group-head{
    overflow: hidden;
    list-style: none;
}

.post-group-head li{
    float: left;
    padding: 5px 10px;
}

.post-group-head li a{
    color:#333;
}

.post-group-head li.active{
    background: #ccc;
}

.post-list-group{
    margin-top: 20px;
}

.post-list-group li{
    overflow: hidden;
    padding-bottom: 20px;
}

.author-avatar-group{
    float: left;
}

.author-avatar-group img{
    width: 50px;
    height: 50px;
    border-radius: 50%;
}

.post-info-group{
    float: left;
    margin-left: 10px;
    border-bottom: 1px solid #e6e6e6;
    width: 85%;
    padding-bottom: 10px;
}

.post-info-group .post-info{
    margin-top: 10px;
    font-size: 12px;
    color: #8c8c8c;
}

.post-info span{
    margin-right: 10px;
}

创建论坛首页界面关联的样式:static/front/front_base.css

a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote, body, canvas, caption, center, cite, code, dd, del, details, dfn, div, dl, dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, html, i, iframe, img, ins, kbd, label, legend, li, mark, menu, nav, object, ol, output, p, pre, q, ruby, s, samp, section, small, span, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, time, tr, tt, u, ul, var, video {
    margin: 0;
    padding: 0;
    border: 0;
    vertical-align: baseline;
    list-style: none;
}

.main-container{
    width: 990px;
    margin: 0 auto;
    overflow: hidden;
}

.lg-container{
    width: 730px;
    float: left;
}

.sm-container{
    width: 250px;
    float: right;
}

2、首页模板继承

创建论坛首页界面模板文件:templates/front/front_base.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{% block title %}

    {% endblock %}</title>

    <!--  bootstrap支持css,js的样式  -->
    <script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
    <link href="http://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

    <!--  关联front_index.html的css样式  -->
    <link href="{{ url_for('static', filename='front/css/front_index.css') }}" rel="stylesheet">
    <link href="{{ url_for('static', filename='front/css/front_base.css') }}" rel="stylesheet">

    <!--  模板继承,补充内容使用  -->
    {% block head %}

    {% endblock %}
</head>
<body>
<!--  bootstrop中复制来的导航条  -->
<nav class="navbar navbar-default">
  <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="#">BBS论坛</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="/">首页 <span class="sr-only">(current)</span></a></li>
        <!--        <li><a href="#">Link</a></li>     -->
<!--        <li class="dropdown">-->
<!--          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <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">
        <!--    关联登录注册的url,front.signin是类视图中路由决定     -->
        <li><a href="{{ url_for('front.signin') }}">登陆</a></li>
        <li><a href="{{ url_for('front.signup') }}">注册</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>-->
<!--          </ul>-->
<!--        </li>-->
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>
<!--   bootstrop中复制的导航条代码结束 -->

<!-- 模板继承 -->
{% block main_content %}

{% endblock %}

</body>
</html>

修改论坛首页界面文件:templates/front/front_index.html

{% extends 'front/front_base.html' %}

{% block title %}
    首页
{% endblock %}

<!-- 模板继承 -->
{% block main_content %}
    <!--   居中样式  -->
    <div class="main-container">
        <div class="lg-container">
            <!-- bootstrop中复制来的轮播图  -->
            <div id="carousel-example-generic" class="carousel slide" data-ride="carousel">
      <!-- 指令 -->
      <ol class="carousel-indicators">
        <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
        <li data-target="#carousel-example-generic" data-slide-to="1"></li>
        <li data-target="#carousel-example-generic" data-slide-to="2"></li>
      </ol>

      <!-- 轮播图 -->
      <div class="carousel-inner" role="listbox">
        <div class="item active">
    <!--    轮播图路径    -->
          <img src=".\static\common\images\logo.png" alt="...">
          <div class="carousel-caption">
            ...
          </div>
        </div>
        <div class="item">
    <!--   轮播图路径      -->
          <img src=".\static\common\images\1.png" alt="...">
          <div class="carousel-caption">
            ...
          </div>
        </div>
    <!--  不同轮播图    -->
          <div class="item">
    <!--   轮播图路径      -->
          <img src=".\static\common\images\2.png" alt="...">
          <div class="carousel-caption">
            ...
          </div>
        </div>
        ...
      </div>

      <!-- 轮播图左右切换按钮 -->
      <a class="left carousel-control" href="#carousel-example-generic" role="button" data-slide="prev">
        <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
        <span class="sr-only">Previous</span>
      </a>
      <a class="right carousel-control" href="#carousel-example-generic" role="button" data-slide="next">
        <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
        <span class="sr-only">Next</span>
      </a>
    </div>
            <!-- bootstrop中复制来的轮播图 代码结束   -->

        </div>
    </div>
    <!--  居中样式  -->
{% endblock %}

3、轮播图后台管理

创建后台轮播图管理的html页面:templates/cms/cms_banners.html

{% extends 'cms/cms_base.html' %}

{% block title %}
    轮播图管理
{% endblock %}

{% block page_title %}
    {{ self.title() }}
{% endblock %}

{% block head %}
    <script src="https://cdn.staticfile.org/Plupload/2.1.1/moxie.js"></script>
    <script src="https://cdn.staticfile.org/Plupload/2.1.1/plupload.dev.js"></script>
    <script src="https://cdn.staticfile.org/qiniu-js-sdk/1.0.14-beta/qiniu.js"></script>
    <script src="{{ url_for('static', filename='common/lgqiniu.js') }}"></script>
    <script src="{{ url_for('static',filename='cms/js/banners.js') }}"></script>
    <style>
        .top-box button{
            float: right;
        }
    </style>
{% endblock %}

{% block content %}


<div class="top-box">
    <button class="btn btn-warning" data-toggle="modal" data-target="#banner-dialog">添加轮播图</button>
</div>
<table class="table table-bordered">
    <thead>
    <tr>
        <th>名称</th>
        <th>图片链接</th>
        <th>跳转链接</th>
        <th>优先级</th>
        <th>创建时间</th>
        <th>操作</th>
    </tr>
    </thead>
    <tbody>
    {% for banner in banners %}
        <tr data-name="{{ banner.name }}" data-image="{{ banner.image_url }}" data-link="{{ banner.link_url }}"
            data-priority="{{ banner.priority }}" data-id="{{ banner.id }}">
            <td>{{ banner.name }}</td>

            <!--            -->
            <td><a href="{{ banner.image_url }}" target="_blank">{{ banner.image_url|truncate(length=20) }}</a></td>
            <!--            -->
            <td><a href="{{ banner.link_url }}" target="_blank">{{ banner.link_url }}</a></td>
            <td>{{ banner.priority }}</td>
            <td>{{ banner.create_time }}</td>

            <td>
                <button class="btn btn-default btn-xs edit-banner-btn">编辑</button>
                <button class="btn btn-danger btn-xs delete-banner-btn">删除</button>
            </td>
        </tr>
    {% endfor %}
    </tbody>
</table>

<!-- Modal -->
<div class="modal fade" id="banner-dialog" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
                </button>
                <h4 class="modal-title" id="myModalLabel">轮播图</h4>
            </div>
            <div class="modal-body">
                <form action="" class="form-horizontal">
                    <div class="form-group">
                        <label class="col-sm-2 control-label">名称:</label>
                        <div class="col-sm-10">
                            <input type="text" class="form-control" name="name" placeholder="轮播图名称">
                        </div>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label">图片:</label>
                        <div class="col-sm-7">
                            <input type="text" class="form-control" name="image_url" placeholder="轮播图图片">
                        </div>
                        <button class="btn btn-info col-sm-2" id="upload-btn">添加图片</button>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label">跳转:</label>
                        <div class="col-sm-10">
                            <input type="text" class="form-control" name="link_url" placeholder="跳转链接">
                        </div>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label">权重:</label>
                        <div class="col-sm-10">
                            <input type="number" class="form-control" name="priority" placeholder="优先级">
                        </div>
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
                <button type="button" class="btn btn-primary" id="save-banner-btn">保存</button>
            </div>
        </div>
    </div>
</div>
{% endblock %}

创建后台轮播图页面关联样式:static/cms/js/banners.js

var lgajax = {
    'get':function(args) {
        args['method'] = 'get';
        this.ajax(args);
    },
    'post':function(args) {
        args['method'] = 'post';
        this.ajax(args);
    },
    'ajax':function(args) {
        // 设置csrftoken
        this._ajaxSetup();
        $.ajax(args);
    },
    '_ajaxSetup': function() {
        $.ajaxSetup({
            'beforeSend':function(xhr,settings) {
                if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
                    var csrftoken = $('meta[name=csrf-token]').attr('content');
                    xhr.setRequestHeader("X-CSRFToken", csrftoken)
                }
            }
        });
    }
};

$(function () {
    $("#save-banner-btn").click(function (event) {
        event.preventDefault();
        var self = $(this);
        var dialog = $("#banner-dialog");
        var nameInput = $("input[name='name']");
        var imageInput = $("input[name='image_url']");
        var linkInput = $("input[name='link_url']");
        var priorityInput = $("input[name='priority']");


        var name = nameInput.val();
        var image_url = imageInput.val();
        var link_url = linkInput.val();
        var priority = priorityInput.val();
        var submitType = self.attr('data-type');
        var bannerId = self.attr("data-id");

        if(!name || !image_url || !link_url || !priority){
            lgalert.alertInfoToast('请输入完整的轮播图数据!');
            return;
        }

        var url = '';
        if(submitType == 'update'){
            url = '/cms/ubanner/';
        }else{
            url = '/cms/abanner/';
        }
        // form 发送 <form action="提交的地址" method="post">
        lgajax.post({
            "url": url,
            'data':{
                'name':name,
                'image_url': image_url,
                'link_url': link_url,
                'priority':priority,
                'banner_id': bannerId
            },
            'success': function (data) {
                dialog.modal("hide");
                if(data['code'] == 200){
                    // 重新加载这个页面
                    window.location.reload();
                }else{
                    lgalert.alertInfo(data['message']);
                }
            },
            'fail': function () {
                lgalert.alertNetworkError();
            }
        });
    });
});

$(function () {
    $(".edit-banner-btn").click(function (event) {
        var self = $(this);
        var dialog = $("#banner-dialog");
        dialog.modal("show");

        var tr = self.parent().parent();
        var name = tr.attr("data-name");
        var image_url = tr.attr("data-image");
        var link_url = tr.attr("data-link");
        var priority = tr.attr("data-priority");

        var nameInput = dialog.find("input[name='name']");
        var imageInput = dialog.find("input[name='image_url']");
        var linkInput = dialog.find("input[name='link_url']");
        var priorityInput = dialog.find("input[name='priority']");
        var saveBtn = dialog.find("#save-banner-btn");

        nameInput.val(name);
        imageInput.val(image_url);
        linkInput.val(link_url);
        priorityInput.val(priority);
        saveBtn.attr("data-type",'update');
        saveBtn.attr('data-id',tr.attr('data-id'));
    });
});

$(function () {
    $(".delete-banner-btn").click(function (event) {
        var self = $(this);
        var tr = self.parent().parent();
        var banner_id = tr.attr('data-id');
        lgalert.alertConfirm({
            "msg":"您确定要删除这个轮播图吗?",
            'confirmCallback': function () {
                lgajax.post({
                    'url': '/cms/dbanner/',
                    'data':{
                        'banner_id': banner_id
                    },
                    'success': function (data) {
                        if(data['code'] == 200){
                            window.location.reload();
                        }else{
                            lgalert.alertInfo(data['message']);
                        }
                    }
                })
            }
        });
    });
});

$(function () {
    lgqiniu.setUp({
        'domain': 'http://7xqenu.com1.z0.glb.clouddn.com/',
        'browse_btn': 'upload-btn',
        'uptoken_url': '/c/uptoken/',
        'success': function (up,file,info) {
            var imageInput = $("input[name='image_url']");
            imageInput.val(file.name);
        }
    });
});

视图文件apps/cms/views.py文件:添加轮播图路由banner

# -*- encoding: utf-8 -*-
"""
@File    : views.py
@Time    : 2020/5/11 9:59
@Author  : chen

"""
# 蓝图文件:实现模块化应用,应用可以分解成一系列的蓝图   后端的类视图函数写在这个文件
from flask import (
    request, redirect, url_for,                      # 页面跳转redirect   request请求收集
    Blueprint, render_template, views, session,      # 定义类视图,显示模板文件
    jsonify, g                                       # jsonify强制转换成json数据
)
from exts import db, mail                            # 数据库中更新密码、邮箱等使用

# 导入form表单   .forms代表同级目录下的forms.py       ResetPwdForm修改密码的form信息
from .forms import LoginForm, ResetPwdForm
# 导入forms.py文件中的邮箱验证的表单信息类
from apps.cms.forms import ResetEmailForm

# 导入模型  .models代表同级目录下的models.py   CMSPersmission验证用户不同模块权限
from .models import CMS_User, CMSPersmission, CMSRole
from .decorators import permission_required            # 传参装饰器验证用户不同模块权限

# 导入装饰器:判断当前界面是否是登录界面,不是就将url重定向到登录界面,一般不用,使用的主要是钩子函数
from .decorators import login_required

# 导入restful.py中的访问网页状态码的函数          redis_captcha:redis存储、提取、删除验证码功能
from utils import restful, random_captcha, redis_captcha           # 随机生成验证码函数random_captcha()

# 导入flask-mail中的Message
from flask_mail import Message

cms_bp = Blueprint("cms", __name__, url_prefix='/cms/')     # URL前缀url_prefix

# 钩子函数是在cms_bp创建之后才创建的,顺序在cms_bp创建之后
from .hooks import before_request


@cms_bp.route("/")                                          # 后台界面
# @login_required             # 装饰器判定当前界面是否是登录界面,但是需要每个路由函数都要加该装饰器,比较麻烦,推荐使用钩子函数
def index():
    # return "cms index:后端类视图文件"
    return render_template('cms/cms_index.html')       # 登陆之后进入CMS后台管理界面,路径写全cms/cms_index.html


# 用户注销登录
@cms_bp.route("/logout/")                              # 需要关联到cms/cms_index.html中的注销属性
def logout():
    # session清除user_id
    del session['user_id']
    # 重定向到登录界面
    return redirect(url_for('cms.login'))             # 重定向(redirec)为把url变为重定向的url


# 定义个人中心的路由
@cms_bp.route("/profile/")
def profile():
    return render_template("cms/cms_profile.html")   # 模板渲染(render_template)则不会改变url,模板渲染是用模板来渲染请求的url


# 定义类视图,显示模板文件   用户登录功能实现
class LoginView(views.MethodView):
    def get(self, message=None):                                         # message=None时候不传输信息到cms_login.html页面
        return render_template("cms/cms_login.html", message=message)    # 针对post方法中同样要返回到cms_login.html页面进行代码简化
    
    # 用户登录操作验证
    def post(self):
        # 收集表单信息
        login_form = LoginForm(request.form)
        if login_form.validate():
            # 数据库验证
            email = login_form.email.data
            password = login_form.password.data
            remember = login_form.remember.data
            
            # 查询数据库中的用户信息
            user = CMS_User.query.filter_by(email=email).first()    # 邮箱唯一,用于查询验证用户
            if user and user.check_password(password):              # 验证用户和密码是否都正确
                session['user_id'] = user.id                        # 查询到用户数据时,保存session的id到浏览器
                # session['user_name'] = user.username                # 将数据库中的user.username保存到session中,在hooks.py中判断
                # session['user_email'] = user.email                  # 将数据库中的email保存到session中,方便html调用信息
                # session['user_join_time'] = user.join_time          # 将数据库中的join_time保存到session中,方便html调用信息
                
                if remember:                                        # 如果用户点击了remember选择,在浏览器中进行数据持久化
                    session.permanent = True                        # 数据持久化,默认31天,需要设置session_key在config.py中
            
                # 登录成功,跳转到后台首页
                return redirect(url_for('cms.index'))               # 在蓝图中必须加cms   跳转到index方法
            else:
                # return "邮箱或密码错误"                              # 登录出错,返回结果
                # return render_template("cms/cms_login.html", message="邮箱或密码错误")  # 登录出错,返回结果渲染到cms_login.html页面
                return self.get(message="邮箱或密码错误")             # 传参到get方法中,多加一个传输错误信息的参数到方法中
        else:
            # print(login_form.errors)                                 # forms.py中的错误信息  字典类型数据
            # print(login_form.errors.popitem())                       # forms.py中的错误信息  元祖类型数据
            # return "表单验证错误"                                     # 错误信息需要渲染到cms_login.html页面
            # return self.get(message=login_form.errors.popitem()[1][0])  # 字典类型数据信息提取
            return self.get(message=login_form.get_error())            # login_form是收集到的表单信息,信息提取放置到forms.py的父类中实现
    
    
# 修改密码的类视图验证
class ResetPwd(views.MethodView):
    def get(self):
        return render_template('cms/cms_resetpwd.html')         # 模板渲染到cms_resetpwd.html
    
    # post提交密码修改
    def post(self):
        # 先审查旧密码是否与数据库中的信息相同
        form = ResetPwdForm(request.form)
        if form.validate():
            oldpwd = form.oldpwd.data
            newpwd = form.newpwd.data
            # 对象
            user = g.cms_user
            # 将用户输入的密码进行加密检测是否与数据库中的相同
            if user.check_password(oldpwd):
                # 更新我的密码  将新密码赋值,此时的新密码已经经过验证二次密码是否一致
                user.password = newpwd         # user.password已经调用了models.py中的 @property装饰器进行密码加密
                # 数据库更新
                db.session.commit()
                # return jsonify({"code": 400, "message": "密码修改成功"})        # 代码改写为下面
                return restful.success("密码修改成功")             # 调用restful.py中定义的访问网页成功的函数
            else:
                # 当前用户输入的旧密码与数据库中的不符
                # return jsonify({"code": 400, "message": "旧密码输入错误"})
                return restful.params_error(message="旧密码输入错误")      # 参数错误
        else:
            # ajax 需要返回一个json类型的数据
            # message = form.errors.popitem()[1][0]                     # 收集错误信息
            # return jsonify({"code": 400, "message": message})         # 将数据转换成json类型
            return restful.params_error(message=form.get_error())       # 参数错误,信息的收集在forms.py的父类函数中实现  form是收集到的信息
        

# 定义修改邮箱的类视图 验证
class ResetEmail(views.MethodView):
    def get(self):
        return render_template("cms/cms_resetemail.html")      # 返回到修改邮箱页面url
    
    def post(self):
        form = ResetEmailForm(request.form)                    # 接收邮箱验证的form表单信息
        if form.validate():                                    # 验证表单信息是否通过
            email = form.email.data                            # 获取form表单中填写的邮箱地址
            
            # 查询数据库
            # CMS_User.query.filter_by(email=email).first()
            # CMS_User.query.filter(CMS_User.email == email).first()
            g.cms_user.email = email                           # 数据库中的查询在apps/cms/hooks.py文件中确定了该用户的数据库信息,用全局对象g.cms_user修改邮箱
            db.session.commit()
            return restful.success()                           # 邮箱修改成功
        else:
            return restful.params_error(form.get_error())      # form是这个类中的所有表单信息
        
        
# 发送测试邮件进行验证
@cms_bp.route("/send_email/")
def send_mail():
    message = Message('邮件发送', recipients=['727506892@qq.com'], body='测试邮件发送')   # 主题:邮件发送;收件人:recipients;邮件内容:测试邮件发送
    mail.send(message)                   # 发送邮件
    return "邮件已发送"


# 邮件发送
class EmailCaptcha(views.MethodView):
    def get(self):                                  # 根据resetemail.js中的ajax方法来写函数,不需要post请求
        email = request.args.get('email')           # 查询email参数是否存在
        if not email:
            return restful.params_error('请传递邮箱参数')
        
        # 发送邮件,内容为一个验证码:4、6位数字英文组合
        captcha = random_captcha.get_random_captcha(4)            # 生成4位验证码
        message = Message('BBS论坛邮箱验证码', recipients=[email], body='您的验证码是:%s' % captcha)
        
        # 异常处理
        try:
            mail.send(message)
        except:
            return restful.server_error(message="服务器错误,邮件验证码未发送!")   # 发送异常,服务器错误
        
        # 验证码保存,一般有时效性,且频繁请求变化,所以保存在Redis中
        redis_captcha.redis_set(key=email, value=captcha)        # redis中都是键值对类型,存储验证码
        return restful.success("邮件验证码发送成功!")
    

# 轮播图管理路由
@cms_bp.route("/banners/")
def banners():
    return render_template("cms/cms_banners.html")


# 帖子管理路由 ,需要和cms_base.js中命名的相同才可以
@cms_bp.route("/posts/")
@permission_required(CMSPersmission.POSTER)                # 传参装饰器验证不同用户不同模块权限
def posts():
    return render_template("cms/cms_posts.html")
    

# 评论管理路由
@cms_bp.route("/comments/")
@permission_required(CMSPersmission.COMMENTER)             # 传参装饰器验证不同用户不同模块权限
def comments():
    return render_template("cms/cms_comments.html")


# 板块管理路由
@cms_bp.route("/boards/")
@permission_required(CMSPersmission.BOARDER)               # 传参装饰器验证不同用户不同模块权限
def boards():
    return render_template("cms/cms_boards.html")


# 前台用户管理路由
@cms_bp.route("/fusers/")
@permission_required(CMSPersmission.FRONTUSER)             # 传参装饰器验证不同用户不同模块权限
def fuser():
    return render_template("cms/cms_fuser.html")


# 后用户管理路由
@cms_bp.route("/cusers/")
@permission_required(CMSPersmission.CMSUSER)               # 传参装饰器验证不同用户不同模块权限
def cuser():
    return render_template("cms/cms_cuser.html")


# 添加登录路由
cms_bp.add_url_rule("/login/", view_func=LoginView.as_view('login'))    # view_func 命名操作名字,"/login/"路由地址

# 类视图函数添加绑定路由  注意类视图需要修改ResetPwd.as_view('resetpwd')
cms_bp.add_url_rule("/resetpwd/", view_func=ResetPwd.as_view('resetpwd'))  # view_func 命名操作名字,/resetpwd/路由地址

# 添加修改邮箱的类视图路由绑定,路由的命名和cms_base.js中的命名要相同,否则不关联,url=/resetemail/必须要和resetemail.js中的ajax绑定的路由相同
cms_bp.add_url_rule("/resetemail/", view_func=ResetEmail.as_view('resetemail'))

# 绑定路由,路由的命名和cms_base.js中的命名要相同,必须要和resetemail.js中的ajax绑定的路由相同
cms_bp.add_url_rule("/email_captcha/", view_func=EmailCaptcha.as_view('email_captcha'))

模板文件绑定轮播图路由:templates/cms/cms_base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <!--  在头文件中接收csrf信息  -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{% block title %}

    {% endblock %}</title>
    <script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
    <link href="http://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

<!--  关联本地的cms_base.css样式 后台管理界面CMS的样式 -->
    <link rel="stylesheet" href="{{ url_for('static', filename='cms/css/cms_base.css') }}">
<!--  关联本地的cms_base.js样式 后台管理界面CMS的样式 -->
    <script src="{{ url_for('static', filename='cms/js/cms_base.js') }}"></script>

<!--  提示框的静态资源文件  -->
    <link rel="stylesheet" href="{{ url_for('static', filename='common/sweetalert/sweetalert.css') }}">
<!-- 关联提示框的js样式  -->
    <script src="{{ url_for('static', filename='common/sweetalert/lgalert.js') }}"></script>
    <script src="{{ url_for('static', filename='common/sweetalert/sweetalert.min.js') }}"></script>

<!--  预留空间,给之后的html文件进行修改调整  -->
    {% block head %}

    {% endblock %}

</head>
<body>
     <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
      <div class="container-fluid">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
            <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="#">论坛CMS管理系统</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
          <ul class="nav navbar-nav navbar-right">
        <!--       从数据库中调用用户名,g对象全局调用g.cms_user对象  .username是该对象的一个字段属性      -->
            <li><a href="#">{{ g.cms_user.username }}</a></li>

        <!--  用户注销,关联到views.py中的@cms_bp.route("/logout/")路由,重定向到该路由      -->
            <li><a href="{{ url_for('cms.logout') }}">注销</a></li>
          </ul>
          <form class="navbar-form navbar-right">
            <input type="text" class="form-control" placeholder="查找...">
          </form>
        </div>
      </div>
    </nav>

    <div class="container-fluid">
      <div class="row">
          <div class="col-sm-3 col-md-2 sidebar">
              <ul class="nav-sidebar">
                <li class="unfold"><a href="#">首页</a></li>
                <li class="profile-li">
                    <a href="#">个人中心<span></span></a>
                    <ul class="subnav">

                        <!--          url重定向到/cms/profile/下   路由在views.py中定义了       -->
                        <li><a href="{{ url_for('cms.profile') }}">个人信息</a></li>
                        <!--         密码修改的url_for 重定向到/cms/resetpwd/  路由在views.py中定义了           -->
                        <li><a href="{{ url_for('cms.resetpwd') }}">修改密码</a></li>
                        <!--         重定向到修改邮箱的url_for=/cms/resetemail/        -->
                        <li><a href="{{ url_for('cms.resetemail') }}">修改邮箱</a></li>
                    </ul>
                </li>

                  <!--  将全局变量的对象命名为user  -->
                  {% set user = g.cms_user %}
                    <!--        {{ url_for('cms.banners') }}绑定路由          -->
                  <li class="nav-group banner-manage"><a href="{{ url_for('cms.banners') }}">轮播图管理</a></li>

                  <!--    判断是否有权限进行管理后台,CMSPersmission.ALL_PERMISSION并没有传输过来,无法识别,需要用到钩子函数中的上下文管理器,在hooks.py中编写 -->
                  {% if  user.has_permissions(CMSPersmission.POSTER) %}
                    <li class="nav-group post-manage"><a href="#">帖子管理</a></li>
                  {% endif %}

                  {% if  user.has_permissions(CMSPersmission.COMMENTER) %}
                    <li class="comments-manage"><a href="#">评论管理</a></li>
                  {% endif %}

                  {% if  user.has_permissions(CMSPersmission.BOARDER) %}
                    <li class="board-manage"><a href="#">板块管理</a></li>
                  {% endif %}

                  {% if  user.has_permissions(CMSPersmission.FRONTUSER) %}
                    <li class="nav-group user-manage"><a href="#">前台用户管理</a></li>
                  {% endif %}

                  {% if  user.has_permissions(CMSPersmission.CMSUSER) %}
                    <li class="nav-group cmsuser-manage"><a href="#">CMS用户管理</a></li>
                  {% endif %}

                  {% if  user.is_developer %}
                    <li class="cmsrole-manage"><a href="#">CMS组管理</a></li>
                  {% endif %}

            </ul>
          </div>
          <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
            <h1>{% block page_title %}

            {% endblock %}</h1>
            <div class="main_content">
                {% block content %}

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

后台模板图页面添加轮播图管理样式:static/cms/js/cms_base.js

/**
 * Created by Administrator on 2016/12/17.
 */

$(function () {
    $('.nav-sidebar>li>a').click(function (event) {
        var that = $(this);
        if(that.children('a').attr('href') == '#'){
            event.preventDefault();
        }
        if(that.parent().hasClass('unfold')){
            that.parent().removeClass('unfold');
        }else{
            that.parent().addClass('unfold').siblings().removeClass('unfold');
        }
        console.log('coming....');
    });

    $('.nav-sidebar a').mouseleave(function () {
        $(this).css('text-decoration','none');
    });
});


$(function () {
    var url = window.location.href;
    if(url.indexOf('profile') >= 0){
        var profileLi = $('.profile-li');
        profileLi.addClass('unfold').siblings().removeClass('unfold');
        profileLi.children('.subnav').children().eq(0).addClass('active').siblings().removeClass('active');
    } else if(url.indexOf('resetpwd') >= 0){
        var profileLi = $('.profile-li');
        profileLi.addClass('unfold').siblings().removeClass('unfold');
        profileLi.children('.subnav').children().eq(1).addClass('active').siblings().removeClass('active');
    } else if(url.indexOf('resetemail') >= 0){
        var profileLi = $('.profile-li');
        profileLi.addClass('unfold').siblings().removeClass('unfold');
        profileLi.children('.subnav').children().eq(2).addClass('active').siblings().removeClass('active');
    } else if(url.indexOf('posts') >= 0){
        var postManageLi = $('.post-manage');
        postManageLi.addClass('unfold').siblings().removeClass('unfold');
    }else if(url.indexOf('boards') >= 0){
        var boardManageLi = $('.board-manage');
        boardManageLi.addClass('unfold').siblings().removeClass('unfold');
    }else if(url.indexOf('permissions') >= 0){
        var permissionManageLi = $('.permission-manage');
        permissionManageLi.addClass('unfold').siblings().removeClass('unfold');
    }else if(url.indexOf('roles') >= 0){
        var roleManageLi = $('.role-manage');
        roleManageLi.addClass('unfold').siblings().removeClass('unfold');
    }else if(url.indexOf('users') >= 0){
        var userManageLi = $('.user-manage');
        userManageLi.addClass('unfold').siblings().removeClass('unfold');
    }else if(url.indexOf('cmsuser_manage') >= 0){
        var cmsuserManageLi = $('.cmsuser-manage');
        cmsuserManageLi.addClass('unfold').siblings().removeClass('unfold');
    }else if(url.indexOf('cmsrole_manage') >= 0){
        var cmsroleManageLi = $('.cmsrole-manage');
        cmsroleManageLi.addClass('unfold').siblings().removeClass('unfold');
    }else if(url.indexOf('comments') >= 0) {
        var commentsManageLi = $('.comments-manage');
        commentsManageLi.addClass('unfold').siblings().removeClass('unfold');
    }else if(url.indexOf('banner') >= 0) {                                                  // 新添加的轮播图管理样式
        var bannersManageLi = $('.banner-manage');
        bannersManageLi.addClass('unfold').siblings().removeClass('unfold');
    }
});

实现效果如下:
在这里插入图片描述

4、轮播图删改

后台模型文件:apps/cms/models.py:创建轮播图的模型

# -*- encoding: utf-8 -*-
"""
@File    : models.py
@Time    : 2020/5/11 10:00
@Author  : chen
后台模型文件:apps/cms/models.py
"""
# 定义后端用户模型
from exts import db                                                               # 数据库
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash         # 导入密码加密,解密方法的库


# 权限定义,不是模型,没有继承db.Model
class CMSPersmission(object):
    # 255 二进制表示所有的权限
    ALL_PERMISSION = 0b11111111          # 每一位数代表一个权限,共7个权限,8位1个字节
    
    # 访问权限
    VISITOR        = 0b00000001
    
    # 管理帖子
    POSTER         = 0b00000010
    
    # 管理评论
    COMMENTER      = 0b00000100
    
    # 管理板块
    BOARDER        = 0b00001000
    
    # 管理后台用户
    CMSUSER        = 0b00010000
    # 管理前台用户
    FRONTUSER      = 0b00100000
    # 管理管理员用户
    ADMINER        = 0b01000000


# 权限与角色是多对多的关系,创建他们的中间表
cms_role_user = db.Table(
    "cms_role_user",
    db.Column("cms_role_id", db.Integer, db.ForeignKey('cms_role.id'), primary_key=True),
    db.Column("cms_user_id", db.Integer, db.ForeignKey('cms_user.id'), primary_key=True),
)


# 角色模型定义   继承了db.Model
class CMSRole(db.Model):
    __tablename__ = 'cms_role'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)      # 主键  自增
    name = db.Column(db.String(50), nullable=False)                       # 非空
    desc = db.Column(db.String(250), nullable=False)                      # 非空
    creat_time = db.Column(db.DateTime, default=datetime.now)
    permission = db.Column(db.Integer, default=CMSPersmission.VISITOR)    # 默认先给游客权限

    # 反向查询属性,关联中间表secondary=cms_role_user,对应了CMS_User模型,建立模型联系,不映射到数据库中
    users = db.relationship('CMS_User', secondary=cms_role_user, backref="roles")    # roles是CMS_User的外键
    
    
# 后台用户模型定义
class CMS_User(db.Model):
    __tablename__ = 'cms_user'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)           # 主键  自增
    username = db.Column(db.String(150), nullable=False)                       # 非空
    # password = db.Column(db.String(150), nullable=False)
    _password = db.Column(db.String(150), nullable=False)                      # 密码加密操作修改字段
    email = db.Column(db.String(50), nullable=False, unique=True)              # 非空、唯一
    join_time = db.Column(db.DateTime, default=datetime.now)                   # 默认当前时间
    
    # 修改密码加密操作中的字段,在manage.py映射数据库时候,使用字段还是保持相同
    def __init__(self, username, password, email):
        self.username = username
        self.password = password         # 调用该方法 返回下面的self._password数值,
        self.email = email
    
    # 密码加密操作
    @property
    def password(self):                   # 密码取值
        return self._password

    @password.setter                      # 密码加密
    def password(self, raw_password):
        self._password = generate_password_hash(raw_password)

    # 用于验证后台登录密码是否和数据库一致,raw_password是后台登录输入的密码
    def check_password(self, raw_password):
        result = check_password_hash(self.password, raw_password)   # 相当于用相同的hash加密算法加密raw_password,检测与数据库中是否一致
        return result
    
    # 封装用户的权限
    @property
    def permission(self):
        if not self.roles:           # 反向查询属性,backref="roles",
            return 0                 # 没有任何权限
        
        # 所有权限
        all_permissions = 0
        
        for role in self.roles:                    # 循环调用所有角色
            permissions = role.permission         # 将这个角色的权限都取出来  role.permission代表CMSRole中的属性
            all_permissions |= permissions         # 当前这个角色的权限都在all_permissions
            
        return all_permissions
    
    # 判断用户所具有的权限
    def has_permissions(self, permission):
        all_permissions = self.permission                 # 调用permission(self)方法
        #  若所有权限0b11111111 & 用户权限     等于 本身,则代表具有该权限
        result = all_permissions & permission == permission
        # print(result)
        return result
        
    # 判断是否是开发人员
    @property
    def is_developer(self):
         return self.has_permissions(CMSPersmission.ALL_PERMISSION)       # 调用has_permissions方法并传入所有权限
         
 
# 轮播图的模型创建
class BannerModel(db.Model):
    __tablename__ = 'banner'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键  自增
    name = db.Column(db.String(250), nullable=False)                  # 非空
    # 图片链接
    image_url = db.Column(db.String(250), nullable=False)             # 轮播图的链接资源
    # 跳转链接
    link_url = db.Column(db.String(50), nullable=False)
    priority = db.Column(db.Integer, default=0)                       # 权重选项
    create_time = db.Column(db.DateTime, default=datetime.now)        # 创建时间

模型映射到数据库中
在这里插入图片描述

视图添加轮播图功能

添加轮播图提交的表单信息文件:apps/cms/forms.py

# -*- encoding: utf-8 -*-
"""
@File    : forms.py
@Time    : 2020/5/11 10:00
@Author  : chen
forms表单信息:apps/cms/forms.py
"""
# forms表单信息
from wtforms import Form, StringField, IntegerField, ValidationError
from wtforms.validators import Email, InputRequired, Length, EqualTo, URL # EqualTo验证新密码是否相同,URL验证
from utils.redis_captcha import redis_get                                       # 导入验证码模块


# 创父类form表单,用于输出错误信息
class BaseForm(Form):
    def get_error(self):
        message = self.errors.popitem()[1][0]          # 错误信息的收集,字典类型数据信息提取
        return message


# 登录页面中的Form表单      继承父类form
class LoginForm(BaseForm):
    email = StringField(validators=[Email(message="请输入正确的邮箱"),  InputRequired(message="请输入邮箱")])
    password = StringField(validators=[Length(3, 15, message='请输入正确长度的密码')])   # 长度可以先设置短的,方便项目测试
    remember = IntegerField()                # 记住cookie操作  赋值为0或1


# 修改密码页面中的form表单信息    继承父类form
class ResetPwdForm(BaseForm):
    oldpwd = StringField(validators=[Length(3, 15, message="密码长度有误")])
    newpwd = StringField(validators=[Length(3, 15, message="密码长度有误")])
    newpwd2 = StringField(validators=[EqualTo("newpwd", message="两次输入密码不一致")])
    

# 定义设置邮箱的表单信息,进行提交时候使用
class ResetEmailForm(BaseForm):
    email = StringField(validators=[Email(message="请输入正确格式的邮箱")])                    # 名称email与cms_resetemail.html中的要相同
    captcha = StringField(validators=[Length(min=4, max=4, message="请输入正确长度的验证码")]) # 名称captcha与cms_resetemail.html中的要相同
    
    # 验证redis中的字段与数据库中的字段是否相同
    def validate_captcha(self, field):           # 方法命名规则是:validate_字段名()
        # 表单提交上来的验证码
        email = self.email.data
        captcha = self.captcha.data
        
        # 取redis中保存的验证码             第一个redis_captcha是新对象,第二个redis_captcha是redis_captcha.py文件
        redis_captcha = redis_get(email)
        if not redis_captcha or captcha.lower() != redis_captcha.lower():    # 不区分大小写
            raise ValidationError('邮箱验证码错误')


# 定义轮播图的表单信息
class AddBannerForm(BaseForm):
    # Form表单名称根据static/cms/js/banners.js中的ajax.post发送的data中
    name = StringField(validators=[InputRequired(message="请输入轮播图名称")])
    image_url = StringField(validators=[InputRequired(message="请输入轮播图片链接"), URL(message="图片链接有误")])
    link_url = StringField(validators=[InputRequired(message="请输入轮播图上跳转链接"), URL(message="跳转链接有误")])
    priority = IntegerField(validators=[InputRequired(message="请输入轮播图优先级")])
   

添加轮播图路由:视图文件:apps/cms/views.py文件

# -*- encoding: utf-8 -*-
"""
@File    : views.py
@Time    : 2020/5/11 9:59
@Author  : chen
视图文件:apps/cms/views.py文件
"""
# 蓝图文件:实现模块化应用,应用可以分解成一系列的蓝图   后端的类视图函数写在这个文件
from flask import (
    request, redirect, url_for,                      # 页面跳转redirect   request请求收集
    Blueprint, render_template, views, session,      # 定义类视图,显示模板文件
    jsonify, g                                       # jsonify强制转换成json数据
)
from exts import db, mail                            # 数据库中更新密码、邮箱等使用

# 导入form表单   .forms代表同级目录下的forms.py       ResetPwdForm修改密码的form信息
from .forms import LoginForm, ResetPwdForm
# 导入forms.py文件中的邮箱验证的表单信息类
from apps.cms.forms import ResetEmailForm
# 导入添加轮播图的表单信息
from apps.cms.forms import AddBannerForm
# 导入轮播图模型BannerModel
from apps.cms.models import BannerModel

# 导入模型  .models代表同级目录下的models.py   CMSPersmission验证用户不同模块权限
from .models import CMS_User, CMSPersmission, CMSRole
from .decorators import permission_required            # 传参装饰器验证用户不同模块权限

# 导入装饰器:判断当前界面是否是登录界面,不是就将url重定向到登录界面,一般不用,使用的主要是钩子函数
from .decorators import login_required

# 导入restful.py中的访问网页状态码的函数          redis_captcha:redis存储、提取、删除验证码功能
from utils import restful, random_captcha, redis_captcha           # 随机生成验证码函数random_captcha()

# 导入flask-mail中的Message
from flask_mail import Message

cms_bp = Blueprint("cms", __name__, url_prefix='/cms/')     # URL前缀url_prefix

# 钩子函数是在cms_bp创建之后才创建的,顺序在cms_bp创建之后
from .hooks import before_request


@cms_bp.route("/")                                          # 后台界面
# @login_required             # 装饰器判定当前界面是否是登录界面,但是需要每个路由函数都要加该装饰器,比较麻烦,推荐使用钩子函数
def index():
    # return "cms index:后端类视图文件"
    return render_template('cms/cms_index.html')       # 登陆之后进入CMS后台管理界面,路径写全cms/cms_index.html


# 用户注销登录
@cms_bp.route("/logout/")                              # 需要关联到cms/cms_index.html中的注销属性
def logout():
    # session清除user_id
    del session['user_id']
    # 重定向到登录界面
    return redirect(url_for('cms.login'))             # 重定向(redirec)为把url变为重定向的url


# 定义个人中心的路由
@cms_bp.route("/profile/")
def profile():
    return render_template("cms/cms_profile.html")   # 模板渲染(render_template)则不会改变url,模板渲染是用模板来渲染请求的url


# 定义类视图,显示模板文件   用户登录功能实现
class LoginView(views.MethodView):
    def get(self, message=None):                                         # message=None时候不传输信息到cms_login.html页面
        return render_template("cms/cms_login.html", message=message)    # 针对post方法中同样要返回到cms_login.html页面进行代码简化
    
    # 用户登录操作验证
    def post(self):
        # 收集表单信息
        login_form = LoginForm(request.form)
        if login_form.validate():
            # 数据库验证
            email = login_form.email.data
            password = login_form.password.data
            remember = login_form.remember.data
            
            # 查询数据库中的用户信息
            user = CMS_User.query.filter_by(email=email).first()    # 邮箱唯一,用于查询验证用户
            if user and user.check_password(password):              # 验证用户和密码是否都正确
                session['user_id'] = user.id                        # 查询到用户数据时,保存session的id到浏览器
                # session['user_name'] = user.username                # 将数据库中的user.username保存到session中,在hooks.py中判断
                # session['user_email'] = user.email                  # 将数据库中的email保存到session中,方便html调用信息
                # session['user_join_time'] = user.join_time          # 将数据库中的join_time保存到session中,方便html调用信息
                
                if remember:                                        # 如果用户点击了remember选择,在浏览器中进行数据持久化
                    session.permanent = True                        # 数据持久化,默认31天,需要设置session_key在config.py中
            
                # 登录成功,跳转到后台首页
                return redirect(url_for('cms.index'))               # 在蓝图中必须加cms   跳转到index方法
            else:
                # return "邮箱或密码错误"                              # 登录出错,返回结果
                # return render_template("cms/cms_login.html", message="邮箱或密码错误")  # 登录出错,返回结果渲染到cms_login.html页面
                return self.get(message="邮箱或密码错误")             # 传参到get方法中,多加一个传输错误信息的参数到方法中
        else:
            # print(login_form.errors)                                 # forms.py中的错误信息  字典类型数据
            # print(login_form.errors.popitem())                       # forms.py中的错误信息  元祖类型数据
            # return "表单验证错误"                                     # 错误信息需要渲染到cms_login.html页面
            # return self.get(message=login_form.errors.popitem()[1][0])  # 字典类型数据信息提取
            return self.get(message=login_form.get_error())            # login_form是收集到的表单信息,信息提取放置到forms.py的父类中实现
    
    
# 修改密码的类视图验证
class ResetPwd(views.MethodView):
    def get(self):
        return render_template('cms/cms_resetpwd.html')         # 模板渲染到cms_resetpwd.html
    
    # post提交密码修改
    def post(self):
        # 先审查旧密码是否与数据库中的信息相同
        form = ResetPwdForm(request.form)
        if form.validate():
            oldpwd = form.oldpwd.data
            newpwd = form.newpwd.data
            # 对象
            user = g.cms_user
            # 将用户输入的密码进行加密检测是否与数据库中的相同
            if user.check_password(oldpwd):
                # 更新我的密码  将新密码赋值,此时的新密码已经经过验证二次密码是否一致
                user.password = newpwd         # user.password已经调用了models.py中的 @property装饰器进行密码加密
                # 数据库更新
                db.session.commit()
                # return jsonify({"code": 400, "message": "密码修改成功"})        # 代码改写为下面
                return restful.success("密码修改成功")             # 调用restful.py中定义的访问网页成功的函数
            else:
                # 当前用户输入的旧密码与数据库中的不符
                # return jsonify({"code": 400, "message": "旧密码输入错误"})
                return restful.params_error(message="旧密码输入错误")      # 参数错误
        else:
            # ajax 需要返回一个json类型的数据
            # message = form.errors.popitem()[1][0]                     # 收集错误信息
            # return jsonify({"code": 400, "message": message})         # 将数据转换成json类型
            return restful.params_error(message=form.get_error())       # 参数错误,信息的收集在forms.py的父类函数中实现  form是收集到的信息
        

# 定义修改邮箱的类视图 验证
class ResetEmail(views.MethodView):
    def get(self):
        return render_template("cms/cms_resetemail.html")      # 返回到修改邮箱页面url
    
    def post(self):
        form = ResetEmailForm(request.form)                    # 接收邮箱验证的form表单信息
        if form.validate():                                    # 验证表单信息是否通过
            email = form.email.data                            # 获取form表单中填写的邮箱地址
            
            # 查询数据库
            # CMS_User.query.filter_by(email=email).first()
            # CMS_User.query.filter(CMS_User.email == email).first()
            g.cms_user.email = email                           # 数据库中的查询在apps/cms/hooks.py文件中确定了该用户的数据库信息,用全局对象g.cms_user修改邮箱
            db.session.commit()
            return restful.success()                           # 邮箱修改成功
        else:
            return restful.params_error(form.get_error())      # form是这个类中的所有表单信息
        
        
# 发送测试邮件进行验证
@cms_bp.route("/send_email/")
def send_mail():
    message = Message('邮件发送', recipients=['727506892@qq.com'], body='测试邮件发送')   # 主题:邮件发送;收件人:recipients;邮件内容:测试邮件发送
    mail.send(message)                   # 发送邮件
    return "邮件已发送"


# 邮件发送
class EmailCaptcha(views.MethodView):
    def get(self):                                  # 根据resetemail.js中的ajax方法来写函数,不需要post请求
        email = request.args.get('email')           # 查询email参数是否存在
        if not email:
            return restful.params_error('请传递邮箱参数')
        
        # 发送邮件,内容为一个验证码:4、6位数字英文组合
        captcha = random_captcha.get_random_captcha(4)            # 生成4位验证码
        message = Message('BBS论坛邮箱验证码', recipients=[email], body='您的验证码是:%s' % captcha)
        
        # 异常处理
        try:
            mail.send(message)
        except:
            return restful.server_error(message="服务器错误,邮件验证码未发送!")   # 发送异常,服务器错误
        
        # 验证码保存,一般有时效性,且频繁请求变化,所以保存在Redis中
        redis_captcha.redis_set(key=email, value=captcha)        # redis中都是键值对类型,存储验证码
        return restful.success("邮件验证码发送成功!")
    

# 轮播图管理路由
@cms_bp.route("/banners/")
def banners():
    # 通过模型中定义的权重priority的倒叙来排序
    banners = BannerModel.query.order_by(BannerModel.priority.desc()).all()
    return render_template("cms/cms_banners.html", banners=banners)           # 传输banners数据到cms_banners.html界面渲染


# 添加轮播图路由,且方法需要与static/cms/js/banners.js中绑定的方法POST相同
@cms_bp.route("/abanner/", methods=['POST'])
def abanner():
    form = AddBannerForm(request.form)                  # 接收添加轮播图的form表单信息
    if form.validate():
        name = form.name.data
        image_url = form.image_url.data
        link_url = form.link_url.data
        priority = form.priority.data
        
        banner = BannerModel(name=name, image_url=image_url, link_url=link_url, priority=priority)     # 轮播图模型
        db.session.add(banner)                                                                         # 提交数据库
        db.session.commit()
        return restful.success()                                                                       # 轮播图信息提交成功
    else:
        return restful.params_error(message=form.get_error())                                          # 表单信息错误


# 帖子管理路由 ,需要和cms_base.js中命名的相同才可以
@cms_bp.route("/posts/")
@permission_required(CMSPersmission.POSTER)                # 传参装饰器验证不同用户不同模块权限
def posts():
    return render_template("cms/cms_posts.html")
    

# 评论管理路由
@cms_bp.route("/comments/")
@permission_required(CMSPersmission.COMMENTER)             # 传参装饰器验证不同用户不同模块权限
def comments():
    return render_template("cms/cms_comments.html")


# 板块管理路由
@cms_bp.route("/boards/")
@permission_required(CMSPersmission.BOARDER)               # 传参装饰器验证不同用户不同模块权限
def boards():
    return render_template("cms/cms_boards.html")


# 前台用户管理路由
@cms_bp.route("/fusers/")
@permission_required(CMSPersmission.FRONTUSER)             # 传参装饰器验证不同用户不同模块权限
def fuser():
    return render_template("cms/cms_fuser.html")


# 后用户管理路由
@cms_bp.route("/cusers/")
@permission_required(CMSPersmission.CMSUSER)               # 传参装饰器验证不同用户不同模块权限
def cuser():
    return render_template("cms/cms_cuser.html")


# 添加登录路由
cms_bp.add_url_rule("/login/", view_func=LoginView.as_view('login'))    # view_func 命名操作名字,"/login/"路由地址

# 类视图函数添加绑定路由  注意类视图需要修改ResetPwd.as_view('resetpwd')
cms_bp.add_url_rule("/resetpwd/", view_func=ResetPwd.as_view('resetpwd'))  # view_func 命名操作名字,/resetpwd/路由地址

# 添加修改邮箱的类视图路由绑定,路由的命名和cms_base.js中的命名要相同,否则不关联,url=/resetemail/必须要和resetemail.js中的ajax绑定的路由相同
cms_bp.add_url_rule("/resetemail/", view_func=ResetEmail.as_view('resetemail'))

# 绑定路由,路由的命名和cms_base.js中的命名要相同,必须要和resetemail.js中的ajax绑定的路由相同
cms_bp.add_url_rule("/email_captcha/", view_func=EmailCaptcha.as_view('email_captcha'))

轮播图链接渲染后台管理界面

循环数据库中banners信息,数据从视图文件:apps/cms/views.py中传输渲染过来
后台轮播图页面文件:templates/cms/cms_banners.html

{% extends 'cms/cms_base.html' %}

{% block title %}
    轮播图管理
{% endblock %}

{% block page_title %}
    {{ self.title() }}
{% endblock %}

{% block head %}
    <script src="https://cdn.staticfile.org/Plupload/2.1.1/moxie.js"></script>
    <script src="https://cdn.staticfile.org/Plupload/2.1.1/plupload.dev.js"></script>
    <script src="https://cdn.staticfile.org/qiniu-js-sdk/1.0.14-beta/qiniu.js"></script>
    <script src="{{ url_for('static', filename='common/lgqiniu.js') }}"></script>
    <script src="{{ url_for('static',filename='cms/js/banners.js') }}"></script>
    <style>
        .top-box button{
            float: right;
        }
    </style>
{% endblock %}

{% block content %}


<div class="top-box">
    <button class="btn btn-warning" data-toggle="modal" data-target="#banner-dialog">添加轮播图</button>
</div>
<table class="table table-bordered">
    <thead>
    <tr>
        <th>名称</th>
        <th>图片链接</th>
        <th>跳转链接</th>
        <th>优先级</th>
        <th>创建时间</th>
        <th>操作</th>
    </tr>
    </thead>
    <tbody>
    <!--   循环数据库中banners信息,数据从视图文件:apps/cms/views.py中传输渲染过来 -->
    {% for banner in banners %}
        <tr data-name="{{ banner.name }}" data-image="{{ banner.image_url }}" data-link="{{ banner.link_url }}"
            data-priority="{{ banner.priority }}" data-id="{{ banner.id }}">
            <td>{{ banner.name }}</td>

            <!--     truncate(length=20)将链接的显示长度控制在20字符       -->
            <td><a href="{{ banner.image_url }}" target="_blank">{{ banner.image_url|truncate(length=20) }}</a></td>
            <!--     轮播图跳转链接显示  权重、创建时间     -->
            <td><a href="{{ banner.link_url }}" target="_blank">{{ banner.link_url }}</a></td>
            <td>{{ banner.priority }}</td>
            <td>{{ banner.create_time }}</td>

            <td>
                <button class="btn btn-default btn-xs edit-banner-btn">编辑</button>
                <button class="btn btn-danger btn-xs delete-banner-btn">删除</button>
            </td>
        </tr>
    {% endfor %}
    </tbody>
</table>

<!-- Modal -->
<div class="modal fade" id="banner-dialog" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
                </button>
                <h4 class="modal-title" id="myModalLabel">轮播图</h4>
            </div>
            <div class="modal-body">
                <form action="" class="form-horizontal">
                    <div class="form-group">
                        <label class="col-sm-2 control-label">名称:</label>
                        <div class="col-sm-10">
                            <input type="text" class="form-control" name="name" placeholder="轮播图名称">
                        </div>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label">图片:</label>
                        <div class="col-sm-7">
                            <input type="text" class="form-control" name="image_url" placeholder="轮播图图片">
                        </div>
                        <button class="btn btn-info col-sm-2" id="upload-btn">添加图片</button>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label">跳转:</label>
                        <div class="col-sm-10">
                            <input type="text" class="form-control" name="link_url" placeholder="跳转链接">
                        </div>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label">权重:</label>
                        <div class="col-sm-10">
                            <input type="number" class="form-control" name="priority" placeholder="优先级">
                        </div>
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
                <button type="button" class="btn btn-primary" id="save-banner-btn">保存</button>
            </div>
        </div>
    </div>
</div>
{% endblock %}

关联js文件:static/cms/js/banners.js

var lgajax = {
    'get':function(args) {
        args['method'] = 'get';
        this.ajax(args);
    },
    'post':function(args) {
        args['method'] = 'post';
        this.ajax(args);
    },
    'ajax':function(args) {
        // 设置csrftoken
        this._ajaxSetup();
        $.ajax(args);
    },
    '_ajaxSetup': function() {
        $.ajaxSetup({
            'beforeSend':function(xhr,settings) {
                if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
                    var csrftoken = $('meta[name=csrf-token]').attr('content');
                    xhr.setRequestHeader("X-CSRFToken", csrftoken)
                }
            }
        });
    }
};

$(function () {
    // 保存轮播图按钮
    $("#save-banner-btn").click(function (event) {
        event.preventDefault();
        var self = $(this);
        var dialog = $("#banner-dialog");
        var nameInput = $("input[name='name']");                           // 获得表单输入的信息
        var imageInput = $("input[name='image_url']");
        var linkInput = $("input[name='link_url']");
        var priorityInput = $("input[name='priority']");


        var name = nameInput.val();
        var image_url = imageInput.val();
        var link_url = linkInput.val();
        var priority = priorityInput.val();
        var submitType = self.attr('data-type');
        var bannerId = self.attr("data-id");

        if(!name || !image_url || !link_url || !priority){
            lgalert.alertInfoToast('请输入完整的轮播图数据!');
            return;
        }

        var url = '';
        if(submitType == 'update'){
            url = '/cms/ubanner/';               //  修改轮播图选项update
        }else{
            url = '/cms/abanner/';               //  添加轮播图选项 add
        }
        // form 发送 <form action="提交的地址" method="post">
        lgajax.post({                       // 方法是post,在视图文件:apps/cms/views.py文件中添加轮播图路由方法需要为POST
            "url": url,
            'data':{                             // Form表单名称
                'name':name,
                'image_url': image_url,
                'link_url': link_url,
                'priority':priority,
                'banner_id': bannerId
            },
            'success': function (data) {
                dialog.modal("hide");                            // 添加轮播图Form表单界面隐藏
                if(data['code'] == 200){
                    // 重新加载这个页面
                    window.location.reload();                   // 发送成功,页面刷新
                }else{
                    lgalert.alertInfo(data['message']);         // 弹出异常信息
                }
            },
            'fail': function () {
                lgalert.alertNetworkError();
            }
        });
    });
});

$(function () {
    $(".edit-banner-btn").click(function (event) {
        var self = $(this);
        var dialog = $("#banner-dialog");
        dialog.modal("show");

        var tr = self.parent().parent();
        var name = tr.attr("data-name");
        var image_url = tr.attr("data-image");
        var link_url = tr.attr("data-link");
        var priority = tr.attr("data-priority");

        var nameInput = dialog.find("input[name='name']");
        var imageInput = dialog.find("input[name='image_url']");
        var linkInput = dialog.find("input[name='link_url']");
        var priorityInput = dialog.find("input[name='priority']");
        var saveBtn = dialog.find("#save-banner-btn");

        nameInput.val(name);
        imageInput.val(image_url);
        linkInput.val(link_url);
        priorityInput.val(priority);
        saveBtn.attr("data-type",'update');
        saveBtn.attr('data-id',tr.attr('data-id'));
    });
});

$(function () {
    // 删除轮播图选项按钮
    $(".delete-banner-btn").click(function (event) {
        var self = $(this);
        var tr = self.parent().parent();
        var banner_id = tr.attr('data-id');
        lgalert.alertConfirm({
            "msg":"您确定要删除这个轮播图吗?",
            'confirmCallback': function () {
                lgajax.post({
                    'url': '/cms/dbanner/',
                    'data':{
                        'banner_id': banner_id
                    },
                    'success': function (data) {
                        if(data['code'] == 200){
                            window.location.reload();
                        }else{
                            lgalert.alertInfo(data['message']);
                        }
                    }
                })
            }
        });
    });
});

$(function () {
    lgqiniu.setUp({
        'domain': 'http://7xqenu.com1.z0.glb.clouddn.com/',
        'browse_btn': 'upload-btn',
        'uptoken_url': '/c/uptoken/',
        'success': function (up,file,info) {
            var imageInput = $("input[name='image_url']");
            imageInput.val(file.name);
        }
    });
});

实现效果如下
在这里插入图片描述

编辑和删除功能

视图文件:apps/cms/views.py文件

# -*- encoding: utf-8 -*-
"""
@File    : views.py
@Time    : 2020/5/11 9:59
@Author  : chen
视图文件:apps/cms/views.py文件
"""
# 蓝图文件:实现模块化应用,应用可以分解成一系列的蓝图   后端的类视图函数写在这个文件
from flask import (
    request, redirect, url_for,                      # 页面跳转redirect   request请求收集
    Blueprint, render_template, views, session,      # 定义类视图,显示模板文件
    jsonify, g                                       # jsonify强制转换成json数据
)
from exts import db, mail                            # 数据库中更新密码、邮箱等使用

# 导入form表单   .forms代表同级目录下的forms.py       ResetPwdForm修改密码的form信息
from .forms import LoginForm, ResetPwdForm
# 导入forms.py文件中的邮箱验证的表单信息类
from apps.cms.forms import ResetEmailForm
# 导入 添加轮播图 的表单信息
from apps.cms.forms import AddBannerForm
# 导入 更新轮播图 的表单信息
from apps.cms.forms import UpdateBannerForm
# 导入轮播图模型BannerModel
from apps.cms.models import BannerModel

# 导入模型  .models代表同级目录下的models.py   CMSPersmission验证用户不同模块权限
from .models import CMS_User, CMSPersmission, CMSRole
from .decorators import permission_required            # 传参装饰器验证用户不同模块权限

# 导入装饰器:判断当前界面是否是登录界面,不是就将url重定向到登录界面,一般不用,使用的主要是钩子函数
from .decorators import login_required

# 导入restful.py中的访问网页状态码的函数          redis_captcha:redis存储、提取、删除验证码功能
from utils import restful, random_captcha, redis_captcha           # 随机生成验证码函数random_captcha()

# 导入flask-mail中的Message
from flask_mail import Message

cms_bp = Blueprint("cms", __name__, url_prefix='/cms/')     # URL前缀url_prefix

# 钩子函数是在cms_bp创建之后才创建的,顺序在cms_bp创建之后
from .hooks import before_request


@cms_bp.route("/")                                          # 后台界面
# @login_required             # 装饰器判定当前界面是否是登录界面,但是需要每个路由函数都要加该装饰器,比较麻烦,推荐使用钩子函数
def index():
    # return "cms index:后端类视图文件"
    return render_template('cms/cms_index.html')       # 登陆之后进入CMS后台管理界面,路径写全cms/cms_index.html


# 用户注销登录
@cms_bp.route("/logout/")                              # 需要关联到cms/cms_index.html中的注销属性
def logout():
    # session清除user_id
    del session['user_id']
    # 重定向到登录界面
    return redirect(url_for('cms.login'))             # 重定向(redirec)为把url变为重定向的url


# 定义个人中心的路由
@cms_bp.route("/profile/")
def profile():
    return render_template("cms/cms_profile.html")   # 模板渲染(render_template)则不会改变url,模板渲染是用模板来渲染请求的url


# 定义类视图,显示模板文件   用户登录功能实现
class LoginView(views.MethodView):
    def get(self, message=None):                                         # message=None时候不传输信息到cms_login.html页面
        return render_template("cms/cms_login.html", message=message)    # 针对post方法中同样要返回到cms_login.html页面进行代码简化
    
    # 用户登录操作验证
    def post(self):
        # 收集表单信息
        login_form = LoginForm(request.form)
        if login_form.validate():
            # 数据库验证
            email = login_form.email.data
            password = login_form.password.data
            remember = login_form.remember.data
            
            # 查询数据库中的用户信息
            user = CMS_User.query.filter_by(email=email).first()    # 邮箱唯一,用于查询验证用户
            if user and user.check_password(password):              # 验证用户和密码是否都正确
                session['user_id'] = user.id                        # 查询到用户数据时,保存session的id到浏览器
                # session['user_name'] = user.username                # 将数据库中的user.username保存到session中,在hooks.py中判断
                # session['user_email'] = user.email                  # 将数据库中的email保存到session中,方便html调用信息
                # session['user_join_time'] = user.join_time          # 将数据库中的join_time保存到session中,方便html调用信息
                
                if remember:                                        # 如果用户点击了remember选择,在浏览器中进行数据持久化
                    session.permanent = True                        # 数据持久化,默认31天,需要设置session_key在config.py中
            
                # 登录成功,跳转到后台首页
                return redirect(url_for('cms.index'))               # 在蓝图中必须加cms   跳转到index方法
            else:
                # return "邮箱或密码错误"                              # 登录出错,返回结果
                # return render_template("cms/cms_login.html", message="邮箱或密码错误")  # 登录出错,返回结果渲染到cms_login.html页面
                return self.get(message="邮箱或密码错误")             # 传参到get方法中,多加一个传输错误信息的参数到方法中
        else:
            # print(login_form.errors)                                 # forms.py中的错误信息  字典类型数据
            # print(login_form.errors.popitem())                       # forms.py中的错误信息  元祖类型数据
            # return "表单验证错误"                                     # 错误信息需要渲染到cms_login.html页面
            # return self.get(message=login_form.errors.popitem()[1][0])  # 字典类型数据信息提取
            return self.get(message=login_form.get_error())            # login_form是收集到的表单信息,信息提取放置到forms.py的父类中实现
    
    
# 修改密码的类视图验证
class ResetPwd(views.MethodView):
    def get(self):
        return render_template('cms/cms_resetpwd.html')         # 模板渲染到cms_resetpwd.html
    
    # post提交密码修改
    def post(self):
        # 先审查旧密码是否与数据库中的信息相同
        form = ResetPwdForm(request.form)
        if form.validate():
            oldpwd = form.oldpwd.data
            newpwd = form.newpwd.data
            # 对象
            user = g.cms_user
            # 将用户输入的密码进行加密检测是否与数据库中的相同
            if user.check_password(oldpwd):
                # 更新我的密码  将新密码赋值,此时的新密码已经经过验证二次密码是否一致
                user.password = newpwd         # user.password已经调用了models.py中的 @property装饰器进行密码加密
                # 数据库更新
                db.session.commit()
                # return jsonify({"code": 400, "message": "密码修改成功"})        # 代码改写为下面
                return restful.success("密码修改成功")             # 调用restful.py中定义的访问网页成功的函数
            else:
                # 当前用户输入的旧密码与数据库中的不符
                # return jsonify({"code": 400, "message": "旧密码输入错误"})
                return restful.params_error(message="旧密码输入错误")      # 参数错误
        else:
            # ajax 需要返回一个json类型的数据
            # message = form.errors.popitem()[1][0]                     # 收集错误信息
            # return jsonify({"code": 400, "message": message})         # 将数据转换成json类型
            return restful.params_error(message=form.get_error())       # 参数错误,信息的收集在forms.py的父类函数中实现  form是收集到的信息
        

# 定义修改邮箱的类视图 验证
class ResetEmail(views.MethodView):
    def get(self):
        return render_template("cms/cms_resetemail.html")      # 返回到修改邮箱页面url
    
    def post(self):
        form = ResetEmailForm(request.form)                    # 接收邮箱验证的form表单信息
        if form.validate():                                    # 验证表单信息是否通过
            email = form.email.data                            # 获取form表单中填写的邮箱地址
            
            # 查询数据库
            # CMS_User.query.filter_by(email=email).first()
            # CMS_User.query.filter(CMS_User.email == email).first()
            g.cms_user.email = email                           # 数据库中的查询在apps/cms/hooks.py文件中确定了该用户的数据库信息,用全局对象g.cms_user修改邮箱
            db.session.commit()
            return restful.success()                           # 邮箱修改成功
        else:
            return restful.params_error(form.get_error())      # form是这个类中的所有表单信息
        
        
# 发送测试邮件进行验证
@cms_bp.route("/send_email/")
def send_mail():
    message = Message('邮件发送', recipients=['727506892@qq.com'], body='测试邮件发送')   # 主题:邮件发送;收件人:recipients;邮件内容:测试邮件发送
    mail.send(message)                   # 发送邮件
    return "邮件已发送"


# 邮件发送
class EmailCaptcha(views.MethodView):
    def get(self):                                  # 根据resetemail.js中的ajax方法来写函数,不需要post请求
        email = request.args.get('email')           # 查询email参数是否存在
        if not email:
            return restful.params_error('请传递邮箱参数')
        
        # 发送邮件,内容为一个验证码:4、6位数字英文组合
        captcha = random_captcha.get_random_captcha(4)            # 生成4位验证码
        message = Message('BBS论坛邮箱验证码', recipients=[email], body='您的验证码是:%s' % captcha)
        
        # 异常处理
        try:
            mail.send(message)
        except:
            return restful.server_error(message="服务器错误,邮件验证码未发送!")   # 发送异常,服务器错误
        
        # 验证码保存,一般有时效性,且频繁请求变化,所以保存在Redis中
        redis_captcha.redis_set(key=email, value=captcha)        # redis中都是键值对类型,存储验证码
        return restful.success("邮件验证码发送成功!")
    

# 轮播图管理路由
@cms_bp.route("/banners/")
def banners():
    # 通过模型中定义的权重priority的倒叙来排序
    banners = BannerModel.query.order_by(BannerModel.priority.desc()).all()
    return render_template("cms/cms_banners.html", banners=banners)           # 传输banners数据到cms_banners.html界面渲染


# 添加轮播图功能路由,且方法需要与static/cms/js/banners.js中绑定的方法POST相同
@cms_bp.route("/abanner/", methods=['POST'])
def abanner():
    form = AddBannerForm(request.form)                  # 接收添加轮播图的form表单信息
    if form.validate():
        name = form.name.data
        image_url = form.image_url.data
        link_url = form.link_url.data
        priority = form.priority.data
        
        banner = BannerModel(name=name, image_url=image_url, link_url=link_url, priority=priority)     # 轮播图模型
        db.session.add(banner)                                                                         # 提交数据库
        db.session.commit()
        return restful.success()                                                                       # 轮播图信息提交成功
    else:
        return restful.params_error(message=form.get_error())                                          # 表单信息错误


# 修改 轮播图 路由,方法与static/cms/js/banners.js中绑定的方法POST相同
@cms_bp.route("/ubanner/", methods=['POST'])
def ubanner():
    # 修改根据banner_id查询再修改
    form = UpdateBannerForm(request.form)             # 表单信息UpdateBannerForm中的request
    if form.validate():                                # 先查询页面表单信息是否存在
        banner_id = form.banner_id.data               # 收集用户输入的表单信息
        name = form.name.data
        image_url = form.image_url.data
        link_url = form.link_url.data
        priority = form.priority.data
        
        banner = BannerModel.query.get(banner_id)     # 通过轮播图的模型BannerModel的banner_id查询数据库中轮播图对象
        if banner:                                     # 再查询数据库对象数据是否存在
            banner.name = name                        # 将UpdateBannerForm中收集到的form信息命名给数据库中的banner对象
            banner.image_url = image_url
            banner.link_url = link_url
            banner.priority = priority
            
            db.session.commit()                       # 数据库信息直接提交修改即可,不用添加新的对象
            return restful.success()
        else:
            return restful.params_error(message=form.get_error())    # 表单信息错误
    

# 删除  轮播图路由,路由命名与banners.js绑定
@cms_bp.route("/dbanner/", methods=['POST'])
def dbanner():
    '''
    request.form.get("key", type=str, default=None)      获取表单数据
    request.args.get("key")                              获取get请求参数
    request.values.get("key")                            获取所有参数
    '''
    # 修改根据banner_id查询再修改,获取post请求参数         get请求方式使用request.args.get()
    banner_id = request.form.get('banner_id')            # 获取表单数据
    if not banner_id:
        return restful.params_error(message="轮播图不存在")
    
    banner = BannerModel.query.get(banner_id)           # 根据banner_id查询数据库
    if banner:
        db.session.delete(banner)                       # 删除该banner
        db.session.commit()
        return restful.success()                        # 返回成功
    else:
        return restful.params_error("轮播图不存在")      # 根据banner_id查询数据库信息不存在
    

# 帖子管理路由 ,需要和cms_base.js中命名的相同才可以
@cms_bp.route("/posts/")
@permission_required(CMSPersmission.POSTER)                # 传参装饰器验证不同用户不同模块权限
def posts():
    return render_template("cms/cms_posts.html")
    

# 评论管理路由
@cms_bp.route("/comments/")
@permission_required(CMSPersmission.COMMENTER)             # 传参装饰器验证不同用户不同模块权限
def comments():
    return render_template("cms/cms_comments.html")


# 板块管理路由
@cms_bp.route("/boards/")
@permission_required(CMSPersmission.BOARDER)               # 传参装饰器验证不同用户不同模块权限
def boards():
    return render_template("cms/cms_boards.html")


# 前台用户管理路由
@cms_bp.route("/fusers/")
@permission_required(CMSPersmission.FRONTUSER)             # 传参装饰器验证不同用户不同模块权限
def fuser():
    return render_template("cms/cms_fuser.html")


# 后用户管理路由
@cms_bp.route("/cusers/")
@permission_required(CMSPersmission.CMSUSER)               # 传参装饰器验证不同用户不同模块权限
def cuser():
    return render_template("cms/cms_cuser.html")


# 添加登录路由
cms_bp.add_url_rule("/login/", view_func=LoginView.as_view('login'))    # view_func 命名操作名字,"/login/"路由地址

# 类视图函数添加绑定路由  注意类视图需要修改ResetPwd.as_view('resetpwd')
cms_bp.add_url_rule("/resetpwd/", view_func=ResetPwd.as_view('resetpwd'))  # view_func 命名操作名字,/resetpwd/路由地址

# 添加修改邮箱的类视图路由绑定,路由的命名和cms_base.js中的命名要相同,否则不关联,url=/resetemail/必须要和resetemail.js中的ajax绑定的路由相同
cms_bp.add_url_rule("/resetemail/", view_func=ResetEmail.as_view('resetemail'))

# 绑定路由,路由的命名和cms_base.js中的命名要相同,必须要和resetemail.js中的ajax绑定的路由相同
cms_bp.add_url_rule("/email_captcha/", view_func=EmailCaptcha.as_view('email_captcha'))

定义修改和删除轮播图的Form表单:forms表单信息:apps/cms/forms.py

# -*- encoding: utf-8 -*-
"""
@File    : forms.py
@Time    : 2020/5/11 10:00
@Author  : chen
forms表单信息:apps/cms/forms.py
"""
# forms表单信息
from wtforms import Form, StringField, IntegerField, ValidationError
from wtforms.validators import Email, InputRequired, Length, EqualTo, URL  # EqualTo验证新密码是否相同,URL验证
from utils.redis_captcha import redis_get  # 导入验证码模块


# 创父类form表单,用于输出错误信息
class BaseForm(Form):
    def get_error(self):
        message = self.errors.popitem()[1][0]  # 错误信息的收集,字典类型数据信息提取
        return message


# 登录页面中的Form表单      继承父类form
class LoginForm(BaseForm):
    email = StringField(validators=[Email(message="请输入正确的邮箱"), InputRequired(message="请输入邮箱")])
    password = StringField(validators=[Length(3, 15, message='请输入正确长度的密码')])  # 长度可以先设置短的,方便项目测试
    remember = IntegerField()  # 记住cookie操作  赋值为0或1


# 修改密码页面中的form表单信息    继承父类form
class ResetPwdForm(BaseForm):
    oldpwd = StringField(validators=[Length(3, 15, message="密码长度有误")])
    newpwd = StringField(validators=[Length(3, 15, message="密码长度有误")])
    newpwd2 = StringField(validators=[EqualTo("newpwd", message="两次输入密码不一致")])


# 定义设置邮箱的表单信息,进行提交时候使用
class ResetEmailForm(BaseForm):
    email = StringField(validators=[Email(message="请输入正确格式的邮箱")])  # 名称email与cms_resetemail.html中的要相同
    captcha = StringField(
        validators=[Length(min=4, max=4, message="请输入正确长度的验证码")])  # 名称captcha与cms_resetemail.html中的要相同
    
    # 验证redis中的字段与数据库中的字段是否相同
    def validate_captcha(self, field):  # 方法命名规则是:validate_字段名()
        # 表单提交上来的验证码
        email = self.email.data
        captcha = self.captcha.data
        
        # 取redis中保存的验证码             第一个redis_captcha是新对象,第二个redis_captcha是redis_captcha.py文件
        redis_captcha = redis_get(email)
        if not redis_captcha or captcha.lower() != redis_captcha.lower():  # 不区分大小写
            raise ValidationError('邮箱验证码错误')


# 定义 添加轮播图 的表单信息
class AddBannerForm(BaseForm):
    # Form表单名称根据static/cms/js/banners.js中的ajax.post发送的data中
    name = StringField(validators=[InputRequired(message="请输入轮播图名称")])
    image_url = StringField(validators=[InputRequired(message="请输入轮播图片链接"), URL(message="图片链接有误")])
    link_url = StringField(validators=[InputRequired(message="请输入轮播图上跳转链接"), URL(message="跳转链接有误")])
    priority = IntegerField(validators=[InputRequired(message="请输入轮播图优先级")])


# 定义 修改轮播图 的表单信息
class UpdateBannerForm(AddBannerForm):        # 继承AddBannerForm,收集表单信息一样,只多出来一个查询字段banner_id
    # 根据banner_id查询 修改 轮播图
    banner_id = IntegerField(validators=[InputRequired(message="轮播图不存在")])

创建轮播图模型,并映射到数据库中:
后台模型文件:apps/cms/models.py

# -*- encoding: utf-8 -*-
"""
@File    : models.py
@Time    : 2020/5/11 10:00
@Author  : chen
后台模型文件:apps/cms/models.py
"""
# 定义后端用户模型
from exts import db                                                               # 数据库
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash         # 导入密码加密,解密方法的库


# 权限定义,不是模型,没有继承db.Model
class CMSPersmission(object):
    # 255 二进制表示所有的权限
    ALL_PERMISSION = 0b11111111          # 每一位数代表一个权限,共7个权限,8位1个字节
    
    # 访问权限
    VISITOR        = 0b00000001
    
    # 管理帖子
    POSTER         = 0b00000010
    
    # 管理评论
    COMMENTER      = 0b00000100
    
    # 管理板块
    BOARDER        = 0b00001000
    
    # 管理后台用户
    CMSUSER        = 0b00010000
    # 管理前台用户
    FRONTUSER      = 0b00100000
    # 管理管理员用户
    ADMINER        = 0b01000000


# 权限与角色是多对多的关系,创建他们的中间表
cms_role_user = db.Table(
    "cms_role_user",
    db.Column("cms_role_id", db.Integer, db.ForeignKey('cms_role.id'), primary_key=True),
    db.Column("cms_user_id", db.Integer, db.ForeignKey('cms_user.id'), primary_key=True),
)


# 角色模型定义   继承了db.Model
class CMSRole(db.Model):
    __tablename__ = 'cms_role'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)      # 主键  自增
    name = db.Column(db.String(50), nullable=False)                       # 非空
    desc = db.Column(db.String(250), nullable=False)                      # 非空
    creat_time = db.Column(db.DateTime, default=datetime.now)
    permission = db.Column(db.Integer, default=CMSPersmission.VISITOR)    # 默认先给游客权限

    # 反向查询属性,关联中间表secondary=cms_role_user,对应了CMS_User模型,建立模型联系,不映射到数据库中
    users = db.relationship('CMS_User', secondary=cms_role_user, backref="roles")    # roles是CMS_User的外键
    
    
# 后台用户模型定义
class CMS_User(db.Model):
    __tablename__ = 'cms_user'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)           # 主键  自增
    username = db.Column(db.String(150), nullable=False)                       # 非空
    # password = db.Column(db.String(150), nullable=False)
    _password = db.Column(db.String(150), nullable=False)                      # 密码加密操作修改字段
    email = db.Column(db.String(50), nullable=False, unique=True)              # 非空、唯一
    join_time = db.Column(db.DateTime, default=datetime.now)                   # 默认当前时间
    
    # 修改密码加密操作中的字段,在manage.py映射数据库时候,使用字段还是保持相同
    def __init__(self, username, password, email):
        self.username = username
        self.password = password         # 调用该方法 返回下面的self._password数值,
        self.email = email
    
    # 密码加密操作
    @property
    def password(self):                   # 密码取值
        return self._password

    @password.setter                      # 密码加密
    def password(self, raw_password):
        self._password = generate_password_hash(raw_password)

    # 用于验证后台登录密码是否和数据库一致,raw_password是后台登录输入的密码
    def check_password(self, raw_password):
        result = check_password_hash(self.password, raw_password)   # 相当于用相同的hash加密算法加密raw_password,检测与数据库中是否一致
        return result
    
    # 封装用户的权限
    @property
    def permission(self):
        if not self.roles:           # 反向查询属性,backref="roles",
            return 0                 # 没有任何权限
        
        # 所有权限
        all_permissions = 0
        
        for role in self.roles:                    # 循环调用所有角色
            permissions = role.permission         # 将这个角色的权限都取出来  role.permission代表CMSRole中的属性
            all_permissions |= permissions         # 当前这个角色的权限都在all_permissions
            
        return all_permissions
    
    # 判断用户所具有的权限
    def has_permissions(self, permission):
        all_permissions = self.permission                 # 调用permission(self)方法
        #  若所有权限0b11111111 & 用户权限     等于 本身,则代表具有该权限
        result = all_permissions & permission == permission
        # print(result)
        return result
        
    # 判断是否是开发人员
    @property
    def is_developer(self):
         return self.has_permissions(CMSPersmission.ALL_PERMISSION)       # 调用has_permissions方法并传入所有权限
         
 
# 轮播图的模型创建
class BannerModel(db.Model):
    __tablename__ = 'banner'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键  自增
    name = db.Column(db.String(250), nullable=False)                  # 非空
    # 图片链接
    image_url = db.Column(db.String(250), nullable=False)             # 轮播图的链接资源
    # 跳转链接
    link_url = db.Column(db.String(50), nullable=False)
    priority = db.Column(db.Integer, default=0)                       # 权重选项
    create_time = db.Column(db.DateTime, default=datetime.now)        # 创建时间
    
    # 删除标志字段    0代表删除  1代表未删除
    is_delete = db.Column(db.Integer, default=1)

映射到数据库:
在这里插入图片描述

5、本地文件上传七牛云

七牛云官网
注册登录之后,点击 “对象存储” ,再“新建存储空间”,“确认创建”
在这里插入图片描述
进入七牛云SDK文档:七牛云python版本SDK
在这里插入图片描述
在这里插入图片描述
安装 pip install qiniu
在这里插入图片描述
创建上传本地文件到七牛云功能:utils/upload_qiniuyun.py

# -*- encoding: utf-8 -*-
"""
@File    : upload_qiniuyun.py
@Time    : 2020/5/28 10:14
@Author  : chen
上传本地文件到七牛云功能:utils/upload_qiniuyun.py
"""
from qiniu import Auth, put_file, etag
import qiniu.config

# 需要填写你的 Access Key 和 Secret Key
access_key = '自己账号个人中心的密钥管理复制过来'
secret_key = '自己账号个人中心的密钥管理复制过来'

# 构建鉴权对象
q = Auth(access_key, secret_key)

# 要上传的空间
bucket_name = 'xx'

# 上传后保存的文件名
key = '自己固定来写'

# 生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)

# 要上传文件的本地路径    上传路径自己写
localfile = r'E:\ENV\flask项目-cBMOsSmb\Flask项目实战-BBS\static\common\images\logo.png'

ret, info = put_file(token, key, localfile)
print(info)
assert ret['key'] == key
assert ret['hash'] == etag(localfile)

在这里插入图片描述
上传成功后显示:

在这里插入图片描述

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值