1. 程式人生 > 實用技巧 >08.flask部落格專案實戰三之表單

08.flask部落格專案實戰三之表單

配套視訊教程

本文B站配套視訊教程

實現:如何通過Web表單接受使用者的輸入。

其中,Web表單是任何Web應用程式中基本的構建塊之一。在此,將使用表單來允許使用者提交部落格帖子,以及登入應用程式。

Flask-WTF簡介和安裝

在Flask中,處理應用程式中的Web表單,將使用到Flask-WTF擴充套件庫,它是Flask和WTForms的簡單整合,主要功能有:使用CSRF(Cross-site request forgery,譯作 跨站請求偽造)令牌保護表單、檔案上傳、支援reCAPTCHA(譯作 反全自動區分計算機和人類的圖靈測試,簡單點就是:驗證碼)。擴充套件是Flask生態系統中非常重要的一部分。今後還會需要更多的擴充套件。

進入虛擬環境中,安裝Flask-WTF:pip install flask-wtf

(venv) D:\microblog>pip install flask-wtf
Collecting flask-wtf
  Using cached https://files.pythonhosted.org/packages/60/3a/58c629472d10539ae5167dc7c1fecfa95dd7d0b7864623931e3776438a24/Flask_WTF-0.14.2-py2.py3-none-any.whl
Requirement already satisfied: Flask in d:\microblog\venv\lib\site-packages (from flask-wtf)
Collecting WTForms (from flask-wtf)
  Using cached https://files.pythonhosted.org/packages/9f/c8/dac5dce9908df1d9d48ec0e26e2a250839fa36ea2c602cc4f85ccfeb5c65/WTForms-2.2.1-py2.py3-none-any.whl
Requirement already satisfied: Jinja2>=2.10 in d:\microblog\venv\lib\site-packages (from Flask->flask-wtf)
Requirement already satisfied: click>=5.1 in d:\microblog\venv\lib\site-packages (from Flask->flask-wtf)
Requirement already satisfied: Werkzeug>=0.14 in d:\microblog\venv\lib\site-packages (from Flask->flask-wtf)
Requirement already satisfied: itsdangerous>=0.24 in d:\microblog\venv\lib\site-packages (from Flask->flask-wtf)
Requirement already satisfied: MarkupSafe>=0.23 in d:\microblog\venv\lib\site-packages (from Jinja2>=2.10->Flask->flask-wtf)
Installing collected packages: WTForms, flask-wtf
Successfully installed WTForms-2.2.1 flask-wtf-0.14.2

將附帶安裝WTForms,因為它是Flask-WTF的一部分。在D:\microblogvenv\Lib\site-packages下將看到新安裝的這倆個擴充套件。

副檔名 版本號 簡要說明
flask-wtf 0.14.2 2017-08-13釋出,主要功能:使用CSRF令牌保護表單、檔案上傳、支援reCAPTCHA
wtforms 2.2.1 建立表單

配置 configuration

目前為止,這個應用程式足夠簡單,無需擔心它的配置。Flask(以及Flask擴充套件)在如何執行操作方面提供了很多自由,並需要做一些決定,並將這些決定作為一個配置變數列表傳遞給框架。

應用程式 有多種格式可指定配置選項

。最基本的方案:在app.config這個字典中,將定義的變數作為鍵。形如:

app = Flask(__name__)
app.config['SECRET_KEY'] = 'I am a secret, you can't guess.'
#需要的話,可繼續新增更多的變數

儘管上述語法可為Flask成功建立配置選項,但根據關注點分離原則(Separation of concerns, SoC),所以不要將配置放在建立應用程式的相同位置,而是:將配置儲存在單獨的.py檔案中,並使用類儲存配置變數,將該.py檔案放在專案頂級目錄下。
D:\microblog\config.py:金鑰配置

import os
class Config:
	SECRET_KEY = os.environ.get('SECRET_KEY') or 'you will never guess'

SECRET_KEY這個配置變數,將會被Flask及其擴充套件使用其值作為加密祕鑰,用於生產簽名或令牌。而Flask-WTF使用它來保護Web表單來免受CSFR攻擊。

金鑰的值是具有兩個術語的表示式,由or運算子連線。第一個術語是查詢環境變數的值;第二個術語是一個硬編碼的字串。當然這個安全性還是很低的。當將應用程式部署在生產伺服器上時,得設定一個安全級別高的。

其中os.environ是獲取本機系統的各種資訊(如環境變數等,你打印出來就明白了,哈哈),它是一個字典。我覺得os.environ.get('SECRET_KEY')在開發環境中並沒有用,是None,不知部署後是什麼。
有了上述這個配置檔案,接下來得讓Flask應用程式讀取並應用它。在建立Flask應用程式例項後,就用app.config.from_object()方法完成:

app/init.py:Flask配置

