第4章 Web表單
我們?cè)诘诙陆榻B過(guò)請(qǐng)求對(duì)象,它包含有客戶端請(qǐng)求的全部信息。尤其是,可以通過(guò)request.form訪問(wèn)通過(guò)POST請(qǐng)求提交的所有表單數(shù)據(jù)。
雖然Flask的請(qǐng)求對(duì)象支持處理web表單,但實(shí)際上要做的工作既多又冗長(zhǎng)重復(fù)。最具有代表性的就是生成html格式的Web代碼和驗(yàn)證提交數(shù)據(jù)的有效性。
Flask-WTF擴(kuò)展使處理表單工作變成一種愉悅的體驗(yàn)。這個(gè)擴(kuò)展是Flask對(duì)agnostic框架的WTForms包裝集成而來(lái)的。
Flask-WTF及其依賴(lài)可以通過(guò)pip安裝:
(venv)$pip install flask-wtf
跨站請(qǐng)求偽造(CSRF)防護(hù)
Flask-WTF默認(rèn)配置為保護(hù)所有表單防御CSRF攻擊。所謂CSRF攻擊就是惡意站點(diǎn)向冒用受害者身份信息向其登陸的網(wǎng)站發(fā)送請(qǐng)求。
為了實(shí)現(xiàn)CSRF保護(hù),F(xiàn)lask-WTF需要程序配置加密密鑰。Flask-WTF使用該密鑰生成令牌以確認(rèn)請(qǐng)求的表單數(shù)據(jù)合法可信。例子4-1展示了如何配置密鑰。
Example 4-1. hello.py: Flask-WTF configuration
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your hard to guess string'
app.config字典通常存儲(chǔ)了各類(lèi)配置變量以供框架、擴(kuò)展或者是程序自身調(diào)用。使用標(biāo)準(zhǔn)字段語(yǔ)法即可向app.config對(duì)象添加配置值。該配置對(duì)象也有相應(yīng)方法可以從文件或環(huán)境配置中導(dǎo)入配置值。
SECRET_KEY配置變量通常被Flask或其他一些第三方擴(kuò)展用作加密的密鑰。恰如其名,加密強(qiáng)度就與這個(gè)變量值是否足夠難猜。所以為你每個(gè)程序都選擇不同的密鑰,且保證這個(gè)字符串無(wú)人知曉。
警告
為了更安全,這個(gè)密鑰應(yīng)該被存儲(chǔ)在環(huán)境變量當(dāng)中,這要好過(guò)嵌在代碼里。這一情況在第七章有描述。
表單類(lèi)
使用Flask-WTF時(shí),每個(gè)表單都由繼承自Form的一個(gè)類(lèi)來(lái)表現(xiàn)。這個(gè)類(lèi)定義了表單對(duì)象中的字段列表。每個(gè)字段對(duì)象可以有一個(gè)或多個(gè)驗(yàn)證器——檢查用戶提交的數(shù)據(jù)是否有效。
例子4-2展示了一個(gè)只有一個(gè)文本字段和提交按鈕的簡(jiǎn)單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')
表單中的字段是作為類(lèi)的變量定義的,每個(gè)類(lèi)變量都被賦值為帶有字段類(lèi)型的對(duì)象。在上面例子中,NameForm表單有一個(gè)名為name的文本字段和一個(gè)名字為submit的提交按鈕。stringField類(lèi)表現(xiàn)為一個(gè)帶有 type="text"屬性的<input>元素。SubmitField類(lèi)則生成帶有type="submit"屬性的<input>元素。第一個(gè)字段構(gòu)造函數(shù)的第一參數(shù)是label,用來(lái)在顯示成html時(shí)使用。包含在stringField構(gòu)造函數(shù)中的validators參數(shù)定義了一個(gè)檢查器列表,在接收到用戶提交數(shù)據(jù)后用來(lái)檢查。Required()驗(yàn)證器則用來(lái)確保不提交空字段。
提醒
Flask-WTF擴(kuò)展定義了Form基礎(chǔ)類(lèi),所以應(yīng)該從flask.ext.wtf中導(dǎo)入。而字段和驗(yàn)證器則直接從WTForms包中導(dǎo)入。
表4-1列出了WTForms支持的標(biāo)準(zhǔn)html字段。
字段類(lèi)型 說(shuō)明
StringField 文本框
TextAreaField 多行文本框
PasswordField 密碼文本框
HiddenField 隱藏的文本框
DateField 接收指定格式datetime.date值的文本框
DateTimeField 接收指定格式datetime.datetime值的文本框
IntegerField 接收整數(shù)值的文本框
DecimalField 接收decimal.Decimal 值的文本框
FloatField 接收浮點(diǎn)數(shù)值的文本字段
BooleanField 帶有 True , False值的選擇框
RadioField 單選按鈕列表
SelectField 下拉選擇列表
SelectMultipleField 下拉多選列表
FileField 上傳文件域
SubmitField 表單提交按鈕
FormField 作為字段嵌入的表單
FieldList 指定類(lèi)型的字段列表
表4-2列出了WTForms內(nèi)置的驗(yàn)證器:
驗(yàn)證器 說(shuō)明
Email 驗(yàn)證郵件地址
EqualTo 比較兩個(gè)字段的值; 在需要比較重復(fù)輸入密碼時(shí)格外有用
IPAddress 驗(yàn)證 IPv4 網(wǎng)絡(luò)地址
Length 驗(yàn)證輸入字符產(chǎn)長(zhǎng)度是否符合指定值
NumberRange 驗(yàn)證輸入數(shù)值是否在指定范圍內(nèi)
Optional 允許輸入字段值為空,跳過(guò)附加的驗(yàn)證器
Required 檢查是否有值
Regexp 根據(jù)指定表達(dá)式驗(yàn)證是否符合
URL 檢查 URL是否合法
AnyOf 檢查輸入是否符合列表中的某項(xiàng)
NoneOf 檢查輸入是否不符合列表中的全部項(xiàng)
表單的HTML顯示
當(dāng)調(diào)用時(shí),表單字段從模板中將自己顯示成html。假設(shè)視圖函數(shù)把名為form的NameForm實(shí)例傳遞給模板,模板將生成一個(gè)簡(jiǎn)單的html表單,如下:
<form method="POST">
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
當(dāng)然啦,有點(diǎn)簡(jiǎn)陋。為了改進(jìn)表單外觀,我們給調(diào)用傳遞一些參數(shù)把它們顯示成html字段屬性。那么,你可以給字段添加上id或者class屬性來(lái)定義css樣式:
<form method="POST">
{{ form.name.label }} {{ form.name(id='my-text-field') }}
{{ form.submit() }}
</form>
但是,即使帶上了html屬性,通過(guò)這種方法顯示表單也很不可取。最好的辦法就是無(wú)論何時(shí)都利用Bootstrap自身的form格式來(lái)定義。只需要簡(jiǎn)單調(diào)用Flask-Bootstrap提供的高水平輔助函數(shù),就可以使用bootstrap預(yù)定義Form樣式來(lái)顯示flask-WTF表單。使用Flask-Bootstrap,上面的例子可以顯示如下:
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
類(lèi)似在普通python代碼中那樣,import指令允許倒入模板元素并可以在多個(gè)模板中使用。導(dǎo)入的bootstrap/wtf.html文件定義了使用Bootstrap來(lái)顯示Flask-WTF的輔助函數(shù)。wtf.quick_form()函數(shù)獲取flask-wtf表單對(duì)象并用默認(rèn)的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區(qū)域目前有兩段。第一段是顯示歡迎信息的頁(yè)頭部分。此處使用了模板條件判斷。Jinja2中的條件判斷格式是
{% if variable %}
...
{% else %}
...
{% endif %}
如果條件為真,就把if和else指令之間的內(nèi)容顯示到模板中。如果條件為假,則輸出else和endif 之間的內(nèi)容。如果name參數(shù)未定義的話,示例模板將顯示輸入"Hello,Stranger!"。content的第二段則使用wtf.quick_form()函數(shù)顯示輸出NameForm對(duì)象。
視圖函數(shù)中的表單處理
在新版的hello.py中,index()視圖函數(shù)將顯示表單并接收其再次提交的數(shù)據(jù)。例子4-4顯示了更新后index()視圖函數(shù):
@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參數(shù)告訴Flask將該視圖函數(shù)在url映射中注冊(cè)成GET和POST的處理器。如果methods沒(méi)有值,視圖函數(shù)將只被注冊(cè)為GET請(qǐng)求的處理器。
把POST添加到method列表中是必須的,因?yàn)榻^大部分表單提交作為POST請(qǐng)求來(lái)處理更為方便。以GET請(qǐng)求方式來(lái)提交表單也是可行的,但GET請(qǐng)求沒(méi)有body(只有頭部?)數(shù)據(jù)是作為URL查詢(xún)字符串附加在URL上,在瀏覽器地址欄里是可見(jiàn)的。因此和因其他一些原因,表單提交絕大部分是以POST請(qǐng)求的形式處理的。
本地變量name用來(lái)存儲(chǔ)表單中有效的name數(shù)據(jù);如果表單中的name無(wú)效,那么變量name將被初始化為None。視圖函數(shù)提前創(chuàng)建NameForm類(lèi)的實(shí)例以顯示表單。當(dāng)表單提交后,如果所有數(shù)據(jù)驗(yàn)證通過(guò)validate_on_summit()方法將返回True。否則返回False。服務(wù)器根據(jù)這個(gè)返回值決定重新顯示表單還是進(jìn)行下一步處理。
當(dāng)用戶第一次訪問(wèn)時(shí),服務(wù)器會(huì)接收到?jīng)]有表單數(shù)據(jù)的GET請(qǐng)求,這時(shí)validate_on_submit()將返回False,if聲明的主體部分將被跳過(guò),轉(zhuǎn)而根據(jù)表單對(duì)象渲染模板,把參數(shù)name變量設(shè)置為None。用戶就會(huì)看到瀏覽器中顯示出表單。
當(dāng)用戶提交表單,服務(wù)器會(huì)接收到帶有數(shù)據(jù)的POST請(qǐng)求。在validate_on_submit()中會(huì)對(duì)name字段調(diào)用required()驗(yàn)證器。如果name不為空,驗(yàn)證器會(huì)接受它,validate_on_submit()返回True?,F(xiàn)在用戶輸入的name可以作為字段的data屬性來(lái)訪問(wèn)。在if聲明的主體內(nèi)部,name被賦值給本地變量name,通過(guò)設(shè)置data屬性為空(空字符串)來(lái)清空表單字段。在最后一行,使用render_template()顯示模板,但這一次,name參數(shù)已被表單中的name字段賦值,所以就顯示個(gè)性化的歡迎信息了。
圖4-1顯示當(dāng)用戶第一次訪問(wèn)站點(diǎn)時(shí)的頁(yè)面樣子。當(dāng)他提交一個(gè)名字后,程序?qū)⒎祷貍€(gè)性化的歡迎信息。而表單仍舊顯示在下方,需要的話用戶可以輸入另外一個(gè)名字。

