WTForm表單編程javascript
在網頁中,爲了和用戶進行信息交互老是不得不出現一些表單。flask設計了WTForm表單庫來使flask能夠更加簡便地管理操做表單數據。WTForm中最重要的幾個概念以下:css
Form類,開發者自定義的表單必須繼承自Form類或者其子類。Form類最主要的功能是經過其所包含的Field類提供對錶單內數據的快捷訪問方式。html
各類Field類,即字段。通常而言每一個Field類都對應一個input的HTML標籤。好比WTForm自帶的一些Field類好比BooleanField就對應<input type="checkbox">,SubmitField就對應<input type="submit">等等。前端
Validator類。這個類用於驗證用戶輸入的數據的合法性。好比Length驗證器能夠用於驗證輸入數據的長度,FileAllowed驗證上傳文件的類型等等。java
另外,flask爲了防範csfr(cross-site request forgery)攻擊,默認在使用flask-wtf以前要求app必定要設置過secret_key。最簡單地能夠經過app.config['SECRET_KEY'] = 'xxxx'來配置。app的配置涉及到如何架構整個項目目錄,在之後再講,這裏默認這個SECRET_KEY已經配置完成。python
■ 表單類jquery
一個自定義表單類的例子以下:ajax
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出一個紅色的危險級別的消息。