from flask import Flask
from config import Config#從config模組匯入Config類

app = Flask(__name__)
app.config.from_object(Config)

from app import routes

檢視剛才配置的金鑰是什麼:

(venv) D:\microblog>python
Python 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from microblog import app
>>> app.config['SECRET_KEY']
'you will never guess'
>>>

使用者登入表單

Flask-WTF擴充套件使用Python類來表示Web表單。表單類只是將表單的欄位定義為類變數。

再次根據SoC(關注點分離原則),新建一個forms.py模組來存放Web表單類。在此,定義一個使用者登入表單,要求使用者輸入使用者名稱、密碼,還包含“Remember Me”複選框、提交按鈕。
app/forms.py:使用者登入表單

from flask_wtf import FlaskForm#從flask_wtf包中匯入FlaskForm類
from wtforms import StringField,PasswordField,BooleanField,SubmitField#匯入這些類
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
	username = StringField('Username', validators=[DataRequired()])
	password = PasswordField('Password', validators=[DataRequired()])
	remember_me = BooleanField('Remember Me')
	submit = SubmitField('Sign In')

在Flask生態下,Flask擴充套件一般都使用flask_<name>這樣的命名約定作為在模組中頂級匯入的符號。在這個情況下,Flask-WTF的所有符號都在flask_wtf下,這也是FlaskForm基類在app/forms.py頂部匯入的地方。

from wtforms import StringField,PasswordField,BooleanField,SubmitField

這條語句表示:這個使用者登入表單的欄位型別的4個類是直接從WTForms包匯入的,因為Flask-WTF擴充套件是不提供自定義(欄位型別?)版本。對於每個欄位,將在LoginForm類中將物件建立為類變數。每個欄位都有一個描述或標籤作為第一個引數。

在某些欄位中看到的可選引數validators將驗證行為附加到欄位中,如使用者名稱、密碼肯定是需要進行驗證的。DataRequired驗證器只是簡單地檢查該欄位不會提交為空。當然還有其他的驗證器可用。

使用者登入-表單模板

有了上一步的登入表單,接下來得將表單新增到HTML模板中,讓其在網頁上呈現。
LoginForm類中定義的欄位知道如何將自己渲染為HTML。
app/templates/login.html:使用者登入表單模板

{% extends "base.html" %}
{% block content %}
	<h1>Sign In</h1>
	<form action="" method="post" novalidate>
		{{ form.hidden_tag() }}
		<p>
			{{ form.username.label }}<br>
			{{ form.username(size=32) }}
		</p>
		<p>
			{{ form.password.label }}<br>
			{{ form.password(size=32) }}
		</p>
		<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
		<p>{{ form.submit() }}</p>
	</form>
{% endblock %}

這個使用者登入表單模板使用了extends繼承語句繼承base.html模板,以確保一致的佈局,即基礎模板包含了所有頁面的頂部導航欄。

在此的使用者登入表單模板期望從LoginForm類例項化的表單物件作為引數給出,這個引數將由登入檢視函式(目前還未編寫)傳送。

以下將講述HTML知識,上述這段HTML程式碼中:<form>標籤用作Web表單的容器。
其中

  1. action屬性表示用於告知瀏覽器當用戶在表單中輸入資訊提交時應使用的URL。該屬性設定為空字串時,表單將提交到當前位於位址列的URL,即在頁面上呈現表單的URL。
  2. method屬性用於指定在將表單提交到伺服器時應使用的HTTP請求方法。預設情況下,是通過GET請求傳送它。但幾乎在所有情況下,使用POST請求會獲得更好的使用者體驗,因為此類請求可在請求正文中提交表單資料,GET請求將表單欄位新增到URL,會讓瀏覽器位址列變得混亂。
  3. novalidate屬性用於告知瀏覽器不對此表單中的欄位運用驗證,這有效地將此任務留給伺服器中執行的Flask應用程式。當然,使用novalidate完全是可選的,但對於第一種形式,設定它是很重要的,因為這將允許在本章後面的測試伺服器端驗證。
  4. form.hidden_tag()這個模板引數 生成一個隱藏欄位,其中包括用來防止CSRF攻擊的令牌。要使表單受保護,需要做的是包含此隱藏欄位,並在Flask配置中定義的SECRET_KEY變數。

寫過HTML Web表單的同學可能會發現這個模板中沒有HTML欄位,這是因為表單物件中的欄位知道如何將自己呈現(渲染)為HTML,需要做的就是{{ form.<field_name>.label }}需要的欄位標籤、{{ form.<field_name>() }}需要的欄位。對於需要其他HTML屬性的欄位,可將這些屬性作為引數傳遞。此模板中的使用者名稱、密碼欄位將size作為引數新增到<input>這個HTML標籤作為屬性。這還是可將CSS類、或ID附加到表單欄位的方法。

