在flask中处理表单使用flask-wtf非常方便,涉及到的插件和包有Flask-WTF,WTForms。内容有表单的创建使用流程,一些最佳实践,还有在页面显示提示消息的简单方式,配合Flask内置的 flash()。

Flask的requset对象包含了client端发送过来的所有请求,在request.form中就有POST方法提交过来的表单数据。直接使用这些数据可以搞定表单的操作,不过不方便,于是有了Flask-WTF这个插件,它将WTForms这个包嵌入Flask里,简化Flask下的使用。

pip安装flask-wtf:

(venv) $ pip install flask-wtf

WTForms应该也同时被安装了。

跨站请求伪造(Cross-Site Request Forgery,CSRF) 保护

CSRF的原理不具体讲了,很简单,感兴趣直接网上搜即可。

Flask-WTF默认提供对CSRF的保护。应用里需要设置一个加密用的key,Flask-WTF利用这个key生成一个加密的记号来验证request带过来的表单数据。看看实例:

app = Flask(__name__)
app.config['SECRET_KEY'] = 'www.h3blog.com'

app.config 是应用保存配置的一个字典。可以直接在字典里增加配置。SECRET_KEY这个配置变量被Flask和一些第三方插件使用,对不同的应用配置不同的值增加点可靠性。

另外,这个值最好放到环境变量里,直接写到代码里不太好。

表单类

使用Flask-WTF的时候,每一个表单都是类的形式,这个类需要继承自Form。这个类里定义一些代表表单各类域的对象,每个对象可以有多个验证器(validators)。验证器可以确保用户的输入是有效的。

原例子:

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import Required

class NameForm(FlaskForm):
    name = StringField('名字', validators=[Required()])
    submit = SubmitField('提交')

表单中的域在类中都定义成类变量。上例中,NameForm类里有文本域name和提交按钮submit两个。StringField代表有type="text"属性的input元素。SubmitField代表有type="submit"属性的input元素。构造器的第一个参数是后续渲染表单时候用到的标签(label)。

下例是一个带有文本域和提交按钮的表单例子:

from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, PasswordField,SubmitField
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
    openid = StringField('openid', validators=[DataRequired()])
    remember_me = BooleanField('remember_me', default = False)
    password = PasswordField('password',validators=[DataRequired()])
    submit = SubmitField('submit')

表单中的域在类中都定义成类变量。上例中,LoginForm类里有字符串域openid,复选框remember_me, 密码域password,提交按钮submit。分别代表小面信息:

<input id="openid" name="openid" type="text" value="">
<input id="remember_me" name="remember_me" type="checkbox" value="y">
<input id="password" name="password" type="password" value="">
<input id="submit" name="submit" type="submit" value="submit">

构造器的第一个参数是后续渲染表单时候用到的标签(label)。

在StringField里的validators参数定义了一些验证器,这些验证器会在用户提交数据前检查数据是否有效。Required验证器确保提交的内容不能为空。

WTForms提供的各种HTML域:

StringField                 文本
TextAreaField               多行文本
PasswordField               密码类文本
HiddenField                 隐藏文本
DateField                   接收给定格式的 datetime.datevalue 的文本
DateTimeField               接收给定格式的 datetime.datetimevalue 的文本T
IntegerField                接收整数的文本
DecimalField                接收decimal.Decimal类型值的文本
FloatField                  接收浮点类型值的文本
BooleanField                选是否的复选框
RadioField                  包含多个互斥选项的复选框
SelectField                 下拉菜单
SelectMultipleField         可多选的下拉菜单
FileField                   文件上传
SubmitField                 提交
FormField                   讲一个表单作为域放入另一个表单里
FieldList                   一组给定类型的域

WTForms提供的各种验证器:

validator                   Description
Email                       邮箱格式
EqualTo                     比较两个域的值例如在要求输入两次密码的时候
IPAddress                   IPv4 地址
Length                      按字符串的长度验证
NumberRange                 输入数字需在某范围内
Optional                    允许不填不填的时候就忽略其他验证器
Required                    必填
Regexp                      通过一个正则表达式验证
URL                         URL格式
AnyOf                       属于一组可能值中的一个 
NoneOf                      不属于一组可能值中的任何一个

渲染表单

表单的各类域在模板中渲染时表现为可调用的对象。假设将一个NameForm的实例name作为参数传入模板。

<form method="POST">
    {{ form.name.label }} {{ form.name() }}
    {{ form.submit() }}
</form>

这样渲染出来的页面不美观,可以尝试改进下,在调用里传入一些参数,这些参数都会被转化为这个域的属性。然后你可以用CSS自己搞定美化问题:

<form method="POST">
    {{ form.name.label }} {{ form.name(id='my-text-field') }}
    {{ form.submit() }}
</form>

上述方式显然很累,之前加入了Bootstrap的支持,Flask-Bootstrap插件其实也对Flask-WTF创建的表单有高层接口的支持,可以用Bootstrap来修饰一下。然后表单的模板就可以简单写成:

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

从其他模板import个函数进来之前提到过,wtf.quick_form函数接受一个Flask-WTF的表单,然后用Bootstrap默认的样式渲染。

现在,首页index.html已经改为:

{% 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 %}

这里还用了一个if else结构,如果传入了name,就显示传入的值,否则就显示Stranger。

表单的各类域在模板中渲染时表现为可调用的对象。假设将一个LoginForm的实例openid作为参数传入模板。

<form action="" method="post" name="login">
    {{form.hidden_tag()}}
        {{ form.openid.label }}
        {{form.openid(size=80)}}
        {% for error in form.openid.errors %}
            <span style="color: red;">{{ error }}</span>
        {% endfor %}<br>

        密码:
        {{ form.password }}
        {% for error in form.password.errors %}
            <span style="color: red;">{{ error }}</span>
        {% endfor%}
        {{form.remember_me}} Remeber Me</p>
        提交:
        {{ form.submit(value='登录') }}
</form>

视图函数中的表单处理

修改hello.py,在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参数,这里是把index()注册为GET和POST请求的处理者。如果不提供methods这个参数,试图函数默认只处理GET请求。

这里对index()增加视图函数对POST请求的支持是必须的,因为用户的提交操作使用POST请求更方便处理。使用GET请求来提交表单也可以,但是GET请求的数据都是附加在URL后面作为请求字符串,在浏览器的地址栏可以看到。由此,以及一些其他原因,表单的提交通常都是用POST请求完成的。

继续看改动后的代码,form.validate_on_submit()这个方法,只在用户提交了数据并且数据通过验证器的检查之后,才返回True,其他时候都返回False。用这个方法判断是否对模板进行处理。

看下一般处理流程。用户第一次访问这个应用,使用的是GET请求,不带数据,form.validate_on_submit()这个方法返回False,return就返回一个空白的表单,name值是None。

用户提交了表单后,sercer收到携带数据的POST请求,form.validate_on_submit()这个方法会启动之前设置的Required()验证器,这里name不为空就通过验证,form.validate_on_submit()返回True。然后提取出表单数据保存下来,把表单数据清空。再把name和表单传入render_template(),重新渲染的模板就有了变化。

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    print form.openid
    print form.remember_me
    print form.password
    print form.submit

    if form.validate_on_submit():
        flash('Login requested for OpenID="' + form.openid.data + '", remember_me=' + str(form.remember_me.data) + ' password: '
)
        return redirect('/index')
    return render_template('login.html', title = 'Sign In', form = form)