第4章 Web表单
我们在第二章介绍过请求对象,它包含有客户端请求的全部信息。尤其是,可以通过request.form访问通过POST请求提交的所有表单数据。
虽然Flask的请求对象支持处理web表单,但实际上要做的工作既多又冗长重复。最具有代表性的就是生成html格式的Web代码和验证提交数据的有效性。
Flask-WTF扩展使处理表单工作变成一种愉悦的体验。这个扩展是Flask对agnostic框架的WTForms包装集成而来的。
Flask-WTF及其依赖可以通过pip安装:
(venv)$pip install flask-wtf
跨站请求伪造(CSRF)防护
Flask-WTF默认配置为保护所有表单防御CSRF攻击。所谓CSRF攻击就是恶意站点向冒用受害者身份信息向其登陆的网站发送请求。
为了实现CSRF保护,Flask-WTF需要程序配置加密密钥。Flask-WTF使用该密钥生成令牌以确认请求的表单数据合法可信。例子4-1展示了如何配置密钥。
Example 4-1. hello.py: Flask-WTF configuration
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your hard to guess string'
app.config字典通常存储了各类配置变量以供框架、扩展或者是程序自身调用。使用标准字段语法即可向app.config对象添加配置值。该配置对象也有相应方法可以从文件或环境配置中导入配置值。
SECRET_KEY配置变量通常被Flask或其他一些第三方扩展用作加密的密钥。恰如其名,加密强度就与这个变量值是否足够难猜。所以为你每个程序都选择不同的密钥,且保证这个字符串无人知晓。
警告
为了更安全,这个密钥应该被存储在环境变量当中,这要好过嵌在代码里。这一情况在第七章有描述。
表单类
使用Flask-WTF时,每个表单都由继承自Form的一个类来表现。这个类定义了表单对象中的字段列表。每个字段对象可以有一个或多个验证器——检查用户提交的数据是否有效。
例子4-2展示了一个只有一个文本字段和提交按钮的简单web表单
Example 4-2. hello.py: Form class definition
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>元素。第一个字段构造函数的第一参数是label,用来在显示成html时使用。包含在stringField构造函数中的validators参数定义了一个检查器列表,在接收到用户提交数据后用来检查。Required()验证器则用来确保不提交空字段。
提醒
Flask-WTF扩展定义了Form基础类,所以应该从flask.ext.wtf中导入。而字段和验证器则直接从WTForms包中导入。
表4-1列出了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 指定类型的字段列表
表4-2列出了WTForms内置的验证器:
验证器 说明
Email 验证邮件地址
EqualTo 比较两个字段的值; 在需要比较重复输入密码时格外有用
IPAddress 验证 IPv4 网络地址
Length 验证输入字符产长度是否符合指定值
NumberRange 验证输入数值是否在指定范围内
Optional 允许输入字段值为空,跳过附加的验证器
Required 检查是否有值
Regexp 根据指定表达式验证是否符合
URL 检查 URL是否合法
AnyOf 检查输入是否符合列表中的某项
NoneOf 检查输入是否不符合列表中的全部项
表单的HTML显示
当调用时,表单字段从模板中将自己显示成html。假设视图函数把名为form的NameForm实例传递给模板,模板将生成一个简单的html表单,如下:
<form method="POST">
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
当然啦,有点简陋。为了改进表单外观,我们给调用传递一些参数把它们显示成html字段属性。那么,你可以给字段添加上id或者class属性来定义css样式:
<form method="POST">
{{ form.name.label }} {{ form.name(id='my-text-field') }}
{{ form.submit() }}
</form>
但是,即使带上了html属性,通过这种方法显示表单也很不可取。最好的办法就是无论何时都利用Bootstrap自身的form格式来定义。只需要简单调用Flask-Bootstrap提供的高水平辅助函数,就可以使用bootstrap预定义Form样式来显示flask-WTF表单。使用Flask-Bootstrap,上面的例子可以显示如下:
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
类似在普通python代码中那样,import指令允许倒入模板元素并可以在多个模板中使用。导入的bootstrap/wtf.html
文件定义了使用Bootstrap来显示Flask-WTF的辅助函数。wtf.quick_form()函数获取flask-wtf表单对象并用默认的bootstrap样式显示。完成的hello.py模板如例子4-3所示:
Example 4-3. templates/index.html: Using Flask-WTF and Flask-Bootstrap to render a form
{% 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 %}
模板的content区域目前有两段。第一段是显示欢迎信息的页头部分。此处使用了模板条件判断。Jinja2中的条件判断格式是
{% if variable %}
...
{% else %}
...
{% endif %}
如果条件为真,就把if和else指令之间的内容显示到模板中。如果条件为假,则输出else和endif 之间的内容。如果name参数未定义的话,示例模板将显示输入"Hello,Stranger!"。content的第二段则使用wtf.quick_form()函数显示输出NameForm对象。
视图函数中的表单处理
在新版的hello.py中,index()视图函数将显示表单并接收其再次提交的数据。例子4-4显示了更新后index()视图函数:
@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添加到method列表中是必须的,因为绝大部分表单提交作为POST请求来处理更为方便。以GET请求方式来提交表单也是可行的,但GET请求没有body(只有头部?)数据是作为URL查询字符串附加在URL上,在浏览器地址栏里是可见的。因此和因其他一些原因,表单提交绝大部分是以POST请求的形式处理的。
本地变量name用来存储表单中有效的name数据;如果表单中的name无效,那么变量name将被初始化为None。视图函数提前创建NameForm类的实例以显示表单。当表单提交后,如果所有数据验证通过validate_on_summit()方法将返回True。否则返回False。服务器根据这个返回值决定重新显示表单还是进行下一步处理。
当用户第一次访问时,服务器会接收到没有表单数据的GET请求,这时validate_on_submit()将返回False,if声明的主体部分将被跳过,转而根据表单对象渲染模板,把参数name变量设置为None。用户就会看到浏览器中显示出表单。
当用户提交表单,服务器会接收到带有数据的POST请求。在validate_on_submit()中会对name字段调用required()验证器。如果name不为空,验证器会接受它,validate_on_submit()返回True。现在用户输入的name可以作为字段的data属性来访问。在if声明的主体内部,name被赋值给本地变量name,通过设置data属性为空(空字符串)来清空表单字段。在最后一行,使用render_template()显示模板,但这一次,name参数已被表单中的name字段赋值,所以就显示个性化的欢迎信息了。
图4-1显示当用户第一次访问站点时的页面样子。当他提交一个名字后,程序将返回个性化的欢迎信息。而表单仍旧显示在下方,需要的话用户可以输入另外一个名字。
图4-2:输入姓名后,显示个性化的欢迎信息
如果用户留空name进行提交,required()验证器将捕捉这一错误,就像图4-3显示那样。
图4-3
注意,这里实现了很多自动功能哦。这是一个绝佳的例子,很好的展示了像Flask-WTF和Flask-Bootstrap这样拥有良好设计的扩展的能给你的程序带来的强大助力。
重定向和用户会话
最新版本的hello.py还有一个可用性问题。如果你输入你的名字提交后,再点击浏览器的刷新按钮,你可能看到一个模糊的警告,要求你确认再次提交表单。这是因为刷新浏览器页面时,浏览器会重复最后一个请求。如果最后一个请求是带有表单数据的POST,这个刷新就很可能导致数据的重复提交——这个动作往往是不希望发生的。
很多用户不理解浏览器的这个警告。因此,永远不要把POST请求作为浏览器发出的最后一个请求,这点是web程序公认的好惯例。
这一惯例可以通过使用带有重定向(redirect)的POST请求替代普通POST来实现。重定向是一种特殊的响应,它使用url替代了html代码字符串。当浏览器接受到这个响应,它就向重定向的URL地址发起一个GET请求,也就是要显示的页面。该页面可能要花费几微秒来加载——因为这是发送给服务器的第二个请求,当然啦,用户不会知道其中的差异。现在最后一个请求是GET,所以刷新命令就会正常工作了。这个小窍门来自于Post/Redriect/GET pattern。
但是,注意,这又带来了第二个问题。当程序处理POST请求的时候,他在form.name.data访问到了用户输入的name,但随着请求(POST)一结束,表单数据就丢失了(重定向因为数据为空而将无法正确响应)。因为POST请求是和一个重定向一起处理的,程序需要存储name以便于重定向请求能够获取到它来构建正确的响应。
程序可以在相邻请求之间“记住”一些东西——通过把他们存储在“用户会话”当中,对每个连接的客户端来说都是私密存储。在第二章中,用户会话作为一个和请求上下文相关的变量被介绍过。他被称为"会话"(session)并可以像标准Python字典一样被访问。
提醒
默认情况下,用户会话被使用SECRET_KEY加密后存储在客户端的cookie中。任何对cookie内容的篡改都会导致签名无效,同时让会话也失效
例子4-5展示了视图函数index()的新版本,它实现了重定向和用户会话:
Example 4-5. hello.py: Redirects and user sessions
from flask import Flask, render_template, session, redirect, url_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'))
在前一版本中,本地变量name被用来存储用户表单中输入的name。现在这个变量被存储在用户会话当中session['name'],这样在这个请求之后也会被记住。
现在来自于有效表单数据的请求随着redirect()——一个生成http重定向响应的辅助函数——的调用而结束。redirect()函数以要转向的URL作为参数。本例中使用的重定向url是根url,所以虽然也可以简单的写成redirect('/')
,但仍旧使用了Flask的URL生成函数url_for()。这是因为这个函数使用URL映射,它保证了与已定义的路由兼容并且可以在路由名称发生变化时自动生效。我们推荐你使用这个函数。
url_for()唯一一个参数就是“结束点(endpoint)”名称——也就是每个路由的内部名称。默认情况下,路由的结束点名称就是其对应的视图函数名。本例中,处理根URL的视图函数是index(),所以传给url_for()的是index。
最后一个变化就是在render_template()函数中,现在他使用session.get('name')直接从会话中获取name值。就像对普通字典操作一样,使用get()请求字典的键可以避免找不到该键而则触发错误。因为get()不存在的键时,将返回默认值None。
在这个版本的程序中,你可以看到以你希望的方式刷新页面。
闪现信息
有时候,在请求完成后给予用户一个状态更新的提醒是很有用处的。它可以在客户端闪现一个确认消息或警告或者一个错误(仅限于当前请求应答周期)。一个典型的例子就是当你提交有错误登录表单给网站,服务器将返回一个带有无效用户名或密码的错误提示信息的登录表单。
Flask将这一功能放在核心功能里。例子4-6展示了如何使用flash()函数来实现这一点。
<small>Example 4-6. hello.py: Flashed messages</small>
from flask import Flask, render_template, session, redirect, url_for, flash
@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
form.name.data = ''
return redirect(url_for('index'))
return render_template('index.html',form = form, name = session.get('name'))
在本例中,每次提交的name都会被拿来跟保存在用户会话中的上一次同一表单提交的name做比较,如果两者不一样,flash()函数就会被调用,带着在下一响应被发送回客户端时显示的信息。仅仅呼叫flash()并不足以显示出信息,还需要在程序的对应模板中显示它。显示闪现消息最好的地方就是在基础模板中,因为这样一来所有页面都会自动显示。Flask创建了get_flahsed_message()函数来获取并在模板中显示闪现消息。例子4-7展示了这一点:
Example 4-7. templates/base.html: Flash message rendering
{% 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>
{% endblock %}
在这个例子里,我们使用了bootstrap的警告样式来显示消息。这里使用了循环来逐条显示——可能在上一个请求应答周期中多次调用了falsh(),从而生成一个消息队列。get_flashed_messages()获取到的消息不会被转到下一次调用这个函数的时候,所以这些消息仅出现一次就消失了(仅限于本会话周期)。
能够通过表单来获取用户数据是大部分程序的必备功能,所以能持久存储数据的能力也是必不可少。下一章的主题就是Flask使用数据库。
<<第三章 模板 第五章 EMail>>