转载于:http://pdf.us/2017/10/03/451.html,感谢这位大神
《Flask Web开发:基于Python的Web应用开发实战》学习笔记
这里是第一部分的学习笔记。第一部分:Flask简介
准备工作Git
git clone https://github.com/miguelgrinberg/flasky.git
git checkout 1a #签出到某个版本
git reset --hard #若修改代码,强行还原到签出状态
git fetch --all
git fetch --tags
git reset --hard origin/master
fetch从远程仓库更新本地仓库的提交历史和标签,但并不改动原文件;git reset才真正写入。
git diff 2a 2b #查看版本间的区别
git tag #查看所有标签
git log --oneline --graph #查看标签历史
第一章 安装
flask官网:http://flask.pocoo.org/
使用虚拟环境
pip install virtualenv (15.1.0)
yum install python-virtualenv (1.10.1)
virtualenv venv #创建虚拟环境
. venv/bin/activate #进入虚拟环境
deactivate #退出虚拟环境
pip install flask #安装flask
第二章 程序的基本结构
初始化
from flask import Flask
app = Flask(__name__)
__name__参数用于决定程序的根目录,以便找到相对根目录的其它资源
路由和视图函数
路由,处理URL与函数之间的关系,常使用app.route修饰器
视图函数的返回值是响应
|
@
app
.
route
(
'/user/'
)
def
user
(
name
)
:
return
'Hello,%s!'
%
name
|
动态类型:
默认,字符串;int,整数,如;float,浮点数;path,包含斜线/的字符串
启动服务器
if __name__='__main__':
app.run(debug=True) #激活调试器和重载程序
一个完整的程序
hello.py:
|
from
flask
import
Flask
app
=
Flask
(
__name__
)
@
app
.
route
(
'/'
)
def
index
(
)
:
return
'Hello,World!'
if
__name__
==
'__main__'
:
app
.
run
(
debug
=
True
)
|
请求-响应循环
上下文在线程级别全局可访问
user_agent=request.headers.get('User-Agent') #该方法获取User-Agent
Flask上下文全局变量:
变量名 |
上下文 |
说明 |
current_app |
程序上下文 |
当前激活程序的程序实例 |
g |
程序上下文 |
处理请求时用作临时存储,每次请求均会重设 |
request |
请求上下文 |
请求对象,封装客户端请求内容 |
session |
请求上下文 |
用户会话,存储请求间需要记住的内容 |
app.app_context()可以获得程序上下文,之后再.push()推送上下文,然后才可以使用current_app;收回程序上下文则是pop()。
form hello import app
form flask import current_app
app_ctx=app.app_context()
app_ctx.push()
current_app.name
app_ctx.pop()
请求调度
生成映射的方法:1、app.route修饰器;2、app.add_url_rule()。app.add_url_rule('/ma/','index',hello.index)
查看url映射:app.url_map。 请求方法中,HEAD,OPTIONS由Flask自动处理,不需要单独写方法。
请求钩子
请求钩子用于在请求处理之前或之后执行,使用修饰器实现。
1、before_first_request:在处理第一个请求这前执行;
2、before_request:在每次请求前执行;
3、after_request:若无未处理的异常,在每次请求后执行;
4、teardown_request:即便有未处理的异常,也在每次请求后执行
在请求钩子函数和视图函数之前共享数据,一般使用g
响应
响应可指定状态码,如:return '
Hello,World!
',400
响应还可带第三个参数,由首部(header)组成的字典。响应也可以返回给Response对象,该对象也包含三个参数。与前面的相对应。response设置cookie的例子:
1
2
3
4
5
6
7
8
9
10
11
12
|
from
flask
import
Flask
,
<
strong
>
make_response
<
/
strong
>
app
=
Flask
(
__name__
)
@
app
.
route
(
'/'
)
def
index
(
)
:
response
=
<
strong
>
make_response
<
/
strong
>
(
'Hello,World!'
)
response
.
set_cookie
(
'answer'
,
'42'
)
return
response
if
__name__
==
'__main__'
:
app
.
run
(
debug
=
True
)
|
重定向302
from flask import redirect
...
return redirect('http://pdf.us/')
abort函数,用于处理错误,但不会将控制权还给调用它的函数,而是抛出异常将控制权交给Web服务器
from flask import abort
...
user=load_user(id)
if not user:
abort(404)
return '
Hello,%s
' % user
Flask扩展
Flask-Script扩展,支持命令行选项,安装:pip install flask-script
在程序中注册扩展的方法
from flask.ext.script import Manager #最新的写法是from flask_script import Manager
manager=Manager(app)
#...
if __name=='__main__':
manager.run()
专门为Flask开发的扩展都暴露在falsk.ext命令空间下。
python hello.py runserver -h 0.0.0.0 -p 80 -d -r #启动开发服务器的正确姿势
第三章 模板
业务逻辑与表现逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
from
flask
import
Flask
,
render_template
from
flask_script
import
Manager
app
=
Flask
(
__name__
)
manager
=
Manager
(
app
)
@
app
.
route
(
'/'
)
def
index
(
)
:
return
render_template
(
'index.html'
)
@
app
.
route
(
'/user/'
)
def
user
(
name
)
:
return
render_template
(
'user.html'
,
name
=
name
)
if
__name__
==
'__main__'
:
manager
.
run
(
)
|
其中,index.html和user.html在templates目录下。user.html内容为:
Hello,{{name}}
模板中的变量
形如{{ mydict['key'] }} {{ mylist[3] }} {{ myobj.somemethod() }} 都支持
过滤器
{{
Hello
|safe }}
过滤器 |
说明 |
safe |
渲染值时不转义 |
capitalize |
首字母大写 |
lower |
小写 |
upper |
大写 |
title |
每个单词首字母大写 |
trim |
渲染时去掉首尾空格 |
striptags |
渲染之前把值中所有HTML标签都删掉 |
控制结构
条件控制 if...else...endif
|
{% if user %}
Hello,{{ user }}
{% else %}
Hello,Stranger
{% endif %}
|
for循环
|
{% for comment in comments %}
{{ comment }}
{% endfor %}
|
宏,类似于函数
|
#定义宏render_comment(comment)
{% macro render_comment(comment) %}
{{ comment }}
{% endmacro %}
{% for comment in comments %}
#调用宏
{{ render_comment(comment) }}
{% endfor %}
|
宏也可以导入使用
|
{% import 'macro.html' as macro %}
{% for comment in comments %}
{{ macro.render_comment(comment) }}
{% endfor %}
|
能重复使用的代码片也可以使用导入
|
{% include 'common.html' %}
|
模板的继承
基模板base.html
|
{% block head %}
{% block title %}{% endblock %} - My Application
{% endblock %}
{% block body %}
{% endblock %}
|
衍生模板index.html
|
{
%
extends
"base.html"
%
}
{
%
block
title
%
}Index
{
%
endblock
%
}
{
%
block
head
%
}
{
{
super
(
)
}
}
{
%
endblock
%
}
{
%
block
body
%
}
Hello,World!
{
%
endblock
%
}
|
注意head块,因为不是空的,所以使用super()获取原来的内容。
使用Flask-Bootstrap
pip install flask-bootstrap
初始化
from flask.ext.bootstrap import Bootstrap #from flask_bootstrap import Bootstrap
#...
bootstrap = Bootstrap(app)
使用{% extends "bootstrap/base.html" %},然后可以定义各种块。在定义styles和scripts块时,需要特别注意,要使用{{super()}},否则基模板中的相应块中的内容会丢失。
自定义错误页面
|
@
app
.
errorhandler
(
404
)
def
gage_not_found
(
e
)
:
return
render_template
(
'404.html'
)
,
404
@
app
.
errorhandler
(
500
)
def
internet_server_error
(
e
)
:
return
render_template
(
'500.html'
)
,
500
|
采用模板继承机制,简化模板制作
链接
url_for('index') 得到 '/' 这样引用:{{ url_for('index') }}
url_for('index',_external=True) 得到绝对地址
生成动态地址时,将动态部分作为关键字参数传入:url_for('user',name='john',_external=True)
动态地址还可以传入额外参数:url_for('index',page=2) 生成:/?page=2
静态文件
通常存放地static文件夹下面,可以有子文件夹
url_for('static',filename='css/style.css',_external=True)将返回.../static/css/style.css
Flask-Moment本地化日期时间
pip install flask-moment
初始化
from flask.ext.moment import Moment #from flask_moment import Moment
moment=Moment(app)
使用moment时,还需要引入jquery.js库【该库bootstrap已经引用】和moment.js库
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
使用:
视频函数
from datetime import datetime
#...
@app.route('/')
def index():
return render_template('index.html',current_time=datetime.utcnow())模板:
{{ moment(current).format('LLL') }} #L~LLLL代表不同复杂度,LTS
{{ moment(current).fromNow(refresh=True) }} #相对时间,a minute ago
Flask-Moment实现了moment.js中的format(),fromNow(),fromTime(),calendar(),valueOf(),unix()方法,具体查看文档http://momentjs.com/docs/#/displaying/
设置语言:{{ moment.lang('es') }}
第四章 Web表单
request.form能获取POST请求中提交的表单数据
或者使用Flask-WTF扩展
pip install flask-wtf
WTF需要设置一个密钥,用于验证表单数据真伪,设置方法:
app.config['SECRET_KEY'] = 'yourkey'
app.config字典用于存储框架、扩展和程序本身的配置变量。
表单类
每个Web表单都继承自Form类,这个类定义表单中的字段,每个字段都用对象表示,字段对象可附属验证函数。表单的字段都定义为类变量,类变量的值是相应字段类型的对象。
定义表单类
|
from
flask
.
ext
.
wtf
import
Form
#该名称已更新为FlaskForm
from
wtforms
import
StringField
,
SubmitField
from
wtforms
.
validators
import
Required
class
NameForm
(
Form
)
:
name
=
StringField
(
'What is your name?'
,
validators
=
[
Required
(
)
]
)
submit
=
SubmitField
(
'Submit'
)
字段第一个参数是标签
|
其中,from flask.ext.wtf import Form可用from flask_wtf import Form替换
字段类型 |
说明 |
StringField |
文本字段 |
TextField |
多行文本 |
PasswordField |
密码文本 |
HiddenField |
隐藏文本 |
DateField |
文本,值为datetime.date格式 |
DatetimeField |
文本,值为datetime.datetime格式 |
IntegerField |
文本,值为整数 |
DecimalField |
文本,值为decimal.Decimal |
FloatField |
文本,值为浮点 |
BooleanField |
复选框,值为True或False |
RadioField |
一线单选框 |
SelectField |
下拉列表 |
SelectMultipleField |
下接列表,可多选 |
FileField |
文件上传 |
SubmitField |
提交按钮 |
FormField |
嵌套表单 |
FieldList |
一组指定类型的字段 |
验证函数 |
说明 |
Email |
电子邮件地址 |
EqualTo |
比较两个字段的值,常用于二次密码验证 |
IPAddress |
IPv4地址 |
Length |
字符串上度 |
NumberRange |
数字范围 |
Optional |
无输入值时跳过其它验证函数 |
Required |
不为NULL |
Regexp |
使用正则表达式验证 |
URL |
URL |
AnyOf |
确保输入值在可选值列表中 |
NoneOf |
确认输入值不在可选值列表中 |
渲染表单
表单字段可调用,在模板中调用后会渲染成HTML。
例如视图函数中将表单通过参数form传入模板,则在模板中这样渲染:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#视图函数
@app.route('/')
def index():
form=NameForm() #引入表格
return render_template('index.html',form=form)
#模板
{{ form.hidden_tag() }} #令牌
{{ form.name.label }}{{ form.name() }}
{{ form.submit() }}
#可以为字段指定参数,指定id或者class属性,然后定义CSS
{{ form.name(class='myclass',id='myid') }}
|
更好的方式是使用bootstrap:
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
在视图函数中处理表单
|
@
app
.
route
(
'/'
,
methods
=
[
'GET'
,
'POST'
]
)
def
index
(
)
:
name
=
None
form
=
NameForm
(
)
if
form
.
validate_on_submit
(
)
:
name
=
form
.
name
.
data
form
.
name
.
data
=
''
return
render_template
(
'index.html'
,
form
=
form
,
name
=
name
)
|
form.validate_on_submit方法在所有字段通过验证函数,且点击提交后,为True,否则为False.
重定向和用户会话
最好不要将POST请求作为浏览器发送的最后一个请求!
Post/重定向/Get模式
默认情况下,用户会话session保存在客户端cookie中,使用SECRET_KEY进行加密签名,如果篡改,签名就会失效
|
from
flask
import
session
,
redirect
,
usl
_for
@
app
.
route
(
'/'
,
methods
=
[
'GET'
,
'POST'
]
)
def
index
(
)
:
form
=
NameForm
(
)
if
form
.
validate_on_submit
(
)
:
session
[
'name'
]
=
form
.
name
.
data
return
redirect
(
url_for
(
'index'
)
)
return
render_template
(
'index.html'
,
form
=
form
,
name
=
session
.
get
(
'name'
)
)
#对于不存在的键,get()会返回默认值None
|
Flash消息
确认消息、警告消息、错误消息
Flask开放函数get_flashed_messages()给模板,用于模板获取并渲染消息
{% for message in
get_flashed_messages() %}
{{ message }}
{% endfor %}
第五章 数据库
对象关系映射——ORM——SQL
对象文档映射——ODM——NoSQL
Flask-SQLAlchemy
pip install flask-sqlalchemy
数据库引擎 |
URI |
MySQL |
mysql://username:password@hostname/database |
Postgres |
postgresql://username:password@hostname/database |
SQLite |
sqlite:////absolute/path/to/database |
SQLite |
sqlite:///c:/absolute/path/to/database |
使用mysql时,pip install mysql-python 安装MySQLdb模块,然后手动新建数据库database
初始化数据库
|
from
flask
.
ext
.
sqlalchemy
import
SQLAlchemy
#from flask_sqlalchemy import SQLAlchemy
import
os
basedir
=
os.path
.
abspath
(
os.path
.
dirname
(
__file__
)
)
#__file__为当前脚本文件
app
.
config
[
'SQLALCHEMY_DATABASE_URI'
]
=
'sqlite:////'
+
os.path
.
join
(
basedir
,
'data.sqlite'
)
app
.
config
[
'SQLALCHEMY_COMMIT_ON_TEARDOWN'
]
=
True
#自动提交
app
.
config
[
'SQLALCHEMY_TRACK_MODIFICATIONS'
]
=
False
#非必须
db
=
SQLAlchemy
(
app
)
|
定义模型
模型是一个类,模型一般对应一张表,类属性对应数据库表中的列
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class
Role
(
db
.
Model
)
:
__tablename__
=
'roles'
#定义数据库中实际的表名
id
=
db
.
Column
(
db
.
Integer
,
primary_key
=
True
)
#主键
name
=
db
.
Column
(
db
.
String
(
64
)
,
unique
=
True
)
#unique不允许重复值
def
__repr__
(
self
)
:
#返回可读性的字符串,方便调试,可不定义
return
''
%
self
.
name
class
User
(
db
.
Model
)
:
__tablename__
=
'users'
id
=
db
.
Column
(
db
.
Integer
,
primary_key
=
True
)
username
=
db
.
Column
(
db
.
String
(
64
)
,
unique
=
True
,
index
=
True
)
def
__repr__
(
self
)
:
return
''
%
self
.
username
|
常用列类型及对应的Python类型
类型名 |
Python类型 |
说明 |
Integer |
int |
普通整数,32位 |
SmallInteger |
int |
整数,16位 |
BigInteger |
int/long |
不限制精度整数 |
Float |
float |
浮点数 |
Numeric |
decimal.Decimal |
定点数 |
String |
str |
变长字符串 |
Text |
str |
变长字符串,较长 |
Unicode |
unicode |
变长Unicode字符串 |
UnicodeText |
unicode |
变长Unicode字符串,较长 |
Boolean |
bool |
布尔值 |
Date |
datetime.date |
日期 |
Time |
datetime.time |
时间 |
DateTime |
datetime.datetime |
日期和时间 |
Interval |
datetime.timedelta |
时间间隔 |
Enum |
str |
一组字符串 |
PickleType |
任何Python对象 |
自动使用Pickle序列化 |
LargeBinary |
str |
二进制文件 |
选项名 |
说明 |
primary_key |
主键 |
unique |
不允许出现重复值 |
index |
索引 |
nullable |
允许使用空值 |
default |
为该列定认默认值 |
关系
一对多关系:Role(一),User(多)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class Role(db.Model):
__tablename__='roles'
id=db.Column(db.Integer,primary_key=True)
name=db.Column(db.String(64),unique=True)
users=db.relationship('User',backref='role') #一
def __repr__(self):
return '' % self.name
class User(db.Model):
__tablename__='users'
id=db.Column(db.Integer,primary_key=True)
username=db.Column(db.String(64),unique=True,index=True)
role_id=db.Column(db.Integer,db.ForeignKey('roles.id')) #多
def __repr__(self):
return '' % self.username
|
关于一对多关系的进一步说明:在多的一方添加外键,外键指明了对应另外一方的哪一列。在一的一方的users属性代表这个关系的面向对象视角,对Role类的实例(行)可以通过users属性返回关联列表,relationship第一个参数指明了关系的另一端,backref参数向User模型添加role属性,从而定义反向关系。
选项名 |
说明 |
backref |
在关系的另一个模型中添加反向引用 |
primaryjoin |
明确指定两个模型之间使用的联结条件。只在模棱两可的关系中指定 |
lazy |
指定如何加载相关记录 |
select(首次访问按需加载) |
immediate(源对象加载后就加载) |
joined(加载记录,但使用联结) |
subquery(立即加载但使用子查询) |
noload(永不加载) |
dynamic(不加载记录,但提供加载记录的查询) |
uselist |
若设为False,不使用列表,而使用标量值 |
order_by |
指定排序方式 |
secondary |
指定多对多关系中关系表的名字 |
secondaryjoin |
SQLAlchemy无法自行决定时,指定多对多关系中的二级联结条件 |
一对一关系:使用一对多关系,但调用db.relationship()时,把uselist设为False,把多变成一;
多对一关系:使用一对多关系,对调两个表;或者把外键和db.relationship()都放到多这一侧;
多对多关系:使用关系表
数据库操作
创建表
根据模型类创建数据库:db.create_all()
如果数据库表已经存在于数据库,那么create_all将不会重新创建或更新这个表。如果修改数模型后,要将修改应用到数据库,则只有先删除再重新创建[会清空数据]:
db.drop_all()
db.create_all()
插入行
>>> from hello import Role,User,db
>>> admin_role=Role(name='Admin')
>>> mod_role=Role(name='Moderator')
>>> user_role=Role(name='User')
>>> user_john=User(username='john',
role=admin_role)
>>> user_susan=User(username='susan',
role=user_role)
>>> user_david=User(username='david',
role=user_role)
>>>
user_dong=User(username='dong',role_id=1)
>>>
db.session.add(admin_role)
>>> db.session.add(mod_role)
>>> db.session.add(user_role)
>>> db.session.add(user_john)
>>> db.session.add(user_susan)
>>> db.session.add(user_david)
>>> db.session.add(user_dong)
###
db.session.add_all([admin_role,mod_role,user_role,user_joho,user_susan,user_david,user_dong])
>>>
db.session.commit()
### db.session.rollback() 回滚会话
因为id在提交前不会生成,主键由SQLAlchemy自己处理,不需要设置
修改行
>>> admin_role.name='Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
删除行
>>> db.session.delete(mod_role)
>>> db.session.commit()
查询行
### 查询所有记录
>>> Role.query.all()
[, ]
### 使用过滤器
>>> User.query.
filter_by(role=user_role).all()
[, ]
### 查询原生SQL
>>> str(User.query.filter_by(role=user_role))
'SELECT users.id AS users_id, users.username AS users_username, users.role_id AS users_role_id \nFROM users \nWHERE %s = users.role_id'
常用的查询过滤器:
过滤器 |
说明 |
filter() |
把过滤器添加到原查询上,返回一个新查询 |
filter_by() |
把等值过滤器添加到原查询上,返回一个新查询 |
limit() |
限制原查询返回结果数量,返回一个新查询 |
offset() |
偏移原查询返回结果,返回一个新查询 |
order_by() |
对原查询结果进行排序,返回一个新查询 |
group_by() |
对原查询进行分组,返回一个新查询 |
查询执行函数
方法 |
说明 |
all() |
以列表形式返回所有结果 |
first() |
返回查询第一个结果,若无,返回None |
first_or_404() |
返回查询第一个结果,若无,中止,返回404 |
get() |
返回指定主键对应的行,若无,返回None |
get_or_404() |
返回指定主键对应的行,若无,中止,返回404 |
count() |
返回查询结果数量 |
paginate() |
返回Paginate对象,包含指定范围的结果 |
一对多关系的查询
>>> users=user_role.users
>>> list(users)
[, ]
>>> users[0].role
>>> user_role.users.order_by(User.username).all()
[, ]
>>> user_role.users.count()
2L
在视图函数中操作数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#@app.route('/', methods=['GET', 'POST'])
#def index():
# form = NameForm()
# if form.validate_on_submit():
user
=
User
.
query
.
filter_by
(
username
=
form
.
name
.
data
)
.
first
(
)
# if user is None:
user
=
User
(
username
=
form
.
name
.
data
)
db
.
session
.
add
(
user
)
# session['known']=False
# else:
# session['known']=True
# session['name']=form.name.data
# form.name.data=''
# return redirect(url_for('index'))
# return render_template('index.html', form=form, name=session.get('name'),known=session.get('known',False))
|
注意,这里并没有db.session.commit(),
因为app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
集成Python shell
使用shell时,每次需要导入app,db,User,Role,为简化操作,可以为shell添加上下文:
|
from
flask_script
import
Shell
#...
def
make_shell_context
(
)
:
return
dict
(
app
=
app
,
db
=
db
,
User
=
User
,
Role
=
Role
)
manager
.
add_command
(
"shell"
,
Shell
(
make_context
=
make_shell_context
)
)
#...
|
数据库迁移
不丢失数据的更新数据库
创建迁移仓库
pip install flask-migrate
初始化(
注意,需要传入db)
from flask.ext.migrate import Migrate,MigrateCommand
#from flask_migrate import Migrate,MigrateCommand
#...
migrate=Migrate(app,
db)
manager.add_command('db',MigrateCommand)
维护数据库前,先创建迁移仓库:
python hello.py db init
会生成migrations目录
创建迁移脚本
upgrade()把改动应用到数据库
downgrade()将改动删除
自动创建的迁移不一定总是正确,需要认真检查!!
python hello.py db migrate -m "inital migration"
更新数据库
检查并修正好脚本后(位于migtarions/versions目录)后,使用db upgrade迁移:
python hello.py db upgrade
第六章 电子邮件
pip install flask-mail
Flask-Mail的默认参数
MAIL_SERVER:localhost
MAIL_PORT:25
MAIL_USE_TLS:False
MAIL_USE_SSL:False
MAIL_USERNAME:None
MAIL_PASSWORD:None初始化:
from flask_mail import Mailapp.config['MAIL_SERVER']='smtp.126.com'
app.config['MAIL_PORT']=465
app.config['MAIL_USE_SSL']=True
app.config['MAIL_USERNAME']=os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD']=os.environ.get('MAIL_PASSWORD')
#export MAIL_USERNAME='mymailname'
#export MAIL_PASSWORD='mymailpassword'mail=Mail(app)
命令行测试发送邮件
>>> from flask_mail import Message
>>> from hello import mail
>>> msg=Message('test',sender='
[email protected]',recipients=['
[email protected]'])
### 奇怪的是只能自己给自己发,给别人发会报535错误,被当成垃圾邮件啦?
### msg.body='text body'
### msg.html='
test mail
>>> with app.app_context():
... mail.send(msg)
在程序中添加发送邮件功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
from
flask_mail
import
Message
app
.
config
[
'FLASKY_MAIL_SUBJECT_PREFIX'
]
=
'[Flasky]'
app
.
config
[
'FLASKY_MAIL_SENDER'
]
=
'Flasy Admin '
#app.config['FLASKY_ADMIN']=os.environ.get('FLASKY_ADMIN')
def
send_email
(
to
,
subject
,
template
,
*
*
kwargs
)
:
msg
=
Message
(
app
.
config
[
'FLASKY_MAIL_SUBJECT_PREFIX'
]
+
subject
,
sender
=
app
.
config
[
'FLASKY_MAIL_SENDER'
]
,
recipients
=
[
to
]
)
msg
.
body
=
render_template
(
template
+
'.txt'
,
*
*
kwargs
)
msg
.
html
=
render_template
(
template
+
'.html'
,
*
*
kwargs
)
mail
.
send
(
msg
)
#@app.route('/', methods=['GET', 'POST'])
#def index():
# form = NameForm()
# if form.validate_on_submit():
# user=User.query.filter_by(username=form.name.data).first()
# if user is None:
# user=User(username=form.name.data)
# db.session.add(user)
# session['known']=False
if
app
.
config
[
'FLASKY_ADMIN'
]
:
send_email
(
app
.
config
[
'FLASKY_ADMIN'
]
,
'New User'
,
'mail/new_user'
,
user
=
user
)
# else:
# session['known']=True
# session['name']=form.name.data
# form.name.data=''
# return redirect(url_for('index'))
# return render_template('index.html', form=form, name=session.get('name'),known=session.get('known',False))
|
为配合,需要templates目录下新建mail子目录,里面存放邮件的模板。模板示例:
User {{ user.username }} has joined.
异步发送邮件
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from
threading
import
Thread
def
send_async_email
(
app
,
msg
)
:
with
app
.
app_context
(
)
:
mail
.
send
(
msg
)
#def send_email(to,subject,template,**kwargs):
# msg=Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX']+subject,sender=app.config['FLASKY_MAIL_SENDER'],recipients=[to])
# msg.body=render_template(template+'.txt',**kwargs)
# msg.html=render_template(template+'.html',**kwargs)
thr
=
Thread
(
target
=
send_async_email
,
args
=
[
app
,
msg
]
)
thr
.
start
(
)
return
thr
|
第七章 大型程序的结构
项目结构:
配置选项
使用配置类。公共配置放于Config类,开发,测试,生产分别使用继承自Config类的类,最后生成一个config字典。调用字典时:config['DevelopmentConfig']()生成所有配置。
配置条目相应要做修改:
app.config['SECRET_KEY'] = os.environ.get('Key') or 'hard to guess string'
改为:
SECRET_KEY= os.environ.get('Key') or 'hard to guess string'
SECRET_KEY= os.environ.get('Key') or 'hard to guess string'
如果环境中有,则使用环境中的值,环境中无,则使用配置中的值
程序包
使用程序工厂函数
延迟创建程序实例,创建时使用工厂函数。工厂函数在app包的构造文件中定义__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
from
flask
import
Flask
,
render_template
from
flask_bootstrap
import
Bootstrap
from
flask_mail
import
Mail
from
flask_moment
import
Moment
from
flask_sqlalchemy
import
SQLAlchemy
from
config
import
config
bootstrap
=
Bootstrap
(
)
mail
=
Mail
(
)
moment
=
Moment
(
)
db
=
SQLAlchemy
(
)
def
create_app
(
config_name
)
:
app
=
Flask
(
__name__
)
app
.
config
.
from_object
(
config
[
config_name
]
)
config
[
config_name
]
.
init_app
(
app
)
bootstrap
.
init_app
(
app
)
mail
.
init_app
(
app
)
moment
.
init_app
(
app
)
db
.
init_app
(
app
)
#注册蓝本到程序
from
.
main
import
main
as
main_blueprint
app
.
register_blueprint
(
main_blueprint
)
return
app
|
在蓝本中实现程序功能
蓝本中定义的路由处于休眠状态,只有蓝本注册到程序上后,路由才成为程序的一部分。
app/main/__init__.py创建蓝本
|
from
flask
import
Blueprint
#第一个参数为蓝本的名字,第二个参数为蓝本的包或者模块
main
=
Blueprint
(
'main'
,
__name__
)
from
.
import
views
,
errors
|
注册蓝本到程序,app/__init__.py中
蓝本中的错误处理程序:app/main/errors.py
|
from
flask
import
render_template
from
.
import
main
@
main
.
app_errorhandler
(
404
)
def
page_not_found
(
e
)
:
return
render_template
(
'404.html'
)
,
404
@
main
.
app_errorhandler
(
500
)
def
internal_server_error
(
e
)
:
return
render_template
(
'500.html'
)
,
500
|
蓝本中的路由:app/main/views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from
datetime
import
datetime
from
flask
import
render_template
,
session
,
redirect
,
url_for
from
.
import
main
from
.
forms
import
NameForm
from
.
.
import
db
from
.
.
models
import
User
@
main
.
route
(
'/'
,
methods
=
[
'GET'
,
'POST'
]
)
def
index
(
)
:
#...
return
redirct
(
url_for
(
'.index'
)
)
#或者main.index
#...
|
Flask会为蓝本全部端点加上命名空间,空间名就是蓝本名,所以这里的url_for函数参数为main.index可简写为.index。同一蓝本中重定向可以简写,但是跨蓝本的重定向必须使用有命名空间的端点名。
启动脚本
manage.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#! /usr/bin/env python
import
os
from
app
import
create_app
,
db
from
app
.
models
import
User
,
Role
from
flask_script
import
Manager
,
Shell
from
flask_migrate
import
Migrate
,
MigrateCommand
app
=
create_app
(
os
.
getenv
(
'FLASK_CONFIG'
)
or
'default'
)
manager
=
Manager
(
app
)
migrate
=
Migrate
(
app
,
db
)
def
make_shell_context
(
)
:
return
dict
(
app
=
app
,
db
=
db
,
User
=
User
,
Role
=
Role
)
manager
.
add_command
(
"shell"
,
Shell
(
make_context
=
make_shell_context
)
)
manager
.
add_command
(
'db'
,
MigrateCommand
)
if
__name
==
'__main__'
:
manager
.
run
(
)
|
需求文件
requirements.txt用于记录所有依赖包及精确的版本号
生成:pip freeze >requirements.txt
使用:pip install -r requirements.txt
单元测试
test/test_basics.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import
unittest
from
flask
import
current_app
from
app
import
create_app
,
db
class
BasicTestCase
(
unittest
.
TestCase
)
:
def
setUp
(
self
)
:
self
.
app
=
create_app
(
'testing'
)
self
.
app_context
=
self
.
app
.
app_context
(
)
self
.
app_context
.
push
(
)
db
.
create_all
(
)
def
tearDown
(
self
)
:
db
.
session
.
remove
(
)
db
.
drop_all
(
)
self
.
app_context
.
pop
(
)
def
test_app_exists
(
self
)
:
self
.
assertFalse
(
current_app
is
None
)
def
test_app_is_testing
(
self
)
:
self
.
assertTrue
(
current_app
.
config
[
'TESTING'
]
)
|
setUp()和tearDown()分别在测试前后运行,名字以test_开头的函数都作为测试执行。
test/__init__.py可为空,因为unittest会扫描所有模块并查找测试。
为方便测试,可以添加启动命令:
|
@
manager
.
command
def
test
(
)
:
"""Run the unit tests."""
import
unittest
tests
=
unittest
.
TestLoader
(
)
.
discover
(
'tests'
)
unittest
.
TextTestRunner
(
verbosity
=
2
)
.
run
(
tests
)
|
测试:python manager.py test
创建数据库
python manager.py db upgrade