Flask Web Development 第四章读书笔记 Web表单

第四章 Web表单

序:为什么需要Flask-wtf

第 2 章中介绍的请求对象包含客户端发出的所有请求信息。
其中, request.form 能获取POST 请求中提交的表单数据。
尽管 Flask 的请求对象提供的信息足够用于处理 Web 表单,
但有些任务很单调,而且要重复操作。
比如,生成表单的 HTML 代码和验证提交的表单数据。

Flask-WTF( http://pythonhosted.org/Flask-WTF/) 扩展可以把处理 Web 表单的过程变成一种愉悦的体验。
这个扩展对独立的 WTForms( http://wtforms.simplecodes.com)包进行了包装,方便集成到 Flask 程序中。

Flask-WTF 及其依赖可使用 pip 安装:
(venv) $ pip install flask-wtf

4.1 跨站请求伪造保护

为何需要CSRF保护

默认情况下, Flask-WTF 能保护所有表单免受跨站请求伪造
( Cross-Site Request Forgery,CSRF)的攻击。
恶意网站把请求发送到被攻击者已登录的其他网站时就会引发 CSRF 攻击。
为了实现 CSRF 保护,
Flask-WTF 需要程序设置一个密钥。

如何设置密钥

Flask-WTF 使用这个密钥生成加密令牌,
再用令牌验证请求中表单数据的真伪。
例:hello.py: 设置 Flask-WTF

app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'

app.config 字典可用来存储框架、扩展和程序本身的配置变量。
使用标准的字典句法就能把配置值添加到 app.config 对象中。
这个对象还提供了一些方法,
可以从文件或环境中导入配置值。

SECRET_KEY 配置变量是通用密钥,
可在 Flask 和多个第三方扩展中使用。
如其名所示,加密的强度取决于变量值的机密程度。
不同的程序要使用不同的密钥,
而且要保证其他人不知道你所用的字符串。

为了增强安全性,密钥不应该直接写入代码,
而要保存在环境变量中。这一技术会在第 7 章介绍。

4.2 表单类

表单的结构

使用 Flask-WTF 时,
每个 Web 表单都由一个继承自 Form 的类表示。
这个类定义表单中的一组字段,
每个字段都用对象表示。
字段对象可附属一个或多个验证函数。
验证函数用来验证用户提交的输入值是否符合要求。

一个简单的web表单

from flask_wtf import FlaskForm  # 0.13开始不推荐原书的Form
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired  # 原书是Required,官网最新示例为DataRequired
 
class NameForm(Form):
    name = StringField('What is your name?', validators=[DataRequired()])
    submit = SubmitField('Submit')

在这个示例中,
NameForm 表单中有一个名为 name 的文本字段
和一个名为 submit 的提交按钮。

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 一组指定类型的字段

WTForms验证函数

验证函数 说明
Email 验证电子邮件地址
EqualTo 比较两个字段的值;常用于要求输入两次密码进行确认的情况
IPAddress 验证 IPv4 网络地址
Length 验证输入字符串的长度
NumberRange 验证输入的值在数字范围内
Optional 无输入值时跳过其他验证函数
Required 确保字段中有数据
Regexp 使用正则表达式验证输入值
URL 验证 URL
AnyOf 确保输入值在可选值列表中
NoneOf 确保输入值不在可选值列表中

4.3 把表单渲染成HTML

如何调用上节的NameForm表单

可以通过参数form传入模板,例如:

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

<form>标签是html语言用来显示表单的,
而缩进部分的form则是传入的参数,
是由hello.py中的render_temlate传进模板的。

如何渲染表单

这个表单还很简陋。要想改进表单的外观,
可以为字段指定 id 或 class 属性,
然后在CSS样式表里改变对应id或class的外观:

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

hidden_tag用来渲染所有的隐藏Field。

为什么使用Flask-Bootstrap渲染更好

即便能指定 id 或 class 属性,
但按照这种方式渲染表单的工作量还是很大,
所以在条件允许的情况下最好能使用 Bootstrap 中的预定义表单样式。

Flask-Bootstrap 使用预定义样式渲染整个 Flask-WTF 表单,
只需一次调用即可完成。

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

wtf.quick_form() 函数的参数为 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 %}

模板的内容区(page_content)现在有两部分。
第一部分是页面头部(page_header),
第二部分使用 wtf.quick_form() 渲染上节的NameForm 实例。
这个程序必须和下节重定义的index()一起使用才行。

Jinja2 中的条件语句格式为 {% if condition %}...{% else %}...{% endif %}。

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)

可以看到在路由中,多了一个methods,
后面除了默认值GET,还多了POST方法,
可以在网上搜下RESTFUL的简单说明。

NameForm的实例form会被Flask-Bootstrap渲染成网页上的表单,
只有当用户在表单中提交数据时,
才会执行if嵌套的语句,调用POST方法提交数据,
否则只是调用GET方法,显示空表单。

没必要把request.form(4.3第一个示例,form标签内容)传给Flask-wtf,
它自己会自动读取。
并且validate_on_submit会检查是否是POST,并且是否是有效数据。

因为每个表单都必须提交(submit),
所以validate_on_submit用来确认所有数据段的验证通过。

4.5 重定向和用户会话

