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附加到表單字段的方法。

用戶登錄-表單視圖

在編寫完上一步的用戶登錄表單模板后,想要在瀏覽器中看到此表單的最后一步是:在應用程序中編寫一個它的視圖函數,用於渲染該模板。

因此,編寫一個映射到/login URL的視圖函數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 123456@qq.com,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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM