第二章介绍的请求对象包含客户端发出的所有请求信息,其中request.form能获取POST请求中提交的表单数据。
尽管Flask的请求对象提供的信息足够用于处理Web表单,但有些任务很单调,而且要重复操作。比如,生成表单的HTML代码和验证提交的表单数据。
Flask-WTF扩展可以把处理Web表单的过程变成一种愉悦的体验。这个扩展对独立的WTForms包进行了包装,方便集成到Flask程序中。Flask-WTF及其依赖可以通过pip命令安装: pip install flask-wtf
4.1 跨站请求伪造保护
默认情况下,Flask-WTF可以保护所有表单免受跨站请求伪造(CSRF)的攻击。恶意网站把请求发送到被攻击者已登录的其他网站时就会引发CSRF攻击。
为了实现CSRF保护,Flask-WTF需要程序设置一个密钥,Flask-WTF使用这个密钥生成加密令牌,再用令牌验证请求中表单数据的真伪。设置秘钥的方法:
app = Flask(__name__)
app.config['SECRET_KEY'] = 'A Code String'
app.config字典可以用来存储框架、扩展和程序本身的配置变量。使用标准的字典句法就能把配置值添加到app.config对象中。这个对象还提供了一些方法,可以从文件或环境中导入配置值。
SECRET_KEY配置变量是通用密钥,可在Flask和多个第三方扩展中使用。加密的强度取决于变量值的机密程度。不同的程序要使用不同的密钥,而且要保证其他人不知道你所用的字符串。为了增强安全性,这个配置值不应该被直接写在代码中,而是保存在环境变量中,后面会介绍这一技术。
4.2表单类
使用Flask-WTF时,每个Web表单都由一个继承自Form的类表示。这个类定义表单中的一组字段,每个字段都用对象表示。字段对象可附属一个或多个验证函数。验证函数用来验证用户提交的输入值是否符合要求。
下面是一个简单的表单,包含一个文本字段和一个提交按钮
示例 Hello.py:定义表单类
from flask_wtf import FlaskForm # 原书中是 from flask_wtf import Form,而Form已经修改为FlaskForm,未来Form将会被移除。
from wtforms import StringField,SubmitField
from wtforms.validators import DataRequired
class NameForm(FlaskForm):
name = StringField('What is your name?',validators=[DataRequired()])
submit = SubmitField('Submit')
这个表单中的字段都定义为类变量,类变量的值是相应字段类型的对象。在这个示例中,NameForm表单有一个名为name的文本字段和一个名为submit的提交按钮,StringF
ield类表示属性为type = ‘text’的<input>元素。SubmitF
ield类表示属性为type = ‘submit’的<input>元素。字段构造函数的第一个参数是把表单渲染成HTML时使用的符号。
StringField构造函数中的可选参数validators指定一个由验证函数组成的列表,在接受用户提交的数据之前验证数据。验证函数DataRequired()确保提交的字段不为空。
在这里我们可以看到,FlaskForm基类由Flask-WTF扩展定义,所以从flask_wtf中导入。字段和验证函数却可以直接从WTForms包中导入。
WTForms支持的HTML标准字段:
字段类型 | 说明 |
TextAreaField | 多行文本字段 |
PasswordField | 密码文本字段 |
HiddentField | 隐藏文本字段 |
DateField | 文本字段,值为datetime.date格式 |
DateTimeField |
文本字段,值为datetime.datetime格式
|
IntegerField | 文本字段,值为整数 |
DecimalField | 文本字段,值为decimal.Decimal |
FloatField | 文本字段,值为浮点数 |
BooleanField | 复选框,值为True和False |
RadioField | 一组单选框 |
SelectField | 下拉列表 |
SelectMultipleField | 下拉列表,可选多个值 |
FileField | 文件上传字段 |
SubmitField | 表单提交按钮 |
StringField | 文本字段 |
FormField | 把表单作为字段嵌入另一个表单 |
FieldList | 一组指定类型的字段 |
4.3 把表单渲染成HTML
表单字段是可调用的,在模板中调用后会渲染成HTML。假设视图函数把一个NameForm实例通过参数form传入模板,在模板中生成一个简单的表单,如下所示:
<form method="post">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
当然,这个表单还很简陋。想要改变表单的外观,可以把参数传入渲染字段的函数,传入的参数会被转换为HTML属性。例如,可以为字段指定id或class属性,然后定义CSS样式。
<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,上述表单可使用下面的方式渲染:
{% import "bootstrap/wtf.thml" as wtf %}
{{ wtf.quick_form(form) }}
导入的bootstrap/wtf.html文件中定义了一个使用Bootstrap渲染Flask-WTF表单对象的辅助函数。wtf.quick_form()函数中的参数为Flask-WTF表单对象,使用Bootstrap的默认样式渲染传入的表单。hello.py的完整模板如下所示:
示例 templates/index.html : 使用Flask-WTF和Flask-Bootstrap渲染表单
{% 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指令之间的值,否则渲染else和endif指令之间的值。在这个例子中,如果没有定义模板变量name,则会渲染字符串Hello,Stranger!
内容区的第二部分使用wtf.quickform()函数渲染NameForm
对象。
4.4 在视图函数中处理表单
在新版hello.py中,视图函数index()不仅要渲染表单,还要接收表单中的数据。更新后的index()视图函数:
示例 hello.py:路由方法
@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请求。form.validate_on_submit()会调用form类的name字段上附属的DataRequired()验证函数。如果名字不为空,就能通过验证,validate_on_submit()返回True。现在用户输入的名字可以通过字段的data属性获取。在if语句中,把名字赋值给局部变量name,然后再把data属性设为空字符串,从而清空表单字段。最后一行会调用render_templates()函数渲染模板,但这一次参数name的值为表单中输入的名字,因此会显示一个针对该用户的欢迎消息。
4.5 重定向与用户回话
from flask import Flask,render_template,url_for,redirect,session
@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'))
在程序的前一个版本中,局部变量name被用于存储用户在表单中输入的名字,这个变量现在保存在用户会话中,即session['name'],所以在两次请求之间也能记住输入的值。
from flask import flash
@app.route('/')
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('Name Changed!')
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html',form = form,name = session.get('name'))
在这个示例中,每次提交的名字都会和存储在用户会话中的名字进行比较,而会话中存储的名字是前一次在表单中提交的数据,如果两个名字不一样,就会调用flash()函数,在发给客户端的下一个响应中显示一个消息。
示例 templates/base.html:渲染Flash消息
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class = "alert alert-warning">
<button type = 'button' class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
</div>
在这个示例中,使用了Bootstrap提供的警报CSS样式渲染警告信息。
在模板中使用循环是因为在之前的请求循环中每次调用flash()函数都会生成一个消息,所以可能有多个消息在排队等待显示。get_flashed_messages()函数获取的消息在下次调用时不会再次返回,因此Flash消息只显示一次,然后就消失了。
从Web表单中获取用户输入的数据是大多数程序都需要的功能,把数据保存在永久存储器中也是一样。下一章将会介绍如何在Flask中使用数据库。