Python Flask - 使用 WTForm 快速建立表單 feet. flask檔案佈局 & bootstrap
前言
在網站中常常用到各式各樣的表單(e.g. 註冊、登入、留言等),然而每次都要重寫表單實在太麻煩,flask提供flask-wtform套件幫助我們快速建立表單。但單純的表單是沒有辦法滿足我虛榮的內心🤷🏻,剛好bootstrap也支援wtform,我們利用bootstrap美化我們的表單。而隨著我們專案變得更大,檔案與code也變得更多更複雜,我們在import檔案時也遇到的一點狀況,為此也影響我們專案的佈局。
💡 若你想要安心服用這篇文章,建議你先瞧瞧
Python Flask - 使用 Flask Bootstrap 渲染你的HTML
Python Flask - 使用 Flask SQLAlchemy 建立資料庫
安裝與使用flask-wtf & WTForm
安裝flask-wtf會連同WTForm一起安裝
1
pip install flask-wtf
新增froms.py設定註冊表格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37# froms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import(
DataRequired,
Length,
Email,
EqualTo,
ValidationError
)
from app import User
# 繼承flask_wtf.FlaskForm
class RegisterForm(FlaskForm):
username = StringField("Username", validators=[DataRequired(), Length(min=8, max=20)])
# "tableName",
# validators(檢查條件):
# DataRequired: 是否為null,
# Length長度限制
email = StringField("Email", validators=[DataRequired(), Email()])
password = PasswordField("Password", validators=[DataRequired(), Length(min=8, max=20)])
confirm = PasswordField("Repeat Password", validators=[DataRequired(), EqualTo("password")])
# 要求user重新輸入,並與password相同
submit = SubmitField("Register")
# 檢查username是否已存在
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError("Username already token")
# wtform支援設定錯誤訊息
# 檢查email是否已存在
def validate_email(self, email):
email = User.query.filter_by(email=email.data).first()
if email:
raise ValidationError("Email already token")在app.py引入RegisterForm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60# app.py
from flask import (
Flask,
render_template, # 導向html
flash, # 顯示訊息
redirect, # 重新導向url
url_for # 轉換URL
)
from flask_bootstrap import Bootstrap
from flask_sqlalchemy import SQLAlchemy
from froms import RegisterForm # 引入forms.py
app = Flask(__name__)
bootstrap = Bootstrap(app)
# 設定資料庫儲存位置
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/mydatabase.sqlite'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# 使用form必須設定私鑰(用於傳輸form的data)
app.config['SECRET_KEY'] = 'A_VERY_LONG_SECRET_KEY'
# 在正式環境中必須設定一個足夠長的私有密鑰,這裡僅示意
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
password = db.Column(db.String(20), nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return f'<User {self.username}>'
def index():
return render_template("index.html")
# 設定註冊頁面URL
def register():
form = RegisterForm() # 使用froms.py的RegisterForm表單
if form.validate_on_submit():
username = form.username.data
email = form.email.data
password = form.password.data
# 新建User
user = User(username=username, password=password, email=email)
db.session.add(user)
# 儲存至資料庫
db.session.commit()
# 使用flash函數顯示註冊成功訊息
flash("Congrats, registration success!", category="success")
# category:
# success: 綠
# info: 藍
# danger: 紅
return redirect(url_for("index")) # 重新導向"index"
return render_template("register.html", form=form) # 載入/templates模板在/templates新增register.html模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<!-- register.html -->
<h1>This is Register Page.</h1>
<div class="row">
<div class="col-md-6">
<!-- 顯示flash -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- 顯示表單 -->
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
</div>
</div>這時可以運行app.py,但會報下面這個錯誤訊息
ImportError: cannot import name 'User' from partially initialized module 'app' (most likely due to a circular import)
這是因為app.py引入forms.py(RegisterForm)時,forms.py又引入app.py(User),形成A導入B,B又導入A的Import Error(導入錯誤)狀況發生。而好的部署能夠有效防止這類的狀況發生,並且讓其他開發人員不會看到你的code就頭暈 😵💫
flask佈局
在常見的專案當中,會將config.py(設定檔)與app.py or run.py(執行檔)至於專案最外層,其餘副程式會置於/app當中。
/app中的__init__.py必須存在(即使為null也無妨),python才可以識別資料夾
1 | . |
在Python Flask - 使用 Flask SQLAlchemy 建立資料庫時,我們資料庫位於/tmp內,這裡我沒有特別指定位置,flask會自動在專案內新增/instance,並將資料庫儲存於此資料夾下
run.py
1
2
3
4
5# run.py
from app import app
if __name__ == "__main__":
app.run(debug=True)config.py
1
2
3
4
5
6
7
8
9# config.py
class Config(object):
# 設定資料庫儲存位置
SQLALCHEMY_DATABASE_URI = 'sqlite:///database.sqlite'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 使用form必須設定私鑰(用於傳輸form的data)
app.config['SECRET_KEY'] = 'A_VERY_LONG_SECRET_KEY'
# 在正式環境中必須設定一個足夠長的私有密鑰,這裡僅示意⚠️ 關於金鑰這類型的變數通常不會儲存專案檔當中(因版控git會上傳至remote(雲端)),而是放在環境變數當中(未來有機會再特別說明)
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# __init__.py
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_sqlalchemy import SQLAlchemy
# 引用Config.py 內含db路徑, secret key
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
bootstrap = Bootstrap(app)
db = SQLAlchemy(app)
# 取代原本路由的位置(app的裝飾器)
from app.routes import *routes.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# routes.py
from flask import render_template, flash, redirect, url_for
from app import app, db
from app.froms import RegisterForm
from app.models import User
def index():
return render_template("index.html")
def register():
form = RegisterForm()
if form.validate_on_submit():
username = form.username.data
email = form.email.data
password = form.password.data
user = User(username=username, password=password, email=email)
db.session.add(user)
db.session.commit()
flash("Congrats, registration success!", category="success")
return redirect(url_for("index"))
return render_template("register.html", form=form)forms.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import(
DataRequired,
Length,
Email,
EqualTo,
ValidationError
)
from app.models import User
class RegisterForm(FlaskForm):
username = StringField("Username", validators=[DataRequired(), Length(min=8, max=20)])
# "tableName",
# validators(檢查條件):
# DataRequired: 是否為null
# Length長度
email = StringField("Email", validators=[DataRequired(), Email()])
password = PasswordField("Password", validators=[DataRequired(), Length(min=8, max=20)])
confirm = PasswordField("Repeat Password", validators=[DataRequired(), EqualTo("password")])
submit = SubmitField("Register")
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError("Username already token")
def validate_email(self, email):
email = User.query.filter_by(email=email.data).first()
if email:
raise ValidationError("Email already token")models.py
1
2
3
4
5
6
7
8
9
10# models.py
from app import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return f'<User {self.username}>'
- 完成部署後執行
flask run --debug在http://localhost:5000/register就可以看到我們簡易且醜陋的表單(歡呼🥳)FlaskRegisterForm
flask前端佈局
在/templates下的html檔其實也有很多可以重複利用的地方,為了減少寫相同的code(懶?),這裡也使用相似的概念佈局,同時也用bootstrap美化我們的html
1 | /templates |
navbar.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<!-- navbar.html -->
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ url_for('index') }}">Flask App</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li>
<a href="{{ url_for('index') }}">Home<span class="sr-only">(current)</span></a>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>base.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35<!-- base.html -->
<html>
<head>
<meta charset="UTF-8">
<!-- 引入bootstrap模板 -->
{% include "bootstrap/base.html" %}
</head>
<body>
{% block navbar %}
<!-- 引入navbar.html -->
{% include "navbar.html" %}
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-6">
<!-- 顯示flash message -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
</div>
{% block app_content %}
{% endblock app_content%}
</div>
{% endblock %}
</body>
</html>index.html
1
2
3
4
5
6
7<!-- index.html -->
<!-- 引入base.html -->
{% extends "base.html" %}
{% block app_content %}
<h1>OMG!!! flask-bootsrtap is great.</h1>
{% endblock %}register.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14<!-- register.html -->
<!-- 引入base.html -->
{% extends "base.html" %}
{% block app_content %}
<h1>This is Register Page.</h1>
<div class="row">
<div class="col-md-6">
<!-- 引入bootstrap模板,並引入表單 -->
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
- 更改後就可以看到美滋滋💄的註冊表單與首頁了🥳
FlaskRegisterForm II
雖然現在後端還沒完成,但你仍可以試著輸入資料到表格當中,wtfrom最讚的是: 在前端會依設定先行檢查數據,降低後端的工作量(給你一個讚🫵🏻👍🏻)
💡 因為在forms.py中每一個table都有設定validators屬性來限制client的輸入
結論
- 使用flask-wtf快速建立表單
- 使用wtf.quick_form必須設定
SECRET KEY ❗️ - 重新佈局flask,解決Import Error問題
- 使用bootstrap模板美化wtform
- wtform 在前端時會依設定條件先行檢查form中的數據