第4章 Web表單
我們在第二章介紹過請求對象,它包含有客戶端請求的全部信息。尤其是,可以通過request.form訪問通過POST請求提交的所有表單數據。
雖然Flask的請求對象支持處理web表單,但實際上要做的工作既多又冗長重復。最具有代表性的就是生成html格式的Web代碼和驗證提交數據的有效性。
Flask-WTF擴展使處理表單工作變成一種愉悅的體驗。這個擴展是Flask對agnostic框架的WTForms包裝集成而來的。
Flask-WTF及其依賴可以通過pip安裝:
(venv)$pip install flask-wtf
跨站請求偽造(CSRF)防護
Flask-WTF默認配置為保護所有表單防御CSRF攻擊。所謂CSRF攻擊就是惡意站點向冒用受害者身份信息向其登陸的網站發送請求。
為了實現CSRF保護,Flask-WTF需要程序配置加密密鑰。Flask-WTF使用該密鑰生成令牌以確認請求的表單數據合法可信。例子4-1展示了如何配置密鑰。
Example 4-1. hello.py: Flask-WTF configuration
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your hard to guess string'
app.config字典通常存儲了各類配置變量以供框架、擴展或者是程序自身調用。使用標準字段語法即可向app.config對象添加配置值。該配置對象也有相應方法可以從文件或環境配置中導入配置值。
SECRET_KEY配置變量通常被Flask或其他一些第三方擴展用作加密的密鑰。恰如其名,加密強度就與這個變量值是否足夠難猜。所以為你每個程序都選擇不同的密鑰,且保證這個字符串無人知曉。
警告
為了更安全,這個密鑰應該被存儲在環境變量當中,這要好過嵌在代碼里。這一情況在第七章有描述。
表單類
使用Flask-WTF時,每個表單都由繼承自Form的一個類來表現。這個類定義了表單對象中的字段列表。每個字段對象可以有一個或多個驗證器——檢查用戶提交的數據是否有效。
例子4-2展示了一個只有一個文本字段和提交按鈕的簡單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')
表單中的字段是作為類的變量定義的,每個類變量都被賦值為帶有字段類型的對象。在上面例子中,NameForm表單有一個名為name的文本字段和一個名字為submit的提交按鈕。stringField類表現為一個帶有 type="text"屬性的<input>元素。SubmitField類則生成帶有type="submit"屬性的<input>元素。第一個字段構造函數的第一參數是label,用來在顯示成html時使用。包含在stringField構造函數中的validators參數定義了一個檢查器列表,在接收到用戶提交數據后用來檢查。Required()驗證器則用來確保不提交空字段。
提醒
Flask-WTF擴展定義了Form基礎類,所以應該從flask.ext.wtf中導入。而字段和驗證器則直接從WTForms包中導入。
表4-1列出了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 指定類型的字段列表
表4-2列出了WTForms內置的驗證器:
驗證器 說明
Email 驗證郵件地址
EqualTo 比較兩個字段的值; 在需要比較重復輸入密碼時格外有用
IPAddress 驗證 IPv4 網絡地址
Length 驗證輸入字符產長度是否符合指定值
NumberRange 驗證輸入數值是否在指定范圍內
Optional 允許輸入字段值為空,跳過附加的驗證器
Required 檢查是否有值
Regexp 根據指定表達式驗證是否符合
URL 檢查 URL是否合法
AnyOf 檢查輸入是否符合列表中的某項
NoneOf 檢查輸入是否不符合列表中的全部項
表單的HTML顯示
當調用時,表單字段從模板中將自己顯示成html。假設視圖函數把名為form的NameForm實例傳遞給模板,模板將生成一個簡單的html表單,如下:
<form method="POST">
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
當然啦,有點簡陋。為了改進表單外觀,我們給調用傳遞一些參數把它們顯示成html字段屬性。那么,你可以給字段添加上id或者class屬性來定義css樣式:
<form method="POST">
{{ form.name.label }} {{ form.name(id='my-text-field') }}
{{ form.submit() }}
</form>
但是,即使帶上了html屬性,通過這種方法顯示表單也很不可取。最好的辦法就是無論何時都利用Bootstrap自身的form格式來定義。只需要簡單調用Flask-Bootstrap提供的高水平輔助函數,就可以使用bootstrap預定義Form樣式來顯示flask-WTF表單。使用Flask-Bootstrap,上面的例子可以顯示如下:
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
類似在普通python代碼中那樣,import指令允許倒入模板元素并可以在多個模板中使用。導入的bootstrap/wtf.html
文件定義了使用Bootstrap來顯示Flask-WTF的輔助函數。wtf.quick_form()函數獲取flask-wtf表單對象并用默認的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區域目前有兩段。第一段是顯示歡迎信息的頁頭部分。此處使用了模板條件判斷。Jinja2中的條件判斷格式是
{% if variable %}
...
{% else %}
...
{% endif %}
如果條件為真,就把if和else指令之間的內容顯示到模板中。如果條件為假,則輸出else和endif 之間的內容。如果name參數未定義的話,示例模板將顯示輸入"Hello,Stranger!"。content的第二段則使用wtf.quick_form()函數顯示輸出NameForm對象。
視圖函數中的表單處理
在新版的hello.py中,index()視圖函數將顯示表單并接收其再次提交的數據。例子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)
添加在app.route裝飾器上的methods參數告訴Flask將該視圖函數在url映射中注冊成GET和POST的處理器。如果methods沒有值,視圖函數將只被注冊為GET請求的處理器。
把POST添加到method列表中是必須的,因為絕大部分表單提交作為POST請求來處理更為方便。以GET請求方式來提交表單也是可行的,但GET請求沒有body(只有頭部?)數據是作為URL查詢字符串附加在URL上,在瀏覽器地址欄里是可見的。因此和因其他一些原因,表單提交絕大部分是以POST請求的形式處理的。
本地變量name用來存儲表單中有效的name數據;如果表單中的name無效,那么變量name將被初始化為None。視圖函數提前創建NameForm類的實例以顯示表單。當表單提交后,如果所有數據驗證通過validate_on_summit()方法將返回True。否則返回False。服務器根據這個返回值決定重新顯示表單還是進行下一步處理。
當用戶第一次訪問時,服務器會接收到沒有表單數據的GET請求,這時validate_on_submit()將返回False,if聲明的主體部分將被跳過,轉而根據表單對象渲染模板,把參數name變量設置為None。用戶就會看到瀏覽器中顯示出表單。
當用戶提交表單,服務器會接收到帶有數據的POST請求。在validate_on_submit()中會對name字段調用required()驗證器。如果name不為空,驗證器會接受它,validate_on_submit()返回True?,F在用戶輸入的name可以作為字段的data屬性來訪問。在if聲明的主體內部,name被賦值給本地變量name,通過設置data屬性為空(空字符串)來清空表單字段。在最后一行,使用render_template()顯示模板,但這一次,name參數已被表單中的name字段賦值,所以就顯示個性化的歡迎信息了。
圖4-1顯示當用戶第一次訪問站點時的頁面樣子。當他提交一個名字后,程序將返回個性化的歡迎信息。而表單仍舊顯示在下方,需要的話用戶可以輸入另外一個名字。
圖4-2:輸入姓名后,顯示個性化的歡迎信息
如果用戶留空name進行提交,required()驗證器將捕捉這一錯誤,就像圖4-3顯示那樣。
圖4-3
注意,這里實現了很多自動功能哦。這是一個絕佳的例子,很好的展示了像Flask-WTF和Flask-Bootstrap這樣擁有良好設計的擴展的能給你的程序帶來的強大助力。
重定向和用戶會話
最新版本的hello.py還有一個可用性問題。如果你輸入你的名字提交后,再點擊瀏覽器的刷新按鈕,你可能看到一個模糊的警告,要求你確認再次提交表單。這是因為刷新瀏覽器頁面時,瀏覽器會重復最后一個請求。如果最后一個請求是帶有表單數據的POST,這個刷新就很可能導致數據的重復提交——這個動作往往是不希望發生的。
很多用戶不理解瀏覽器的這個警告。因此,永遠不要把POST請求作為瀏覽器發出的最后一個請求,這點是web程序公認的好慣例。
這一慣例可以通過使用帶有重定向(redirect)的POST請求替代普通POST來實現。重定向是一種特殊的響應,它使用url替代了html代碼字符串。當瀏覽器接受到這個響應,它就向重定向的URL地址發起一個GET請求,也就是要顯示的頁面。該頁面可能要花費幾微秒來加載——因為這是發送給服務器的第二個請求,當然啦,用戶不會知道其中的差異。現在最后一個請求是GET,所以刷新命令就會正常工作了。這個小竅門來自于Post/Redriect/GET pattern。
但是,注意,這又帶來了第二個問題。當程序處理POST請求的時候,他在form.name.data訪問到了用戶輸入的name,但隨著請求(POST)一結束,表單數據就丟失了(重定向因為數據為空而將無法正確響應)。因為POST請求是和一個重定向一起處理的,程序需要存儲name以便于重定向請求能夠獲取到它來構建正確的響應。
程序可以在相鄰請求之間“記住”一些東西——通過把他們存儲在“用戶會話”當中,對每個連接的客戶端來說都是私密存儲。在第二章中,用戶會話作為一個和請求上下文相關的變量被介紹過。他被稱為"會話"(session)并可以像標準Python字典一樣被訪問。
提醒
默認情況下,用戶會話被使用SECRET_KEY加密后存儲在客戶端的cookie中。任何對cookie內容的篡改都會導致簽名無效,同時讓會話也失效
例子4-5展示了視圖函數index()的新版本,它實現了重定向和用戶會話:
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被用來存儲用戶表單中輸入的name。現在這個變量被存儲在用戶會話當中session['name'],這樣在這個請求之后也會被記住。
現在來自于有效表單數據的請求隨著redirect()——一個生成http重定向響應的輔助函數——的調用而結束。redirect()函數以要轉向的URL作為參數。本例中使用的重定向url是根url,所以雖然也可以簡單的寫成redirect('/')
,但仍舊使用了Flask的URL生成函數url_for()。這是因為這個函數使用URL映射,它保證了與已定義的路由兼容并且可以在路由名稱發生變化時自動生效。我們推薦你使用這個函數。
url_for()唯一一個參數就是“結束點(endpoint)”名稱——也就是每個路由的內部名稱。默認情況下,路由的結束點名稱就是其對應的視圖函數名。本例中,處理根URL的視圖函數是index(),所以傳給url_for()的是index。
最后一個變化就是在render_template()函數中,現在他使用session.get('name')直接從會話中獲取name值。就像對普通字典操作一樣,使用get()請求字典的鍵可以避免找不到該鍵而則觸發錯誤。因為get()不存在的鍵時,將返回默認值None。
在這個版本的程序中,你可以看到以你希望的方式刷新頁面。
閃現信息
有時候,在請求完成后給予用戶一個狀態更新的提醒是很有用處的。它可以在客戶端閃現一個確認消息或警告或者一個錯誤(僅限于當前請求應答周期)。一個典型的例子就是當你提交有錯誤登錄表單給網站,服務器將返回一個帶有無效用戶名或密碼的錯誤提示信息的登錄表單。
Flask將這一功能放在核心功能里。例子4-6展示了如何使用flash()函數來實現這一點。
<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都會被拿來跟保存在用戶會話中的上一次同一表單提交的name做比較,如果兩者不一樣,flash()函數就會被調用,帶著在下一響應被發送回客戶端時顯示的信息。僅僅呼叫flash()并不足以顯示出信息,還需要在程序的對應模板中顯示它。顯示閃現消息最好的地方就是在基礎模板中,因為這樣一來所有頁面都會自動顯示。Flask創建了get_flahsed_message()函數來獲取并在模板中顯示閃現消息。例子4-7展示了這一點:
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 %}
在這個例子里,我們使用了bootstrap的警告樣式來顯示消息。這里使用了循環來逐條顯示——可能在上一個請求應答周期中多次調用了falsh(),從而生成一個消息隊列。get_flashed_messages()獲取到的消息不會被轉到下一次調用這個函數的時候,所以這些消息僅出現一次就消失了(僅限于本會話周期)。
能夠通過表單來獲取用戶數據是大部分程序的必備功能,所以能持久存儲數據的能力也是必不可少。下一章的主題就是Flask使用數據庫。
<<第三章 模板 第五章 EMail>>