WTForm表單編程
在網頁中,為了和用戶進行信息交互總是不得不出現一些表單。flask設計了WTForm表單庫來使flask可以更加簡便地管理操作表單數據。WTForm中最重要的幾個概念如下:
Form類,開發者自定義的表單必須繼承自Form類或者其子類。Form類最主要的功能是通過其所包含的Field類提供對表單內數據的快捷訪問方式。
各種Field類,即字段。一般而言每個Field類都對應一個input的HTML標簽。比如WTForm自帶的一些Field類比如BooleanField就對應<input type="checkbox">,SubmitField就對應<input type="submit">等等。
Validator類。這個類用於驗證用戶輸入的數據的合法性。比如Length驗證器可以用於驗證輸入數據的長度,FileAllowed驗證上傳文件的類型等等。
另外,flask為了防范csfr(cross-site request forgery)攻擊,默認在使用flask-wtf之前要求app一定要設置過secret_key。最簡單地可以通過app.config['SECRET_KEY'] = 'xxxx'來配置。app的配置涉及到如何架構整個項目目錄,在以后再講,這里默認這個SECRET_KEY已經配置完成。
■ 表單類
一個自定義表單類的例子如下:
from flask.ext.wtf import Form
#新版本的flask中都會提示現在這種import方法已經過時,最新的import應該是from flask_wtf import Form from wtforms import StringField,BooleanField,HiddenField,TextAreaField,DateTimeField
from wtforms.validators import FileAllowed,Required class BaseForm(Form): id = HiddenField("id") class BulletinForm(BaseForm): dt = DateTimeForm("發布時間",format="%Y-%m-%d %H:%M:%S") title = StringField("標題",validators=[Required()]) content = TextAreaField("內容") valid = BooleanField("是否有效") source = StringField("來源") author = StringField("作者") image = FileField("圖片上傳",validators = [FileAllowed(['jpg','png'],'Images Only!')])
從上面這個例子中可以看到,BaseForm類其實是一些類的基類,它具有一些很多類都會具有的特征,所以被構造成基類讓其他類型的表單再去繼承它。
● 各種Field字段
除了上面提到的這些常用的Field之外,在wtforms中還有以下這些Field可供使用:
PasswordField 密碼字段,自動將輸入轉化為小黑點
DateField 文本字段,格式要求為datetime.date一樣
IntergerField 文本字段,格式要求是整數
DecimalField 文本字段,格式要求和decimal.Decimal一樣
FloatField 文本字段,值是浮點數
BooleanField 復選框,值為True或者False
RadioField 一組單選框
SelectField 下拉列表,需要注意一下的是choices參數確定了下拉選項,但是和HTML中的<select> 標簽一樣,其是一個tuple組成的列表,可以認為每個tuple的第一項是選項的真正的值,而第二項是alias。
MultipleSelectField 可選多個值的下拉列表
● 各種Validator
Validator是驗證函數,把一個字段綁定某個驗證函數之后,flask會在接收表單中的數據之前對數據做一個驗證,如果驗證成功才會接收數據。驗證函數Validator如下,具體的validator可能需要的參數不太一樣,這里只給出一些常用的,更多詳細的用法可以參見wtforms/validators.py文件的源碼,參看每一個validator類需要哪些參數:
*基本上每一個validator都有message參數,指出當輸入數據不符合validator要求時顯示什么信息。
Email 驗證電子郵件地址的合法性,要求正則模式是^.+@([^.@][^@]+)$
EqualTo 比較兩個字段的值,通常用於輸入兩次密碼等場景,可寫參數fieldname,不過注意其是一個字符串變量,指向同表單中的另一個字段的字段名
IPAddress 驗證IPv4地址,參數默認ipv4=True,ipv6=False。如果想要驗證ipv6可以設置這兩個參數反過來。
Length 驗證輸入的字符串的長度,可以有min,max兩個參數指出要設置的長度下限和上限,注意參數類型是字符串,不是INT!!
NumberRange 驗證輸入數字是否在范圍內,可以有min和max兩個參數指出數字上限下限,注意參數類型是字符串,不是INT!!然后在這個validator的message參數里可以設置%(min)s和%(max)s兩個格式化部分,來告訴前端這個范圍到底是多少。其他validator也有這種類似的小技巧,可以參看源碼。
Optional 無輸入值時跳過同字段的其他驗證函數
Required 必填字段
Regexp 用正則表達式驗證值,參數regex='正則模式'
URL 驗證URL,要求正則模式是^[a-z]+://(?P<host>[^/:]+)(?P<port>:[0-9]+)?(?P<path>\/.*)?$
AnyOf 確保值在可選值列表中。參數是values(一個可選值的列表)。特別提下,和SelectField進行配合使用時,不知道為什么SelectField的choices中項的值不能是數字。。否則AnyOf的values參數中即使有相關數字也無法識別出當前選項是合法選項。我懷疑NoneOf可能也是一樣的套路。
NoneOf 確保值不在可選值列表中
此外再多說一句,在教材上和很多其他地方,基本上的路由模式都是把第一次進入表單的GET和提交表單數據時的POST的路徑指向同一個。但是在我下面的那個實踐當中,把POST單獨分離於GET,放到了另一個路由下。這么做可以更加清晰的管理URL,但是會讓validator失效。因為validator驗證時驗證的是POST路由下的表單對象,所以給出的反映也是給出到這個表單對象的,但是按照POST/GET分離的模式,必然要讓POST的響應函數最后重定向到GET路由下,而在GET的響應函數中必然會重新創建一個表單對象,使得之前的validator失效了。
有意思的是flash消息並不受這種模式的影響。因為flash消息是通過后端把消息推入一個消息隊列,然后前端拿去渲染,在POST上去后后端推入隊列的flash消息並不立刻反映到POST路由下的表單對象,而當路由回到GET下,重新建立了表單對象然后渲染表單時,正好把flash消息給渲染出來了。
■ 顯示表單
在建立了表單類之后,可以在響應函數的合適位置實例化表單對象,然后把表單對象作為render_template的參數傳遞給前端的模板。
在模板中通常可以調用一些已經定義好的宏來一鍵生成表單或者表單元素。當然也可以把表單的一個個元素按照一定順序寫到模板中手動地渲染模板。
● Bootstrap表單和flask-bootstrap
默認樣式的表單總是很丑的,如果想要美化表單界面,同時也引進一些宏來方便前端的渲染的話,那么可以考慮一些flask擴展。比如flask-bootstrap就是整合了flask和有名的前段框架bootstrap的擴展,經過pip install flask-bootstrap之后,我們可以到$PYTHON_HOME/Lib/site-package/flask_bootstrap/template/bootstrap目錄下找到一些文件。這里我們要用的是base.html和wtf.html。
base.html通過Jinja2模板語言給我們搭建了一個HTML文件的基本框架。我們可以讓項目中所有HTML文件都{% extends "base.html" %}的話就可以少寫很多代碼。只要在合適的地方重寫某個特定的block就可以了。需要提醒的一點是運用前端的這些擴展有時候會因為依賴或者其他神奇的原因【已經查明,神奇原因就是忘記寫Bootstrap(app)了,如果寫上這個的話,所有JS導入什么的引擎都會幫你做好的】而無法做到成功渲染,需要我們適當地修改源碼。比如base.html中定義了一些神奇的自動尋找bootstrap.css以及jquery.js等的宏,但是用的時候總是報錯。我的做法是把從網上下來的相關前端庫文件放到項目的特定static目錄下,然后修改base.html,去掉自動尋找的宏,手動指定引用路徑。
比如把base.html改造成下面這樣:
{% block doc -%} <!DOCTYPE html> <html{% block html_attribs %}{% endblock html_attribs %}> {%- block html %} <head> {%- block head %} <title>{% block title %}{{title|default}}{% endblock title %}</title> {%- block metas %} <meta name="viewport" content="width=device-width, initial-scale=1.0"> {%- endblock metas %} {%- block styles %} <!-- 把自動尋找宏改成靜態路徑 --> <link rel="stylesheet" href="../static/bootstrap/css/bootstrap.min.css"> {%- endblock styles %} <script type="text/javascript" src="../static/bootstrap/js/jquery.min.js"></script> <script type="text/javascript" src="../static/bootstrap/js/bootstrap.min.js"></script> <!-- 原先的block scripts放在body里面,按照我的習慣放到head里,並且在它之前把兩個js文件引用好。需要額外提醒的是一定要先引用jquery再引用bootstrap.js --> {% block scripts %} {% endblock scripts %} {%- endblock head %} </head> <body{% block body_attribs %}{% endblock body_attribs %}> {% block body -%} {% block navbar %} {%- endblock navbar %} {% block content -%} {%- endblock content %} {%- endblock body %} </body> {%- endblock html %} </html> {% endblock doc -%}
wtf.html是bootstrap為了支持wtforms組件而特別的一個存在。里面有一鍵生成表單的宏。一般而言在響應函數(或者說視圖函數)中我們實例化了一個表單對象,然后把它作為參數傳遞給render_template之后:
from flask_bootstrap import Bootstrap Bootstrap(app)"""這一步很重要!看似沒有什么實際用處,但是指出了這個flask項目前端和bootstrap的關系,如果沒有這句話,\
base.html和wtf.html必須要復制到項目目錄下來,還要改動下源碼。
渲染模板的時候也可能會報錯如找不到bootstrap_is_hidden_field方法,import bootstrap/wtf.html出錯等等 """ @app.route('/form') def form_test(): form = BulletinForm(request.form) return render_template("form.html",form=form)
在我們的模板文件form.html中可以寫這樣:
{% extends "base.html" %}
{# extends一定要寫第一行 #}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
{{ wtf.quick_form(form,form_type="horizontal",horizontal_columns=('lg',5,2))}}
{% endblock content %}
這里只是為了演示,就把表單直接渲染在block content中。wtf定義的quick_form可以一鍵生成表單,而且這個表單是帶bootstrap的CSS樣式的,比如按鈕是白色的,輸入框會發出藍光等等。不帶參數的情況下,表單默認寬度是頁面寬度,個人感覺有點丑,閱讀了一下wtf.html的源碼之后發現可以加入form_type以及horizontal_columns兩個參數來控制渲染的寬度。更多的參數也可以從源碼中去發掘,這里不多說了。看了源碼的另一個發現就是其實quick_form是對表單中的所有表單元素做了一個循環,每個循環都會調用form_field這個方法,所以我們也可以有選擇地進行表單元素的渲染比如:
{% block content %}
<form method='post'>
{{ wtf.form_field(form.title) }}
{{ wtf.form_field(form.author) }}
</form>
{% endblock %}
上面就是只渲染了兩個表單元素。和quick_form不同的是,quick_form會自帶地渲染一個<form>標簽,而form_field需要手動加上一個form標簽的。另外quick_form會對一些自帶的隱藏字段比如CSRFToken(防止跨站攻擊的一種防范手段)自動隱藏好,如果運用form_field可能會在表單的某些地方出現CSRF Token字樣,非常難看。
渲染完成之后的表單,每一個field都自帶id和name,id和name都是表單類中指定的那個field對象的變量名。
■ 獲取表單數據
一般而言,表單在flask的處理函數中表示為flask.request.form。如果表單成功遞交,這個對象中有數據的話,這個對象會是一個ImmutableMultiDict對象而不是我們所熟知的字典類型。ImmutableMultiDict類型有它自己的好處,以及考慮到要和其他flask組件兼容,盡量不要改變其類型。如果實在是不習慣或者只需要進行讀操作的話,那么可以調用ImmutableMultiDict的to_dict()方法來把這種類型的對象轉換成一個經典格式的字典副本。在實際使用過程中,我們通常會增加邏輯判斷form.validate_on_submit來判斷本次request是不是一個帶POST數據,且數據都符合表單定義是給出要求的請求。如果是那么就可以對表單數據做出一些處理,否則還是返回GET請求返回的頁面。
一般默認情況下,對於用wtf渲染出來的表單,action屬性是action=''。也就是說,單擊這個表單的submit默認是把表單數據POST到當前的URL相對路徑下。如果我們有需要把表單數據POST到不同的地方那么就需要在wtf.quick_form中添加action="xxx"的參數。由於之前不熟悉action這個屬性,就多說一句,action可以是相對路徑的某個文件,也可以是某個絕對的URL。
除了通過request這種比較通用的方法來獲取表單數據之外,我們還可以用form本身自帶的屬性來訪問表單數據。比如在validate_on_submit之后,可以直接通過<表單對象名>.<Field名稱>.data來直接訪問表單中某個具體字段提交上來的數據。
*有關換行符的一個小坑:在進行POST請求的時候,表單可以經過Submit來進行POST;另外如果給提交按鈕加一個AJAX的話,也可以通過AJAX來進行POST。比較了一下兩者后發現在換行符的處理上有一點微妙的區別。同樣是取比如說一個文本框中的幾行文字,通過POST到后端的數據其換行符默認是DOS中的\r\n,所以如果后端是在Unix系統上的話那么最好對換行符進行統一的處理。如果換做是通過AJAX,然后POST到后端的數據,同樣的幾行文字其換行符是\n。具體原理是什么還沒來得及深究,懷疑跟表單取數據以及jQuery里的val方法不同有關。
還有一個用ajax傳送數組到后台的坑。首先送到后台的form中,數組內容的key不是ajax中取名的key,而是key[],這一點可以通過在ajax請求中加上traditional:true來避免。第二點,一般的request.form.get方法是無法得到完整的數組列表的。正確的做法應該是request.form.getlist("list_name")。這個list_name要結合前面的考慮,即有沒有后面的那個[]。
下面是一個簡單的,異URL的表單POST和ajax的POST的比較的簡單例子,部分后端代碼:
####view.py#### @app.route('/form',methods=['GET']) def form(): content_form = ContentForm() return render_template("form.html",form=content_form) @app.route('/form_recv',methods=['POST']) def form_recv(): content_form = ContentForm() if content_form.validate_on_submit(): res_dict = request.form.to_dict() content = res_dict.get(u'content').replace('\r\n','\n') with open("static/demofile_form","wb+") as res_file: res_file.write(content) return redirect(url_for('form')) @app.route('/ajaxtest',methods=['POST']) def ajax_test(): content = request.form.get('content') with open("static/demofile_ajax","wb+") as res_file: res_file.write(content) return json.dumps({'code':200})
這里稍微提一下,我一開始想把接受POST請求的ajaxtest這個路由放到另外一個ajax.py的文件中,這樣整理起來方便一些。但是事實證明,這些路由函數似乎不能放在兩個不同的文件中,也就是說整個flask項目的所有@app.route必須都放在view.py里面?這個我是不太相信的。。不知道正確答案是什么。
部分前端代碼:
<!--form.html--> {% block content %} <div class="container"> <h1 style="text-align:center;">Welcome to Form.</h1> <hr> {{ wtf.quick_form(form,form_type='horizontal',horizontal_columns=('lg',3,6),action="/form_recv") }} </div> <div class="container"> <button class="btn btn-default" id="an_btn">ajax按鈕</button> </div> {% endblock %} {% block scripts %} {{ super() }} <!--不要忘了這個super,否則會報沒有JQ庫的錯誤--> <script> $(document).ready(function(){ $("#an_btn").click(function(){ $.ajax({ type:'POST', url:'/ajaxtest', data:{content:$("#content").val()}, datatype:'json', success:function(data) { var obj = JSON.parse(data); if (obj.code != 200) { alert(obj.msg); } else { alert('成功創建文件'); } } }); }); }); </script> {% endblock %}
上面這個例子中content_form中有一個文本輸入框,點擊確認按鈕可以通過表單提交文本框中內容,在后台生成demofile_form這個文件。文件打開模式用wb+時因為只用w時python會自動對換行符做轉換處理。如果后台是windows系統,那么就會變成\r\n。寫入模式改成wb之后,就變成寫入二進制文件,就不會做換行處理了。
點擊ajax按鈕則可以通過ajax把文本框中的內容提交給后台,后台創建文件demofile_ajax之后返回一個json串來告訴前端處理結果。
■ 關於flash消息的使用
在flask中包裝了flash消息,所謂flash消息就是一種前端風格上和Bootstrap相匹配的動態提示消息框。雖然flash消息是獨立屬於flask的一種組件,但是常常用於表單提交后的信息提示,所以放在了表單編程這一篇里面。
flash的使用方法很簡單,如下:
from flask import flash #其余import省略 @app.route('/form',method=['GET','PSOT']) def form(): form = content_form if form.validate_on_submit(): content = request.form.get(u'content') if len(content.split('\r\n')) > 10: flash(u'輸入最多不能超過十行') else: with open('static/demofile','wb+') as f: f.write(content) return render_template('form.html',form=form)
可以看到,在后端合適的位置直接寫flash('message')就可以發送flash消息到前端。那么前端的什么地方才會顯示flash消息呢?這個就需要前端代碼來進行控制了:
<!-- 比如在一個.container里面渲染flash消息的話--> <div class="container"> {% for message in get_flashed_messages() %} <div class="alert alert-danger alert-dismissable"> <button type="button" class="close" data-dismiss="alert">×</button> {{ message }} </div> {% endfor %}
上面的前端代碼就是用bootstrap的alert消息提示框來承載了flash消息。如果願意也可以用其他方式來呈現flash消息的。
● 更加靈活的flash消息
看了兩本書上關於flash消息全部都是這么一塊寫死,打印出來的消息也全部都是同一個分類的。為了實現更加靈活的flash消息渲染,看了下flask/helpers.py的源碼中的flash函數和get_flashed_messages函數,下面提出一些我自己的改良手段。首先在views.py中調用flash函數的時候,除了默認的message參數還可以增加一個category參數來指定這條消息的分級,這時flash出去的對象是一個tuple,其tuple[0]是分級信息,tuple[1]是渲染出來的消息。然后在前端jinja2模板中的get_flashed_message函數中可以添加with_categories=True參數來說明傳遞的是帶分級的消息tuple。如此便可把消息的分級傳遞給前端了。比如下面這個例子中的實踐:
##后端## if content.split('\n') <= 2: flash(message=u'輸入行數過少',category='warning') elif content.split('\n') >= 5: flash(message=u'輸入行數過大',category='danger') ##前端## {% block content %} {% for category,message in get_flashed_message(with_category=True ) %} <div class="alert alert-{{ category }} alert-dismissable"> <button type="button" class="close" data-dismiss="alert">&tims;</button> {{ message }} </div> {% endfor %} {% endblock %}
content可以是一個對應某個輸入框的表單對象,其輸入行數如果小於兩行就flash出一個黃色的警告級別的flash消息,如果大於五行就flash出一個紅色的危險級別的消息。