chapter4 - Web表單
本書使用的是Flask-WTf擴展來處理Web表單,它是把一個獨立的WTForms集成了在Flask程序中。可以使用pip安裝:(venv) $ pip install flask-wtf
4.1 跨站請求偽造保護
默認情況下,Flask-WTF可以保護所有表單免受跨站請求偽造(Cross-Site Request Forgery, CSRF)的攻擊。為了實現這個防護,Flask-WTf需要在程序中設置一個密鑰,然後再使用這個密鑰生成加密令牌,再用令牌驗證請求中表單數據的真偽。設置密鑰的方法如下: hello.py
app = Flask(__name__)app.config[SECRET_KEY] = your secret key
為了安全起見,密鑰是不應該硬編碼到程序中的,而是要保存到環境變數中。到第7章會介紹
4.2 表單類
使用Flask-WTF時,每個web表單都繼續自Form類。這個類中包含了常用HTML表單中的各種欄位,還為這些欄位設置了一些驗證函數可供使用,用於驗證表單的輸入數據。 如下為一個簡單的weg表單,包含了一個文本欄位和一個提交按鈕: hello.py
from flask_wtf import Formfrom wtforms import StringField, SubmitFieldfrom wtforms.validators import Requiredclass NameForm(Form): name = StringField(What is your name?, validators=[Required()]) submit = SubmitField(Submit)
name是一個文本欄位,submit是一個提交按鈕,分別相當於HTML中的:
<input type="text"><input type="submit" value=>
name使用的StringField方法的第一個參數是顯示在頁面中的提示信息,第二個參數是一個表單驗證項,用validators=[]方法實現,該方法接收一個列表,可在列表中放置多個驗證函數。 以下是WTForms支持的HTML標準欄位:
欄位類型說明StringField文本欄位TextAreaField多行文本欄位PasswordField密碼文本欄位HiddenField隱藏文本欄位DateField文本欄位,值為datetime.date格式DateTimeField文本欄位,值為datetime.datetime格式IntegerField文本欄位,值為整數DecimalField文本欄位,值為decimal.DecimalFloatField文本欄位,值為浮點數BooleanField複選框,值為True或FalseRadioField一組單選框SelectField下拉列表SelectMultipleField下拉列表,可選擇多個值FileField文件上傳欄位SubmitField表單提交按鈕FormField把表單作為欄位嵌入另一個表單FieldList一組指定類型的欄位以下是WTForms內建的驗證函數:
驗證函數說明Email驗證電子郵件地址EqualTo比較兩個欄位的值;常用於密碼確認IPAddress驗證IPv4網路地址Length驗證輸入字元串的長度NumberRange驗證輸入的值在數字範圍內Optional無輸入值時跳過其他驗證函數Required確保欄位中有數據Regexp使用正則表達式驗證輸入值URL驗證URLAnyOf確保輸入值在可選值列表中NoneOf確保輸入值不在可選值列表中
以下是幾個欄位和驗證函數的使用示例,完整的使用示例和說明可參考末尾處提供的文檔。
- 密碼錶單欄位的使用:
from wtforms import PasswordFieldpwd1 = PasswordField(請輸入你的密碼:, validators=[Required()])pwd2 = PasswordField(確認密碼:, validators=[Required(), EqualTo(pwd1, message="密碼不匹配")])
首先要導入欄位函數,不過為了方便整個表單的使用,也可以直接使用from wtforms.fields import *的方式導入所有可用表單欄位。PasswordField()方法的第一個參數是提示信息,第二個參數是驗證函數,值是由驗證函數組成的列表,用Required()可以設置必填項,EqualTo()用於匹配兩個表單的輸入,EqualTo()函數的第一個參數是需要進行比對的欄位名稱,第二個參數是匹配失敗的時候的提示信息
- 單選按鈕的使用
from wtforms import RadioFieldradios = RadioField(sex, choices=[(1, one), (2, two)])
與其它欄位類似,第一個參數是名字,但目前還沒有發現有什麼用處,在前端也不顯示。第二個參數就是單選按鈕的各個選項,可以是元組類型組成的列表,元組的第一個參數是value的值,第二個參數是顯示在頁面前端的名字。如上代碼在前端中是這樣子的:
<div class="radio"> <label> <input id="radio-0" name="radio" value="1" type="radio"> one </label></div><div class="radio"> <label> <input id="radio-1" name="radio" value="2" type="radio"> two </label></div>
對,Jinja2默認地用兩個div來分別顯示這兩個radio了,默認地在頁面前端看到的就是一個在上一個在下
- 多選框的使用
from wtforms import BooleanFieldcheckbox = BooleanField(one, default=True)
第一個參數是顯示在頁面的名字,第二個參數可設置是否默認選中,如果不設置或者為False,則不選中
4.3 把表單渲染成HTML
表單欄位是可調用的,可以通過參數例如form傳到模板中,然後可以在模板中生成一個簡單的表單:
<form method="POST"> {{ form.hidden_tag() }} {{ form.name.label }} {{ form.name() }} {{ form.submit() }}</form>
還可以把參數傳入渲染欄位的函數來改進表單的外觀,傳入的參數會被轉換成欄位的HTML屬性。如下代碼可以指定name欄位的id屬性:
<form method="POST"> {{ form.hidden_tag() }} {{ form.name.label }} {{ form.name(id=my-text-field) }} {{ form.submit() }}</form>
當然其實Bootstrap已經為我們提供了一些表單樣式,可以這樣調用:
{% import "bootstrap/wtf.html" as wtf %}{{ wtf.quick_form(form) }}
於是完整的表單代碼如下: 修改 templates/index.html
{% 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 %}
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修飾器中添加了method參數,使得index()視圖函數可以處理GET和POST請求,如果沒有添加的話,則只能處理GET請求,而表單提交通常應使用POST請求。 由於在表單類中為name欄位設置了驗證函數,所以如果驗證通過,則validate_on_submit()方法會返回True,否則返回False。首次載入頁面時,由於表單中沒有數據,因此會返回False,直接載入表單,而在填寫了表單數據並驗證難過後,伺服器會收到一個POST請求,就可以把輸入的數據賦值給name變數並傳到模板中進行渲染。form.name.data = 可以在獲取了表單輸入數據後清空輸入框。 而如果驗證沒通過的話,點擊提交按鈕時會收到一個錯誤提醒。
4.5 重定向和用戶會話
在當前版本的hello.py中,如果在輸入名字後提交表單,再刷新時,會被提示是否要重新發送已填寫數據。在多數情況下,這並不是個理想的處理方式。 使用重定向就可以很好地解決這個問題。Flask中把重定向當成一種特定的響應,其響應的內容是URL。
但還有一個問題,就是當程序處理POST請求結束後,表單中獲取到的用戶輸入數據就丟失了,如果使用重定向,就不能繼續使用這些數據。此時,就要使用到用戶會話,也就是第2章中提到的session。
使用了重定向和用戶會話後的視圖函數如下所示: 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))
本程序使用了用戶會話session來替代了前面的局部變數name,所以在兩次請求之間也能夠記住輸入的數據。 如上代碼中,如果表單輸入正確,則會調用到redirect()函數。該函數用於生成HTTP重定向響應,其參數是重定向的URL,它的第一個參數必須指定端點名,即路由內部的名字,默認情況下路由端點是相應視圖函數的名字。
4.6 Flash消息
Flash消息用於顯示確認消息、警告或者錯誤提醒。
來自Flask文檔中的Flash消息閃現說明:閃現系統的基本工作方式是:在且只在下一個請求中訪問上一個請求結束時記錄的消息。也就是說,當前看到的Flash消息,是上一次請求結束後設置的。
在hello.py文件中使用Flash消息:
from flask import redirectfrom flask import flashfrom flask import url_for@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))
在示例中,每次提交的名字會跟存儲在session中的上一次提交的名字做比較,如果不一樣,則會調用flash()函數,在發給客戶端的下一個響應中顯示消息。
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_message()函數獲取的消息在下次調用時不會再次返回,其實這個函數獲取到的消息是一個列表。
WTForms表單的具體使用可參數官方文檔WTForms Documentation
Flash消息閃現的使用可參Flash文檔-消息閃現推薦閱讀:
※我為什麼不贊同使用Python作為啟蒙語言
※遲來的第一篇文章。
※python 用list of lists表示矩陣的問題?
※如果每天堅持用12個小時學習一門編程語言,一年下來,編程能力會達到什麼程度?
※python多線程下載,進度條顯示問題如何解決?