POST后再刷新页面会出现警告

最新版的 hello.py 存在一个可用性问题。
用户输入名字后提交表单,
然后点击浏览器的刷新按钮,
会看到一个莫名其妙的警告,
要求在再次提交表单之前进行确认。

之所以出现这种情况,
是因为刷新页面时浏览器会重新发送之前已经发送过的最后一个请求。
如果这个请求是一个包含表单数据的 POST 请求,
刷新页面后会再次提交表单。
大多数情况下,这并不是理想的处理方式。

如何避免刷新时POST作为最后一个请求:重定向

很多用户都不理解浏览器发出的这个警告。
基于这个原因,
最好别让 Web 程序把 POST请求作为浏览器发送的最后一个请求。

既然最后一个请求不能是POST方法,
可以尝试在POST后自动添加一个GET方法,
这个GET方法用原来的参数重新获取当前页面。

这种需求的实现方式是使用重定向,
会在把POST+GET封装为一个GET请求,
刷新命令也就能像预期那样使用了。

重定向时会丢失原有的输入数据

但这种方法会带来另一个问题。
程序处理 POST 请求时,
使用 form.name.data 获取用户输入的名字,
可是一旦这个请求结束,
数据也就丢失了。

如果没有用户输入的数据,
重定向后的页面就如同用户没有输入。

如何保存这些数据:用户会话

程序可以把数据存储在用户会话中,
在请求之间“ 记住”数据。
每个连接到服务器的客户端中都有不同的用户会话。

我们在第 2 章介绍过用户会话,
它是请求上下文中的变量,
名为 session,
像标准的 Python 字典一样操作。

默认情况下,
用户会话保存在客户端 cookie 中,
使用设置的 SECRET_KEY 进行加密签名。
如果篡改了 cookie 中的内容,
签名就会失效,会话也会随之失效。

使用重定向和用户会话的Index函数

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('/')
return render_template('index.html', form=form, name=session.get('name'))

新的Index函数以用户会话存储输入的数据,
然后重定向到当前URL,
再把存储好的数据发送给URL对应的模板。

即用session['name']存储form.name.data
然后redirect到index对应的URL'/'
在把session['name']发送给'/'对应的模板。

推荐在重定向时,
使用redirect(url_for('index'))代替redirect('/'),
这样只要不改动index这个名字,
即使改动index对应的URL,
也可以正确地重定向。

form.name.data能够获取表单中name的值,
而session.get('name')则直接从会话中读取name的值。

使用session.get('name')而不是session['name']获取name的值,

可以避免发生未找到键的异常,
对于不存在的键,
get()会返回默认值None,
这点和普通字典一样。

4.6 Flash消息

为什么需要Flask消息

请求完成后,有时需要让用户知道状态发生了变化。
这里可以使用确认消息、警告或者错误提醒。
一个典型例子是,
用户提交了有一项错误的登录表单后,
服务器发回的响应重新渲染了登录表单,
并在表单上面显示一个消息,
提示用户用户名或密码错误。

这种功能是 Flask 的核心特性。
如下例所示,
flash() 函数可实现这种效果。

hello.py:Flash消息

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
        return redirect(url_for('index'))
return render_template('index.html',
    form = form, name = session.get('name'))

在这个示例中,
每次提交的名字都会和存储在用户会话中的名字进行比较,
而会话中存储的名字是前一次在这个表单中提交的数据。
如果两个名字不一样,
就会调用 flash() 函数,
在发给客户端的下一个响应中显示一个消息。

Flash消息暂时不能显示:需要渲染

仅调用 flash() 函数并不能把消息显示出来,
程序使用的模板要渲染这些消息。
最好在基模板中渲染 Flash 消息,
因为这样所有页面都能使用这些消息。
Flask 把 get_flashed_messages() 函数开放给模板,
用来获取并渲染消息,如下例所示。

渲染 Flash 消息:templates/base.html

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

在模板中使用循环是因为在之前的请求循环中,
每次调用 flash() 函数时都会生成一个消息,
所以可能有多个消息在排队等待显示。
get_flashed_messages() 函数获取的消息在下次调用时不会再次返回,
因此 Flash 消息只显示一次,然后就消失了。

在这个示例中,
使用 Bootstrap 提供的警报 CSS 样式渲染警告消息。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 22年12月更新:个人网站关停,如果仍旧对旧教程有兴趣参考 Github 的markdown内容[https://...
    tangyefei阅读 35,156评论 22 257
  • 第4章 Web表单 我们在第二章介绍过请求对象,它包含有客户端请求的全部信息。尤其是,可以通过request.fo...
    易木成华阅读 996评论 0 1
  • 请求对象包含客户端发出的所有请求信息。其中 request.form 能获取 POST 请求中提交的表单数据。 我...
    焉知非鱼阅读 1,111评论 0 2
  • 表单类 一个简单的 Web 表单,包含一个文本字段和一个提交按钮。例如:hello.py:定义表单类 String...
    ZZIXU阅读 1,458评论 0 0
  • 第二部分 Blog例子 第八章 用户验证 大部分程序需要追踪用户身份。当用户连接到程序,通过一系列步骤使自己的身份...
    易木成华阅读 1,275评论 0 4