赞
踩
前台蓝图文件: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, # make_response生成response对象,用于返回前端模板 request, session, g, ) # 导入图像验证码生成文件 from utils.captcha import Captcha # 图形验证码image是二进制数据,需要转换成字节流才能使用 from io import BytesIO # 将图形验证码保存到Redis restful输出信息弹窗 from utils import redis_captcha, restful # 验证码表单信息验证 from .forms import ( SignupForm, # 注册的Form表单信息收集 SigninForm, # 登录的Form表单信息收集 AddPostForm, # 帖子提交表单信息 ) # 导入前台用户模型 from .models import ( Front_User, PostModel, ) # 导入数据库连接 db from exts import db # 确保URL安全的文件:utils/safe_url.py from utils import safe_url from apps.cms.models import ( BannerModel, # 导入后台轮播图模型BannerModel BoardModel, # 导入后台板块管理模型 ) # 导入前台界面权限验证装饰器 from .decorators import login_required front_bp = Blueprint("front", __name__) # 前端不用前缀,直接在首页显示,front是蓝图,在front_signup.html调用生成图形验证码时候需要用 # 权限验证 需要在front_bp产生后,再导入 from .hooks import before_request # BBS的首页界面路由 @front_bp.route("/") def index(): banners = BannerModel.query.order_by(BannerModel.priority.desc()).limit(4) # 通过权重查询,每页显示4条 boards = BoardModel.query.all() # 查询板块中的所有 board_id = request.args.get('board_id', type=int, default=None) # get方法需要使用args,注意这里的数据类型需要改成int posts = PostModel.query.all() # 帖子信息传输 context = { # 多种数据传输到前台界面 "banners": banners, "boards": boards, "current_board_id": board_id, "posts": posts, } return render_template("front/front_index.html", **context) # 渲染到首页界面,查询数据传输到前台界面 # 图形验证码路由 @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): # 判断密码和用户是否正确 # 'front_user_id'命名防止与后台验证session相同,会产生覆盖情况bug session['front_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()) # 表单信息不存在,输出异常信息 # 帖子编辑提交 的类视图 富文本编辑 class PostView(views.MethodView): # 登录验证,实现帖子编辑前进行权限验证 decorators = [login_required] # 表单信息收集,传输 def get(self): # 查询boards数据进行传输 boards = BoardModel.query.all() # boards是list类型 return render_template("front/front_apost.html", boards=boards) # boards数据传输到前端front_apost.html页面 # 帖子的Form表单信息收集查询 def post(self): form = AddPostForm(request.form) # 查询帖子提交的Form表单信息 if form.validate(): title = form.title.data board_id = form.board_id.data # 收集表单中提交的信息 content = form.content.data # 查询用户信息是否在数据库中存在 board = BoardModel.query.get(board_id) if not board: return restful.params_error(message="没有这个版块名称") # 数据库中不存在,返回异常信息 # 数据库中board信息存在,传输数据到数据库表中,并修改名称 post = PostModel(title=title, board_id=board_id, content=content) post.board = board # 外键中的信息修改赋值 post.author = g.front_user # g对象 db.session.add(post) db.session.commit() return restful.success() # 提交成功,为json数据 else: return restful.params_error(message=form.get_error()) # 前台 帖子详情 路由 @front_bp.route("/p/<post_id>") # 蹄子详情路由需要传参帖子id:post_id def post_detail(post_id): post = PostModel.query.get(post_id) # 通过post_id查找数据库中的帖子信息 if not post: return restful.params_error(message="帖子不存在!") return render_template("front/front_detail.html", post=post) # 查找到帖子信息,传输数据到帖子详情页渲染 # 绑定类视图的路由 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"视图中不需要反斜线 front_bp.add_url_rule("/apost/", view_func=PostView.as_view("apost")) # 绑定帖子编辑提交路由
创建 帖子详情页面文件:templates/front/front_detail.html
<!-- 帖子详情页面文件:templates/front/front_detail.html --> {% extends 'front/front_base.html' %} {% block title %}帖子详情{% endblock %} {% block head %} <!-- 百度的富文本编辑器加载 --> <script src="{{ url_for('static', filename='ueditor/ueditor.config.js') }}"></script> <script src="{{ url_for('static', filename='ueditor/ueditor.all.min.js') }}"></script> <link rel="stylesheet" href="{{ url_for('static', filename='front/css/front_pdetail.css') }}"> <script src="{{ url_for('static', filename='front/js/front_pdetail.js') }}"></script> {% endblock %} {% block main_content %} <div class="main-container"> <div class="lg-container"> <div class="post-container"> <!-- 帖子标题,前台蓝图文件:apps/front/views.py中的路由定义中传输过来 --> <h2>{{ post.title }}</h2> <p class="post-info-group"> <span>发表时间:{{ post.create_time }}</span> <!-- author和board这两个字段是PostModel的外键,关联了Front_User和BoardModel模型中的username、name字段 --> <span>作者:{{ post.author.username }}</span> <span>所属板块:{{ post.board.name }}</span> <span>阅读数:0</span> <span>评论数:0</span> </p> <article class="post-content" id="post-content" data-id="{{ post.id }}"> <!-- safe用于转义成安全字符串,content_html才能在页面渲染出标签的效果,content中包含有标签内容 --> {{ post.content_html|safe }} </article> </div> <div class="comment-group"> <h3>评论列表</h3> <ul class="comment-list-group"> {% for comment in post.comments %} <li> <div class="avatar-group"> <img src="{{ url_for('static', filename='common/images/logo.png') }}" alt=""> </div> <div class="comment-content"> <p class="author-info"> <span>{{ comment.author.username }}</span> <span>{{ comment.create_time }}</span> </p> <p class="comment-txt"> {{ comment.content|safe }} </p> </div> </li> {% endfor %} </ul> </div> <div class="add-comment-group"> <h3>发表评论</h3> <!-- 这是绑定front_pdetail.js中的百度文本编辑器的id="editor",这里的标签是 script --> <script id="editor" type="text/plain" style="height:100px;"></script> <div class="comment-btn-group"> <button class="btn btn-primary" id="comment-btn">发表评论</button> </div> </div> </div> <div class="sm-container"></div> </div> {% endblock %}
创建 前台帖子详情页面样式js文件:static/front/js/front_pdetail.js
/** * // 前台帖子详情页面样式js文件:static/front/js/front_pdetail.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(){ var ue = UE.getEditor("editor", { "serverUrl": "/ueditor/upload", // 图片上传路径 toolbars: [ // 复制http://fex.baidu.com/ueditor/#start-toolbar中的多行列表代码 ['fullscreen', 'source', 'undo', 'redo'], ['bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc'], ] }); window.ue = ue; }) $(function () { $("#comment-btn").click(function (event) { event.preventDefault(); // var content = $("#comment").val(); var content = window.ue.getContent(); var post_id = $("#post-content").attr("data-id"); lgajax.post({ 'url': '/acomment/', 'data':{ 'content': content, 'post_id': post_id }, 'success': function (data) { if(data['code'] == 200){ window.location.reload(); }else{ lgalert.alertInfo(data['message']); } } }); // } }); });
创建 前台帖子详情页面样式文件:static/front/css/front_pdetail.css
/** * // 前台帖子详情页面样式文件:static/front/css/front_pdetail.css */ .post-container{ border: 1px solid #e6e6e6; padding: 10px; } .post-info-group{ font-size: 12px; color: #8c8c8c; border-bottom: 1px solid #e6e6e6; margin-top: 20px; padding-bottom: 10px; } .post-info-group span{ margin-right: 20px; } .post-content{ margin-top: 20px; } .post-content img{ max-width: 100%; } .comment-group{ margin-top: 20px; border: 1px solid #e8e8e8; padding: 10px; } .add-comment-group{ margin-top: 20px; padding: 10px; border: 1px solid #e8e8e8; } .add-comment-group h3{ margin-bottom: 10px; } .comment-btn-group{ margin-top: 10px; text-align:right; } .comment-list-group li{ overflow: hidden; padding: 10px 0; border-bottom: 1px solid #e8e8e8; } .avatar-group{ float: left; } .avatar-group img{ width: 50px; height: 50px; border-radius: 50%; } .comment-content{ float: left; margin-left:10px; } .comment-content .author-info{ font-size: 12px; color: #8c8c8c; } .author-info span{ margin-right: 10px; } .comment-content .comment-txt{ margin-top: 10px; }
前台首页页面文件:templates/front/front_index.html,关联帖子详情页的路由,而且需要注意的是:url_for反转需要写的是路由的函数名,传参的name和value需要和js文件绑定。
<!-- 前台首页页面文件: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> <!-- 轮播图左右切换按钮 --> <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 class="post-group"> <ul class="post-group-head"> <li class=""><a href="#">最新</a></li> <li class=""><a href="#">精华帖子</a></li> <li class=""><a href="#">点赞最多</a></li> <li class=""><a href="#">评论最多</a></li> </ul> <ul class="post-list-group"> <!-- 循环帖子信息,首页渲染 --> {% for post in posts %} <li> <div class="author-avatar-group"> <img src="#" alt=""> </div> <div class="post-info-group"> <p class="post-title"> <!-- front.post_detail反转需要写的是路由的函数名,post_id=post.id传输帖子id,post_id是名字,post.id是value --> <a href="{{ url_for('front.post_detail', post_id=post.id) }}">{{ post.title }}</a> <span class="label label-danger">精华帖</span> </p> <p class="post-info"> <!-- post模型中的author外键调用Front_User中的username信息 --> <span>作者:{{ post.author.username }}</span> <span>发表时间:{{ post.create_time }}</span> <span>评论:0</span> <span>阅读:0</span> </p> </div> </li> {% endfor %} </ul> <div style="text-align:center;"> </div> </div> </div> <!-- 帖子标签内容 --> <div class="sm-container"> <div style="padding-bottom:10px;"> <!-- 重定向到/apost/路由,文本编辑界面 --> <a href="{{ url_for('front.apost') }}" class="btn btn-warning btn-block">发布帖子</a> </div> <div class="list-group"> <a href="/" class="list-group-item active">所有板块</a> <!-- 循环显示前台蓝图文件:apps/front/views.py中传输的数据**context --> {% for board in boards %} <!-- 注意这里的current_board_id数据类型是int,才能与board.id相比较 --> {% if current_board_id == board.id %} <!-- url_for('front.index', board_id=board.id)每次点击跳转到front_index.html页面,即当前界面,且传输给一个board_id的参数值,由board.id赋值 --> <a href="{{ url_for('front.index', board_id=board.id ) }}" class="list-group-item active">{{ board.name }}</a> {% else %} <!-- 没被选中,即没有被传输相同的board.id,图标样式是class="list-group-item"> --> <a href="{{ url_for('front.index', board_id=board.id ) }}" class="list-group-item">{{ board.name }}</a> {% endif %} {% endfor %} </div> </div> </div> <!-- 居中样式 --> {% endblock %}
后台模型文件: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) # 创建时间 # 精华帖子模型 创建,在这个表中的帖子post_id都是精华帖子 class HighlightPostModel(db.Model): __tablename__ = 'highlight_post' id = db.Column(db.Integer, primary_key=True, autoincrement=True) # 主键 自增 post_id = db.Column(db.Integer, db.ForeignKey("post.id")) # 外键 create_time = db.Column(db.DateTime, default=datetime.now) # 创建时间 # 反转属性 post = db.relationship("PostModel", backref='highlight')
映射模型到数据库中文件: manage.py,导入后台帖子加精模型
"""
映射模型到数据库中文件: manage.py
"""
# 导入后台模型 才能映射到数据库 ,导入轮播图和文章的管理模块
from apps.cms.models import (
BannerModel,
BoardModel,
HighlightPostModel, # 后台帖子加精模型
)
加精帖子模型映射到数据库:
视图文件:apps/cms/views.py文件,创建帖子加精和取消加精的路由地址、方法,发送posts的数据信息参数,用于渲染到后端html页面。
注意:视图函数必须要有返回值,否则报错:ValueError: View function did not return a response;和 TypeError: The view function did not return a valid response. The function either returned…
# -*- 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, # 导入 板块管理模型 HighlightPostModel, # 帖子加精模型 ) # 导入 帖子 模型文件 from apps.front.models import PostModel 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(): posts = PostModel.query.all() # 数据库查询帖子信息,进行传输到后端页面cms_posts.html渲染 return render_template("cms/cms_posts.html", posts=posts) # 帖子 加精的 后台管理,路由名称在static/cms/js/posts.js文件定义好了 @cms_bp.route("/hpost/", methods=["POST"]) # 方法确定为post方式,默认支持的是get方法 @permission_required(CMSPersmission.POSTER) # 传参装饰器验证不同用户不同模块权限 def hposts(): # 接收外键,post接收方式使用form post_id = request.form.get("post_id") # 接收post_id进行查询 if not post_id: return restful.params_error(message="请输入帖子ID") post = PostModel.query.get(post_id) # 从帖子的数据表中查找该帖子对象 if not post: return restful.params_error(message="没有这篇帖子") highlight = HighlightPostModel() # 创建模型 highlight.post = post # 外键关联,加精帖子补充到新的表中 db.session.add(highlight) db.session.commit() # 提交 return restful.success() # 加精成功,视图函数必须有返回值 # 帖子 取消加精的 后台管理,路由名称在static/cms/js/posts.js文件定义好了 @cms_bp.route("/uhpost/", methods=["POST"]) # 方法确定为post方式,默认支持的是get方法 @permission_required(CMSPersmission.POSTER) # 传参装饰器验证不同用户不同模块权限 def uhposts(): # 接收外键,post接收方式使用form post_id = request.form.get("post_id") # 接收post_id进行查询 if not post_id: return restful.params_error(message="请输入帖子ID") post = PostModel.query.get(post_id) # 从帖子的数据表中查找该帖子对象 if not post: return restful.params_error(message="没有这篇帖子") highlight = HighlightPostModel.query.filter_by(post_id=post_id).first() db.session.delete(highlight) db.session.commit() # 提交 return restful.success() # 视图函数必须有返回值 # 评论管理路由 @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'))
后台管理帖子页面:templates/cms/cms_posts.html,接收视图文件:apps/cms/views.py文件传输过来的posts的数据信息,循环渲染到该html页面。
<!-- 后台管理帖子页面:templates/cms/cms_posts.html --> <!-- 继承模板文件cms/cms_base.html 简化代码 --> {% extends 'cms/cms_base.html' %} <!-- 页面标题 --> {% block title %} 帖子管理 {% endblock %} {% block head %} <script src="{{ url_for('static', filename='cms/js/posts.js') }}"></script> {% endblock %} <!-- 标题 --> {% block page_title %} {{self.title()}} {% endblock %} {% block content %} <table class="table table-bordered"> <thead> <tr> <th>标题</th> <th>发布时间</th> <th>板块</th> <th>作者</th> <th>操作</th> </tr> </thead> <tbody> <!-- 循环渲染帖子信息,posts数据由views.py文件传输过来 --> {% for post in posts %} <!--data-id="{{ post.id }}和data-highlight属性传输给posts.js文件进行判断,{{ 1 if post.highlight else 0 }}三元运算符:如果post.highlight存在显示为1 --> <tr data-id="{{ post.id }}" data-highlight="{{ 1 if post.highlight else 0 }}"> <!-- href链接跳转到前台的帖子路由,调用该路由方法进行反转,并传输名称为post_id,value值为post.id的参数 --> <td><a target="_blank" href="{{ url_for('front.post_detail',post_id=post.id) }}">{{ post.title }}</a></td> <td>{{ post.create_time }}</td> <!-- post.board是PostModel中的board反向查询属性,name是BoardModel中的name字段 --> <td>{{ post.board.name }}</td> <!-- post.author是PostModel中的author反向查询属性,username是Front_User中的username字段 --> <td>{{ post.author.username }}</td> <td> <!-- post数据由views.py文件传输过来,highlight是模型中的post的外键 --> {% if post.highlight %} <button class="btn btn-default btn-xs highlight-btn">取消加精</button> {% else %} <button class="btn btn-default btn-xs highlight-btn">加精</button> {% endif %} <button class="btn btn-danger btn-xs">移除</button> </td> </tr> {% endfor %} </tbody> </table> {% endblock %}
前台模型文件 apps/front/models.py,创建CommentModel评论模型。
# -*- encoding: utf-8 -*- """ @File : models.py @Time : 2020/5/11 10:00 @Author : chen 前台模型文件 apps/front/models.py """ # 前台管理的模型 from exts import db # 数据库连接 import shortuuid # 前台用户id加密 from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash # 导入密码加密,解密方法的库 import enum # 导入枚举 from markdown import markdown # 导入帖子编辑的markdown显示功能库 import bleach # 导入帖子编辑的markdown显示功能库 # 性别选择的类 class GenderEnum(enum.Enum): MALE = 1 FEMALE = 2 SECRET = 3 UNKNOW = 4 # 前台用户模型类 class Front_User(db.Model): __tablename__ = "front_user" # id 类型不用db.Integer类型,使用String是为了防止爆破,同时使用shortuuid进行加密 id = db.Column(db.String(100), primary_key=True, default=shortuuid.uuid) telephone = db.Column(db.String(11), nullable=False, unique=True) # 非空唯一 username = db.Column(db.String(150), nullable=False) _password = db.Column(db.String(150), nullable=False) # 密码加密操作修改字段 email = db.Column(db.String(50), unique=True) realname = db.Column(db.String(50)) avatar = db.Column(db.String(150)) # 头像,二进制数据 signatrue = db.Column(db.String(500)) # 签名 gender = db.Column(db.Enum(GenderEnum), default=GenderEnum.UNKNOW) # 性别枚举类,默认未知 join_time = db.Column(db.DateTime, default=datetime.now) # 默认当前时间 # 修改密码加密操作,manage.py映射数据库时候,使用字段保持相同,由于字段太多,使用传参形式 def __init__(self, *args, **kwargs): if 'password' in kwargs: # 如果传参中包含有password self.password = kwargs.get('password') # 获取该参数值赋值给password kwargs.pop('password') # 模型参数中是_password,不是password,弹出 # super(FrontUser, self).__init__(*args, **kwargs) # python2的写法 super().__init__(*args, **kwargs) # 密码加密操作 @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 # 帖子编辑提交模型 class PostModel(db.Model): __tablename__ = "post" id = db.Column(db.Integer, primary_key=True, autoincrement=True) title = db.Column(db.String(100), nullable=True) # 帖子标题 content = db.Column(db.Text, nullable=True) # 帖子内容 content_html = db.Column(db.Text) create_time = db.Column(db.DateTime, default=datetime.now) # 默认当前时间 # 外键,用于查询排序 board_id = db.Column(db.Integer, db.ForeignKey('cms_board.id')) # 'cms_board.id'中cms_board是cms/models.py的表名 author_id = db.Column(db.String(100), db.ForeignKey('front_user.id'))# 这里的id使用String是因为上面定义前台用户id时,使用的就是Str类型shortuuid # 反向查询属性, board = db.relationship("BoardModel", backref="posts") # posts变成cms/models/BoardModel的属性 author = db.relationship("Front_User", backref="posts") # posts变成Front_User的属性 # 实现将用户输入的content文件text类型转换成content_html的html文件,再进行存储 @staticmethod def content_to_content_html(target, value, oldvalue, initiator): # content_html文件中允许使用的标签集合 allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'ol', 'pre', 'strong', 'ul', 'h1', 'h2', 'h3', 'p', 'img', 'video', 'div', 'iframe', 'p', 'br', 'span', 'hr', 'src', 'class'] # content_html文件中允许使用的属性 allowed_attrs = {'*': ['class'], 'a': ['href', 'rel'], 'img': ['src', 'alt']} # 目标文件content_html,由bleach库进行转换 markdown将源文件显示成html文件 target.content_html = bleach.linkify(bleach.clean( markdown(value, output_format='html'), # output_format='html'输出格式为html tags=allowed_tags, strip=True, attributes=allowed_attrs)) # strip=True去空格 # 监听PostModel.content文件如果调用了set方法,就调用content_to_content_html方法进行转换格式到html文件 db.event.listen(PostModel.content, 'set', PostModel.content_to_content_html) # 添加评论 模型 class CommentModel(db.Model): __tablename__ = "comment" id = db.Column(db.Integer, primary_key=True, autoincrement=True) content = db.Column(db.Text, nullable=True) # 帖子内容 create_time = db.Column(db.DateTime, default=datetime.now) # 默认当前时间 # 添加评论的作者 author_id = db.Column(db.String(100), db.ForeignKey("front_user.id")) # 外键关联front_user表中的id字段 # 帖子id 外键关联post表中的id字段 post_id = db.Column(db.Integer, db.ForeignKey("post.id")) # 反向属性backref命名任意,调用的时候需要一致 post = db.relationship("PostModel", backref="comments") author = db.relationship("Front_User", backref="comments")
通过导入CommentModel 模型到manage.py中,再映射到数据库中:
前台表单信息:apps/front/forms.py,创建评论的Form表单验证。
# -*- encoding: utf-8 -*- """ @File : forms.py @Time : 2020/5/11 10:00 @Author : chen 前台表单信息:apps/front/forms.py """ # 前台form表单信息 from wtforms import Form, StringField, IntegerField, ValidationError from wtforms.validators import EqualTo, Email, InputRequired, Length, Regexp from utils import random_captcha # 随机生成验证码 from utils import redis_captcha # 保存验证码到redis数据库中 # 表单信息的父类文件 class BaseForm(Form): def get_error(self): message = self.errors.popitem()[1][0] # 错误信息的收集,字典类型数据信息提取 return message # 注册界面的Form表单类 class SignupForm(BaseForm): telephone = StringField(validators=[Regexp(r'1[345789]\d{9}', message="请输入正确格式的手机号")]) sms_captcha = StringField(validators=[Regexp(r'\w{4}', message="请输入正确格式的验证码")]) # \w包含字母 username = StringField(validators=[Length(min=2, max=15, message="请输入正确长度的用户名")]) password1 = StringField(validators=[Regexp(r'[0-9a-zA-Z_\.]{3,20}', message="请输入正确格式的密码")]) password2 = StringField(validators=[EqualTo('password1', message="两次输入密码不一致")]) graph_captcha = StringField(validators=[Regexp(r'\w{4}', message="请输入正确格式的验证码")]) # 验证手机验证码字段 def validate_sms_captcha(self, field): telephone = self.telephone.data sms_captcha = self.sms_captcha.data # 获得表单信息 sms_captcha_redis = redis_captcha.redis_get(telephone) # redis数据库中根据手机号调验证码,进行判定是否相同 # 判断用户输入的验证码和redis中取出的验证码是否相同 if not sms_captcha_redis or sms_captcha_redis.lower() != sms_captcha.lower(): raise ValidationError(message="手机验证码输入错误") # if sms_captcha or sms_captcha.lower() == sms_captcha_redis.lower(): # pass # else: # raise ValidationError("验证码输入错误") # 图形验证码字段验证 def validate_graph_captcha(self, field): graph_captcha = self.graph_captcha.data # 表单信息收集 graph_captcha_redis = redis_captcha.redis_get(graph_captcha) # redis中是将验证码的text当做key来保存的,调用也是一样 # 判定图形验证码是否一致 if not graph_captcha_redis or graph_captcha_redis.lower() != graph_captcha.lower(): # print("ceshi") raise ValidationError(message="图形验证码输入错误") # 登录界面的Form表单信息收集 class SigninForm(BaseForm): telephone = StringField(validators=[Regexp(r'1[345789]\d{9}', message="请输入正确格式的手机号")]) password = StringField(validators=[Regexp(r'[0-9a-zA-Z_\.]{3,20}', message="请输入正确格式的密码")]) remember = StringField(IntegerField()) # 帖子的Form表单信息收集 class AddPostForm(BaseForm): title = StringField(validators=[InputRequired(message="请输入标题")]) content = StringField(validators=[InputRequired(message="请输入内容")]) board_id = StringField(validators=[InputRequired(message="请输入板块名称")]) # 评论的Form表单验证 class AddCommentForm(BaseForm): # 表单信息的收集根据网页端发送的参数名称和类型 content = StringField(validators=[InputRequired(message="请输入评论内容")]) post_id = IntegerField(validators=[InputRequired(message="请选择一篇帖子进行评论")])
帖子详情页面文件:templates/front/front_detail.html
<!-- 帖子详情页面文件:templates/front/front_detail.html --> {% extends 'front/front_base.html' %} {% block title %}帖子详情{% endblock %} {% block head %} <!-- 百度的富文本编辑器加载 --> <script src="{{ url_for('static', filename='ueditor/ueditor.config.js') }}"></script> <script src="{{ url_for('static', filename='ueditor/ueditor.all.min.js') }}"></script> <link rel="stylesheet" href="{{ url_for('static', filename='front/css/front_pdetail.css') }}"> <script src="{{ url_for('static', filename='front/js/front_pdetail.js') }}"></script> {% endblock %} {% block main_content %} <div class="main-container"> <div class="lg-container"> <div class="post-container"> <!-- 帖子标题,前台蓝图文件:apps/front/views.py中的路由定义中传输过来 --> <h2>{{ post.title }}</h2> <p class="post-info-group"> <span>发表时间:{{ post.create_time }}</span> <!-- author和board这两个字段是PostModel的外键,关联了Front_User和BoardModel模型中的username、name字段 --> <span>作者:{{ post.author.username }}</span> <span>所属板块:{{ post.board.name }}</span> <span>阅读数:0</span> <span>评论数:0</span> </p> <!-- data-id="{{ post.id }} 传输帖子id到front_pdetail.js文件进行获取 --> <article class="post-content" id="post-content" data-id="{{ post.id }}"> <!-- safe用于转义成安全字符串,content_html才能在页面渲染出标签的效果,content中包含有标签内容 --> {{ post.content_html|safe }} </article> </div> <div class="comment-group"> <h3>评论列表</h3> <ul class="comment-list-group"> <!-- comments是反向引用的属性 --> {% for comment in post.comments %} <li> <div class="avatar-group"> <img src="{{ url_for('static', filename='common/images/logo.png') }}" alt=""> </div> <div class="comment-content"> <p class="author-info"> <!-- comment.author外键,从CommentModel中调用 --> <span>{{ comment.author.username }}</span> <span>{{ comment.create_time }}</span> </p> <p class="comment-txt"> {{ comment.content|safe }} </p> </div> </li> {% endfor %} </ul> </div> <div class="add-comment-group"> <h3>发表评论</h3> <!-- 这是绑定front_pdetail.js中的百度文本编辑器的id="editor",这里的标签是 script --> <script id="editor" type="text/plain" style="height:100px;"></script> <div class="comment-btn-group"> <!-- 绑定id="comment-btn" --> <button class="btn btn-primary" id="comment-btn">发表评论</button> </div> </div> </div> <div class="sm-container"></div> </div> {% endblock %}
需要使用flask插件:flask paginate
映射模型到数据库中文件: manage.py,创建测试数据集200条:
# -*- encoding: utf-8 -*- """ @File : manage.py @Time : 2020/5/10 17:36 @Author : chen 映射模型到数据库中文件: manage.py """ # 数据库添加多条帖子信息,进行验证分页功能 @manage.command def create_test_post(): for i in range(1, 200): # 循环产生200篇帖子信息 title = "标题%s" % i content = "内容%s" % i author = Front_User.query.first() # 查询数据库中所有的用户信息 post = PostModel(title=title, content=content) # 循环的标题内容信息添加给PostModel post.author = author post.board_id = random.randint(2, 7) # 随机选择cms_board表中的id的值为2-7 db.session.add(post) db.session.commit() print("测试帖子添加成功!")
命令行添加测试数据集200条。
前台蓝图文件:apps/front/views.py,将测试数据集传输到前端页面中front_index.html中
# -*- 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, # make_response生成response对象,用于返回前端模板 request, session, g, ) # 导入图像验证码生成文件 from utils.captcha import Captcha # 图形验证码image是二进制数据,需要转换成字节流才能使用 from io import BytesIO # 将图形验证码保存到Redis restful输出信息弹窗 from utils import redis_captcha, restful # 验证码表单信息验证 from .forms import ( SignupForm, # 注册的Form表单信息收集 SigninForm, # 登录的Form表单信息收集 AddPostForm, # 帖子提交表单信息 AddCommentForm, # 添加帖子评论 ) # 导入前台用户模型 from .models import ( Front_User, PostModel, CommentModel, # 评论模型 ) # 导入数据库连接 db from exts import db # 确保URL安全的文件:utils/safe_url.py from utils import safe_url from apps.cms.models import ( BannerModel, # 导入后台轮播图模型BannerModel BoardModel, # 导入后台板块管理模型 ) # 导入分页功能库 from flask_paginate import Pagination, get_page_parameter # 导入前台界面权限验证装饰器 from .decorators import login_required # 导入配置文件 import config front_bp = Blueprint("front", __name__) # 前端不用前缀,直接在首页显示,front是蓝图,在front_signup.html调用生成图形验证码时候需要用 # 权限验证 需要在front_bp产生后,再导入 from .hooks import before_request # BBS的首页界面路由 @front_bp.route("/") def index(): banners = BannerModel.query.order_by(BannerModel.priority.desc()).limit(4) # 通过权重查询,每页显示4条 boards = BoardModel.query.all() # 查询板块中的所有 board_id = request.args.get('board_id', type=int, default=None) # get方法需要使用args,注意这里的数据类型需要改成int page = request.args.get(get_page_parameter(), type=int, default=1) # 获取当前页码 start = (page-1)*config.PER_PAGE # 起始页码是(当前页码-1)*10 end = start + config.PER_PAGE # 每页都是起始页码+10 # 实现根据不同board_id进行帖子分类显示,即用户选择不同板块,显示的帖子种类相对应 if board_id: posts = PostModel.query.filter_by(board_id=board_id).slice(start, end) # 用户选择不同板块,查询相对应板块的数据,slice(start, end)分页 total = PostModel.query.filter_by(board_id=board_id).count() # 计算该板块的总数 else: posts = PostModel.query.slice(start, end) # 帖子信息传输,如果用户不选择板块,查询所有 total = PostModel.query.count() # 计算帖子总数 # pagination是一个对象,bs_version=3是bootstrap的版本为3,per_page参数添加,pagination.links正常显示所有 pagination = Pagination(bs_version=3, page=page, total=total, per_page=config.PER_PAGE, # config.py中的每页10条数据 inner_window=3, outer_window=1) # inner_window=3是内层显示页码的样式,默认为2, # print(pagination.links) # 当数据量小的时候,不显示,添加per_page参数就能解决 context = { # 多种数据传输到前台界面 "banners": banners, "boards": boards, "current_board_id": board_id, "posts": posts, "pagination": pagination, } return render_template("front/front_index.html", **context) # 渲染到首页界面,查询数据传输到前台界面 # 图形验证码路由 @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): # 判断密码和用户是否正确 # 'front_user_id'命名防止与后台验证session相同,会产生覆盖情况bug session['front_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()) # 表单信息不存在,输出异常信息 # 帖子编辑提交 的类视图 富文本编辑 class PostView(views.MethodView): # 登录验证,实现帖子编辑前进行权限验证 decorators = [login_required] # 表单信息收集,传输 def get(self): # 查询boards数据进行传输 boards = BoardModel.query.all() # boards是list类型 return render_template("front/front_apost.html", boards=boards) # boards数据传输到前端front_apost.html页面 # 帖子的Form表单信息收集查询 def post(self): form = AddPostForm(request.form) # 查询帖子提交的Form表单信息 if form.validate(): title = form.title.data board_id = form.board_id.data # 收集表单中提交的信息 content = form.content.data # 查询用户信息是否在数据库中存在 board = BoardModel.query.get(board_id) if not board: return restful.params_error(message="没有这个版块名称") # 数据库中不存在,返回异常信息 # 数据库中board信息存在,传输数据到数据库表中,并修改名称 post = PostModel(title=title, board_id=board_id, content=content) post.board = board # 外键中的信息修改赋值 post.author = g.front_user # g对象 db.session.add(post) db.session.commit() return restful.success() # 提交成功,为json数据 else: return restful.params_error(message=form.get_error()) # 前台 帖子详情 路由 @front_bp.route("/p/<post_id>") # 蹄子详情路由需要传参帖子id:post_id def post_detail(post_id): post = PostModel.query.get(post_id) # 通过post_id查找数据库中的帖子信息 if not post: return restful.params_error(message="帖子不存在!") return render_template("front/front_detail.html", post=post) # 查找到帖子信息,传输数据到帖子详情页渲染 # 添加评论 的路由 @front_bp.route("/acomment/", methods=['POST']) @login_required # 登录验证 def add_comment(): form = AddCommentForm(request.form) # 网页发送的request.form表单信息放入AddCommentForm进行验证 if form.validate(): content = form.content.data # form表单信息 post_id = form.post_id.data post = PostModel.query.get(post_id) # 通过post_id查询帖子信息 if post: comment = CommentModel(content=content) # 将AddCommentForm中验证后的content信息传给CommentModel模型 # 外键关联,反向属性backref,从CommentModel中调用 comment.post = post # 外键关联的是post表中的id字段 comment.author = g.front_user # 将apps/front/hooks.py中的g对象赋值外键的作者的id db.session.add(comment) # 添加对象信息到数据库 db.session.commit() 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"视图中不需要反斜线 front_bp.add_url_rule("/apost/", view_func=PostView.as_view("apost")) # 绑定帖子编辑提交路由
项目配置文件:config.py
'''
项目配置文件:config.py
'''
# 每页显示数据数目
PER_PAGE = 10
前台首页页面文件:templates/front/front_index.html,接收pagination参数渲染到页面。
<!-- 前台首页页面文件: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> <!-- 轮播图左右切换按钮 --> <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 class="post-group"> <ul class="post-group-head"> <li class=""><a href="#">最新</a></li> <li class=""><a href="#">精华帖子</a></li> <li class=""><a href="#">点赞最多</a></li> <li class=""><a href="#">评论最多</a></li> </ul> <ul class="post-list-group"> <!-- 循环帖子信息,首页渲染 --> {% for post in posts %} <li> <div class="author-avatar-group"> <img src="#" alt=""> </div> <div class="post-info-group"> <p class="post-title"> <!-- front.post_detail反转需要写的是路由的函数名,post_id=post.id传输帖子id,post_id是名字,post.id是value --> <a href="{{ url_for('front.post_detail', post_id=post.id) }}">{{ post.title }}</a> <span class="label label-danger">精华帖</span> </p> <p class="post-info"> <!-- post模型中的author外键调用Front_User中的username信息 --> <span>作者:{{ post.author.username }}</span> <span>发表时间:{{ post.create_time }}</span> <span>评论:0</span> <span>阅读:0</span> </p> </div> </li> {% endfor %} </ul> <div style="text-align:center;"> <!-- 页码分页展示, pagination.links数据由 apps/front/views.py传输过来 --> {{ pagination.links }} </div> </div> </div> <!-- 帖子标签内容 --> <div class="sm-container"> <div style="padding-bottom:10px;"> <!-- 重定向到/apost/路由,文本编辑界面 --> <a href="{{ url_for('front.apost') }}" class="btn btn-warning btn-block">发布帖子</a> </div> <div class="list-group"> <a href="/" class="list-group-item active">所有板块</a> <!-- 循环显示前台蓝图文件:apps/front/views.py中传输的数据**context --> {% for board in boards %} <!-- 注意这里的current_board_id数据类型是int,才能与board.id相比较 --> {% if current_board_id == board.id %} <!-- url_for('front.index', board_id=board.id)每次点击跳转到front_index.html页面,即当前界面,且传输给一个board_id的参数值,由board.id赋值 --> <a href="{{ url_for('front.index', board_id=board.id ) }}" class="list-group-item active">{{ board.name }}</a> {% else %} <!-- 没被选中,即没有被传输相同的board.id,图标样式是class="list-group-item"> --> <a href="{{ url_for('front.index', board_id=board.id ) }}" class="list-group-item">{{ board.name }}</a> {% endif %} {% endfor %} </div> </div> </div> <!-- 居中样式 --> {% endblock %}
分页功能实现效果如下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。