0%

Flask——Web表单

Flask——程序的基本结构中介绍的请求对象包含客户端发出的所有请求信息。其中,request.form能获取POST请求中提交的表单数据。

尽管Flask的请求对象提供的信息足够用于处理Web表单,但有些任务很单调,而且要重复操作。比如:生成表单的HTML代码和验证提交的表单数据。

Flask-WTF(https://flask-wtf.readthedocs.io/en/latest/)扩展可以把处理Web表单的过程变成一个愉悦的体验。这个扩展对独立的WTForms(http://wtforms.readthedocs.io/en/latest/)包进行了包装,方便集成到Flask程序中。

Flask-WTF及其依赖可使用pip安装:

(env) [root@server1 myporject]# pip install flask-wtf

跨站请求伪造保护


默认情况下,Flask-WTF能保护所有表单免受跨站请求伪造(Cross-Site Request Forgery,CSRF)的攻击。恶意网站把请求发送到攻击者已登录的其他网站时就会引发CSRF攻击。

为了实现CSRF保护,Flask-WTF需要程序设置一个密钥。Flask-WTF使用这个密钥生成加密令牌,再用令牌验证请求中表单数据的真伪。设置密钥的方法如下:

run.py:设置Flask-WTF

1
2
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'

app.config字典可用来存储框架、扩展和程序本身的配置变量。使用标准的字典句法就能把配置值添加到app.config对象中。这个对象还提供了一些方法,可以从文本或环境中导入配置值。

SECRET_KEY配置变量时通用密钥,可在Flask和多个第三方扩展中使用。如其名所示,加密的强度取决于变量值的机密程度。不同的程序要使用不同的密钥,而且要保证其他人不知道你所用的字符串。

注意:为了增强安全性,密钥不应该直接写入代码,而要保存在环境变量中。

表单类


使用Flask-WTF时,每个Web表单都由一个继承自Form的类表示。这个类定义表单中一组字段,每个字段都用对象表示。字段对象可附属一个或多个验证函数。验证函数用来验证用户提交的输入值是否符合要求。

下面设计一个简单的Web表单,包含一个文本字段和一个提交按钮。

run.py:定义表单类

1
2
3
4
5
6
from flask.ext.wtf import Form
from wtforms import StringField,SubmitField
from wtforms.validators import Required
class NameForm(Form):
name = StringField('What is your name?',validators=[Required()])
submit = SubmitField('Submit')

这个表单中的字段都定义为类变量,类变量的值是相应字段类型的对象。在这个示例中,NameForm表单中有一个名为name的文本字段和一个名为submit的提交按钮。StringField类表示属性为type=”text”的<input>元素。SubmitField类表示属性为type=”submit”的<input>元素。字段构造函数的第一个参数是把表单渲染成HTML时使用的标号。

StringField构造函数中的可选参数validators指定一个由验证函数组成的列表,在接受用户提交的数据之前验证数据。验证函数Required()确保提交的字段不为空。

注意:Form基类由Flask-WTF扩展定义,所以从flask.ext.wtf中导入。字段和验证函数却可以直接从WTForms包中导入。

WTForms支持的HTML标准字段:

字段类型 说明
StringField 文本字段
TextAreaField 多行文本字段
PasswordField 密码文本字段
HiddenField 隐藏文本字段
DateField 文本字段,值为datetime.date格式
DateTimeField 文本字段,值为datetime.datetime格式
IntegerField 文本字段,值为整数
DecimalField 文本字段,值为decimal.Decimal
FloatField 文本字段,值为浮点数
BooleanField 复选框,值为True和False
RadioField 一组单选框
SelectField 下拉列表
SelectMultipleField 下拉列表,可选择多个值
FileField 文件上传字段
SubmitField 表单提交按钮
FormField 把表单作为字段嵌入另一个表单
FieldList 一组指定类型的字段

WTForms验证函数:

验证函数 说明
Email 验证电子邮件地址
EqualTo 比较两个字段的值;常用于要求输入两次密码进行确认的情况
IPAddress 验证IPv4网络地址
Length 验证输入字符串的长度
NumberRange 验证输入的值在数字范围内
Optional 无输入值时跳过其他验证函数
Required 确保字段中有数据
Regexp 使用正则表达式验证输入值
URL 验证URL
AnyOf 确保输入值在可选值列表中
NoneOf 确保输入值不在可选值列表中

把表单渲染成HTML


表单字段是可调用的,在模板中调用会渲染成HTML。假设视图函数把一个NameForm实例通过参数form传入模板,在模板中可以生成一个简单的表单,如下所示:

1
2
3
4
5
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }}{{ form.name() }}
{{ form.submit() }}
</form>

