Flask项目实战——9—(前台轮播图展示、七牛云上传本地文件、板块管理、富文本编辑器)

1、前台轮播图展示

根据权重查询banners数据并传输,渲染到首页界面:前台蓝图文件: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

# 导入轮播图模型BannerModel
from apps.cms.models import BannerModel

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


# BBS的首页界面路由
@front_bp.route("/")
def index():
    banners = BannerModel.query.order_by(BannerModel.priority.desc()).limit(4)   # 通过权重查询,每页显示4条
    return render_template("front/front_index.html", banners=banners)            # 渲染到首页界面,banners查询数据传输到前台界面


# 图形验证码路由
@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不能是当前页面,request.url是当前的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"))          # "signup"视图中不需要反斜线,决定了url_for的路由地址
front_bp.add_url_rule("/signin/", view_func=SinginView.as_view("signin"))          # "signin"视图中不需要反斜线

循环apps/front/views.py文件传输过来的banners数据,并渲染到界面上
前台首页界面: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">
           <!--    循环apps/front/views.py文件传输的banners数据      -->
          {% for banner in banners %}
              <!--    判断是否第一次循环      -->
              {% if loop.first %}
                <div class="item active">
                    {% else %}
                    <div class="item">
              {% endif %}
                  <!--    轮播图路径,style="width: 300px;height: 300px"轮播图大小 -->
                  <img src="{{ banner.image_url }}" alt="..." style="width: 300px;height: 300px">
                  <div class="carousel-caption">
                  </div>
                </div>
         {% endfor %}
<!--          -->
<!--        <div class="item">-->
<!--    &lt;!&ndash;   轮播图路径      &ndash;&gt;-->
<!--          <img src=".\static\common\images\1.png" alt="...">-->
<!--          <div class="carousel-caption">-->
<!--            ...-->
<!--          </div>-->
<!--        </div>-->
<!--    &lt;!&ndash;  不同轮播图    &ndash;&gt;-->
<!--          <div class="item">-->
<!--    &lt;!&ndash;   轮播图路径      &ndash;&gt;-->
<!--          <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 %}

2、七牛云上传本地文件

注意七牛云上传文件的时候,上传空间的地域需要选择地区为:华东
否则上传文件不成功,报错:incorrect region, please use up-z2.qiniup.com
在这里插入图片描述
导入七牛云关联样式:static/common/lgqiniu.js


//'use strict';

var lgqiniu = {
	'setUp': function(args) {
		var domain = args['domain'];
		var params = {
            browse_button:args['browse_btn'],
			runtimes: 'html5,flash,html4', //上传模式,依次退化
			max_file_size: '500mb', //文件最大允许的尺寸
			dragdrop: false, //是否开启拖拽上传
			chunk_size: '4mb', //分块上传时,每片的大小
			uptoken_url: args['uptoken_url'], //ajax请求token的url
			domain: domain, //图片下载时候的域名
			get_new_uptoken: false, //是否每次上传文件都要从业务服务器获取token
			auto_start: true, //如果设置了true,只要选择了图片,就会自动上传
            unique_names: true,
            multi_selection: false,
            filters: {
                mime_types :[
                    {title:'Image files',extensions: 'jpg,gif,png'},
                    {title:'Video files',extensions: 'flv,mpg,mpeg,avi,wmv,mov,asf,rm,rmvb,mkv,m4v,mp4'}
                ]
            },
			log_level: 5, //log级别
			init: {
				'FileUploaded': function(up,file,info) {
					if(args['success']){
						var success = args['success'];
						file.name = domain + file.target_name;
						success(up,file,info);
					}
				},
				'Error': function(up,err,errTip) {
					if(args['error']){
						var error = args['error'];
						error(up,err,errTip);
					}
				},
                'UploadProgress': function (up,file) {
                    if(args['progress']){
                        args['progress'](up,file);
                    }
                },
                'FilesAdded': function (up,files) {
                    if(args['fileadded']){
                        args['fileadded'](up,files);
                    }
                },
                'UploadComplete': function () {
                    if(args['complete']){
                        args['complete']();
                    }
                }
			}
		};

		// 把args中的参数放到params中去
		for(var key in args){
			params[key] = args[key];
		}
		var uploader = Qiniu.uploader(params);
		return uploader;
	}
};

编写获取七牛云的uptoken的接口文件:公共视图文件:apps/common/views.py

# -*- encoding: utf-8 -*-
"""
@File    : views.py
@Time    : 2020/5/11 9:59
@Author  : chen
公共视图文件:apps/common/views.py
"""
# 导入手机验证码生成文件
from utils.send_telephone_msg import send_phone_msg

