文章目录
简介
- 这一部分要实现具体的后台管理逻辑
- 基本逻辑如下:
管理员登录
- 将之前
models
中数据库的认证部分移动到app
初始化文件中 - 这一节的大部分内容都是参考前端页面进行的,这也是为什么上一节先搭建页面
- flask中所有表单提交验证使用
flask_wtf
,可以安装一下先- 激活虚拟环境,
pip install flask-wtf
- 这个扩展里定义好了很多表单要用的字段和验证器,例如字符串、密码、提交等等,也属于模型
- 同样的,在我的基础笔记中有较为详细的解释
- 激活虚拟环境,
- 在
forms.py
中from flask_sqlalchemy import SQLAlchemy from flask import Flask from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField from wtforms.validators import DataRequired, ValidationError class LoginForm(FlaskForm): """管理员登录表单""" account = StringField( label='账号', validators=[ DataRequired('请输入账号!') ], description='账号', render_kw={ "class":"from-control", "placeholder":"请输入账号", "required":"required" } ) pwd = StringField( label='密码', validators=[ DataRequired('请输入账号!') ], description='账号', render_kw={ "class": "from-control", "placeholder": "请输入账号", "required": "required" } # 传递给前端标签的属性,直接从模板中拷贝过来 ) submit = SubmitField( label='登录', render_kw={ "class": "btn btn-primary btn-block btn-flat", } )
- 将模型渲染到模板中
- 模板的操作要经过视图,在
views.py
from app.admin.forms import LoginForm # 后台登录 @admin.route('/login/', methods=["GET", "POST"]) def login(): form = LoginForm() # 反过来执行判断 if form.validate_on_submit(): data = form.data admin = Admin.query.filter_by(name=data['account']).first() # 这是一条包含信息的对象 if not admin.check_pwd(data['pwd']): flash("密码错误!") return redirect(url_for('admin.login')) # 密码正确,保存账号 session['admin'] = data['account'] # 账号 return redirect(request.args.get('next') or url_for('admin.index')) return render_template('admin/login.html', form=form)
- 到模板中替换,
login.html
<input name="user" type="text" class="form-control" placeholder="请输入账号!"> {{ form.account }} <input name="pwd" type="password" class="form-control" placeholder="请输入密码!"> {{ form.pwd }} <a id="btn-sub" type="submit" class="btn btn-primary btn-block btn-flat">登录</a> {{ form.submit }}
- 模板的操作要经过视图,在
- 这里会报需要
csrf
字段,这是一种保护策略(跨站请求伪造),我们可以查看官方文档使用- 我们给app配置一个
SECRET_KEY
,可以使用uuid模块生成 - 然后只需在模板中加入
{{ form.csrf_token }}
即可生成隐藏的csrf标签
- 我们给app配置一个
- 既然有了验证,如何在模板提示验证时的错误信息?在每个表单字段下修改:
{% for err in form.account.errors %} <div class="col-md-12"> <font style="color:red">{{err}}</font> </div> {% endfor %}
- 这个没生效,无伤大雅,后面再看!
- OK,这个不是没生效,而是账户密码都输入了才会验证并显示错误信息!
- 表单验证的结果以及接收数据在视图中处理(前->后)
- 当然,这个验证方法也放在表单模型中管理
def validate_account(self, field): account = field.data admin = Admin.query.filter_by(name=account).count() # 使用数据库模型查询用户信息 if admin == 0: raise ValidationError("账号不存在!")
- 密码经过了hash运算,我们在admin的模型类中定义检验方法
# models.py # class Admin def check_pwd(self, pwd): from werkzeug.security import check_password_hash return check_password_hash(self.pwd, pwd)
- 然后在视图中定义校验密码(账号已存在并传递表单数据给视图)
from flask import Flask, render_template, redirect, url_for, flash, session, request # 后台登录 @admin.route('/login/', methods=["GET", "POST"]) def login(): form = LoginForm() if form.validate_on_submit(): data = form.data admin = Admin.query.filter_by(name=data['account']).first() # 这是一条包含信息的对象 if not admin.check_pwd(data['pwd']): flash("密码错误!") return redirect(url_for('admin.login')) # 密码正确,保存账号 session['admin'] = data['account'] # 账号 return redirect(request.args.get('next') or url_for('admin.index')) return render_template('admin/login.html', form=form)
- 这里用到了消息闪现
flash
,需要前端加点东西
{% for msg in get_flashed_messages() %} <p class="login-box-msg" style="color:red;">{{ msg }}</p> {% endfor %}
- 很多页面需要登录才能访问,使用装饰器限制视图函数
# admin/views.py from functools import wraps # 作用是不改变被装饰函数的信息 def admin_login(f): @wraps(f) def inner(*args, **kwargs): if "admin" not in session: # 不能使用 session['admin'] is None return redirect(url_for('admin.login', next=request.url)) # next参数表示继续之前请求的地址 return f(*args, **kwargs) # 返回,不调用 return inner # 然后我们给每个视图函数加上这个装饰器语法糖,例如 @admin.route('/logout') @admin_login def logout(): session.pop('admin', None) # 清除session return redirect(url_for('admin.login')) # 注意admin.html布局文件中,退出是路由到logout
- session会将用户名和密码都保存,logout之后会将其清除!
- session和cookie的区别是?
标签管理
- 理一下管理员登录的逻辑,前面四步是通用的!
- 在
forms
中定义表单模型 - 在视图中传递
- 在模板中渲染
- 添加csrf验证
- 在表单模型中定义验证器,判断用户是否存在
- 在数据库模型中定义密码校验方法
- 使用装饰器限制页面访问
- 在
- 这里在贴一遍逻辑
class TagForm(FlaskForm): """标签添加表单""" name = StringField( label='标签名称', validators=[ DataRequired("请输入标签名称!") ], description="标签", render_kw={ "class" : "form-control", "id" : "input_name", "placeholder" : "请输入标签名称!" } ) submit = SubmitField( label="添加", render_kw={ "class" : "btn btn-primary" } )
- 定义入库方法
# 标签的添加和列表 @admin.route('/tag/add', methods=["GET", "POST"]) # 这个方法必须定义,否则validator不显示 @admin_login def tag_add(): form = TagForm() print("aaaaaaaaaaaaaaaa") if form.validate_on_submit(): # 没过验证 data = form.data tag = Tag.query.filter_by(name=data['name']).count() if tag == 1: flash("标签已存在!", "err") return redirect(url_for('admin.tag_add')) tag = Tag( name= data['name'] ) db.session.add(tag) db.session.commit() flash("标签添加成功", 'ok') # 第二个参数是固定的一些值,应用到模板 category_filter=["ok"] return render_template('admin/tagadd.html', form=form)
- 我这里因为
{{form.csrf_token}}
这玩意儿写错了搞了半天,要细心!
- 我这里因为
- 在前端找个模板提示信息
{% for msg in get_flashed_messages(category_filter=["ok"]) %} <div class="alert alert-success alert-dismissible"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <h4><i class="icon fa fa-check"></i> 操作成功</h4> {{ msg }} </div> {% endfor %} {% for msg in get_flashed_messages(category_filter=["err"]) %} <div class="alert alert-danger alert-dismissible"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <h4><i class="icon fa fa-ban"></i> 操作失败</h4> {{ msg }} </div> {% endfor %} <div class="form-group">
- 然后在标签列表中分页显示并处理编辑删除操作
- 分页显示在路由中用到正则匹配:
<int:page>
,前端就要传递页码哦!
@admin.route('/tag/list/<int:page>/', methods=['GET']) @admin_login def tag_list(page=None): if page is None: page = 1 page_data = Tag.query.order_by( Tag.addtime.desc() ).paginate(page=page, per_page=2) return render_template('admin/taglist.html', page_data=page_data)
- 处理分页也是用到扩展
SQLAlchemy
,templates中新建ui/admin_page.html
,使用macro
的语法,可以参考官方文档 - 注意这里前端传递参数的方式,还是在
url_for
中使用/
拼接,自动放入url,再正则解析,还不同于查询参数?x=x
- 例如:
{{url_for('admin.tag_edit', id=v.id)}}
# menu.html,初始化第一页 <li> <a href="{{url_for('admin.tag_list', page=1)}}"> <i class="fa fa-circle-o"></i> 标签列表 </a> </li> <!--ui/admin_page.html--> {% macro page(data,url) -%} {% if data %} <ul class="pagination pagination-sm no-margin pull-right"> <li><a href="{{url_for(url,page=1)}}">首页</a></li> {% if data.has_prev %} <li><a href="{{url_for(url,page=data.prev_num)}}">上一页</a></li> {% else %} <li class="disabled"><a href="">上一页</a></li> {% endif %} {% for v in data.iter_pages() %} {% if v==data.page %} <li class="active"><a href="#">{{v}}</a></li> {% else %} <li><a href="{{url_for(url, page=v)}}">{{v}}</a></li> {% endif %} {% endfor %} {% if data.has_next %} <li><a href="{{url_for(url,page=data.next_num)}}">下一页</a></li> {% else %} <li class="disabled"><a href="">上一页</a></li> {% endif %} <li><a href="{{url_for(url, page=data.pages)}}">尾页</a></li> </ul> {% endif %} {% endmacro %} <!--taglist.html--> {% import "ui/admin_page.html" as pg%} <div class="box-footer clearfix"> <!--调用macro函数--> {{pg.page(page_data, 'admin.tag_list')}} </div>
- 分页显示在路由中用到正则匹配:
- 列表删除和编辑
- 编辑和添加同样,涉及到表单提交,视图中都是先渲染到前端,提交表单,再返回验证并入库
- 如果是删除,只需要传递参数(拼接url,正则捕获),修改数据库
- 如果是查询,只需要查询数据库,分页
- 增删改查到这来就都涉及到了!规律是 参数传递 + 视图-前端-视图;还有查询参数没有涉及
- 这个参数传递可能在先也可能在后,这里删除就是先传递id参数,如果列表就是后面传递请求的page
# 标签删除 @admin.route('/tag/del/<int:id>/', methods=['GET']) @admin_login def tag_del(id=None): tag = Tag.query.filter_by(id=id).first_or_404() # 如果没找到会报错 db.session.delete(tag) db.session.commit() flash("删除成功!", "ok") return redirect(url_for('admin.tag_list', page=1)) # html <a href="{{url_for('admin.tag_del', id=v.id)}}" class="label label-danger">删除</a> # 标签编辑 @admin.route('/tag/edit/<int:id>/', methods=['GET', 'POST']) @admin_login def tag_edit(id=None): # 编辑相当于重新提交一个表单 form = TagForm() # 和tagadd共用表单模型 tag = Tag.query.get_or_404(id) # 要修改的标签名 if form.validate_on_submit(): data = form.data # 提交的标签名 tag_count = Tag.query.filter_by(name=data['name']).count() if tag_count == 1: # tag.name != data['name'] and flash("名称已存在", 'err') tag.name = data['name'] db.session.add(tag) db.session.commit() flash("编辑成功!", "ok") return redirect(url_for('admin.tag_list', page=1)) return render_template("admin/tagedit.html", form=form, tag=tag) # 先渲染到前端,再返回验证
- 要新建编辑页面,复制tagadd.html即可,注意使用tag变量的方法:
{{form.name(value=tag.name)}}
{% extends "admin/admin.html" %} {% block content %} <section class="content-header"> <h1>微电影管理系统</h1> <ol class="breadcrumb"> <li><a href="#"><i class="fa fa-dashboard"></i> 标签管理</a></li> <li class="active">编辑标签</li> </ol> </section> <section class="content" id="showcontent"> <div class="row"> <div class="col-md-12"> <div class="box box-primary"> <div class="box-header with-border"> <h3 class="box-title">编辑标签</h3> </div> <form role="form" method="post"> <div class="box-body"> <!--找一个提示成功的框--> {% for msg in get_flashed_messages(category_filter=["ok"]) %} <div class="alert alert-success alert-dismissible"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <h4><i class="icon fa fa-check"></i> 操作成功</h4> {{ msg }} </div> {% endfor %} {% for msg in get_flashed_messages(category_filter=["err"]) %} <div class="alert alert-danger alert-dismissible"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <h4><i class="icon fa fa-ban"></i> 操作失败</h4> {{ msg }} </div> {% endfor %} <div class="form-group"> <label for="input_name">{{form.name.label}}</label> {{form.name(value=tag.name)}} {% for err in form.name.errors %} <div class="col-md-12"> <span style="color: red">{{ err }}</span> </div> {% endfor %} <!--<input type="text" class="form-control" id="input_name" placeholder="请输入标签名称!">--> </div> </div> <div class="box-footer"> {{form.submit}} {{form.csrf_token}} <!--<button type="submit" class="btn btn-primary">添加</button>--> </div> </form> </div> </div> </div> </section> {% endblock %} {% block js%} <script> $(document).ready(function () { $('#m-2').addClass("active"); }) </script> {% endblock %}
- 表单模型不修改,统一叫编辑(添加/修改)
电影管理
- 围绕着“参数传递+视前视”的核心处理思路,开始后台电影管理
- 添加电影
- 视图渲染表单,先根据数据库模型定义表单模型
class MovieForm(FlaskForm): """电影添加表单""" title = StringField( label='电影名称', validators=[ DataRequired("请输入电影名称!") ], description="标签", render_kw={ "class": "form-control", "id": "input_title", "placeholder": "请输入片名!" } ) url = FileField( label='文件', validators=[ DataRequired("请上传文件") ], description="文件" ) info = TextAreaField( label='简介', validators=[ DataRequired("请输入电影简介") ], description="简介", render_kw={ "class": "form-control", "rows":"10", "id": "input_info" } ) logo = FileField( label='logo图片', validators=[ DataRequired("请上传文件") ], description="logo" ) star = SelectField( label='星级', validators=[ DataRequired("请选择星级") ], description="星级", coerce=int, choices=[(1,"1星"),(2,"2星"),(3,"3星"),(4,"4星"),(5,"5星")], render_kw={ "class": "form-control", "id": "input_star" } ) tag_id = SelectField( label='标签', validators=[ DataRequired("请选择标签") ], description="标签", coerce=int, choices=[(v.id, v.name) for v in tags], # 列表推导式,传递到前端只显示v.name,给到视图是v.id render_kw={ "class": "form-control", "id": "input_tag_id" } ) area = StringField( label='地区', validators=[ DataRequired("请输入地区") ], description="地区", render_kw={ "class": "form-control", "id": "input_area", "placeholder" : "请输入地区!" } ) length = StringField( label='片长', validators=[ DataRequired("请输入片长") ], description="地区", render_kw={ "class": "form-control", "id": "input_length", "placeholder" : "请输入片长!" } ) release = StringField( label='上映时间', validators=[ DataRequired("请输入上映时间") ], description="上映时间", render_kw={ "class": "form-control", "id": "input_release_time", "placeholder" : "请输入上映时间!" } ) submit = SubmitField( label="添加", render_kw={ "class": "btn btn-primary" } )
- 视图中导入数据库和表单模型,准备到前端
<!--从流程来讲,不应该忘了这句,限定方法--> <form role="form" method="POST" enctype="multipart/form-data">
- 前端POST之后,回到视图验证,注意文件的保存
# 电影的添加 from werkzeug.utils import secure_filename @admin.route('/movie/add', methods=['GET','POST']) @admin_login def movie_add(): form = MovieForm() if form.validate_on_submit(): data = form.data # 准备一条数据插入 # 过滤数据名 file_url = secure_filename(form.url.data.filename) file_logo = secure_filename(form.logo.data.filename) # 准备存储路径 if not os.path.exists(app.config['UP_DIR']): os.mkdir(app.config['UP_DIR']) os.chmod(app.config['UP_DIR'], "rw") # 更改文件名 file_url = changeFileName(file_url) file_logo = changeFileName(file_logo) # 保存文件 form.url.data.save(app.config['UP_DIR'] + file_url) form.logo.data.save(app.config['UP_DIR'] + file_logo) movie = Movie( title = data['title'], url = file_url, info = data["info"], logo = file_logo, star = int(data["star"]), playnum = 0, commentnum = 0, tag_id = int(data['tag_id']), # 给到视图的是v.id ,和表单模型的choices属性有关 area = data['area'], release_time = data['release'], length = data["length"] ) db.session.add(movie) db.session.commit() flash("添加电影成功!", "ok") return render_template('admin/movieadd.html', form=form)
- 视图渲染表单,先根据数据库模型定义表单模型
- 这里要上传文件,需要在
__init__
配置了,并做处理app.config['UP_DIR'] = os.path.join(os.path.abspath(__file__),"/static/uploads/") # 以当前文件为参考,拼接上传文件的保存路径
- 视图中定义函数,改变上传文件的名称
from werkzeug.utils import secure_filename import os from datetime import datetime def changeFileName(filename): ''' 修改传入文件的名称 :return: ''' file = os.path.splitext(filename) # 将名称和后缀分开 filename = datetime.now().strftime("%Y%m%d%H%M%S")+str(uuid.uuid4().hex)+str(file[-1]) return filename
- 视频图片都是直接存在服务器文件夹,数据库中存储的都是路径
- 这里注意个问题
# 保存文件时使用save方法 form.url.data.save(app.config['UP_DIR'] + file_url) # 是加号,不是逗号 # app中添加配置时 app.config['UP_DIR'] = os.path.join(os.path.abspath(os.path.dirname(__file__)),"static/uploads/") # uploads后面这个 / 必须写 # 情况当前表中数据 delete from movie; # truncate 有外键约束
- 电影列表
- 更新菜单栏链接,加上page=1参数
- 流程还是一样的,视图(查询分页数据)——前端——视图(返回请求页码);这里视图函数只需支持GET方法
@admin.route('/movie/list/<int:page>', methods=['GET']) @admin_login def movie_list(page=None): if page==None: page = 1 # 一个个电影挨着查 page_data = Movie.query.join(Tag).filter( Tag.id == Movie.tag_id # 多表关联时使用 movie为多端 这里就是查到tag表对应的整条记录,使用时用v.tag.name ).order_by( Movie.addtime.desc() ).paginate(page=page, per_page=10) return render_template('admin/movielist.html', page_data=page_data)
- 电影删除,常规操作
@admin.route('/movie/del/<int:id>', methods=['GET']) @admin_login def movie_del(id): movie = Movie.query.get_or_404(int(id)) # 如果没有直接跳到404 db.session.delete(movie) # 评论等数据和movie关联,movie是主表,所以会连带一起删除 db.session.commit() flash("电影删除成功!", "ok") # 小写 ok return redirect(url_for("admin.movie_list", page=1)) # 重定向,需要模板渲染有add edit 和 list
- 需要模板渲染有add edit 和 list,删除只需要重定向
- 电影编辑:参数—视图—前端——视图(保存)
# 编辑电影 @admin.route('/movie/edit/<int:id>', methods=['GET', "POST"]) @admin_login def movie_edit(id): form = MovieForm() form.url.validators = [] # print(form.url.validators) form.logo.validators = [] # 跳过未选择文件的验证 # 问题:required字段搞不掉?打印发现这么写没问题,解决方案: # render_kw = { # "required": False # } # 但是这样会在添加的时候不提示选择文件 # 更好的方案是就该edit.html {{form.logo(required=False)}} movie = Movie.query.get_or_404(int(id)) # 如果没有直接跳到404 # 处理movie某些原信息不显示的问题,通过请求方法区分 if request.method=="GET": form.info.data = movie.info form.star.data = movie.star form.tag_id.data = movie.tag_id # 模板中就不需要 value=movie.info if form.validate_on_submit(): # POST data = form.data m = Movie.query.filter_by(title=data['title']).count() if m==1: flash("电影已存在","err") return redirect(url_for("admin.movie_edit", id=id)) movie.title = data["title"] movie.info = data["info"] movie.star = data["star"] movie.tag_id = data["tag_id"] movie.area = data["area"] movie.length = data["length"] movie.release_time = data["release"] # name属性 # 文件重传 if not os.path.exists(app.config["UP_DIR"]): os.mkdir(app.config["UP_DIR"]) os.chmod(app.config["UP_DIR"], "rw") if form.url.data.filename != "": # 问选择文件就是空,现在有值说明重传了(跟显示原文件无关) file_url = secure_filename(form.url.data.filename) movie.url = changeFileName(file_url) # 更新原数据信息 form.url.data.save(app.config["UP_DIR"] + movie.url) if form.logo.data.filename != "": file_logo = secure_filename(form.logo.data.filename) movie.logo = changeFileName(file_logo) form.logo.data.save(app.config["UP_DIR"] + movie.logo) db.session.add(movie) # 修改 db.session.commit() flash("修改成功", "pk") return redirect(url_for('admin.movie_list', page=1)) return render_template("admin/movieedit.html", form=form, movie=movie) # 渲染出原信息
电影预告管理
- 添加和列表,查数据库发现表单模型字段就两个
-
视图—前端—视图;视图—分页(后台数据—前台渲染);参数传递;文件保存
# 预告的添加和列表 @admin.route('/preview/add', methods=['GET', 'POST']) @admin_login def preview_add(): '''和电影添加一样的过程''' form = PreviewForm() if form.validate_on_submit(): # 过滤 file_logo = secure_filename(form.logo.data.filename) # 准备存储路径 if not os.path.exists(app.config["UP_DIR"]): os.mkdir(app.config["UP_DIR"]) os.chmod(app.config["UP_DIR"], "rw") # 更改文件名 file_logo = changeFileName(file_logo) # 保存文件 form.logo.data.save(app.config["UP_DIR"] + file_logo) data = form.data # 准备一条数据入库 preview = Preview( title = data["title"], logo = file_logo # 存名字即可 ) db.session.add(preview) db.session.commit() flash("添加预告成功!", "ok") return redirect(url_for('admin.preview_add')) return render_template('admin/previewadd.html', form=form) @admin.route('/preview/list/<int:page>') @admin_login def preview_list(page=None): if page==None: page = 1 page_data = Preview.query.order_by( Preview.addtime.desc() ).paginate(page=page, per_page=10) # 视图部分提供数据,字典形式;前台部分使用macro渲染 return render_template('admin/previewlist.html', page_data=page_data) @admin.route('/preview/del/<int:id>') @admin_login def preview_del(id=None): preview = Preview.query.get_or_404(int(id)) # 如果没有直接跳到404 db.session.delete(preview) # 评论等数据和movie关联,movie是主表,所以会连带一起删除 db.session.commit() flash("预告删除成功!", "ok") # 小写 ok return redirect(url_for("admin.preview_list", page=1)) # 重定向,模板渲染有add edit 和 list @admin.route('/preview/edit/<int:id>', methods=['GET', 'POST']) @admin_login def preview_edit(id=None): form = PreviewForm() form.logo.validators = [] preview = Preview.query.get_or_404(int(id)) # 如果没有直接跳到404 if form.validate_on_submit(): # POST data = form.data m = Preview.query.filter_by(title=data['title']).count() if m == 1 and form.logo.data.filename == "": flash("预告已存在", "err") return redirect(url_for("admin.preview_edit", id=id)) preview.title = data["title"] # 准备路径 if not os.path.exists(app.config["UP_DIR"]): os.mkdir(app.config["UP_DIR"]) os.chmod(app.config["UP_DIR"], "rw") if form.logo.data.filename != "": file_logo = secure_filename(form.logo.data.filename) preview.logo = changeFileName(file_logo) form.logo.data.save(app.config["UP_DIR"] + preview.logo) db.session.add(preview) # 修改 db.session.commit() flash("修改成功", "ok") return redirect(url_for('admin.preview_list', page=1)) return render_template("admin/previewedit.html", form=form, preview=preview) # 渲染出原信息
会员管理
- 不知为啥启动网页时连接数据库出问题,需要
pip install cryptography
pip install -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com cryptography
- 向user表中插入一些准备好的数据
-- uuid需要系统生成 insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('鼠','1231','1231@123.com','13888888881','鼠','1f401.png','d32a72bdac524478b7e4f6dfc8394fc0',now()); insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('牛','1232','1232@123.com','13888888882','牛','1f402.png','d32a72bdac524478b7e4f6dfc8394fc1',now()); insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('虎','1233','1233@123.com','13888888883','虎','1f405.png','d32a72bdac524478b7e4f6dfc8394fc2',now()); insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('兔','1234','1234@123.com','13888888884','兔','1f407.png','d32a72bdac524478b7e4f6dfc8394fc3',now()); insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('龙','1235','1235@123.com','13888888885','龙','1f409.png','d32a72bdac524478b7e4f6dfc8394fc4',now()); insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('蛇','1236','1236@123.com','13888888886','蛇','1f40d.png','d32a72bdac524478b7e4f6dfc8394fc5',now()); insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('马','1237','1237@123.com','13888888887','马','1f434.png','d32a72bdac524478b7e4f6dfc8394fc6',now()); insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('羊','1238','1238@123.com','13888888888','羊','1f411.png','d32a72bdac524478b7e4f6dfc8394fc7',now()); insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('猴','1239','1239@123.com','13888888889','猴','1f412.png','d32a72bdac524478b7e4f6dfc8394fc8',now()); insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('鸡','1240','1240@123.com','13888888891','鸡','1f413.png','d32a72bdac524478b7e4f6dfc8394fc9',now()); insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('狗','1241','1241@123.com','13888888892','狗','1f415.png','d32a72bdac524478b7e4f6dfc8394fd0',now()); insert into user(name,pwd,email,phone,info,face,uuid,addtime) values('猪','1242','1242@123.com','13888888893','猪','1f416.png','d32a72bdac524478b7e4f6dfc8394fd1',now());
- 图片也是准备好的,复制到uplaods下
- 这里添加user是前端触发的,属于home的逻辑;需要编写list/edit/del操作
# 分页:后台准备数据,前台macro渲染 # 查询参数可有可无,定义 /<int:page> 后必须每次都传递page #会员列表 @admin.route('/user/list/<int:page>', methods=['GET']) @admin_login def user_list(page=None): if page is None: page = 1 page_data = User.query.order_by( User.addtime.desc() ).paginate(page=page, per_page=4) return render_template('admin/userlist.html', page_data=page_data) # 会员详细信息查看 @admin.route('/user/view/<int:id>') @admin_login def user_view(id): user = User.query.get_or_404(int(id)) return render_template('admin/userview.html', user=user) @admin.route('/user/del/<int:id>', methods=['GET']) @admin_login def user_del(id): user = User.query.get_or_404(int(id)) # 如果没有直接跳到404 db.session.delete(user) # 评论等数据和movie关联,movie是主表,所以会连带一起删除 db.session.commit() flash("会员删除成功!", "ok") # 小写 ok return redirect(url_for("admin.user_list", page=1)) # 重定向,模板渲染有add edit 和 list
- 状态(正常/冻结)先不设置
评论管理
- 同样的,初始化一些数据
-- 清空表后复位自增起点
ALTER TABLE comment auto_increment=1;
-- comment表外键约束movie表,user表
insert into comment(movie_id,user_id,content,addtime) values(7,1,"好看",now());
insert into comment(movie_id,user_id,content,addtime) values(7,2,"不错",now());
insert into comment(movie_id,user_id,content,addtime) values(7,3,"经典",now());
insert into comment(movie_id,user_id,content,addtime) values(7,4,"给力",now());
insert into comment(movie_id,user_id,content,addtime) values(8,5,"难看",now());
insert into comment(movie_id,user_id,content,addtime) values(8,6,"无聊",now());
insert into comment(movie_id,user_id,content,addtime) values(8,7,"乏味",now());
insert into comment(movie_id,user_id,content,addtime) values(8,8,"无感",now());
- 注意要关联查询,包括评论列表和删除
# 评论列表 @admin.route('/comment/list/<int:page>') @admin_login def comment_list(page=None): if page==None: page=1 page_data = Comment.query.join( Movie ).join( User ).filter( Movie.id == Comment.movie_id, User.id == Comment.user_id ).order_by( Comment.addtime.desc() ).paginate(page=page, per_page=5) # {{v.movie.title}} return render_template('admin/commentlist.html', page_data=page_data) @admin.route('/comment/del/<int:id>', methods=['GET']) @admin_login def comment_del(id): comment = Comment.query.get_or_404(int(id)) # 如果没有直接跳到404 db.session.delete(comment) # 评论等数据和movie关联,movie是主表,所以会连带一起删除 db.session.commit() flash("评论删除成功!", "ok") # 小写 ok return redirect(url_for("admin.comment_list", page=1))
电影收藏
- 之前疏忽数据库多加了一个字段:
alter table movcollec drop content;
- 电影收藏列表,删除收藏
# 收藏列表 @admin.route('/collect/list/<int:page>') @admin_login def collect_list(page=None): if page==None: page=1 page_data = MovCollection.query.join( Movie ).join( User ).filter( Movie.id == MovCollection.movie_id, User.id == MovCollection.user_id ).order_by( MovCollection.addtime.desc() ).paginate(page=page, per_page=8) return render_template('admin/collectlist.html', page_data=page_data) @admin.route('/collect/del/<int:id>', methods=['GET']) @admin_login def collect_del(id): collect = MovCollection.query.get_or_404(int(id)) # 如果没有直接跳到404 db.session.delete(collect) db.session.commit() flash("收藏删除成功!", "ok") return redirect(url_for("admin.collect_list", page=1))
管理员密码修改
- 提交新密码,终于要定义新表单模型了,这里也要写验证旧密码的方法
@admin.route('/cpwd', methods=['GET', 'POST']) @admin_login def cpwd(): form = PwdForm() if form.validate_on_submit(): data = form.data # 这里需要传递命名参数,对应字段名;否则会参数异常 admin = Admin.query.filter_by(name=session['admin']).first() from werkzeug.security import generate_password_hash admin.pwd = generate_password_hash(data['new_pwd']) db.session.add(admin) db.session.commit() flash("修改密码成功!重新登录",'ok') return redirect(url_for('admin.logout')) return render_template('admin/pwd.html', form=form)
- 密码要经过加密后存储,user中密码的需要在home的后台加密处理
- 显示管理员上线时间
@admin.context_processor
def tpl_extra():
'''
上下文处理器
:return:
'''
# 创建全局变量,直接在admin.html渲染
data = dict(
online_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)
日志管理
- 根据具体的日志需求,处理逻辑不同
操作日志
- 这部分需要在各种操作时产生数据,例如添加标签日志,添加电影日志等等
# 例如在添加标签时: def tag_add(): form = TagForm() if form.validate_on_submit(): # 没过验证 data = form.data tag = Tag.query.filter_by(name=data['name']).count() if tag == 1: flash("标签已存在!", "err") return redirect(url_for('admin.tag_add')) tag = Tag( name= data['name'] ) db.session.add(tag) db.session.commit() flash("标签添加成功", 'ok') # 第二个参数是固定的一些值,应用到模板 category_filter=["ok"] # 添加操作日志--------------------- oplog = OperateLog( admin_id=session['admin_id'], ip=request.remote_addr, reason="添加标签 %s" % data['name'] ) db.session.add(oplog) db.session.commit() return render_template('admin/tagadd.html', form=form)
- 后续部分需要自己完善,在各种操作后记录操作日志
- 有了操作日志,当然要展示了,登录的管理员目前能看到所有的操作日志
# 操作日志 @admin.route('/oplog/list/<int:page>') @admin_login def oplog_list(page=None): if page==None: page=1 page_data = OperateLog.query.join( Admin ).filter( Admin.id == OperateLog.admin_id ).order_by( OperateLog.addtime.desc() ).paginate(page=page,per_page=3) return render_template('admin/oploglist.html', page_data=page_data)
管理员登录日志
- 这部分需要在登录的时候产生数据
def login(): xxx # 添加登录日志----------------- admin = AdminLog( admin_id = admin.id, ip=request.remote_addr ) db.session.add(admin) db.session.commit() xxx
- list视图逻辑
# 管理员登录日志 @admin.route('/adminlog/list/<int:page>') @admin_login def adminlog_list(page=None): if page==None: page=1 page_data = AdminLog.query.join( Admin ).filter( Admin.id == AdminLog.admin_id ).order_by( AdminLog.addtime.desc() ).paginate(page=page,per_page=2) return render_template('admin/adminloglist.html', page_data=page_data)
- 前端分页渲染即可
会员登录日志
- 因为这部分属于home前后台管理,只能直接初始化几条数据
insert into userlog(user_id,ip,addtime) values(1,"192.168.4.1",now()); insert into userlog(user_id,ip,addtime) values(2,"192.168.4.2",now()); insert into userlog(user_id,ip,addtime) values(3,"192.168.4.3",now()); insert into userlog(user_id,ip,addtime) values(4,"192.168.4.4",now()); insert into userlog(user_id,ip,addtime) values(5,"192.168.4.5",now()); insert into userlog(user_id,ip,addtime) values(6,"192.168.4.6",now()); insert into userlog(user_id,ip,addtime) values(7,"192.168.4.7",now()); insert into userlog(user_id,ip,addtime) values(8,"192.168.4.8",now()); insert into userlog(user_id,ip,addtime) values(9,"192.168.4.9",now());
- 视图逻辑
# 会员登录日志 @admin.route('/userlog/list/<int:page>') @admin_login def userlog_list(page=None): if page==None: page=1 page_data = UserLog.query.join( User ).filter( User.id == UserLog.user_id ).order_by( UserLog.addtime.desc() ).paginate(page=page,per_page=4) return render_template('admin/userloglist.html', page_data=page_data)
- 小结
- admin前后台的管理员、会员、电影、标签、日志等逻辑定义完成
*下一节主要是基于角色的访问控制
- admin前后台的管理员、会员、电影、标签、日志等逻辑定义完成