当然,这个表单还很简陋。要想改进表单的外观,可以把参数传入渲染字段的函数,传入的参数会被转换成字段的HTML属性。例如:可以为字段指定id或class属性,然后定义CSS样式:

1
2
3
4
5
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }}{{ form.name(id='my-text-field') }}
{{ form.submit() }}
</form>

即便能指定HTML属性,但按照这种方法渲染表单的工作量还是很大,所以在条件允许的情况下最好能使用Bootstrap中的表单样式。Flask-Bootstrap提供了一个非常高端的辅助函数,可以使用Bootstrap中预先定义好的表单样式渲染整个Flask-WTF表单,而这些操作只需一次调用即可完成。使用Flask-Bootstrap,上述表单可使用下面的方式渲染:

1
2
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}

import指令的使用方法和普通Python代码一样,允许导入模块中的元素并用在多个模板中。导入的bootstrap/wtf.html文件中定义了一个使用Bootstrap渲染Flask-WTF表单对象的辅助函数。wtf.quick_form()函数的参数为Flask-WTF表单对象,使用Bootstrap的默认样式渲染传入的表单。run.py的完整模板如下:

templates/index.html:使用Flask-WTF和Flask-Bootstrap渲染表单

1
2
3
4
5
6
7
8
9
10
11
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Hello,{% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}

模板的内容区现在有两部分。第一部分是页面头部,显示欢迎消息。这里用到了一个模板条件语句。Jinja2中的条件语句格式为...。如果条件的计算结果为True,那么渲染if和else指令之间的值。如果条件的计算结果为False,则渲染else和endif指令之间的值。在这个示例中,如果没有定义模板变量name,则会渲染字符串”Hello,Stranger!”。内容区的第二部分使用wtf.quick_form()函数渲染NameForm对象。

在视图函数中处理表单


在run.py中,视图函数index()不仅要渲染表单,还要接收表单中的数据。

run.py:路由方法

1
2
3
4
5
6
7
8
@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)

app.route修饰器中添加的methods参数告诉Flask在URL映射中把这个视图函数注册为GET和POST请求的处理程序。如果没指定methods参数,就只把视图函数注册为GET请求的处理程序。

把POST加入方法列表很有必要,因为将提交表单作为POST请求进行处理更加便利。表单也可作为GET请求提交,不过GET请求没有主体,提交的数据以查询字符串的形式附加到URL中,可在浏览器的地址栏中看到。基于这个以及其他多个原因,提交表单大都作为POST请求进行处理。

局部变量name用来存放表单中输入的有效名字,如果没有输入,其值为None。如上述代码所示,在视图函数中创建一个NameForm类实例用于表示表单。提交表单后,如果数据能被所有验证函数接受,那么validate_on_submit()方法的返回值为True,否则返回False。这个函数的返回值决定是重新渲染表单还是处理表单提交的数据。

用户第一次访问程序时,服务器会收到一个没有表单数据的GET请求,所以validate_on_submit()将返回False。if语句的内容被跳过,通过渲染模板处理请求,并传入表单对象和值为None的name变量作为参数。用户会看到浏览器中显示了一个表单。