from utils import restful
from utils.captcha import Captcha
from flask import Blueprint, request, jsonify
from utils import redis_captcha                                 # 图形验证码存储到redis数据库中

# 导入form表单信息验证客户端sign2和服务端sign
from apps.common.forms import SMSCaptchaForm

# 导入七牛云上传文件的依赖库
from qiniu import Auth, put_file, etag
import qiniu.config

common_bp = Blueprint("common", __name__, url_prefix='/c')      # 视图common,url前缀c,在


# 手机验证码生成文件,这部分是只要调用当前路由请求,就会发送短信验证码,
# 需要利用sign = md5(timestamp+telephone+"q3423805gdflvbdfvhsdoa`#$%"),在front_signup.js文件中调用
# @common_bp.route("/sms_captcha/", methods=['POST'])
# def sms_captcha():
#     telephone = request.form.get('telephone')        # 表单信息收集
#
#     if not telephone:
#         return restful.params_error(message="请填写手机号")              # 手机信息不存在,输出错误
#
#     captcha = Captcha.gene_text(number=4)                               # 生成4位验证码,这里生成的是验证码,要发送到手机端的,不能是图形验证码
#     # captcha = get_random_captcha(num=4):                              # 或者使用utils/random_captcha.py文件中的随机生成验证码
#
#     # 调用send_telephone_msg.py中send_phone_msg方法发送4位验证码到手机中
#     if send_phone_msg(telephone, captcha) == 0:                         # 返回成功的状态码为 0
#         return restful.success()
#     else:
#         return restful.params_error("手机验证码发送失败")                 # 手机验证码发送失败


# 在front_signup.js文件中调用sign = md5()验证表单信息.

@common_bp.route("/sms_captcha/", methods=['POST'])
def sms_captcha():
    form = SMSCaptchaForm(request.form)               # 收集form表单信息
    
    if form.validate():                               # 表单信息存在
        # 接收数据
        telephone = form.telephone.data
        captcha = Captcha.gene_text(number=4)         # 生成4位验证码,这里生成的是验证码,要发送到手机端的,不能是图形验证码
        print("发送的手机验证码是:{}".format(captcha))
        
        # 验证发送成功状态码
        if send_phone_msg(telephone, captcha) == 0:                          # 返回成功的状态码为 0
            redis_captcha.redis_set(telephone, captcha)                      # 将telephone对应的手机验证码保存在Redis数据库中
            
            return restful.success()                                         # 返回成功信息提示框
        else:
            return restful.params_error("手机验证码发送失败")                 # 手机验证码发送失败
    else:
        return restful.params_error(message="参数错误")
    

# 创建七牛云上传文件路由,前后台公有
@common_bp.route("/uptoken/")                               # 路由与static/cms/js/banners.js中上传文件路由相同
def uptoken():
    # 需要填写你的 Access Key 和 Secret Key
    access_key = 'F6TFlLqmX4Jxi_OJ86xLVCB8mQ5KRsyzCjGVWPEh'
    secret_key = 'zhCb8cNSR-lifyVCZLPjH3GhD4_W7P5Sgbh9mHah'
    
    # 构建鉴权对象
    q = Auth(access_key, secret_key)
    
    # 要上传的空间
    bucket_name = 'chenhao0406'

    # 生成上传 Token,可以指定过期时间等
    token = q.upload_token(bucket_name)  # 生成token,用于项目上传使用
    
    return jsonify({"uptoken": token})   # 键值对类型

前端中添加js的sdk:七牛云为javascript提供了一个专门用来传文件的接口:
后台轮播图管理:templates/cms/cms_banners.html

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

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

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

{% block head %}
    <!--  七牛云初始化文件,七牛云为javascript提供了一个专门用来传文件的接口  -->
    <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 %}

修改七牛云初始化的参数: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');                           // 获取data-type的属性用于判断
        var bannerId = self.attr("data-id");

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

        var url = '';
        if(submitType == 'update'){                                         // 如果data-type的属性是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');                  // 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://qb0ruw0qs.bkt.clouddn.com/',                  // 修改成自己账户的七牛云域名
        'browse_btn': 'upload-btn',
        'uptoken_url': '/c/uptoken/',                                       // 七牛云上传文件路由名称/uptoken/,与公共视图文件:apps/common/views.py中命名相同
        'success': function (up,file,info) {
            var imageInput = $("input[name='image_url']");
            imageInput.val(file.name);
        }
    });
});

