文章目录
- 简介
- 管理员登录
- 标签管理
- 电影管理
- 电影预告管理
- 会员管理
- 评论管理
- 电影收藏
- 管理员密码修改
- 日志管理
- 操作日志
- 管理员登录日志
- 会员登录日志
- 小结
简介
- 这一部分要实现具体的后台管理逻辑
- 基本逻辑如下:
管理员登录
- 将之前
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, ValidationErrorclass 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.dataadmin = 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.dataadmin = 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_hashreturn 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.dataadmin = 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 Nonereturn 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) # 清除sessionreturn 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.datatag = 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 = 1page_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'] andflash("名称已存在", '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.idrender_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 datetimedef 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)) # 如果没有直接跳到404db.session.delete(movie) # 评论等数据和movie关联,movie是主表,所以会连带一起删除db.session.commit()flash("电影删除成功!", "ok") # 小写 okreturn 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.infoform.star.data = movie.starform.tag_id.data = movie.tag_id # 模板中就不需要 value=movie.infoif form.validate_on_submit(): # POSTdata = form.datam = 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 = 1page_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)) # 如果没有直接跳到404db.session.delete(preview) # 评论等数据和movie关联,movie是主表,所以会连带一起删除db.session.commit()flash("预告删除成功!", "ok") # 小写 okreturn 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)) # 如果没有直接跳到404if form.validate_on_submit(): # POSTdata = form.datam = 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 = 1page_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)) # 如果没有直接跳到404db.session.delete(user) # 评论等数据和movie关联,movie是主表,所以会连带一起删除db.session.commit()flash("会员删除成功!", "ok") # 小写 okreturn 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=1page_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)) # 如果没有直接跳到404db.session.delete(comment) # 评论等数据和movie关联,movie是主表,所以会连带一起删除db.session.commit()flash("评论删除成功!", "ok") # 小写 okreturn 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=1page_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)) # 如果没有直接跳到404db.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_hashadmin.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.datatag = 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=1page_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=1page_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=1page_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前后台的管理员、会员、电影、标签、日志等逻辑定义完成
- 主要分两类逻辑:查看(分页)和编辑(增删改)
- 至于细节部分:表单模型——视图——前端——视图,或者其他吧
- 下一节主要是基于角色的访问控制