使用者登入-表單檢視

在編寫完上一步的使用者登入表單模板後,想要在瀏覽器中看到此表單的最後一步是:在應用程式中編寫一個它的檢視函式,用於渲染該模板。

因此,編寫一個對映到/loginURL的檢視函式login(),並將其傳遞給模板進行渲染。在routes模組中增加程式碼:

app/routes.py:使用者登入檢視函式

from flask import render_template
from app import app
from app.forms import LoginForm

#...

@app.route('/login')
def login():
	form = LoginForm()#表單例項化物件
	return render_template('login.html', title='Sign In', form=form)

上述檢視函式很簡單,從forms.py模組中匯入LoginForm類,然後例項化該類,最後將其傳送到模板。form=formreturn中將form例項物件賦值給form變數,這將獲得表單欄位所需的全部內容。

為了便於訪問登入表單,在基礎模板中改進,即在導航欄中包含指向它的連結:
app/templates/base.html:導航欄中增加登入連結

<div>
	Microblog:
	<a href="/index">Home</a>
	<a href="/login">Login</a>
</div>

此刻,執行應用程式就可瀏覽器中檢視該表單了。效果:圖略

接收表單資料

嘗試點選上述“Sign In”提交按鈕,瀏覽器將出現405錯誤“Method Not Allowed”。圖略

在上一步中,使用者登入的檢視函式執行了一半的工作,即可在網頁上顯示錶單。但它沒有處理使用者提交的資料的邏輯。這是Flask-WTF讓這項邏輯處理變得非常簡單的優勢。更新使用者登入檢視函式程式碼,它接受、驗證使用者提交的資料:
app/routes.py:接收登入憑據

from flask import render_template,flash,redirect

@app.route('/login',methods=['GET','POST'])
def login():
	form = LoginForm()
	if form.validate_on_submit():
		flash('Login requested for user {},remember_me={}'.format(form.username.data,form.remember_me.data))
		return redirect('/index')
	return render_template('login.html',title='Sign In',form=form)

@app.routes()裝飾器中引數methods作用是:告訴Flask這個檢視函式接受GETPOST請求方法,覆蓋預設值(即只接受GET請求)。HTTP協議中,GET請求是將資訊返回給客戶端(如瀏覽器)的請求,到目前為止,該應用程式中的所有請求都屬於這種型別;POST請求通常在瀏覽器上伺服器提交表單資料時使用。上述出現“Method Not Allowed”,是因為瀏覽器嘗試傳送POST請求,而應用程式沒有配置去接受它。

form.validate_on_submit()方法完成所有表單處理工作。當瀏覽器傳送GET接收帶有表單的網頁請求時,此方法將返回False,此時函式會跳過if語句並直接在函式的最後一行呈現模板。
當用戶在瀏覽器按下提交按鈕時,瀏覽器傳送POST請求,form.validate_on_submit()將收集所有資料,執行附加到欄位的所有驗證器,如果一切正常,它將返回True,表明資料有效且可由應用程式處理。但如果至少有一個欄位未通過驗證,則函式就會返回False,接著就像上述GET請求那樣。
form.validate_on_submit()返回True,這個登入檢視函式將呼叫兩個函式,分別是flash()、redirect(),均從flask包匯入的。
flash()用於向用戶顯示訊息,如讓使用者知道某些操作是否成功。目前為止,將使用其機制作為臨時解決方案,因為暫無使用者登入未真實所需的基礎結構,此時只是顯示一條訊息用於確認應用程式已收到憑據。
redirect()用於指示客戶端(瀏覽器)自動導航到作為引數給出的其他頁面(如上述程式碼中的/index頁面,即重定向到應用程式的/index頁面)。

當呼叫flash()函式時,Flask會儲存該訊息,但閃爍的訊息不會神奇地出現在Web頁面中。應用程式的模板需要以適用於站點佈局的方式呈現/渲染這些閃爍的訊息。因此,將這些訊息新增到基礎模板中,以便所有模板都繼承此功能。更新基礎模板
app/templates/base.html:基礎模板中的閃爍訊息

<html>
	<head>
		{% if title %}
			<title>{{ title }} - Microblog</title>
		{% else %}
			<title>Welcome to Microblog</title>
		{% endif %}
	</head>
	<body>
		<div>Microblog:<a href="/index">Home</a><a href="/login">Login</a></div>
		<hr>
		{% with messages = get_flashed_messages() %}
		{% if messages %}
		<ul>
			{% for message in messages %}
			<li>{{ message }}</li>
			{% endfor %}
		</ul>
		{% endif %}
		{% endwith %}
		{% block content %}
		{% endblock %}
	</body>
</html>

