flask视频网站(后台管理)

文章目录

简介

  • 这一部分要实现具体的后台管理逻辑
  • 基本逻辑如下:
    flask视频网站(后台管理)

管理员登录

  • 将之前models中数据库的认证部分移动到app初始化文件中
  • 这一节的大部分内容都是参考前端页面进行的,这也是为什么上一节先搭建页面flask视频网站(后台管理)
  • 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标签
  • 既然有了验证,如何在模板提示验证时的错误信息?在每个表单字段下修改:
    {% 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">
    
    flask视频网站(后台管理)
  • 然后在标签列表中分页显示并处理编辑删除操作
    • 分页显示在路由中用到正则匹配:<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 %}
    
  • 表单模型不修改,统一叫编辑(添加/修改)
    flask视频网站(后台管理)
    flask视频网站(后台管理)

电影管理

  • 围绕着“参数传递+视前视”的核心处理思路,开始后台电影管理
  • 添加电影
    • 视图渲染表单,先根据数据库模型定义表单模型
      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前后台的管理员、会员、电影、标签、日志等逻辑定义完成
      *下一节主要是基于角色的访问控制
上一篇:搭建自动化测试平台Snail_Autotest_Platfrom(4)——用户设计


下一篇:仿照elementui自写表单验证