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

  1. 安裝flask-wtf會連同WTForm一起安裝

    1
    pip install flask-wtf
  2. 新增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")
  3. 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}>'

    @app.route("/index", methods=["GET", "POST"])
    def index():
    return render_template("index.html")

    # 設定註冊頁面URL
    @app.route("/register", methods=["GET","POST"])
    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模板
  4. /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>
  5. 這時可以運行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
2
3
4
5
6
7
8
9
10
11
12
13
.
├── run.py # 執行檔
├── config.py # 設定檔
└── /app
├── __init__.py
├── routes.py # url路徑
├── forms.py # 表單模型
├── models.py # 資料庫table模型
├── /templates # html
│ ├── index.html
│ └── register.html
└── /instance
└── database.sqlite # 儲存DB

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
    @app.route("/index", methods=["GET", "POST"])
    def index():
    return render_template("index.html")

    @app.route("/register", methods=["GET","POST"])
    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}>'
  1. 完成部署後執行flask run --debughttp://localhost:5000/register就可以看到我們簡易且醜陋的表單(歡呼🥳)
    FlaskRegisterForm

flask前端佈局

/templates下的html檔其實也有很多可以重複利用的地方,為了減少寫相同的code(懶?),這裡也使用相似的概念佈局,同時也用bootstrap美化我們的html

1
2
3
4
5
/templates          
├── navbar.html # 導航列
├── base.html # 模板基底
├── index.html
└── register.html
  • 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 -->
    <!DOCTYPE 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 %}
  1. 更改後就可以看到美滋滋💄的註冊表單與首頁了🥳
    FlaskRegisterForm II

雖然現在後端還沒完成,但你仍可以試著輸入資料到表格當中,wtfrom最讚的是: 在前端會依設定先行檢查數據,降低後端的工作量(給你一個讚🫵🏻👍🏻)

💡 因為在forms.py中每一個table都有設定validators屬性來限制client的輸入

結論

  • 使用flask-wtf快速建立表單
  • 使用wtf.quick_form必須設定SECRET KEY❗️
  • 重新佈局flask,解決Import Error問題
  • 使用bootstrap模板美化wtform
  • wtform 在前端時會依設定條件先行檢查form中的數據