3、板块管理

创建板块管理中的文章模型类,再映射到数据库中:后台模型文件: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)
    
    
# 板块管理模型创建
class BoardModel(db.Model):
    __tablename__ = 'cms_board'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键  自增
    name = db.Column(db.String(250), nullable=False)                  # 非空
    create_time = db.Column(db.DateTime, default=datetime.now)        # 创建时间
     

导入manage.py文件中才能映射到数据库,否则不成功:映射模型到数据库中文件: manage.py

# -*- encoding: utf-8 -*-
"""
@File    : manage.py
@Time    : 2020/5/10 17:36
@Author  : chen
映射模型到数据库中文件: manage.py
"""
from flask_script import Manager
from bbs import app     # 需要将当前文件夹设置为当前根目录,才不会报错
from flask_migrate import Migrate, MigrateCommand
from exts import db

# 导入后台模型 才能映射到数据库     导入后端的模型
from apps.cms.models import CMS_User
# 导入后台模型 才能映射到数据库 ,导入轮播图和文章的管理模块
from apps.cms.models import BannerModel, BoardModel

# 导入后台角色模型,映射到数据库         CMSPersmission角色权限定义类
from apps.cms.models import CMSRole, CMSPersmission

# 导入前台模型 才能映射到数据库
from apps.front.models import Front_User

manage = Manager(app)

Migrate(app, db)
manage.add_command('db', MigrateCommand)


# 命令行添加后台用户
@manage.option('-u', '--username', dest='username')
@manage.option('-p', '--password', dest='password')
@manage.option('-e', '--email', dest='email')
def create_cms_user(username, password, email):
    user = CMS_User(username=username, password=password, email=email)
    # 添加映射到数据库,提交至数据库
    db.session.add(user)
    db.session.commit()
    print("cms后台用户添加成功")


# 命令行添加前台用户
@manage.option('-t', '--telephone', dest='telephone')
@manage.option('-u', '--username', dest='username')
@manage.option('-p', '--password', dest='password')
def create_front_user(telephone, username, password):
    user = Front_User(telephone=telephone, username=username, password=password)
    # 添加映射到数据库,提交至数据库
    db.session.add(user)
    db.session.commit()
    print("front前台用户添加成功")


# 添加角色  不传参用command
@manage.command
def create_role():
    # 访问者
    visitor = CMSRole(name="访问者", desc="只能查看数据,不能修改数据")
    visitor.permission = CMSPersmission.VISITOR                         # 权限
    
    # 运营人员
    operator = CMSRole(name="运营人员", desc="管理评论、帖子、管理前台用户")
    # 权限或运算,代表包含有运算中的所有权限     二进制的运算 001|010=011
    operator.permission = CMSPersmission.VISITOR | CMSPersmission.POSTER | CMSPersmission.CMSUSER | \
                          CMSPersmission.COMMENTER | CMSPersmission.FRONTUSER
    
    # 管理员
    admin = CMSRole(name="管理员", desc="拥有本系统大部分权限")
    admin.permission = CMSPersmission.VISITOR | CMSPersmission.POSTER | CMSPersmission.CMSUSER | \
                          CMSPersmission.COMMENTER | CMSPersmission.FRONTUSER | CMSPersmission.BOARDER

    # 开发人员
    developer = CMSRole(name="开发人员", desc="拥有本系统所有权限")
    developer.permission = CMSPersmission.ALL_PERMISSION
    
    # 提交数据库   添加身份字段到数据库中的表,
    db.session.add_all([visitor, operator, admin, developer])
    db.session.commit()
    return "创建角色成功"


# 测试用户权限
@manage.command
def test_permission():
    # user = CMS_User.query.first()                          # 查询第一个用户,当时创建的用户还没有关联权限,所以应该是没有权限
    user = CMS_User.query.get(3)
    print(user)                                              # 显示用户信息
    if user.has_permissions(CMSPersmission.VISITOR):         # has_permissions方法判定是否具有该权限
        print("这个用户有访问者的权限!")
    else:
        print("这个用户有访问者的权限!")


# 添加用户到角色里面
@manage.option("-e", "--email", dest="email")
@manage.option("-n", "--name", dest="name")
def add_user_to_role(email, name):
    user = CMS_User.query.filter_by(email=email).first()                   # 通过邮箱查询用户
    if user:
        role = CMSRole.query.filter_by(name=name).first()                  # 邮箱存在的前提下,通过name查询角色
        if role:
            role.users.append(user)                                        # 将用户添加到角色中,list类型数据,role.users是CMSRole中的外键
            db.session.commit()                                            # 映射到数据库
            print("用户添加到角色成功")
        else:
            print("该角色不存在")
    else:
        print("邮箱不存在")


