返回介绍

Flask 的扩展

发布于 2025-04-20 18:52:14 字数 28265 浏览 0 评论 0 收藏

Flask 扩展的生态非常繁荣,本节介绍其中最常用的扩展。

Flask-Script

Django 提供了如下管理命令:

python manage.py startapp
python manage.py runserver

Flask 也可以通过 Flask-Script 添加运行服务器、设置数据库、定制 shell 等功能的命令。

我们先安装它:

> pip install flask-script

借用之前的文件托管服务项目的代码:

> cp chapter3/section5/*.py chapter4/section2

现在使用 Flask-Script 管理服务(manage.py):

from flask_script import Manager, Server, Shell, prompt_bool

from app import app, db, PasteFile

manager=Manager(app)


def make_shell_context():
    return{
        'db':db,
        'PasteFile':PasteFile,
        'app':app
    }


@manager.command
def dropdb():
    if prompt_bool(
            'Are you sure you want to lose all your data'):
    db.drop_all()


@manager.option('-h', '--filehash', dest='filehash')
def get_file(filehash):
    paste_file=PasteFile.query.filter_by(filehash=filehash).first()
    if not paste_file:
        print 'Not exists'
    else:
        print 'URL is{}'.format(paste_file.get_url('i'))


manager.add_command('shell', Shell(make_context=make_shell_context))
manager.add_command('runserver', Server(
    use_debugger=True, use_reloader=True,
    host='0.0.0.0', port=9000)
)


if__name__=='__main__':
    manager.run()

现在就可以使用如下命令来管理了:

> cd chapter4/section2
> python manage.py get_file-h ec12e434b48648f0a65ac28a28759ba5.jpg #在终端通过
    filehash 获取文件路径
URL is http://localhost:9000/i/ec12e434b48648f0a65ac28a28759ba5.jpg
> python manage.py get_file-h not_exists_file
Not exists
> python manage.py dropdb #在测试环境里可以用来清理数据
Are you sure you want to lose all your data [n]:n
> python manage.py shell #自带了三个内置变量的 shell
In:db
Out:<SQLAlchemy engine='mysql://web:web@localhost:3306/r'>
In:PasteFile
Out:app.PasteFile
> python manage.py runserver #通过 manage.py 启动服务
*Running on http://0.0.0.0:9000/(Press CTRL+C to quit)
*Restarting with stat

在很多地方你可能都会看到使用“fromflask.ext.package import X”的用法,但这种用法已经受到官方反对(http://bit.ly/2aradPt ),要采用“fromflask_package import X”的方式。

如果你已经使用 Flask 0.11,可以考虑使用 Flask 自带的命令行接口替代它。

Flask-DebugToolbar

Django 有非常知名的 Django-DebugToolbar,而 Flask 也有对应的替代工具 Flask-DebugToolbar。

它会在浏览器上添加右边栏,可以快速查看环境变量、上下文内容,方便调试。

我们先安装它:

> pip install flask-debugtoolbar

它的使用也很简单,但是注意 debug 要为 True 才可以看到调试边栏(app_debug.py):

from flask import Flask
from flask_debugtoolbar import DebugToolbarExtension

app=Flask(__name__)

app.debug=True
app.config['SECRET_KEY']='a secret key'

toolbar=DebugToolbarExtension(app)


@app.route('/')
def hello():
    return '<body></body>' #模板需要至少有 body 元素

 
if__name__=='__main__':
    app.run(host='0.0.0.0', port=9000, debug=app.debug)

Flask-DebugToolbar 内置了很多面板,如表 4.1 所示。

表 4.1 Flask-DebugToolbar 内置的面板及功能

面板功能
Versions列出安装的包的版本
Time显示处理当前请求花费的时间的信息
HTTP Headers显示当前请求的 HTTP 头信息
Request Vars显示当前请求带的变量,包含请求参数、cookie 信息等
Config显示 app.config 的变量值
Templates显示模板请求参数信息
SQLAlchemy显示当前请求下的 SQL。需要设置 SQLALCHEMY_RECORD_QUERIES 为 True
Logging显示请求过程中的日志信息
Route List列出 Flask 的路由规则
Profiler对当前请求添加性能分析。默认是关闭的,需要点击红色的钩,让它变成绿色

Flask-Migrate

使用关系型数据库时,修改数据库模型和更新数据库这样的工作时有发生,而且很重要。怎么做到既安全又方便呢?

SQLAlchemy 作者为此开发了迁移框架 Alembic,Flask-Migrate 就是基于 Alembic 做了轻量级封装,并集成到 Flask-Script 中。所有操作都通过 Flask-Script 命令完成。它能跟踪数据库结构的变化,把变化的部分应用到数据库中。

我们先安装它:

> pip install Flask-Migrate

定义一个 User 类(users.py):

from ext import db


class User(db.Model):
    __tablename__='login_users'
    id=db.Column(db.Integer, primary_key=True)
    name=db.Column(db.String(128), nullable=False)
    login_count=db.Column(db.Integer, default=0)
    last_login_ip=db.Column(db.String(128), default='unknown')

现在对文件托管服务添加迁移支持(app_migrate.py):

 from flask import Flask
 from flask_script import Manager
 from flask_migrate import Migrate, MigrateCommand

 from ext import db

 app=Flask(__name__)
 app.config.from_object('config')

 db.init_app(app)
 migrate=Migrate(app, db)
 manager=Manager(app)
 manager.add_command('db', MigrateCommand)


 if__name__=='__main__':
     manager.run()

我们现在想要扩充两个字段 email 和 password,流程如下。

1.初始化迁移工作:

> python chapter4/section2/app_migrate.py db init

这会在当前目录下增加一个 migrations 目录,这个目录应该放进版本库。

2.修改表结构,给 User 类添加 email 和 password 这两个字段:

email=db.Column(db.String(128), nullable=False)
password=db.Column(db.String(256), nullable=False)

3.创建迁移的脚本:

> python chapter4/section2/app_migrate.py db migrate
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added column 'users.email'
INFO [alembic.autogenerate.compare] Detected added column 'users.password'
INFO [alembic.autogenerate.compare] Detected NULL on column 'users.login_count'
    Generating/home/ubuntu/web_develop/migrations/versions/1388a7cf600a_.py ...
        done

这会在 migrations/versions/目录下添加一个执行的脚本,文件名就是版本号。版本的对应关系在当前库的 alembic_version 表中。需要注意的是,假如你的数据库里还有其他的表没有放到迁移脚本中,就会被删掉,所以 app_migrate.py 这样的管理脚本应该覆盖所有重要的表,而所有模型文件都使用“from ext import db”,就可以保证这一点。

这一步并没有实际操作数据库,所以一定要注意终端输出,确定和自己的预想一样。

4.更新数据库:

> python chapter4/section2/app_migrate.py db upgrade
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade->f146d724a693, empty
     message

这样就更新数据库了。

验证一下对数据库结构的修改:

mysql>select*from login_users;

可以看到这两个字段已经出现了。是不是非常方便和安全呢?如果发现有问题也可以很容易地取消更新:

> python chapter4/section2/app_migrate.py db downgrade
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running downgrade 68260ed097cc->, empty message

再来验证一下:

mysql>select*from login_users;

上例中存储 IP 时使用了字符串,这只是为了展示起来更直观,更好的方法是把 IP 转换为整数。

In:import struct
In:import socket
In:def ip2int(addr):
...:    return struct.unpack("!I", socket.inet_aton(addr))[0]
...:
In:def int2ip(addr):
...:    return socket.inet_ntoa(struct.pack("!I", addr))
...:
In:ip2int('10.0.2.2')
Out:167772674

Flask-WTF

Flask-WTF 是一个集成 WTForms 的表单验证和渲染的扩展。我们先安装它;

> pip install Flask-WTF

通过实现一个注册功能的应用来了解它(app_wtf.py)。注册表单的代码如下:

from flask_wtf import Form
from wtforms import TextField, PasswordField
from wtforms.validators import length, Required, EqualTo


class RegistrationForm(Form):
    name=TextField('Username', [length(min=4, max=25)])
    email=TextField('Email Address', [length(min=6, max=35)])
    password=PasswordField('New Password', [
        Required(), EqualTo('confirm', message='Passwords must match')
    ])
    confirm=PasswordField('Repeat Password')

应用代码如下:

from flask import Flask, render_template, request
from flask_wtf.csrf import CsrfProtect

from ext import db
from users import User

app=Flask(__name__, template_folder='../../templates')
app.config.from_object('config')
CsrfProtect(app)
db.init_app(app)


@app.route('/register', methods=['GET', 'POST']) 
def register():
    form=RegistrationForm(request.form)
    if request.method=='POST' and form.validate():
        user=User(name=form.name.data, email=form.email.data,
                  password=form.password.data)
        db.session.add(user)
        db.session.commit()
        return 'register successed!'
return render_template('chapter4/section2/register.html', form=form)


if__name__=='__main__':
    app.run(host='0.0.0.0', port=9000, debug=app.debug)

表单模板(register.html)使用默认的 Jinja2 语法:

> cat templates/chapter4/section2/register.html
{%macro render_field(field)%}
    <div class="form-group">
        <div class="form-label">{{field.label}}</div>
        <div class="form-body">
            {{field(**kwargs)|safe}}
        </div>
        
        {%if field.errors%}
        <ul class="warning">
           {%for error in field.errors%}
           <li>{{error}}</li>
           {%endfor%}
        </ul>
        {%endif%}
    </div>
{%endmacro%}

<form method=post action='/register'>
    {{form.csrf_token}}
    <dl>
        {{render_field(form.name)}}
        {{render_field(form.email)}}
        {{render_field(form.password)}}
        {{render_field(form.confirm)}}
    </dl>
    <p><input type=submit value=Register>
</form>

这样就完成了一个带有 CSRF 保护的注册表单的功能。

Flask-Security

Flask-Security 非常强大,它提供角色管理、权限管理、用户登录、邮箱验证、密码重置、密码加密、跟踪用户登录状态等功能。先安装它:

> pip install flask-security

Flask-Security 提供了 7 种基本模板。如果想要定制模板,可以在应用的模板目录下创建名为 security 的目录,添加对应名字的模板。这 7 种模板类型和用途如表 4.2 所示。

表 4.2 7 种基本模板和用途

模板功能
security/forgot_password.html忘记密码
security/login_user.html用户登录
security/register_user.html用户注册
security/reset_password.html重置密码
security/change_password.html改变密码
security/send_confirmation.html发送确认信息
security/send_login.html无密码方式

可以指定对应的变量替换模板,下面的例子中将使用其他的登录模板,也就是要指定变量 SECURITY_LOGIN_USER_TEMPLATE。

Flask-Security 还提供了 8 种上下文处理的装饰器,类似于钩子。这 8 种处理器类型如表 4.3 所示。

表 4.3 8 种处理器及其功能

处理器功能
context_processor所有视图都会触发
forgot_password_context_processor忘记密码时视图就会触发
login_context_processor登录时视图会触发
register_context_processor注册时视图会触发
reset_password_context_processor重置密码时视图会触发
change_password_context_processor改变密码时视图会触发
send_confirmation_context_processor发送确认信息时视图会触发
send_login_context_processor无密码方式登录时视图会触发

Flask-Security 也提供了 8 种表单,表单作用如表 4.4 所示。

表 4.4 8 种表单及其用途

表单用途
login_form登录
confirm_register_form注册确认
register_form注册
forgot_password_form忘记密码
reset_password_form重置密码
change_password_form改变密码
send_confirmation_form发送确认信息
passwordless_login_form无密码登录

在看例子之前,先了解一个新的概念:“角色”。角色定义了用户的类型。如果一个站点功能很少,只需要普通用户和管理员两种权限类型就可以了。但随着业务的扩展,用户具有的特殊的权限类型越来越多,对于权限管理而言,不能为每个人都授予管理员这样的角色,这个时候就需要实现多种类型的角色权限,不同的角色甚至可以具备多种角色权限。

我们使用位运算做权限控制。位运算在 Linux 文件系统上就有体现,一个用户对文件或目录所拥有的权限分为三种:“可读(1)”、“可写(2)”和“可执行(4)”,它们之间可以任意组合:有可读和可写权限就用 3 来表示(1+2=3);有可读和可执行权限就用 5 来表示(1+4=5),三种权限全部拥有就用 7 表示(1+2+4=7)。为什么选择 1、2、4 这样的有规律的数据呢?先看看下面的例子:

In:int('00000001', 2)
Out:1
In:int('00000010', 2)
Out:2
In:int('00000100', 2)
Out:4

通过标志位判断是否有权限,如果有权限,对应位就置 1,比如三种权限都有,就是:

In:int('00000111', 2)
Out:7

它其实就是对三个二进制位做按位或计算得到的:

In:int('00000100', 2)|int('00000010', 2)|int('00000001', 2)
Out:7

判断权限的时候就把这个值(假设叫作 A)和要判断的权限(叫作 B)做按位与计算,这样就把它们中都为 1 的标志位置为 1,如果结果还等于 B,就说明它有这样的权限,否则说明对应的标志位没有置 1,没有权限:

In:int('00000111', 2)&int('00000001', 2)==int('00000001', 2)
Out:True

In:int('00000100', 2)&int('00000001', 2)==int('00000001', 2)
Out:False

在这里,权限定义如下:

class Permission(object):
    LOGIN=0x01
    EDITOR=0x02
    OPERATOR=0x04
    ADMINISTER=0xff
    PERMISSION_MAP={
        LOGIN:('login', 'Login user'),
        EDITOR:('editor', 'Editor'),
        OPERATOR:('op', 'Operator'),
        ADMINISTER:('admin', 'Super administrator')
    }

其中 ADMINISTER 拥有全部权限,使用 0xff 这个最大值。

Flask-Security 支持 SQLAlchemy、MongoEngine 和 Peewee 定义模型。现在基于 Flask-SQLAlchemy 和之前的文件托管服务来实现一个简单的应用(app_security.py)。

先定义角色模型:

from flask_security import RoleMixin


class Role(db.Model, RoleMixin):
    id=db.Column(db.Integer, primary_key=True)
    name=db.Column(db.String(255), unique=True)
    permissions=db.Column(db.Integer, default=Permission.LOGIN)
    description=db.Column(db.String(255))

给 User 这个模型添加 roles 字段,指向模型 Role,并且添加判断权限的方法:

from functools import reduce
from operator import or_

from flask_security import UserMixin


class User(db.Model, UserMixin):
    id=db.Column(db.Integer, primary_key=True)
    email=db.Column(db.String(255), unique=True)
    password=db.Column(db.String(255))

active=db.Column(db.Boolean())
confirmed_at=db.Column(db.DateTime())
roles=db.relationship('Role', secondary=roles_users,
                      backref=db.backref('users', lazy='dynamic'))

def can(self, permissions):
    if self.roles is None:
        return False
    all_perms=reduce(or_, map(lambda x:x.permissions, self.roles))
    return all_perms&permissions==permissions

def can_admin(self):
    return self.can(Permission.ADMINISTER)

用户和角色是多对多的关系,需要定义一个用于关系的辅助表。对于这个辅助表,强烈建议不使用模型,而是采用一个实际的表:

roles_users=db.Table(
    'roles_users',
    db.Column('user_id', db.Integer,
              db.ForeignKey('user.id')),
    db.Column('role_id', db.Integer, db.ForeignKey('role.id')))

上面的 db.relationship 还使用了 backref,它表示反向引用。举个例子:

    In:r=Role()
    In:r.name='role_1'
    In:r.permissions=Permission.LOGIN
    In:r.description='test role'

    In:u=User()
    In:u.email='1@qq.com'
    In:u.password='123'
    In:u.confirmed_at=datetime.now()
    In:u.roles=[r]

    In:db.session.add(r)
    In:db.session.add(u)
    In:r.users
    Out:<sqlalchemy.orm.dynamic.AppenderBaseQuery at 0x7f6548084dd0>
    In:db.create_all()
    In:r.users.all()
    Out:[<__main__.User at 0x7f65480c2e90>]

Role 对象通过 users 就能反向获取有对应权限的用户列表了。

接着添加 login_context_processor 钩子:

user_datastore=SQLAlchemyUserDatastore(db, User, Role)
security=Security(app, user_datastore, register_form=LoginForm)

@security.login_context_processor
def security_login_processor():
    print 'Login'
    return{}

这个例子中,我们通过添加 before_first_request 钩子实现初始化:

@app.before_first_request
def create_user():
    db.drop_all()
    db.create_all()

    for permissions, (name, desc) in Permission.PERMISSION_MAP.items():
        user_datastore.find_or_create_role(
            name=name, description=desc, permissions=permissions)
    for email, passwd, permissions in (
            ('dongwm@dongwm.com', '123', (
        Permission.LOGIN, Permission.EDITOR)),
            ('admin@dongwm.com', 'admin', (Permission.ADMINISTER,))):
        user_datastore.create_user(email=email, password=passwd)
        for permission in permissions:
            user_datastore.add_role_to_user(
                email, Permission.PERMISSION_MAP[permission][0])
    db.session.commit()

每次在第一次接收请求的时候就会删除相关表,再重新创建这些表,并创建两个用户,用户权限分别如下。

  • dongwm@dongwm.com:它具有 LOGIN 与 EDITOR 两种权限,但有些页面访问不了。
  • admin@dongwm.com:管理员,拥有全部的权限。

然后添加验证访问权限的装饰器:

def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def_deco(*args,**kwargs):
            if not current_user.can(permission):
                abort(403)
        return f(*args,**kwargs)
    return_deco
    return decorator


def admin_required(f):
    return permission_required(Permission.ADMINISTER)(f)

current_user 就是一个 User 对象,通过 User 类添加的 can 方法判断权限。

最后看一下给视图添加权限控制的方法:

@app.route('/')
@login_required
@permission_required(Permission.LOGIN)
def index():
    return 'Login in'


@app.route('/admin/')
@login_required
@admin_required
def admin():
    return 'Only administrators can see this!'

上例使用了自定义的登录模板和自定义的处理器,当然还可以自定义表单。通过 dongwm@dongwm.com 登录后是看不到/admin/页面的,而用 admin@dongwm.com 就可以看到全部页面。

自定义的登录模板(login_user.html)内容如下:

{%from"security/_macros.html"import render_field_with_errors, render_field%}
{%include"security/_messages.html"%}
<h1>Custom Login</h1>
<form action="{{url_for_security('login')}}"method="POST"name="login_user_form">
    {{login_user_form.hidden_tag()}}
    {{render_field_with_errors(login_user_form.email)}}
    {{render_field_with_errors(login_user_form.password)}}
    {{render_field_with_errors(login_user_form.remember)}}
    {{render_field(login_user_form.next)}}
    {{render_field(login_user_form.submit)}}
</form>
{%include"security/_menu.html"%}

User 类中的 password 字段使用了明文存储,这是为了让例子更清晰,生产环境中请勿使用明文存储重要的内容。下一节我们将介绍加密方法。

Flask-RESTful

Flask-RESTful 帮助你快速创建 REST API 服务。先安装它:

> pip install flask-restful

现在实现一个能创建、查询和删除用户的 API 例子(app_restful.py):

from flask import Flask, request
from flask_restful import Resource, Api, reqparse, fields, marshal_with
from flask_sqlalchemy import SQLAlchemy

app=Flask(__name__)
app.config.from_object('config')
api=Api(app)
db=SQLAlchemy(app)

parser=reqparse.RequestParser()
parser.add_argument('admin', type=bool, help='Use super manager mode',
                    default=False)


resource_fields={
    'id':fields.Integer,
    'name':fields.String,
    'address':fields.String
}


class User(db.Model):
    __tablename__='restful_user'
    id=db.Column(db.Integer, primary_key=True)
    name=db.Column(db.String(128), nullable=False)
    address=db.Column(db.String(128), nullable=True)
   
db.create_all()


class UserResource(Resource):
    @marshal_with(resource_fields)
    def get(self, name):
        user=User.query.filter_by(name=name).first()
        return user

    def put(self, name):
       address=request.form.get('address', '')
       user=User(name=name, address=address)
       db.session.add(user)
       db.session.commit()
       return{'ok':0}, 201

def delete(self, name):
    args=parser.parse_args()
    is_admin=args['admin']
    if not is_admin:
         return{'error':'You do not have permissions'}
    user=User.query.filter_by(name=name).first()
    db.session.delete(user)
    db.session.commit()
    return{'ok':0}


api.add_resource(UserResource, '/users/<name>')


if__name__=='__main__':
    app.run(host='0.0.0.0', port=9000, debug=True)

在 Flask-RESTful 中,一个地址下的数据称为资源(Resource)。装饰器 marshal_with 做了把模型实例的属性组合成一个字典的抽象工作。

在终端上验证效果:

> http-f put http://localhost:9000/users/xiaoming address='Beijing'
HTTP/1.0 201 CREATED
Content-Length:16
Content-Type:application/json
Date:Sun, 29 May 2016 04:01:49 GMT
Server:Werkzeug/0.11.10 Python/2.7.11+

{
    "ok":0
}

> http-f put http://localhost:9000/users/wanglang--print b
{
    "ok":0
}

> http-f get http://localhost:9000/users/xiaoming--print b
{
    "address":"Beijing",
    "id":1,
    "name":"xiaoming"
}

> http-f delete http://localhost:9000/users/xiaoming--print b
{
    "error":"You do not have permissions"
}

> http-f delete http://localhost:9000/users/xiaoming admin=1--print b
{
    "ok":0
}

> http-f get http://localhost:9000/users/xiaoming--print b
{
    "address":null,
    "id":0,
    "name":null
}

Flask-Admin

有了 Flask-Admin 的帮助,我们用很少的代码就能像 Django 那样实现一个管理后台。它支持 Pymongo、Peewee、Mongoengine、SQLAlchemy 等数据库使用方法,自带了基于模型的数据管理、文件管理、Redis 的页面命令行等类型后台。尤其是模型的管理后台,甚至可以细粒度定制字段级别的权限。

我们先安装它:

> pip install Flask-Admin

现在基于 Flask-Login 和 Flask-SQLAlchemy 实现包含如下功能的后台(app_admin.py):

  • 可以在后台操作数据库中的数据。
  • 静态文件管理。
  • 在导航栏添加一些链接和视图,比如笔者的 GitHub 地址、Google 链接以及回首页的链接。还添加一个动态的链接,点击它可以登录和退出。当登录后会动态地添加一个“Authenticated”的链接。
  • 自定义点击“Authenticated”的链接后看到的模板。

第一步还是定义 User 模型,本例借用 users.py 里面的 User,再继承 UserMixin 即可:

from ext import db
from users import User as_User


class User(_User, UserMixin):
    pass

登录管理器和 4.2.6 节介绍的 Flask-Login 信号的用法相同,这里就不展示了。

接着添加主页、登录和退出的视图:

from flask_login import login_user, logout_user


USERNAME='xiaoming'


@app.route('/')
def index():
    return '<a href="/admin/">Click me to get to Admin!</a>'


@app.route('/login/')
def login_view():
    user=User.query.filter_by(name=USERNAME).first()
    login_user(user)
    return redirect(url_for('admin.index'))


@app.route('/logout/')
def logout_view():
    logout_user()
    return redirect(url_for('admin.index'))

这样的视图只作为管理后台的可点击链接来用:

from flask_admin import Admin
from flask_admin.base import MenuLink


class AuthenticatedMenuLink(MenuLink):
    def is_accessible(self):
        return current_user.is_authenticated


class NotAuthenticatedMenuLink(MenuLink):
    def is_accessible(self):
        return not current_user.is_authenticated


admin=Admin(app, name='web_develop', template_mode='bootstrap3')
admin.add_link(NotAuthenticatedMenuLink(name='Login',
                                        endpoint='login_view'))
admin.add_link(AuthenticatedMenuLink(name='Logout',
                                     endpoint='logout_view'))

也可以直接使用 url 参数指定地址:

admin.add_link(MenuLink(name='Back Home', url='/'))
admin.add_link(MenuLink(name='Google', category='Links',
                        url='http://www.google.com/'))
admin.add_link(MenuLink(name='GitHub', category='Links',
                        url='https://github.com/dongweiming'))

其中 category 会创建一个叫作 Links 的下拉菜单,把 Google 和 GitHub 链接放进去。

在 Flask-Admin 中指定视图需要继承它提供的 BaseView,或者使用 contrib 中自带的视图类,比如 FileAdmin 和 ModelView:

from flask_admin.base import BaseView, expose
from flask_admin.contrib.sqla import ModelView
from flask_admin.contrib.fileadmin import FileAdmin


class MyAdminView(BaseView):
    @expose('/')
    def index(self):
        return self.render('chapter5/section2/authenticated-admin.html')

    def is_accessible(self):
        return current_user.is_authenticated


admin.add_view(ModelView(User, db.session))

path=os.path.join(os.path.dirname(__file__), '../../static')
admin.add_view(FileAdmin(path, '/static/', name='Static Files'))

#创建一个名为 Authenticated 的链接,但是必须登录才能访问
admin.add_view(MyAdminView(name='Authenticated'))

其中有如下细节需注意。

  • MyAdminView 定义的首页地址没有验证是不能访问的。
  • 上面提到的视图可以通过设置 endpoint 参数自定义链接,比如“ModelView(User, db.session)”生成的子路径是“/admin/user”,修改为:“ModelView(User, db.session, endpoint='new_user')”,就可以使用“/admin/new_user”来访问了。
  • 提到的 authenticated-admin.html 模板就是我们自定义的点击 Authenticated 的链接后看到的模板。它的内容如下:
{%extends 'admin/master.html'%}
{%block body%}
    Hello World from Authenticated Admin!
{%endblock%}

最后使用 before_first_request 钩子初始化数据库:

@app.before_first_request
def create_user():
    db.drop_all()
    db.create_all()

    user=User(name=USERNAME, email='a@dongwm.com', password='123')
    db.session.add(user)
    db.session.commit()

本例可以使用 xiaoming 这个固定的用户来执行登录、退出等操作。

添加管理后台后,当访问 http://localhost:9000/的时候,将显示 index 函数的返回内容。访问 http://localhost:9000/admin/就可以看到未登录的管理后台了,页面只有一个菜单栏,如图 4.1 所示。

图 4.1 未登录的管理后台

其中:

  • User 这个链接是通过 ModelView 实现的,也就是在后台操作 User 表。操作功能包含修改、创建、删除等。
  • Static Files 这个链接是通过 FileAdmin 实现的,它可以管理项目的静态文件。操作功能包含重命名、上传、查看和删除文件,创建目录等。
  • Links 下拉菜单中包含了如笔者的 GitHub 地址、Google 链接等选项。
  • 最后一个是 Login,这个按钮链接是动态的,登录前显示“Login”,登录后显示“Logout”。当登录后会菜单栏会动态添加一个“Authenticated”的链接。
  • 登录后点击 Authenticated 的链接可以看到 authenticated-admin.html 模板渲染的内容,如果未登录就访问这个链接地址,则返回 403 错误。

Flask-Assets

Web 网站的 Javascript 和 CSS 源文件很多。举个例子,某应用的结构如下:

> cd~/web_develop/chapter4/section2
> tree static
static
├──css
|  ├──all.min.css
|  ├──base-min.css
|  └──buttons-min.css
├──js
|    ├──all.min.js
|    ├──src
|    |  ├──click.es6
|    |  ├──common.js
|    |  └──highlight.js
|    └──vendor
|       └──jquery.min.js
└──scss
   ├──common.scss
   └──forms.scss

static 目录下有 3 个文件夹。

  • css:存放了外部的 CSS 库和一些已压缩的 CSS 文件。
  • scss:存放组件 SCSS 源文件。
  • js:存放外部的 JavaScript 库和其他 JavaScript 文件。其中 src 子目录下存放使用原生 JavaScript 和 ES6 这两种方式编写的源代码,通过后缀名来分辨类型。

ES6 是 ECMAScript6 的简称,也称为 ES2015,它是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标是让 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。现在很多互联网公司都已经开始使用 ES6、React 等技术重构已有项目或者在新的项目中使用,所以本节也演示一下如何在 Flask-Assets 中使用 ES6。由于现在用户浏览器还没有完全支持 ES6,所以需要借助 Babel 这个转码器,将 ES6 代码转为 ES5 代码,从而支持大多数 JavaScript 环境。

首先安装 Babel 和对应的预设:

> sudo apt-get install npm-yq
> sudo npm install--global babel-cli
> sudo ln-s`which nodejs`/usr/bin/node
> npm install babel-preset-es2015

其中的 babel-preset-es2015 就是预设。默认的 Babel 还没有“翻译”代码的能力,而仅仅把代码从一处拷贝到另一处。需要通过这样的预设(presets,也就是一组插件)来指示 Babel 去“翻译”ES6 代码。

如上例,前端资源会非常多,尤其是组件化开发的方式下。不能在模板中把这些组件挨个引用进来,因为这会增加网络延时,造成页面访问很慢。常见的解决方法是合并和压缩。Flask-Assets 是 webassets 库的 Flask 扩展,它提供了更简单的使用 webassets 的方式。由于 webassets 在添加 Babel 支持之后并没有发布新版本,需要克隆项目安装 webassets 之后再安装 Flask-Assets:

> git clone https://github.com/miracle2k/webassets
> cd webassets
> python setup.py install
> pip install flask_assets

webassets 自带的过滤器很多,主要包含 4 种类型,如表 4.5 所示。

表 4.5 webassets 自带的过滤器

过滤器用途
Babel用来翻译 ES6 代码
JavaScript 压缩常用的有 rjsmin、yui_js(也就是雅虎的 YUI Compressor)、jsmin 和 closure_js
CSS 压缩常用的有 cssmin、yui_css 和 cleancss
编译器常用的有 less、sass、pyscss、libsass、stylus 和 typescript

我们选择 jsmin 作为 JavaScript 压缩工具,cssmin 作为 CSS 压缩工具,pyscss 作为 CSS 编译工具。先安装它们:

> pip install jsmin cssmin pyscss

为了方便管理,把合并、压缩的内容单独存放(assets.py):

from webassets.filter import get_filter
from flask_assets import Environment, Bundle

css_common=Bundle('scss/common.scss',
                  filters='pyscss', output='css/common.css',
                  debug=False)

css_all=Bundle('css/base-min.css', css_common,
               'css/buttons-min.css',
               filters='cssmin', output='css/all.min.css')

js_common=Bundle('js/src/*.js', output='js/common.js')

es2015=get_filter('babel', presets='es2015')
es2015_all=Bundle('js/src/*.es6', output='js/es6.js', filters=es2015)

js_all=Bundle(
    'js/vendor/jquery.min.js',
    js_common, es2015_all,
    filters='jsmin', output='js/all.min.js')


def init_app(app):
    webassets=Environment(app)
    webassets.register('css_all', css_all)
    webassets.register('js_all', js_all)
    webassets.manifest='cache' if not app.debug else False
    webassets.cache=not app.debug
    webassets.debug=app.debug

其中,Bundle 既可以接受纯字符串的路径,也接受其他的 Bundle 实例。

对模块 assets 添加 init_app 函数是为了使其更像一个 Flask 扩展(app_assets.py ):

from flask import Flask, render_template

import assets


app=Flask(__name__)
assets.init_app(app)


@app.route('/')
def hello():
    return render_template('index.html')

在 init_app 函数中注册了 css_all 和 js_all 这两个资源,需要在模板中使用如下 Jinja2 语法来引用:

{%assets"css_all"-%}
    <link rel="stylesheet"href="{{ASSET_URL}}">
{%-endassets%}

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。