用户提交表单后,服务器收到一个包含数据的POST请求。validate_on_submit()会调用name字段上附属的Required()验证函数。如果名字不为空,就能通过验证,validate_on_submit()返回True。现在,用户输入的名字可通过字段的data属性获取。在if语句中,把名字赋值给局部变量name,然后再把data属性设为空字符串,从而清空表单字段。最后一行调用render_template()函数渲染模板,但这一次参数name的值为表单中输入的名字,因此会显示一个针对该用户的欢迎消息。

完整代码示例如下:

templates/base.html

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
{% extends "bootstrap/base.html" %}

{% block title %}Flasky{% endblock %}

{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}

{% block content %}
<div class="container">
{% block page_content %}{% endblock %}
</div>
{% endblock %}

templates/index.html

1
2
3
4
5
6
7
8
9
10
11
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Hello,{% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}

run.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
from flask import Flask,render_template
from flask.ext.bootstrap import Bootstrap
from flask.ext.wtf import Form
from wtforms import StringField,SubmitField
from wtforms.validators import Required

app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
bootstrap = Bootstrap(app)

class NameForm(Form):
name = StringField('What is your name?',validators=[Required()])
submit = SubmitField('Submit')

@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)

if __name__ == '__main__':
app.run('0.0.0.0')

Flask-WTF Web表单

上图是用户首次访问网站时浏览器显示的表单。用户提交名字后,程序会生成一个针对该用户的欢迎消息(如下图)。欢迎消息下方还是会显示这个表单,以便用户输入新名字。

提交后显示的Web表单

如果用户提交表单之前没有输入名字,Required()验证函数会捕获这个错误(如下图)。注意一下扩展自动提供了多少功能。这说明像Flask-WTF和Flask-Bootstrap这样设计良好的扩展能让程序具有强大的功能。

验证失败后显示的Web表单

重定向和用户会话


上述的run.py存在一个可用性问题。用户输入名字后提交表单,然后点击浏览器的刷新按钮,会看到一个莫名其妙的警告,要求在再次提交表单之前进行确认。之所以出现这种情况,是因为刷新页面时浏览器会重新发送之前已经发送过的最后一个请求。如果这个请求是一个包含表单数据的POST请求,刷新页面后会再次提交表单。大多数情况下,这并不是理想的处理方式。

很多用户都不理解浏览器发出的这个警告。基于这个原因,最好别让Web程序把POST请求作为浏览器发送的最后一个请求。

这种需求的实现方式是,使用重定向作为POST请求的响应,而不是使用常规响应。重定向是一种特殊的响应,响应内容是URL,而不是包含HTML代码的字符串。浏览器收到这种响应时,会向重定向的URL发起GET请求,显示页面的内容。这个页面的加载可能要多花几微秒,因为要先把第二个请求发给服务器。除此之外,用户不会察觉到有什么不同。现在,最后一个请求是GET请求,所以刷新命令能像预期的那样正常使用了。这个技巧称为Post/重定向/Get模式。

但这种方式会带来另一个问题。程序处理POST请求时,使用form.name.data获取用户输入的名字,可是一旦这个请求结束,数据也就丢失了。因为这个POST请求使用重定向处理,所以程序需要保存输入的名字,这样重定向后的请求才能获得并使用这个名字,从而构建真正的响应。

程序可以把数据存储在用户会话中,在请求之间”记住”数据。用户会话是一种私有存储,存在于每个连接到服务器的客户端中。它是请求上下文中的变量,名为session,像标准的Python字典一样操作。

注意:默认情况下,用户会话保存在客户端cookie中,使用设置的SECRET_KEY进行加密签名。如果篡改了cookie中的内容,签名就会失效,会话也会随之失效。

更新之后的run.py如下,实现了重定向和用户会话,模板文件没有变动:

run.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
from flask import Flask,render_template,session,redirect,url_for
from flask.ext.bootstrap import Bootstrap
from flask.ext.wtf import Form
from wtforms import StringField,SubmitField
from wtforms.validators import Required