if __name__ == '__main__':
    manage.run()

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

板块管理页面

增删改查功能实现

先创建板块的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="轮播图不存在")])
    

# 定义 增加板块管理 的表单信息
class AddBoardsForm(BaseForm):
    # 板块名称
    name = StringField(validators=[InputRequired(message="板块名称不存在")])
    

# 编辑 板块管理名称
class UpdateBoardsForm(AddBoardsForm):
    # 修改编辑板块名称的时候需要使用board_id进行查询,再修改
    board_id = IntegerField(validators=[InputRequired(message="请输入板块ID")])

创建路由,并收集表单信息,查询数据库中信息再进行增删改查操作:视图文件: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                            # 数据库中更新密码、邮箱等使用

from apps.cms.forms import (
    LoginForm, ResetPwdForm,                        # ResetPwdForm修改密码的form信息
    ResetEmailForm,                                 # 导入forms.py文件中的邮箱验证的表单信息类
    AddBannerForm,                                  # 导入 添加轮播图 的表单信息
    UpdateBannerForm,                               # 导入 更新轮播图 的表单信息
    AddBoardsForm,                                  # 导入 增加板块管理 的表单信息
    UpdateBoardsForm,                               # 导入 编辑板块管理 的表单信息
)

from apps.cms.models import (
    CMS_User,                                       # 后台用户模型
    CMSPersmission,                                 # CMSPersmission验证用户不同模块权限
    CMSRole,                                        # 用户角色模型
    BannerModel,                                    # 导入 轮播图模型BannerModel
    BoardModel,                                     # 导入 板块管理模型
)

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_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')            # 获取表单数据,这里没有单独创建删除的Form表单,使用之前创建的
    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():
    boards = BoardModel.query.all()                        # 数据库查询所有板块名称
    return render_template("cms/cms_boards.html", boards=boards)        # 数据渲染到cms_boards.html


# 增加 板块管理名称 路由,与static/cms/js/banners.js中绑定的方法、路由要相同
@cms_bp.route("/aboard/", methods=['POST'])
@permission_required(CMSPersmission.BOARDER)               # 传参装饰器验证不同用户不同模块权限
def aboards():
    form = AddBoardsForm(request.form)                     # 表单信息传输过来,方便修改调用
    if form.validate():
        name = form.name.data                              # 表单信息收集
        
        board = BoardModel(name=name)                      # 添加信息到板块模型中
        db.session.add(board)
        db.session.commit()                                # 提交数据库
        return restful.success()                           # 数据库添加成功
    else:
        return restful.params_error(message=form.get_error())    # 表单信息错误


# 编辑 板块管理名称 路由,与static/cms/js/banners.js中绑定的方法、路由要相同
@cms_bp.route("/uboard/", methods=['POST'])
@permission_required(CMSPersmission.BOARDER)             # 传参装饰器验证不同用户不同模块权限
def uboards():
    form = UpdateBoardsForm(request.form)                # 表单信息传输过来,方便修改调用
    if form.validate():
        board_id = form.board_id.data                    # 表单信息收集
        name = form.name.data
        
        board = BoardModel.query.get(board_id)           # 根据表单中提交的board_id查询数据库中对象信息
        if board:
            board.name = name                            # 表单中提交的name命名给数据库中对象的名字
            db.session.commit()                          # 修改数据后提交数据库
            return restful.success()                     # 数据库修改成功
        else:
            return restful.params_error(message="没有这个分类板块")  # 数据库中对象信息不存在
    else:
        return restful.params_error(message=form.get_error())  # 表单信息错误


# 删除 板块管理名称 路由,与static/cms/js/banners.js中绑定的方法、路由要相同
@cms_bp.route("/dboard/", methods=['POST'])
@permission_required(CMSPersmission.BOARDER)             # 传参装饰器验证不同用户不同模块权限
def dboards():
    board_id = request.form.get('board_id')                  # 查询表单信息中的board_id,这里没有单独创建删除的Form表单,使用之前创建的
    if not board_id:
        return restful.params_error(message="分类板块不存在")  # 表单信息不存在

    board = BoardModel.query.get(board_id)               # 根据表单中提交的board_id查询数据库中对象信息,注意.get
    if not board:
        return restful.params_error(message="分类板块不存在")  # 数据库中对象信息不存在
    
    db.session.delete(board)                             # 删除数据库中的信息
    db.session.commit()                                  # 提交数据库修改
    return restful.success()                             # 删除成功


