Flask Web Development 第四章讀書筆記 Web表單

第四章 Web表單

序:為什么需要Flask-wtf

第 2 章中介紹的請求對象包含客戶端發(fā)出的所有請求信息。
其中, request.form 能獲取POST 請求中提交的表單數(shù)據(jù)。
盡管 Flask 的請求對象提供的信息足夠用于處理 Web 表單,
但有些任務很單調,而且要重復操作。
比如,生成表單的 HTML 代碼和驗證提交的表單數(shù)據(jù)。

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)的攻擊。
惡意網站把請求發(fā)送到被攻擊者已登錄的其他網站時就會引發(fā) CSRF 攻擊。
為了實現(xiàn) CSRF 保護,
Flask-WTF 需要程序設置一個密鑰。

如何設置密鑰

Flask-WTF 使用這個密鑰生成加密令牌,
再用令牌驗證請求中表單數(shù)據(jù)的真?zhèn)巍?br> 例:hello.py: 設置 Flask-WTF

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

app.config 字典可用來存儲框架、擴展和程序本身的配置變量。
使用標準的字典句法就能把配置值添加到 app.config 對象中。
這個對象還提供了一些方法,
可以從文件或環(huán)境中導入配置值。

SECRET_KEY 配置變量是通用密鑰,
可在 Flask 和多個第三方擴展中使用。
如其名所示,加密的強度取決于變量值的機密程度。
不同的程序要使用不同的密鑰,
而且要保證其他人不知道你所用的字符串。

為了增強安全性,密鑰不應該直接寫入代碼,
而要保存在環(huán)境變量中。這一技術會在第 7 章介紹。

4.2 表單類

表單的結構

使用 Flask-WTF 時,
每個 Web 表單都由一個繼承自 Form 的類表示。
這個類定義表單中的一組字段,
每個字段都用對象表示。
字段對象可附屬一個或多個驗證函數(shù)。
驗證函數(shù)用來驗證用戶提交的輸入值是否符合要求。

一個簡單的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 文本字段,值為整數(shù)
DecimalField 文本字段,值為 decimal.Decimal
FloatField 文本字段,值為浮點數(shù)
BooleanField 復選框,值為 True 和 False
RadioField 一組單選框
SelectField 下拉列表
SelectMultipleField 下拉列表,可選擇多個值
FileField 文件上傳字段
SubmitField 表單提交按鈕
FormField 把表單作為字段嵌入另一個表單
FieldList 一組指定類型的字段

WTForms驗證函數(shù)

驗證函數(shù) 說明
Email 驗證電子郵件地址
EqualTo 比較兩個字段的值;常用于要求輸入兩次密碼進行確認的情況
IPAddress 驗證 IPv4 網絡地址
Length 驗證輸入字符串的長度
NumberRange 驗證輸入的值在數(shù)字范圍內
Optional 無輸入值時跳過其他驗證函數(shù)
Required 確保字段中有數(shù)據(jù)
Regexp 使用正則表達式驗證輸入值
URL 驗證 URL
AnyOf 確保輸入值在可選值列表中
NoneOf 確保輸入值不在可選值列表中

4.3 把表單渲染成HTML

如何調用上節(jié)的NameForm表單

可以通過參數(shù)form傳入模板,例如:

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

<form>標簽是html語言用來顯示表單的,
而縮進部分的form則是傳入的參數(shù),
是由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() 函數(shù)的參數(shù)為 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 %}

模板的內容區(qū)(page_content)現(xiàn)在有兩部分。
第一部分是頁面頭部(page_header),
第二部分使用 wtf.quick_form() 渲染上節(jié)的NameForm 實例。
這個程序必須和下節(jié)重定義的index()一起使用才行。

Jinja2 中的條件語句格式為 {% if condition %}...{% else %}...{% endif %}。

4.4 在視圖函數(shù)中處理表單

更新后的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)

可以看到在路由中,多了一個methods,
后面除了默認值GET,還多了POST方法,
可以在網上搜下RESTFUL的簡單說明。

NameForm的實例form會被Flask-Bootstrap渲染成網頁上的表單,
只有當用戶在表單中提交數(shù)據(jù)時,
才會執(zhí)行if嵌套的語句,調用POST方法提交數(shù)據(jù),
否則只是調用GET方法,顯示空表單。

沒必要把request.form(4.3第一個示例,form標簽內容)傳給Flask-wtf,
它自己會自動讀取。
并且validate_on_submit會檢查是否是POST,并且是否是有效數(shù)據(jù)。

因為每個表單都必須提交(submit),
所以validate_on_submit用來確認所有數(shù)據(jù)段的驗證通過。

4.5 重定向和用戶會話

POST后再刷新頁面會出現(xiàn)警告

最新版的 hello.py 存在一個可用性問題。
用戶輸入名字后提交表單,
然后點擊瀏覽器的刷新按鈕,
會看到一個莫名其妙的警告,
要求在再次提交表單之前進行確認。

之所以出現(xiàn)這種情況,
是因為刷新頁面時瀏覽器會重新發(fā)送之前已經發(fā)送過的最后一個請求。
如果這個請求是一個包含表單數(shù)據(jù)的 POST 請求,
刷新頁面后會再次提交表單。
大多數(shù)情況下,這并不是理想的處理方式。

