請求對象包含客戶端發出的所有請求信息。其中 request.form
能獲取 POST 請求中提交的表單數據。
我們在 Flask 中使用 Flask-WTF 擴展來處理 Web 表單。WTF 中的 F 就是 Form 而不是 Fuck。這個擴展對獨立的 WTForms 包進行了包裝, 方便集成到 Flask 程序中。
# 安裝
pip install flask-wtf
跨站請求偽造保護
默認情況下,Flask-WTF 能保護所有表單免受跨站請求偽造(Cross-Site Request Forgery,CSRF)的攻擊。惡意網站把請求發送到被攻擊者已登錄的其他網站時就會引發 CSRF 攻擊。
為了實現 CSRF 保護,Flask-WTF 需要程序設置一個密鑰。Flask-WTF 使用這個密鑰生成加密令牌,再用令牌驗證請求中表單數據的真偽。設置密鑰的方法如示例 4-1 所示。
示例 4-1 hello.py: 設置 Flask-WTF
app = Flask(__name__)
app.config['SECRET_KEY'] = '難猜的字符串'
app.config
字典可用來存儲框架、擴展和 app 本身的配置變量。使用標準的字典句法就能把配置值添加到 app.config 對象中。這個對象還提供了一些方法, 可以從文件或環境中導入配置值。
SECRET_KEY 配置變量是通用密鑰,可在 Flask 和多個第三方擴展中使用。如其名所示,加密的強度取決于變量值的機密程度。不同的程序要使用不同的密鑰,而且要保證其他人不知道你所用的字符串。
表單類
使用 Flask-WTF 時, 每一個 Web 表單都由一個繼承自 Form 的類表示。這個類定義表單中的一組字段, 每個字段都用對象表示。字段對象可附屬一個或多個驗證函數。驗證函數用來驗證用戶提交的輸入值是否符合要求。
示例 4-2 hello.py: 定義表單類
form flask.ext.wtf import Form
# Python 3 => form flask_wtf import From
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>
元素。字段構造函數的第一個參數是把表單渲染成 HTML 時使用的標號
。
StringField 構造函數中的可選參數 validators 指定一個由驗證函數組成的列表,在接受用戶提交的數據之前驗證數據。驗證函數 Required()
確保提交的字段不為空。
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 內建的驗證函數如下表所示:
驗證函數 | 說 明 |
---|---|
驗證電子郵件地址 | |
EqualTo | 比較兩個字段的值;常用于要求輸入兩次密碼進行確認的情況 |
IPAddress | 驗證 IPv4 網絡地址 |
Length | 驗證輸入字符串的長度 |
NumberRange | 驗證輸入的值在數字范圍內 |
Optional | 無輸入值時跳過其他驗證函數 |
Required | 確保字段中有數據 |
Regexp | 使用正則表達式驗證輸入值 |
URL | 驗證 URL |
AnyOf | 確保輸入值在可選值列表中 |
NoneOf | 確保輸入值不在可選值列表中 |
把表單渲染成 HTML
表單字段是可調用的,在模板中調用后會渲染成 HTML。假設視圖函數把一個 NameForm
實例通過參數 form
傳入模板(.html),在模板中可以生成一個簡單的表單,如下所示:
<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>
還是很繁瑣, 我們來使用 Flask-Bootstrap 中的表單樣式, Flask-Bootstrap 提供了一個非常高端的輔助函數,可以使用 Bootstrap 中預先定義好的表單樣式渲染整個 Flask-WTF 表單:
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
導入的 bootstrap/wtf.html 文件中定義了一個使用 Bootstrap 渲染 Falsk-WTF 表單對象的輔助函數。 wtf.quick_form()
函數的參數為 Flask-WTF 表單對象,使用 Bootstrap 的默認
樣式渲染傳入的表單。hello.py 的完整模板如示例 4-3 所示:
- 示例 4-3 templates/index.html:使用 Flask-WTF 和 Flask-Bootstrap 渲染表單
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flask{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
在視圖函數中處理表單
在新版 hello.py 中,視圖函數 index() 不僅要渲染表單,還要接收表單中的數據。示例 4-4 是更新后的 index() 視圖函數。
示例 4-4 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 請求。 validate_on_submit()
會調用 name 字段上附屬的 Required() 驗證函數。如果名字不為空,就能通過驗證, validate_on_submit()
返回 True ?,F在,用戶輸入的名字可通過字段的 data 屬性獲取。在 if 語句中,把名字賦值給局部變量 name ,然后再把 data 屬性設為空字符串,從而清空表單字段。最后一行調用 render_template() 函數渲染模板,但這一次參數 name 的值為表單中輸入的名字,因此會顯示一個針對該用戶的歡迎消息。
最后的 hello.py 如下:
# -*- coding:utf8 -*-
from flask import Flask, render_template
from flask_bootstrap import Bootstrap
from flask_script import Manager
from flask_moment import Moment
from datetime import date
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import Required
app = Flask(__name__)
app.config['SECRET_KEY'] = 'jskffhhfdkjgjkdfghkfgnk'
manager = Manager(app)
bootstrap = Bootstrap(app)
moment = Moment(app)
class NameForm(FlaskForm):
name = StringField('What is your name', validators=[Required()])
submit = SubmitField('submit')
@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('form.html',form=form,name=name)
@app.route('/user/<name>')
def user(name):
return render_template("user.html",name=name)
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_errpr(e):
return render_template('505.html'), 500
@app.route('/date')
def datetime():
return render_template('index.html', time=date(2016,9,30))
if __name__ == '__main__':
manager.run()
如果你從 GitHub 上克隆了這個程序的 Git 倉庫,那么可以執行
git checkout 4a 簽出程序的這個版本
。 這真是一個不錯的方法。
重定向和用戶會話
最新版的 hello.py 存在一個可用性問題。用戶輸入名字后提交表單,然后點擊瀏覽器的刷新按鈕,會看到一個莫名其妙的警告,要求在再次提交表單之前進行確認。之所以出現這種情況,是因為刷新頁面時瀏覽器會重新發送之前已經發送過的最后一個請求。如果這個請求是一個包含表單數據的 POST 請求,刷新頁面后會再次提交表單。大多數情況下,這并不是理想的處理方式。
很多用戶都不理解瀏覽器發出的這個警告?;谶@個原因,最好別讓 Web 程序把 POST 請求作為瀏覽器發送的最后一個請求。
這種需求的實現方式是,使用重定向作為 POST 請求的響應,而不是使用常規響應。重定向是一種特殊的響應,響應內容是 URL,而不是包含 HTML 代碼的字符串。瀏覽器收到這種響應時,會向重定向的 URL 發起 GET 請求,顯示頁面的內容。這個頁面的加載可能
要多花幾微秒,因為要先把第二個請求發給服務器。除此之外,用戶不會察覺到有什么不同?,F在,最后一個請求是 GET
請求,所以刷新命令能像預期的那樣正常使用了。這個技巧稱為 Post/ 重定向 /Get 模式
。
但這種方法會帶來另一個問題。程序處理 POST 請求時,使用 form.name.data 獲取用戶輸入的名字,可是一旦這個請求結束,數據也就丟失了。因為這個 POST 請求使用重定向處理,所以程序需要保存輸入的名字,這樣重定向后的請求才能獲得并使用這個名字,從而構建真正的響應。
程序可以把數據存儲在用戶會話中,在請求之間“記住”數據。用戶會話是一種私有存儲,存在于每個連接到服務器的客戶端中。我們在第 2 章介紹過用戶會話,它是請求上下文中的變量,名為 session ,像標準的 Python 字典一樣操作。
默認情況下,用戶會話保存在客戶端 cookie 中,使用設置的 SECRET_KEY 進行加密簽名。如果篡改了 cookie 中的內容,簽名就會失效,會話也會隨之失效。
示例 4-5 是 index() 視圖函數的新版本,實現了重定向和用戶會話。
示例 4-5 hello.py:重定向和用戶會話
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 被用于存儲用戶在表單中輸入的名字。這個變量現在保存在用戶會話中,即 session['name']
,所以在兩次請求之間也能記住輸入的值?,F在,包含合法表單數據的請求最后會調用 redirect() 函數。 redirect() 是個輔助函數,用來生成 HTTP 重定向響應。 redirect() 函數的參數是重定向的 URL,這里使用的重定向URL 是程序的根地址,因此重定向響應本可以寫得更簡單一些,寫成 redirect('/')
,但卻會使用 Flask 提供的 URL 生成函數 url_for() 。推薦使用 url_for() 生成 URL,因為這個函數使用 URL 映射生成 URL,從而保證 URL 和定義的路由兼容,而且修改路由名字后依然可用。
url_for() 函數的第一個且唯一必須指定的參數是端點名,即路由的內部名字。默認情況下,路由的端點是相應視圖函數的名字。在這個示例中,處理根地址的視圖函數是 index() ,因此傳給 url_for() 函數的名字是 index 。最后一處改動位于 render_function() 函數中,使用 session.get('name') 直接從會話中讀取 name 參數的值。和普通的字典一樣,這里使用 get() 獲取字典中鍵對應的值以避免未找到鍵的異常情況,因為對于不存在的鍵, get() 會返回默認值 None 。
Flash消息
請求完成后,有時需要讓用戶知道狀態發生了變化。這里可以使用確認消息、警告或者錯誤提醒。一個典型例子是,用戶提交了有一項錯誤的登錄表單后,服務器發回的響應重新渲染了登錄表單,并在表單上面顯示一個消息,提示用戶用戶名或密碼錯誤。這種功能是 Flask 的核心特性。如示例 4-6 所示, flash()
函數可實現這種效果。
示例 4-6 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 消息,因為這樣所有頁面都能使用這些消息。Flask 把 get_flashed_messages()
函數開放給模板,用來獲取并渲染消息,如示例 4-7 所示。
示例 4-7 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>
{% endblock %}
在這個示例中,使用 Bootstrap 提供的警報 CSS 樣式渲染警告消息(如圖 4-4 所示)。
在模板中使用循環是因為在之前的請求循環中每次調用 flash() 函數時都會生成一個消息,所以可能有多個消息在排隊等待顯示。 get_flashed_messages() 函數獲取的消息在下次調用時不會再次返回,因此 Flash 消息只顯示一次,然后就消失了。
從 Web 表單中獲取用戶輸入的數據是大多數程序都需要的功能,把數據保存在永久存儲器中也是一樣。