摘自 李辉《Flask Web开发实战:入门、进阶与原理解析》
WTForms支持在Python中使用类定义表单,然后直接通过类定义生成对应的HTML代码。
使用Flask-WTF处理表单
扩展Flask-WTF集成了WTForms,使用它可以在Flask中更方便地使用WTForms。Flask-WTF将表单数据解析、CSRF保护、文件上传等功能与Flask集成。
Flask-WTF默认为每个表单启用CSRF保护,它会为我们自动生成和验证CSRF令牌。默认情况下,Flask-WTF使用程序密钥来对CSRF令牌进行签名,所以我们需要为程序设置密钥:
app.secret_key = 'secret string'
定义WTForms表单类
当使用WTForms创建表单时,表单由Python类表示,这个类继承从WTForms导入的Form基类。一个表单由若干个输入字段组成,这些字段分别用表单类的类属性来表示:
>>> from wtforms import Form, StringField, PasswordField, BooleanField, SubmitField
>>> from wtforms.validators import DataRequired, Length
>>> class LoginForm(Form):
... username = StringField('Username', validators=[DataRequired()])
... password = PasswordField('Password', validators=[DataRequired(), Length(8, 128)])
... remember = BooleanField('Remember me')
... submit = SubmitField('Log in')
每个字段属性通过实例化WTForms提供的字段类表示。字段属性的名称将作为对应HTML<input>元素的name属性及id属性值。
字段属性名称大小写敏感,不能以下划线或validate开头。
![ad25776bca892d8e77a054548481d23b.png](https://img-blog.csdnimg.cn/img_convert/ad25776bca892d8e77a054548481d23b.png)
通过实例化字段类时传入的参数,我们可以对字段进行设置。
![a20d2997ff5877b6b78c93b036b0d66c.png](https://img-blog.csdnimg.cn/img_convert/a20d2997ff5877b6b78c93b036b0d66c.png)
在WTForms中,验证器(validator)是一系列用于验证字段数据的类,我们在实例化字段类时使用validators关键字来指定附加的验证器列表。验证器从wtforms.validators模块中导入。
![add16aa5f7d70e286bd6afa91f24599e.png](https://img-blog.csdnimg.cn/img_convert/add16aa5f7d70e286bd6afa91f24599e.png)
在实例化验证类时,message参数用来传入自定义错误消息,如果没有设置则使用内置的英文错误消息。
name = StringField('Your Name', validators=[DataRequired(message=u'名字不能为空!')])
validators参数接收一个传入可调用对象组成的列表。内置的验证器通过实现了__call__()方法的类表示,所以我们需要在验证器后添加括号。
当使用Flask-WTF定义表单时,我们仍然使用WTForms提供的字段类和验证器,创建的方式也完全相同,只不过表单类要继承Flask-WTF提供的FlaskForm类。FlaskForm类继承自Form类,进行了一些设置,并附加了一些辅助方法,以便与Flask集成。
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired(), Length(8, 128)])
remember = BooleanField('Remember me')
submit = SubmitField('Log in')
配置键WTF_CSRF_ENABLED用来设置是否开启CSRF保护,默认为True。Flask-WTF会自动在实例化表单类时添加一个包含CSRF令牌值的隐藏字段,字段名为csrf_token。
输出HTML代码
实例化表单类,然后将实例属性转换成字符串或直接调用就可以获取表单字段对应的HTML代码:
>>> form = LoginForm()
>>> form.username()
u'<input id="username" name="username" type="text" value="">'
>>> form.submit()
u'<input id="submit" name="submit" type="submit" value="Submit">'
字段的<label>元素的HTML代码则可以通过“form.字段名.label”的形式获取:
>>> form.username.label()
u'<label for="username">Username</label>'
>>> form.submit.label()
u'<label for="submit">Submit</label>'
在创建HTML表单时,我们经常会需要使用HTML<input>元素的其他属性来对字段进行设置。比如,添加class属性设置对应的CSS类为字段添加样式;添加placeholder属性设置占位文本。默认情况下,WTForms输出的字段HTML代码只会包含id和name属性,属性值均为表单类中对应的字段属性名称。如果要添加额外的属性,通常有两种方法。
1.使用render_kw属性
比如下面为username字段使用render_kw设置了placeholder HTML属性:
username = StringField('Username', render_kw={'placeholder': 'Your Username'})
这个字段被调用后输出的HTML代码如下所示:
<input type="text" id="username" name="username" placeholder="Your Username">
2.在调用字段时传入
在调用字段属性时,通过添加括号使用关键字参数的形式也可以传入字段额外的HTML属性:
>>> form.username(style='width: 200px;', class_='bar')
u'<input class="bar" id="username" name="username" style="width: 200px;" type="text">'
class是Python的保留关键字,在这里我们使用class_来代替class,渲染后的<input>会获得正确的class属性,在模板中调用时则可以直接使用class。
通过上面的方法也可以修改id和name属性,但表单被提交后,WTForms需要通过name属性来获取对应的数据,所以不能修改name属性值。
在模板中渲染表单
为了能够在模板中渲染表单,我们需要把表单类实例传入模板。
from forms import LoginForm
@app.route('/basic')
def basic():
form = LoginForm()
return render_template('login.html', form=form)
在模板中,只需要调用表单类的属性即可获取字段对应的HTML代码,如果需要传入参数,也可以添加括号:
<form method="post">
{{ form.csrf_token }} <!-- 渲染CSRF令牌隐藏字段 -->
{{ form.username.label }}{{ form.username }}<br>
{{ form.password.label }}{{ form.password }}<br>
{{ form.remember }}{{ form.remember.label }}<br>
{{ form.submit }}<br>
</form>
Flask-WTF为表单类实例提供了一个form.hidden_tag()方法,这个方法会依次渲染表单中所有的隐藏字段。因为csrf_token字段也是隐藏字段,所以当这个方法被调用时也会渲染csrf_token字段。
处理表单数据
从获取数据到保存数据大致会经历以下步骤:
- 解析请求,获取表单数据。
- 对数据进行必要的转换,比如将勾选框的值转换成Python的布尔值。
- 验证数据是否符合要求,同时验证CSRF令牌。
- 如果验证未通过则需要生成错误消息,并在模板中显示错误消息。
- 如果通过验证,就把数据保存到数据库或做进一步处理。
使用Flask-WTF和WTForms可以极大地简化这些步骤。
提交表单
在HTML中,当<form>标签声明的表单中类型为submit的提交字段被单击时,就会创建一个提交表单的HTTP请求,请求中包含表单各个字段的数据。表单的提交行为主要由三个属性控制:
![1ad5656410862eababe2f1b2cc4f7e8f.png](https://img-blog.csdnimg.cn/img_convert/1ad5656410862eababe2f1b2cc4f7e8f.png)
当使用GET方法提交表单数据时,表单的数据会以查询字符串的形式附加在请求的URL里,GET方式仅适用于长度不超过3000个字符,且不包含敏感信息的表单。
出于安全的考虑,我们一般使用POST方法提交表单。使用POST方法时,按照默认的编码类型,表单数据会被存储在请求主体中,比如:
POST /basic HTTP/1.0
...
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
username=greyli&password=12345
验证表单数据
客户端验证
客户端验证(client side validation)是指在客户端(比如Web浏览器)对用户的输入值进行验证。比如,使用HTML5内置的验证属性即可实现基本的客户端验证(type、required、min、max、accept等)。比如,下面的username字段添加了required标志:
<input type="text" name="username" required>
除了使用HTML5提供的属性实现基本的客户端验证,我们通常会使用JavaScript实现完善的验证机制。
客户端方式可以实时动态提示用户输入是否正确,只有用户输入正确后才会将表单数据发送到服务器。客户端验证可以增强用户体验,降低服务器负载。
服务器端验证
服务器端验证(server side validation)是指用户把输入的数据提交到服务器端,在服务器端对数据进行验证。如果验证出错,就在返回的响应中加入错误信息。用户修改后再次提交表单,直到通过验证。
WTForms验证机制
WTForms验证表单字段的方式是在实例化表单类时传入表单数据,然后对表单实例调用validate()方法。这会逐个对字段调用字段实例化时定义的验证器,返回表示验证结果的布尔值。如果验证失败,就把错误消息存储到表单实例的errors属性对应的字典中。
>>> from wtforms import Form, StringField, PasswordField, BooleanField
>>> from wtforms.validators import DataRequired, Length
>>> class LoginForm(Form):
... username = StringField('Username', validators=[DataRequired()])
... password = PasswordField('Password', validators=[DataRequired(), Length(8, 128)])
>>> form = LoginForm(username='', password='123')
>>> form.data # 表单数据字典
{'username': '', 'password': '123'}
>>> form.validate()
False
>>> form.errors # 错误消息字典
{'username': [u'This field is required.'], 'password': [u'Field must be at least 6 characters long.']}
>>> form2 = LoginForm(username='greyli', password='123456')
>>> form2.data
{'username': 'greyli', 'password': '123456'}
>>> form2.validate()
True
>>> form2.errors
{}
因为我们的表单使用POST方法提交,如果单纯使用WTForms,我们在实例化表单类时需要首先把request.form传入表单类,而使用Flask-WTF时,表单类继承的FlaskForm基类默认会从request.form获取表单数据,所以不需要手动传入。
使用POST方法提交的表单,其数据会被Flask解析为一个字典,可以通过请求对象的form属性获取(request.form);使用GET方法提交的表单的数据同样会被解析为字典,不过要通过请求对象的args属性获取(request.args)。
在视图函数中验证表单
from flask import request
...
@app.route('/basic', methods=['GET', 'POST'])
def basic():
form = LoginForm() # GET + POST
if request.method == 'POST' and form.validate():
... # 处理POST请求
return render_template('forms/basic.html', form=form) # 处理GET请求
表单类的data属性是一个匹配所有字段与对应数据的字典,我们一般直接通过“form.字段属性名.data”的形式来获取对应字段的数据。
from flask import Flask, render_template, redirect, url_for, flash
...
@app.route('/basic', methods=['GET', 'POST'])
def basic():
form = LoginForm()
if form.validate_on_submit():
username = form.username.data
flash('Welcome home, %s!' % username)
return redirect(url_for('index'))
return render_template('basic.html', form=form)
在模板中渲染错误消息
如果form.validate_on_submit()返回False,那么说明验证没有通过。对于验证未通过的字段,WTForms会把错误消息添加到表单类的errors属性中。我们一般会直接通过字段名来获取对应字段的错误消息列表,即“form.字段名.errors”。
<form method="post">
{{ form.csrf_token }}
{{ form.username.label }}<br>
{{ form.username() }}<br>
{% for message in form.username.errors %}
<small class="error">{{ message }}</small><br>
{% endfor %}
{{ form.password.label }}<br>
{{ form.password }}<br>
{% for message in form.password.errors %}
<small class="error">{{ message }}</small><br>
{% endfor %}
{{ form.remember }}{{ form.remember.label }}<br>
{{ form.submit }}<br>
</form>