圖4-2:輸入姓名后,顯示個(gè)性化的歡迎信息

如果用戶留空name進(jìn)行提交,required()驗(yàn)證器將捕捉這一錯(cuò)誤,就像圖4-3顯示那樣。
圖4-3

注意,這里實(shí)現(xiàn)了很多自動(dòng)功能哦。這是一個(gè)絕佳的例子,很好的展示了像Flask-WTF和Flask-Bootstrap這樣擁有良好設(shè)計(jì)的擴(kuò)展的能給你的程序帶來(lái)的強(qiáng)大助力。
重定向和用戶會(huì)話
最新版本的hello.py還有一個(gè)可用性問(wèn)題。如果你輸入你的名字提交后,再點(diǎn)擊瀏覽器的刷新按鈕,你可能看到一個(gè)模糊的警告,要求你確認(rèn)再次提交表單。這是因?yàn)樗⑿聻g覽器頁(yè)面時(shí),瀏覽器會(huì)重復(fù)最后一個(gè)請(qǐng)求。如果最后一個(gè)請(qǐng)求是帶有表單數(shù)據(jù)的POST,這個(gè)刷新就很可能導(dǎo)致數(shù)據(jù)的重復(fù)提交——這個(gè)動(dòng)作往往是不希望發(fā)生的。
很多用戶不理解瀏覽器的這個(gè)警告。因此,永遠(yuǎn)不要把POST請(qǐng)求作為瀏覽器發(fā)出的最后一個(gè)請(qǐng)求,這點(diǎn)是web程序公認(rèn)的好慣例。
這一慣例可以通過(guò)使用帶有重定向(redirect)的POST請(qǐng)求替代普通POST來(lái)實(shí)現(xiàn)。重定向是一種特殊的響應(yīng),它使用url替代了html代碼字符串。當(dāng)瀏覽器接受到這個(gè)響應(yīng),它就向重定向的URL地址發(fā)起一個(gè)GET請(qǐng)求,也就是要顯示的頁(yè)面。該頁(yè)面可能要花費(fèi)幾微秒來(lái)加載——因?yàn)檫@是發(fā)送給服務(wù)器的第二個(gè)請(qǐng)求,當(dāng)然啦,用戶不會(huì)知道其中的差異?,F(xiàn)在最后一個(gè)請(qǐng)求是GET,所以刷新命令就會(huì)正常工作了。這個(gè)小竅門(mén)來(lái)自于Post/Redriect/GET pattern。
但是,注意,這又帶來(lái)了第二個(gè)問(wèn)題。當(dāng)程序處理POST請(qǐng)求的時(shí)候,他在form.name.data訪問(wèn)到了用戶輸入的name,但隨著請(qǐng)求(POST)一結(jié)束,表單數(shù)據(jù)就丟失了(重定向因?yàn)閿?shù)據(jù)為空而將無(wú)法正確響應(yīng))。因?yàn)镻OST請(qǐng)求是和一個(gè)重定向一起處理的,程序需要存儲(chǔ)name以便于重定向請(qǐng)求能夠獲取到它來(lái)構(gòu)建正確的響應(yīng)。
程序可以在相鄰請(qǐng)求之間“記住”一些東西——通過(guò)把他們存儲(chǔ)在“用戶會(huì)話”當(dāng)中,對(duì)每個(gè)連接的客戶端來(lái)說(shuō)都是私密存儲(chǔ)。在第二章中,用戶會(huì)話作為一個(gè)和請(qǐng)求上下文相關(guān)的變量被介紹過(guò)。他被稱(chēng)為"會(huì)話"(session)并可以像標(biāo)準(zhǔn)Python字典一樣被訪問(wèn)。
提醒
默認(rèn)情況下,用戶會(huì)話被使用SECRET_KEY加密后存儲(chǔ)在客戶端的cookie中。任何對(duì)cookie內(nèi)容的篡改都會(huì)導(dǎo)致簽名無(wú)效,同時(shí)讓會(huì)話也失效
例子4-5展示了視圖函數(shù)index()的新版本,它實(shí)現(xiàn)了重定向和用戶會(huì)話:
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被用來(lái)存儲(chǔ)用戶表單中輸入的name。現(xiàn)在這個(gè)變量被存儲(chǔ)在用戶會(huì)話當(dāng)中session['name'],這樣在這個(gè)請(qǐng)求之后也會(huì)被記住。
現(xiàn)在來(lái)自于有效表單數(shù)據(jù)的請(qǐng)求隨著redirect()——一個(gè)生成http重定向響應(yīng)的輔助函數(shù)——的調(diào)用而結(jié)束。redirect()函數(shù)以要轉(zhuǎn)向的URL作為參數(shù)。本例中使用的重定向url是根url,所以雖然也可以簡(jiǎn)單的寫(xiě)成redirect('/'),但仍舊使用了Flask的URL生成函數(shù)url_for()。這是因?yàn)檫@個(gè)函數(shù)使用URL映射,它保證了與已定義的路由兼容并且可以在路由名稱(chēng)發(fā)生變化時(shí)自動(dòng)生效。我們推薦你使用這個(gè)函數(shù)。
url_for()唯一一個(gè)參數(shù)就是“結(jié)束點(diǎn)(endpoint)”名稱(chēng)——也就是每個(gè)路由的內(nèi)部名稱(chēng)。默認(rèn)情況下,路由的結(jié)束點(diǎn)名稱(chēng)就是其對(duì)應(yīng)的視圖函數(shù)名。本例中,處理根URL的視圖函數(shù)是index(),所以傳給url_for()的是index。
最后一個(gè)變化就是在render_template()函數(shù)中,現(xiàn)在他使用session.get('name')直接從會(huì)話中獲取name值。就像對(duì)普通字典操作一樣,使用get()請(qǐng)求字典的鍵可以避免找不到該鍵而則觸發(fā)錯(cuò)誤。因?yàn)間et()不存在的鍵時(shí),將返回默認(rèn)值None。
在這個(gè)版本的程序中,你可以看到以你希望的方式刷新頁(yè)面。
閃現(xiàn)信息
有時(shí)候,在請(qǐng)求完成后給予用戶一個(gè)狀態(tài)更新的提醒是很有用處的。它可以在客戶端閃現(xiàn)一個(gè)確認(rèn)消息或警告或者一個(gè)錯(cuò)誤(僅限于當(dāng)前請(qǐng)求應(yīng)答周期)。一個(gè)典型的例子就是當(dāng)你提交有錯(cuò)誤登錄表單給網(wǎng)站,服務(wù)器將返回一個(gè)帶有無(wú)效用戶名或密碼的錯(cuò)誤提示信息的登錄表單。
Flask將這一功能放在核心功能里。例子4-6展示了如何使用flash()函數(shù)來(lái)實(shí)現(xiàn)這一點(diǎn)。
<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都會(huì)被拿來(lái)跟保存在用戶會(huì)話中的上一次同一表單提交的name做比較,如果兩者不一樣,flash()函數(shù)就會(huì)被調(diào)用,帶著在下一響應(yīng)被發(fā)送回客戶端時(shí)顯示的信息。僅僅呼叫flash()并不足以顯示出信息,還需要在程序的對(duì)應(yīng)模板中顯示它。顯示閃現(xiàn)消息最好的地方就是在基礎(chǔ)模板中,因?yàn)檫@樣一來(lái)所有頁(yè)面都會(huì)自動(dòng)顯示。Flask創(chuàng)建了get_flahsed_message()函數(shù)來(lái)獲取并在模板中顯示閃現(xiàn)消息。例子4-7展示了這一點(diǎn):
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 %}
在這個(gè)例子里,我們使用了bootstrap的警告樣式來(lái)顯示消息。這里使用了循環(huán)來(lái)逐條顯示——可能在上一個(gè)請(qǐng)求應(yīng)答周期中多次調(diào)用了falsh(),從而生成一個(gè)消息隊(duì)列。get_flashed_messages()獲取到的消息不會(huì)被轉(zhuǎn)到下一次調(diào)用這個(gè)函數(shù)的時(shí)候,所以這些消息僅出現(xiàn)一次就消失了(僅限于本會(huì)話周期)。
能夠通過(guò)表單來(lái)獲取用戶數(shù)據(jù)是大部分程序的必備功能,所以能持久存儲(chǔ)數(shù)據(jù)的能力也是必不可少。下一章的主題就是Flask使用數(shù)據(jù)庫(kù)。
<<第三章 模板 第五章 EMail>>