Flask Web开发 4.0 Web表单

第二章介绍的请求对象包含客户端发出的所有请求信息,其中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一组指定类型的字段
WTForms内建的验证函数:

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的值为表单中输入的名字,因此会显示一个针对该用户的欢迎消息。
首次访问时,效果:
输入并提交名字之后:
如果没有输入就点击Submit,那么DataRequired()验证函数会捕获这个错误,如图:

一定要仔细分析分析这个生成表单、提交表单的过程。

4.5 重定向与用户回话

现在,我们发现hello.py的一个可用性问题:当用户输入名字提交表单,然后点击浏览器的刷新按钮,会看到一个莫名其妙的警告:
之所以出现这种情况,是因为刷新页面时浏览器会重新发送之前发送过的最后一个请求。如果这个请求是个包含表单数据的POST请求,刷新页面会再次提交该表单。大多数情况下,这并不是理想的处理方式。基于这个原因,最好别让我们的Web程序把POST请求作为浏览器发送的最后一个请求。
这种需求的实现方式是 ,使用重定向作为POST请求的响应,而不是使用常规响应。重定向是一种特殊的响应,响应内容是URL,而不是包含HTML代码的字符串。浏览器收到这种响应时,会向重定向的URL发起GET请求,显示页面的内容。这个页面的加载可能要多花几微秒,因为要先把第二个请求发送给服务器。除此之外,用户不会察觉到有什么不同。现在,最后一个请求是GET请求,所以刷新命令就能像预期的那样正常使用了。这个技巧称为Post/重定向/Get模式。
但是这种方法会出现另一个问题。程序处理POST请求时,使用form.name.data获取用户输入的名字,可是一旦这个请求结束,数据也就丢失了。因为这个POST请求使用重定向处理,所以程序需要保存输入的名字,这样重定向后的请求才能获得并使用这个名字,从而构建真正的响应。
程序可以把数据存储在用户会话中,在请求之间“记住”数据。用户会话是一种私有存储,存在于每个连接到服务器的客户端中。我们在第二章中介绍过用户会话,它是请求上下文中的变量,名为session,像标准的Python字典一样操作。
tip:默认情况下,用户会话会保存在客户端cookie中,使用设置的SECRET_KEY进行加密签名。如果篡改了cookie中的内容,签名就会失效,会话也会随之失效。
示例 hello.py :实现了重定向和用户会话的index()新版本
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'],所以在两次请求之间也能记住输入的值。
现在,包含合法表单数据的请求最后会调用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。
使用这个版本的程序,刷新页面,效果将会和我们预期的一样。
4.6 Flash消息
请求完成后,有时需要让用户知道状态发生了变化。这里可以使用确认消息、警告或者错误提醒。一个典型例子是,用户提交了一项错误的登录表单后,服务器发回的响应重新渲染了登录表单,并在表单上面显示一个消息,提示用户用户名或密码错误。
这种功能是Flask的核心特性。flash()函数可以实现这种效果。
示例 hello.py:Flash消息
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()函数,在发给客户端的下一个响应中显示一个消息。
仅调用flash()函数并不能把消息显示出来,程序使用的模板要渲染这些消息。最好在基模板中渲染Flash消息,因为这样所有的页面都能使用这些消息。Flask把get_flashed_message()函数开放给模板,用来获取并渲染消息。

示例 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中使用数据库。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值