上述程式碼中,使用with結構將呼叫get_flashed_messages()的結果分配給變數messages,都在模板的上下文。這個get_flashed_messages()函式來自Flask,並返回flash()之前已註冊的所有訊息的列表。接著if語句判斷messages是否具有某些內容,在這種情況下,一個ul標籤被渲染成每個訊息作為一個li標籤列表項。而這種渲染風格看起來不太好,但Web應用程式樣式化的主題將在稍後出現。

這些閃爍的訊息的一個有趣屬性是:一旦通過get_flashed_messages()請求它們,它們就會從列表中刪除,因此它們在flash()呼叫後只出現一次。

執行程式,再次測試表單是如何工作的。確保將使用者名稱或密碼欄位為空來提交表單,以檢視DataRequired驗證器如何暫停提交過程。

C:\Users\Administrator>d:

D:\>cd D:\microblog\venv\Scripts

D:\microblog\venv\Scripts>activate
(venv) D:\microblog\venv\Scripts>cd D:\microblog

(venv) D:\microblog>flask run
 * Serving Flask app "microblog.py"
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [07/Aug/2018 16:16:26] "GET /login HTTP/1.1" 200 -

使用者名稱或密碼為空時提交表單,網頁沒反應。都不為空時,隨意輸入。
圖略

點選Sign in按鈕後,倒是出現了一條訊息:Login requested for user [email protected],remember_me=Flase
圖略

增強欄位驗證

附加到表單欄位的驗證器可防止無效資料接受到應用程式中。應用程式處理無效表單輸入的方式是重新顯示錶單,讓使用者進行必要的更正。

當提交無效資料時,卻沒有明顯提示使用者提交的資料有問題,只是重新返回表單,這將影響使用者體檢。因此,現在的任務是:通過在驗證失敗的每個欄位傍邊增加有意義的錯誤提示來改善使用者體驗。

實際上,表單驗證器已經生成了這些描述性錯誤訊息,因此,缺少的是在模板中用於渲染/呈現它們的一些額外邏輯。在使用者登入模板的使用者名稱、密碼欄位中新增欄位驗證訊息:更新程式碼
app/templates/login.html:提示欄位驗證錯誤訊息

{% extends "base.html" %}

{% block content %}
    <h1>Sign In</h1>
    <form action="" method="post" novalidate>
        {{ form.hidden_tag() }}
        <p>
			{{ form.username.label }}<br>
			{{ form.username(size=32) }}<br>
			{% for error in form.username.errors %}
			<span style="color:red;">[{{ error }}]</span>
			{% endfor %}
        </p>
        <p>
			{{ form.password.label }}<br>
			{{ form.password(size=32) }}<br>
			{% for error in form.password.errors %}
			<span style="color:red;">[{{ error }}]</span>
			{% endfor %}
        </p>
        <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

上述程式碼中,只是在使用者名稱、密碼欄位之後新增for迴圈,以紅色字型訊息渲染錯誤訊息。一般規則下,任何附加驗證器的欄位都會通過form.<field_name>.errors新增錯誤訊息。這將是一個列表,因為欄位可以附加多個驗證器,並且多個可能提供錯誤訊息提示給使用者。

如果嘗試提交空使用者名稱或密碼的表單,將看到紅色錯誤提示,效果:圖略

生成URL

使用者登入表單現在比較完整了,下面將學習在模板包含連結和重定向的方法。 例:基礎模板中的當前導航欄

<div>
     Microblog:
     <a href="/index">Home</a>
     <a href="/login">Login</a>
</div>

登入檢視函式還定義了傳遞給redirect()函式的連結:

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect('/index')
    # ...

直接在模板、原始檔中編寫連結的一個問題是:如果將來某天要重新組織連結,將不得不修改整個應用程式的這個連結,搜尋、替換。

為更好地控制這些連結,Flask提供了一個名為url_for()函式,它使用URL的內部對映到檢視函式來生成URL。例:url_for('login')返回/loginurl_for('index')返回/indexurl_for()中的引數就是端點名稱,也就是檢視函式的名字。

使用函式名稱而不是URL的優點:URL比檢視函式名稱更可能發生變化;某些URL很可能包含動態元件,手動生成這URL需要連線多個元素,這極易出錯,而url_for()能生成這些複雜的URL。

因此,今後每次應用程式要生成URL時,都使用url_for()
更新基礎模板中的程式碼:
app/templates/base.html:使用url_for()進行連結

...
    <div>
        Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        <a href="{{ url_for('login') }}">Login</a>
    </div>
...

更新login()檢視函式中的程式碼:
app/routes.py:對連結使用url_for()函式

from flask import render_template, flash, redirect, url_for

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect(url_for('index'))
    # ...

目前為止,專案結構:

microblog/
    venv/
    app/
        templates/
            base.html
            index.html
            login.html
        __init__.py
        forms.py
        routes.py
    microblog.py

參考
https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-iii-web-forms