【百度云搜索蒸辆,搜各種資料:http://bdy.lqkweb.com】
【搜網(wǎng)盤赌朋,搜各種資料:http://www.swpan.cn】
本文翻譯自 The Flask Mega-Tutorial Part III: Web Forms
這是Flask Mega-Tutorial系列的第三部分伶贰,我將告訴你如何使用Web表單。
在第二章中我為應(yīng)用主頁創(chuàng)建了一個(gè)簡單的模板提陶,并使用諸如用戶和用戶動(dòng)態(tài)的模擬對(duì)象延刘。在本章中,我將解決這個(gè)應(yīng)用程序中仍然存在的眾多遺漏之一极谊,那就是如何通過Web表單接受用戶的輸入诡右。
Web表單是所有Web應(yīng)用程序中最基本的組成部分之一。 我將使用表單來為用戶發(fā)表動(dòng)態(tài)和登錄認(rèn)證提供途徑轻猖。
在繼續(xù)閱讀本章之前帆吻,確保你的microblog應(yīng)用程序狀態(tài)和上一章完結(jié)時(shí)一致,并且運(yùn)行時(shí)不會(huì)報(bào)任何錯(cuò)誤咙边。
本章的GitHub鏈接為:Browse, Zip, Diff.
Flask-WTF簡介
我將使用Flask-WTF插件來處理本應(yīng)用中的Web表單猜煮,它對(duì)WTForms進(jìn)行了淺層次的封裝以便和Flask完美結(jié)合。這是本應(yīng)用引入的第一個(gè)Flask插件败许,但絕不是最后一個(gè)友瘤。插件是Flask生態(tài)中的舉足輕重的一部分,F(xiàn)lask故意設(shè)計(jì)為只包含核心功能以保持代碼的整潔檐束,并暴露接口以對(duì)接解決不同問題的插件。
Flask插件都是常規(guī)的Python三方包束倍,可以使用pip
安裝被丧。 那就繼續(xù)在你的虛擬環(huán)境中安裝Flask-WTF吧:
(venv) $ pip install flask-wtf
配置
到目前為止盟戏,這個(gè)應(yīng)用程序都非常簡單,因此我不需要考慮它的配置甥桂。 但是柿究,除了最簡單的應(yīng)用,你會(huì)發(fā)現(xiàn)Flask(也可能是Flask插件)為使用者提供了一些可自由配置的選項(xiàng)黄选。你需要決定傳入什么樣的配置變量列表到框架中蝇摸。
有幾種途徑來為應(yīng)用指定配置選項(xiàng)。最基本的解決方案是使用app.config
對(duì)象办陷,它是一個(gè)類似字典的對(duì)象貌夕,可以將配置以鍵值的方式存儲(chǔ)其中。例如民镜,你可以這樣做:
app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... add more variables here as needed
上面的代碼雖然可以為應(yīng)用創(chuàng)建配置啡专,但是我有松耦合的癖好。因此制圈,我不會(huì)讓配置和應(yīng)用代碼處于同一個(gè)部分们童,而是使用稍微復(fù)雜點(diǎn)的結(jié)構(gòu),將配置保存到一個(gè)單獨(dú)的文件中鲸鹦。
使用類來存儲(chǔ)配置變量兼砖,才是我真正的風(fēng)格。我會(huì)將這個(gè)配置類存儲(chǔ)到單獨(dú)的Python模塊擅这,以保持良好的組織結(jié)構(gòu)辩稽。下面就讓你見識(shí)一下這個(gè)存儲(chǔ)在頂級(jí)目錄下(microblog/config.py),名為config.py的模塊的配置類吧:
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
簡單的不像話嵌戈,有沒有覆积? 配置設(shè)置被定義為Config
類中的屬性。 一旦應(yīng)用程序需要更多配置選項(xiàng)熟呛,直接依樣畫葫蘆宽档,附加到這個(gè)類上即可,稍后如果我發(fā)現(xiàn)需要多個(gè)配置集庵朝,則可以創(chuàng)建它的子類÷鹪現(xiàn)在則不用操心。
SECRET_KEY
是我添加的唯一配置選項(xiàng)九府,對(duì)大多數(shù)Flask應(yīng)用來說椎瘟,它都是極其重要的。Flask及其一些擴(kuò)展使用密鑰的值作為加密密鑰侄旬,用于生成簽名或令牌肺蔚。Flask-WTF插件使用它來保護(hù)網(wǎng)頁表單免受名為Cross-Site Request Forgery或CSRF(發(fā)音為“seasurf”)的惡意攻擊。顧名思義儡羔,密鑰應(yīng)該是隱密的宣羊,因?yàn)橛伤a(chǎn)生的令牌和簽名的加密強(qiáng)度保證璧诵,取決于除了可信維護(hù)者之外,沒有任何人能夠獲得它仇冯。
密鑰被定義成由or
運(yùn)算符連接兩個(gè)項(xiàng)的表達(dá)式之宿。第一個(gè)項(xiàng)查找環(huán)境變量SECRET_KEY
的值,第二個(gè)項(xiàng)是一個(gè)硬編碼的字符串苛坚。這種首先檢查環(huán)境變量中是否存在這個(gè)配置比被,找不到的情況下就使用硬編碼字符串的配置變量的模式你將會(huì)反復(fù)看到。在開發(fā)階段泼舱,安全性要求較低等缀,因此可以直接使用硬編碼字符串。但是柠掂,當(dāng)應(yīng)用部署到生產(chǎn)服務(wù)器上的時(shí)候项滑,我將設(shè)置一個(gè)獨(dú)一無二且難以揣摩的環(huán)境變量,這樣涯贞,服務(wù)器就擁有了一個(gè)別人未知的安全密鑰了枪狂。
擁有了這樣一份配置文件,我還需要通知Flask讀取并使用它宋渔≈菁玻可以在生成Flask應(yīng)用之后,利用app.config.from_object()
方法來完成這個(gè)操作:
from flask import Flask
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
from app import routes
導(dǎo)入Config
類的方式皇拣,乍一看可能會(huì)讓人感到困惑严蓖,不過如果你注意到從flask
包導(dǎo)入Flask
類的過程,就會(huì)發(fā)現(xiàn)這其實(shí)是類似的操作氧急。 顯而易見颗胡,小寫的“config”是Python模塊config.py的名字,另一個(gè)含有大寫“C”的是類吩坝。
正如我上面提到的毒姨,可以使用app.config
中的字典語法來訪問配置項(xiàng)。 在下面的Python交互式會(huì)話中钉寝,你可以看到密鑰的值:
>>> from microblog import app
>>> app.config['SECRET_KEY']
'you-will-never-guess'
用戶登錄表單
Flask-WTF插件使用Python類來表示W(wǎng)eb表單弧呐。表單類只需將表單的字段定義為類屬性即可。
為了再次踐行我的松耦合原則嵌纲,我會(huì)將表單類單獨(dú)存儲(chǔ)到名為app/forms.py的模塊中俘枫。就讓我們來定義用戶登錄表單來做一個(gè)開始吧,它會(huì)要求用戶輸入username和password逮走,并提供一個(gè)“remember me”的復(fù)選框和提交按鈕:
from flask_wtf import 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')
大多數(shù)Flask插件使用flask_ <name>
命名約定來導(dǎo)入鸠蚪,F(xiàn)lask-WTF的所有內(nèi)容都在flask_wtf
包中。在本例中,app/forms.py模塊的頂部從flask_wtf
導(dǎo)入了名為FlaskForm
的基類邓嘹。
由于Flask-WTF插件本身不提供字段類型酣栈,因此我直接從WTForms包中導(dǎo)入了四個(gè)表示表單字段的類。每個(gè)字段類都接受一個(gè)描述或別名作為第一個(gè)參數(shù)汹押,并生成一個(gè)實(shí)例來作為LoginForm
的類屬性。
你在一些字段中看到的可選參數(shù)validators
用于驗(yàn)證輸入字段是否符合預(yù)期起便。DataRequired
驗(yàn)證器僅驗(yàn)證字段輸入是否為空棚贾。更多的驗(yàn)證器將會(huì)在未來的表單中接觸到。
表單模板
下一步是將表單添加到HTML模板以便渲染到網(wǎng)頁上榆综。 令人高興的是在LoginForm類中定義的字段支持自渲染為HTML元素妙痹,所以這個(gè)任務(wù)相當(dāng)簡單。 我將把登錄模板存儲(chǔ)在文件*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 %}
一如第二章怯伊,在這個(gè)模板中我再次使用了extends
來繼承base.html
基礎(chǔ)模板。事實(shí)上判沟,我將會(huì)對(duì)所有的模板繼承基礎(chǔ)模板耿芹,以保持頂部導(dǎo)航欄風(fēng)格統(tǒng)一。
這個(gè)模板需要一個(gè)form參數(shù)的傳入到渲染模板的函數(shù)中挪哄,form來自于LoginForm
類的實(shí)例化吧秕,不過我現(xiàn)在還沒有編寫它。
HTML<form>
元素被用作Web表單的容器迹炼。 表單的action
屬性告訴瀏覽器在提交用戶在表單中輸入的信息時(shí)應(yīng)該請(qǐng)求的URL砸彬。 當(dāng)action
設(shè)置為空字符串時(shí),表單將被提交給當(dāng)前地址欄中的URL斯入,即當(dāng)前頁面砂碉。 method
屬性指定了將表單提交給服務(wù)器時(shí)應(yīng)該使用的HTTP請(qǐng)求方法。 默認(rèn)情況下是用GET
請(qǐng)求發(fā)送刻两,但幾乎在所有情況下增蹭,使用POST
請(qǐng)求會(huì)提供更好的用戶體驗(yàn),因?yàn)檫@種類型的請(qǐng)求可以在請(qǐng)求的主體中提交表單數(shù)據(jù)闹伪, GET
請(qǐng)求將表單字段添加到URL沪铭,會(huì)使瀏覽器地址欄變得混亂。
form.hidden_tag()
模板參數(shù)生成了一個(gè)隱藏字段偏瓤,其中包含一個(gè)用于保護(hù)表單免受CSRF攻擊的token
杀怠。 對(duì)于保護(hù)表單,你需要做的所有事情就是在模板中包括這個(gè)隱藏的字段厅克,并在Flask配置中定義SECRET_KEY
變量赔退,F(xiàn)lask-WTF會(huì)完成剩下的工作。
如果你以前編寫過HTML Web表單,那么你會(huì)發(fā)現(xiàn)一個(gè)奇怪的現(xiàn)象——在此模板中沒有HTML表單元素硕旗,這是因?yàn)楸韱蔚淖侄螌?duì)象的在渲染時(shí)會(huì)自動(dòng)轉(zhuǎn)化為HTML元素窗骑。 我只需在需要字段標(biāo)簽的地方加上{{ form.<field_name>.label }}
,需要這個(gè)字段的地方加上{{ form.<field_name>() }}
漆枚。 對(duì)于需要附加HTML屬性的字段创译,可以作為關(guān)鍵字參數(shù)傳遞到函數(shù)中。 此模板中的username和password字段將size
作為參數(shù)墙基,將其作為屬性添加到<input>
HTML元素中软族。 你也可以通過這種手段為表單字段設(shè)置class和id屬性。
表單視圖
完成這個(gè)表單的最后一步就是編寫一個(gè)新的視圖函數(shù)來渲染上面創(chuàng)建的模板残制。
函數(shù)的邏輯只需創(chuàng)建一個(gè)form實(shí)例立砸,并將其傳入渲染模板的函數(shù)中即可,然后用/login URL來關(guān)聯(lián)它初茶。這個(gè)視圖函數(shù)也存儲(chǔ)到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導(dǎo)入LoginForm
類,并生成了一個(gè)實(shí)例傳入模板恼布。form=form
的語法看起來奇怪螺戳,這是Python函數(shù)或方法傳入關(guān)鍵字參數(shù)的方式,左邊的form
代表在模板中引用的變量名稱桥氏,右邊則是傳入的form實(shí)例温峭。這就是獲取表單字段渲染結(jié)果的所有代碼了。
在基礎(chǔ)模板templates/base.html的導(dǎo)航欄上添加登錄的鏈接字支,以便訪問:
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
此時(shí)凤藏,你可以驗(yàn)證結(jié)果了。運(yùn)行該應(yīng)用堕伪,在瀏覽器的地址欄中輸入http://localhost:5000/
揖庄,然后點(diǎn)擊頂部導(dǎo)航欄中的“Login”鏈接來查看新的登錄表單。 是不是非常炫酷欠雌?
接收表單數(shù)據(jù)
點(diǎn)擊提交按鈕蹄梢,瀏覽器將顯示“Method Not Allowed”錯(cuò)誤。為什么呢富俄? 這是因?yàn)橹暗牡卿浺晥D功能到目前為止只完成了一半的工作禁炒。 它可以在網(wǎng)頁上顯示表單,但沒有邏輯來處理用戶提交的數(shù)據(jù)霍比。Flask-WTF可以輕松完成這部分工作幕袱, 以下是視圖函數(shù)的更新版本,它接受和驗(yàn)證用戶提交的數(shù)據(jù):
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)
這個(gè)版本中的第一個(gè)新東西是路由裝飾器中的methods
參數(shù)悠瞬。 它告訴Flask這個(gè)視圖函數(shù)接受GET
和POST
請(qǐng)求们豌,并覆蓋了默認(rèn)的GET
涯捻。 HTTP協(xié)議規(guī)定對(duì)GET
請(qǐng)求需要返回信息給客戶端(本例中是瀏覽器)。 本應(yīng)用的所有GET
請(qǐng)求都是如此望迎。 當(dāng)瀏覽器向服務(wù)器提交表單數(shù)據(jù)時(shí)障癌,通常會(huì)使用POST
請(qǐng)求(實(shí)際上用GET
請(qǐng)求也可以,但這不是推薦的做法)辩尊。之前的“Method Not Allowed”錯(cuò)誤正是由于視圖函數(shù)還未配置允許POST
請(qǐng)求涛浙。 通過傳入methods
參數(shù),你就能告訴Flask哪些請(qǐng)求方法可以被接受摄欲。
form.validate_on_submit()
實(shí)例方法會(huì)執(zhí)行form校驗(yàn)的工作蝗拿。當(dāng)瀏覽器發(fā)起GET
請(qǐng)求的時(shí)候,它返回False
蒿涎,這樣視圖函數(shù)就會(huì)跳過if
塊中的代碼,直接轉(zhuǎn)到視圖函數(shù)的最后一句來渲染模板惦辛。
當(dāng)用戶在瀏覽器點(diǎn)擊提交按鈕后劳秋,瀏覽器會(huì)發(fā)送POST
請(qǐng)求。form.validate_on_submit()
就會(huì)獲取到所有的數(shù)據(jù)胖齐,運(yùn)行字段各自的驗(yàn)證器玻淑,全部通過之后就會(huì)返回True
,這表示數(shù)據(jù)有效呀伙。不過补履,一旦有任意一個(gè)字段未通過驗(yàn)證,這個(gè)實(shí)例方法就會(huì)返回False
剿另,引發(fā)類似GET
請(qǐng)求那樣的表單的渲染并返回給用戶箫锤。稍后我會(huì)在添加代碼以實(shí)現(xiàn)在驗(yàn)證失敗的時(shí)候顯示一條錯(cuò)誤消息。
當(dāng)form.validate_on_submit()
返回True
時(shí)雨女,登錄視圖函數(shù)調(diào)用從Flask導(dǎo)入的兩個(gè)新函數(shù)谚攒。 flash()
函數(shù)是向用戶顯示消息的有效途徑。 許多應(yīng)用使用這個(gè)技術(shù)來讓用戶知道某個(gè)動(dòng)作是否成功氛堕。我將使用這種機(jī)制作為臨時(shí)解決方案馏臭,因?yàn)槲覜]有基礎(chǔ)架構(gòu)來真正地登錄用戶。 顯示一條消息來確認(rèn)應(yīng)用已經(jīng)收到登錄認(rèn)證憑據(jù)讼稚,我認(rèn)為對(duì)當(dāng)前來說已經(jīng)足夠了括儒。
登錄視圖函數(shù)中使用的第二個(gè)新函數(shù)是redirect()
。這個(gè)函數(shù)指引瀏覽器自動(dòng)重定向到它的參數(shù)所關(guān)聯(lián)的URL锐想。當(dāng)前視圖函數(shù)使用它將用戶重定向到應(yīng)用的主頁帮寻。
當(dāng)你調(diào)用flash()
函數(shù)后,F(xiàn)lask會(huì)存儲(chǔ)這個(gè)消息痛倚,但是卻不會(huì)奇跡般地直接出現(xiàn)在頁面上规婆。模板需要將消息渲染到基礎(chǔ)模板中,才能讓所有派生出來的模板都能顯示出來。更新后的基礎(chǔ)模板代碼如下:
<html>
<head>
{% if title %}
<title>{{ title }} - microblog</title>
{% else %}
<title>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
結(jié)構(gòu)在當(dāng)前模板的上下文中來將get_flashed_messages()
的結(jié)果賦值給變量messages
抒蚜。get_flashed_messages()
是Flask中的一個(gè)函數(shù)掘鄙,它返回用flash()
注冊(cè)過的消息列表。接下來的條件結(jié)構(gòu)用來檢查變量messages
是否包含元素嗡髓,如果有操漠,則在<ul>
元素中,為每條消息用<li>
元素來包裹渲染饿这。這種渲染的樣式結(jié)果看起來不會(huì)美觀浊伙,之后會(huì)有主題講到Web應(yīng)用的樣式。
閃現(xiàn)消息的一個(gè)有趣的屬性是长捧,一旦通過get_flashed_messages
函數(shù)請(qǐng)求了一次嚣鄙,它們就會(huì)從消息列表中移除,所以在調(diào)用flash()
函數(shù)后它們只會(huì)出現(xiàn)一次串结。
時(shí)機(jī)成熟哑子,再次測(cè)試表單吧,將username和password字段留空并點(diǎn)擊提交按鈕來觀察DataRequired
驗(yàn)證器是如何中斷提交處理流程的肌割。
完善字段驗(yàn)證
表單字段的驗(yàn)證器可防止無效數(shù)據(jù)被接收到應(yīng)用中卧蜓。 應(yīng)用處理無效表單輸入的方式是重新顯示表單,以便用戶進(jìn)行更正把敞。
如果你嘗試過提交無效的數(shù)據(jù)弥奸,相信你會(huì)注意到,雖然驗(yàn)證機(jī)制查無遺漏奋早,卻沒有給出表單錯(cuò)誤的具體線索盛霎。下一個(gè)任務(wù)是通過在驗(yàn)證失敗的每個(gè)字段旁邊添加有意義的錯(cuò)誤消息來改善用戶體驗(yàn)。
實(shí)際上伸蚯,表單驗(yàn)證器已經(jīng)生成了這些描述性錯(cuò)誤消息摩渺,所缺少的不過是模板中的一些額外的邏輯來渲染它們。
這是給username和password字段添加了驗(yàn)證描述性錯(cuò)誤消息渲染邏輯之后的登錄模板:
{% 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 %}
我做的唯一的改變是剂邮,在username和password字段之后添加for循環(huán)以便用紅色字體來渲染驗(yàn)證器添加的錯(cuò)誤信息摇幻。通常情況下,擁有驗(yàn)證器的字段都會(huì)用form.<field_name>.errors
來渲染錯(cuò)誤信息挥萌。 一個(gè)字段的驗(yàn)證錯(cuò)誤信息結(jié)果是一個(gè)列表绰姻,因?yàn)樽侄慰梢愿郊佣鄠€(gè)驗(yàn)證器,并且多個(gè)驗(yàn)證器都可能會(huì)提供錯(cuò)誤消息以顯示給用戶引瀑。
如果你嘗試在未填寫username和password字段的情況下提交表單狂芋,就可以看到顯眼的紅色錯(cuò)誤信息了。
生成鏈接
現(xiàn)在的登錄表單已經(jīng)相當(dāng)完整了憨栽,但在結(jié)束本章之前帜矾,我想討論在模板和重定向中包含鏈接的妥當(dāng)方法翼虫。 到目前為止,你已經(jīng)看到了一些定義鏈接的例子屡萤。 例如珍剑,這是當(dāng)前基礎(chǔ)模板中的導(dǎo)航欄代碼:
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
登錄視圖函數(shù)同樣定義了一個(gè)傳入到redirect()
函數(shù)作為參數(shù)的鏈接:
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# ...
return redirect('/index')
# ...
直接在模板和源文件中硬編碼鏈接存在隱患,如果有一天你決定重新組織鏈接死陆,那么你將不得不在整個(gè)應(yīng)用中搜索并替換這些鏈接招拙。
為了更好地管理這些鏈接,F(xiàn)lask提供了一個(gè)名為url_for()
的函數(shù)措译,它使用URL到視圖函數(shù)的內(nèi)部映射關(guān)系來生成URL别凤。 例如,url_for('login')
返回/login
领虹,url_for('index')
返回/index
规哪。 url_for()
的參數(shù)是endpoint名稱,也就是視圖函數(shù)的名字塌衰。
你可能會(huì)問由缆,為什么使用函數(shù)名稱而不是URL? 事實(shí)是猾蒂,URL比起視圖函數(shù)名稱變更的可能性更高。 稍后你會(huì)了解到的第二個(gè)原因是是晨,一些URL中包含動(dòng)態(tài)組件肚菠,手動(dòng)生成這些URL需要連接多個(gè)元素,枯燥乏味且容易出錯(cuò)罩缴。 url_for()
生成這種復(fù)雜的URL就方便許多蚊逢。
因此,從現(xiàn)在起箫章,一旦我需要生成應(yīng)用鏈接烙荷,我就會(huì)使用url_for()
∶始牛基礎(chǔ)模板中的導(dǎo)航欄部分代碼變更如下:
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('login') }}">Login</a>
</div>
login()
視圖函數(shù)也做了相應(yīng)變更:
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'))
# ...