Flask 的扩展
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%}
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论