# 前台用户管理路由
@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'))

路由创建命名、关联按钮实现,方法名称POST确定绑定等: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 () {
    // 添加按钮
    $("#add-board-btn").click(function (event) {
        event.preventDefault();
        lgalert.alertOneInput({
            'text':'请输入板块名称!',
            'placeholder': '板块名称',
            'confirmCallback': function (inputValue) {
                lgajax.post({
                    'url': '/cms/aboard/',                         // 路由名称与视图文件:apps/cms/views.py文件中定义要一样
                    'data': {
                        'name': inputValue
                    },
                    'success': function (data) {
                        if(data['code'] == 200){
                            window.location.reload();
                        }else{
                            lgalert.alertInfo(data['message']);
                        }
                    }
                });
            }
        });
    });
});

$(function () {
    // 编辑按钮
    $(".edit-board-btn").click(function () {
        var self = $(this);
        var tr = self.parent().parent();
        var name = tr.attr('data-name');                                   // 查询添加板块名称的属性,用于编辑板块名称的时候渲染到placeholder
        var board_id = tr.attr("data-id");

        lgalert.alertOneInput({
            'text': '请输入新的板块名称!',
            'placeholder': name,                                          // 编辑修改的时候,显示最早输入时候的名称
            'confirmCallback': function (inputValue) {
                lgajax.post({
                    'url': '/cms/uboard/',                               // 路由名称与视图文件:apps/cms/views.py文件中定义要一样
                    'data': {
                        'board_id': board_id,                           // 'board_id'是表单信息中输入的name,value值为board_id
                        'name': inputValue
                    },
                    'success': function (data) {
                        if(data['code'] == 200){
                            window.location.reload();
                        }else{
                            lgalert.alertInfo(data['message']);
                        }
                    }
                });
            }
        });
    });
});


$(function () {
    // 删除按钮
    $(".delete-board-btn").click(function (event) {
        var self = $(this);
        var tr = self.parent().parent();
        var board_id = tr.attr('data-id');
        lgalert.alertConfirm({
            "msg":"您确定要删除这个板块吗?",
            'confirmCallback': function () {
                lgajax.post({
                    'url': '/cms/dboard/',                                // 路由名称与视图文件:apps/cms/views.py文件中定义要一样
                    'data':{
                        // form  input name value
                        'board_id': board_id                             // 'board_id'是表单信息中输入的name,value值为board_id
                    },
                    'success': function (data) {
                        if(data['code'] == 200){
                            window.location.reload();
                        }else{
                            lgalert.alertInfo(data['message']);
                        }
                    }
                })
            }
        });
    });
});

模板继承文件修改路由绑定: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) %}
                    <!--        {{ url_for('cms.boards') }}关联路由          -->
                    <li class="board-manage"><a href="{{ url_for('cms.boards') }}">板块管理</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>

渲染到后台界面html文件:templates/cms/cms_boards.html

<!--  继承模板文件cms/cms_base.html  简化代码 -->
{% extends 'cms/cms_base.html' %}

<!-- 页面标题 -->
{% block title %}
    板块管理
{% endblock %}

<!--  标题  -->
{% block page_title %}
    {{self.title()}}
{% endblock %}


{% block head %}
<script src="{{ url_for('static', filename='cms/js/boards.js') }}"></script>
{% endblock %}

{% block content %}
<div class="top-box">
    <button class="btn btn-warning" style="float:right;" id="add-board-btn">添加新板块</button>
</div>
<table class="table table-bordered">
    <thead>
    <tr>
        <th>板块名称</th>
        <th>帖子数量</th>
        <th>创建时间</th>
        <th>操作</th>
    </tr>
    </thead>
    <tbody>
    <!--  循环board信息, boards数据由视图文件:apps/cms/views.py文件传输而来 -->
    {% for board in boards %}
        <tr data-name="{{ board.name }}" data-id="{{ board.id }}">
            <td>{{ board.name }}</td>
            <td>0</td>
            <td>{{ board.create_time }}</td>
            <td>
                <button class="btn btn-default btn-xs edit-board-btn">编辑</button>
                <button class="btn btn-danger btn-xs delete-board-btn">删除</button>
            </td>
        </tr>
    {% endfor %}
    </tbody>
</table>
{% endblock %}

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

4、富文本编辑器

推荐使用:Editor.md
还可以百度一下其他的富文本编辑器:
进入官网后,下载安装:
在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值