app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
bootstrap = Bootstrap(app)

class NameForm(Form):
name = StringField('What is your name?',validators=[Required()])
submit = SubmitField('Submit')

@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'))

if __name__ == '__main__':
app.run('0.0.0.0')

在程序的前一个版本中,局部变量name被用于存储用户在表单中输入的名字。这个变量现在保存在用户会话中,即session[‘name’],所以在两次请求之间也能记住输入的值。

现在,包含合法表单数据的请求最后会调用redirect()函数。redirect()是个辅助函数,用来生成HTTP重定向响应。redirect()函数的参数是重定向的URL,这里使用的重定向URL是程序的根地址,因为重定向响应本可以写得简单一些,写成redirect(‘/‘),但却会使用Flask提供的URL生成函数url_for()。推荐使用url_for()生成URL,因为这个函数使用URL映射生成URL,从而保证URL和定义的路由兼容,而且修改路由名字后依然可用。

url_for()函数的第一个且唯一必须指定的参数是端点名,即路由的内部名字。默认情况下,路由的端点是相应视图函数的名字。在这个示例中,处理根地址的视图函数是index(),因此传给url_for()函数的名字是index。

最后一处改动位于render_template()函数中,使用session.get(‘name’)直接从会话中读取name参数的值。和普通的字典一样,这里使用get()获取字典中键对应的值以避免未找到键的异常情况,因为对于不存在的值,get()会返回默认值None。

使用这个版本的程序时,刷新浏览器页面,看到的新页面就和预期一样了。

Flash消息

请求完成后,有时需要让用户知道状态发生了变化。这里可以使用确认消息、警告或者错误提醒。一个典型例子是,用户提交了有一项错误的登录表单后,服务器发回的响应重新渲染了登录表单,并在表单上面显示一个消息,提示用户用户名或者密码错误。

这种功能是Flask的核心特性。flash()函数可实现这种效果。

run.py:Flash消息

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,session,redirect,url_for,flash
from flask.ext.bootstrap import Bootstrap
from flask.ext.wtf import Form
from wtforms import StringField,SubmitField
from wtforms.validators import Required

app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
bootstrap = Bootstrap(app)

class NameForm(Form):
name = StringField('What is your name?',validators=[Required()])
submit = SubmitField('Submit')

@app.route('/',methods=['GET','POST'])
def index():
form = NameForm()
if form.validate_on_submit():
old_name = session.get('name')
if old_name is not None and old_name != form.name.data:
flash('Looks like you have changed your name!')
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html',form=form,name=session.get('name'))

if __name__ == '__main__':
app.run('0.0.0.0')

在这个示例中,每次提交的名字都会和存储在用户会话中的名字进行比较,而会话中存储的名字是前一次在这个表单中提交的数据。如果两个名字不一样,就会调用flash()函数,在发给客户端的下一个响应中显示一个消息。

仅调用flash()函数并不能把消息显示出来,程序使用的模板要渲染这些消息。最好在基模板中渲染Flash消息,因为这样所有页面都能使用这些消息。Flask把get_flashed_messages()函数开放到模板,用来获取并渲染消息。

templates/base.html:渲染Flash消息

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
32
33
34
35
36
37
{% extends "bootstrap/base.html" %}

{% block title %}Flasky{% endblock %}

{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}

{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">&times;</button>
{{ message }}
</div>
{% endfor %}

{% block page_content %}{% endblock %}
</div>
{% endblock %}

在这个示例中,使用Bootstrap提供的警报CSS样式渲染警告消息。

在模板中使用循环是因为在之前的请求循环中每次调用flash()函数时都会生成一个消息,所以可能有多个消息在排队等待显示。get_flashed_messages()函数获取的消息在下次调用时不会再次返回,因此Flash消息只显示一次,然后就消失了。

参考书籍:《Flask Web开发——基于Python的Web应用开发实战》


- - - - - - - - - 本文结束啦感谢您阅读 - - - - - - - - -