如何避免刷新時POST作為最后一個請求:重定向

很多用戶都不理解瀏覽器發(fā)出的這個警告。
基于這個原因,
最好別讓 Web 程序把 POST請求作為瀏覽器發(fā)送的最后一個請求。

既然最后一個請求不能是POST方法,
可以嘗試在POST后自動添加一個GET方法,
這個GET方法用原來的參數(shù)重新獲取當前頁面。

這種需求的實現(xiàn)方式是使用重定向,
會在把POST+GET封裝為一個GET請求,
刷新命令也就能像預期那樣使用了。

重定向時會丟失原有的輸入數(shù)據(jù)

但這種方法會帶來另一個問題。
程序處理 POST 請求時,
使用 form.name.data 獲取用戶輸入的名字,
可是一旦這個請求結束,
數(shù)據(jù)也就丟失了。

如果沒有用戶輸入的數(shù)據(jù),
重定向后的頁面就如同用戶沒有輸入。

如何保存這些數(shù)據(jù):用戶會話

程序可以把數(shù)據(jù)存儲在用戶會話中,
在請求之間“ 記住”數(shù)據(jù)。
每個連接到服務器的客戶端中都有不同的用戶會話。

我們在第 2 章介紹過用戶會話,
它是請求上下文中的變量,
名為 session,
像標準的 Python 字典一樣操作。

默認情況下,
用戶會話保存在客戶端 cookie 中,
使用設置的 SECRET_KEY 進行加密簽名。
如果篡改了 cookie 中的內容,
簽名就會失效,會話也會隨之失效。

使用重定向和用戶會話的Index函數(shù)

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函數(shù)以用戶會話存儲輸入的數(shù)據(jù),
然后重定向到當前URL,
再把存儲好的數(shù)據(jù)發(fā)送給URL對應的模板。

即用session['name']存儲form.name.data
然后redirect到index對應的URL'/'
在把session['name']發(fā)送給'/'對應的模板。

推薦在重定向時,
使用redirect(url_for('index'))代替redirect('/'),
這樣只要不改動index這個名字,
即使改動index對應的URL,
也可以正確地重定向。

form.name.data能夠獲取表單中name的值,
而session.get('name')則直接從會話中讀取name的值。

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

可以避免發(fā)生未找到鍵的異常,
對于不存在的鍵,
get()會返回默認值None,
這點和普通字典一樣。

4.6 Flash消息

為什么需要Flask消息

請求完成后,有時需要讓用戶知道狀態(tài)發(fā)生了變化。
這里可以使用確認消息、警告或者錯誤提醒。
一個典型例子是,
用戶提交了有一項錯誤的登錄表單后,
服務器發(fā)回的響應重新渲染了登錄表單,
并在表單上面顯示一個消息,
提示用戶用戶名或密碼錯誤。

這種功能是 Flask 的核心特性。
如下例所示,
flash() 函數(shù)可實現(xiàn)這種效果。

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'))

在這個示例中,
每次提交的名字都會和存儲在用戶會話中的名字進行比較,
而會話中存儲的名字是前一次在這個表單中提交的數(shù)據(jù)。
如果兩個名字不一樣,
就會調用 flash() 函數(shù),
在發(fā)給客戶端的下一個響應中顯示一個消息。

Flash消息暫時不能顯示:需要渲染

僅調用 flash() 函數(shù)并不能把消息顯示出來,
程序使用的模板要渲染這些消息。
最好在基模板中渲染 Flash 消息,
因為這樣所有頁面都能使用這些消息。
Flask 把 get_flashed_messages() 函數(shù)開放給模板,
用來獲取并渲染消息,如下例所示。

渲染 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 %}

在模板中使用循環(huán)是因為在之前的請求循環(huán)中,
每次調用 flash() 函數(shù)時都會生成一個消息,
所以可能有多個消息在排隊等待顯示。
get_flashed_messages() 函數(shù)獲取的消息在下次調用時不會再次返回,
因此 Flash 消息只顯示一次,然后就消失了。

在這個示例中,
使用 Bootstrap 提供的警報 CSS 樣式渲染警告消息。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 22年12月更新:個人網站關停,如果仍舊對舊教程有興趣參考 Github 的markdown內容[https://...
    tangyefei閱讀 35,224評論 22 257
  • 第4章 Web表單 我們在第二章介紹過請求對象,它包含有客戶端請求的全部信息。尤其是,可以通過request.fo...
    易木成華閱讀 1,053評論 0 1
  • 請求對象包含客戶端發(fā)出的所有請求信息。其中 request.form 能獲取 POST 請求中提交的表單數(shù)據(jù)。 我...
    焉知非魚閱讀 1,145評論 0 2
  • 表單類 一個簡單的 Web 表單,包含一個文本字段和一個提交按鈕。例如:hello.py:定義表單類 String...
    ZZIXU閱讀 1,490評論 0 0
  • 第二部分 Blog例子 第八章 用戶驗證 大部分程序需要追蹤用戶身份。當用戶連接到程序,通過一系列步驟使自己的身份...
    易木成華閱讀 1,322評論 0 4