WTForms 簡化了 Flask 應用程式中 Web 表單的建立和管理,提供便捷的資料處理和驗證機制。開發者可以定義表單類別,並使用內建或自定義的驗證器確保資料有效性。文章也探討了自定義欄位和元件的建立,例如根據資料函式庫動態生成下拉選單選項,以及客製化前端顯示方式。此外,文章詳細說明瞭檔案上傳的完整流程,包含設定儲存路徑、安全檔名處理以及資料函式庫整合。最後,文章強調了 CSRF 保護的重要性,並提供如何在表單和 AJAX 請求中加入 CSRF Token 的方法,確保應用程式安全性。
使用 WTForms 建立 Web 表單
本章節將介紹如何使用 WTForms 套件在 Flask 應用程式中建立和管理 Web 表單。WTForms 提供了一個簡單且有效的方式來處理表單資料和驗證。
建立產品表單
首先,我們需要定義一個表單類別來處理產品的建立。在 models.py 中,我們定義了一個 ProductForm 類別:
from decimal import Decimal
from flask_wtf import FlaskForm
from wtforms import StringField, DecimalField, SelectField
from wtforms.validators import InputRequired, NumberRange
class ProductForm(FlaskForm):
name = StringField('名稱', validators=[InputRequired()])
price = DecimalField('價格', validators=[InputRequired(), NumberRange(min=Decimal('0.0'))])
category = SelectField('類別', validators=[InputRequired()], coerce=int)
這個表單類別包含了三個欄位:name、price 和 category。每個欄位都有對應的驗證器,以確保輸入的資料是有效的。
內容解密:
StringField用於輸入字串型別的資料,例如產品名稱。DecimalField用於輸入小數型別的資料,例如產品價格。NumberRange驗證器確保價格不小於 0。SelectField用於選擇類別,coerce=int表示選擇的值將被轉換為整數。
處理表單提交
在 views.py 中,我們定義了一個 create_product 方法來處理表單提交:
@catalog.route('/product-create', methods=['GET', 'POST'])
def create_product():
form = ProductForm(meta={'csrf': False})
categories = [(c.id, c.name) for c in Category.query.all()]
form.category.choices = categories
if form.validate_on_submit():
name = form.name.data
price = form.price.data
category = Category.query.get_or_404(form.category.data)
product = Product(name, price, category)
db.session.add(product)
db.session.commit()
flash('產品 %s 已建立' % name, 'success')
return redirect(url_for('catalog.product', id=product.id))
if form.errors:
flash(form.errors, 'danger')
return render_template('product-create.html', form=form)
內容解密:
form.validate_on_submit()用於檢查表單是否被提交且資料是否有效。- 如果資料有效,則建立一個新的產品例項並儲存到資料函式庫中。
- 如果資料無效,則顯示錯誤訊息。
驗證欄位
WTForms 提供了多種驗證器來檢查輸入的資料。在上面的例子中,我們使用了 InputRequired 和 NumberRange 驗證器。
建立共用表單
我們可以建立一個共用表單類別來包含共用的欄位。例如,我們可以建立一個 NameForm 類別:
class NameForm(FlaskForm):
name = StringField('名稱', validators=[InputRequired()])
然後,其他表單類別可以繼承自這個共用表單類別:
class ProductForm(NameForm):
price = DecimalField('價格', validators=[InputRequired(), NumberRange(min=Decimal('0.0'))])
category = SelectField('類別', validators=[InputRequired()], coerce=int)
class CategoryForm(NameForm):
pass
內容解密:
NameForm類別包含了一個name欄位。ProductForm和CategoryForm類別繼承自NameForm,因此也包含了name欄位。
使用 WTForms 處理 Web 表單
自定義欄位與驗證
在前面的章節中,我們使用 SelectField 來處理產品的類別選擇。現在,我們將自定義一個欄位來自動從資料函式庫中擷取類別選項。
如何實作
首先,在 models.py 中定義一個自定義的 CategoryField 類別:
class CategoryField(SelectField):
def iter_choices(self):
categories = [(c.id, c.name) for c in Category.query.all()]
for value, label in categories:
yield (value, label, self.coerce(value) == self.data)
def pre_validate(self, form):
for v, _ in [(c.id, c.name) for c in Category.query.all()]:
if self.data == v:
break
else:
raise ValueError(self.gettext('Not a valid choice'))
class ProductForm(NameForm):
price = DecimalField('Price', validators=[InputRequired(), NumberRange(min=Decimal('0.0'))])
category = CategoryField('Category', validators=[InputRequired()], coerce=int)
程式碼解析
class CategoryField(SelectField):
# ...
- 我們定義了一個
CategoryField類別,繼承自SelectField。 iter_choices方法用於產生類別選項的迭代器,直接從資料函式庫中擷取類別資料。
def iter_choices(self):
categories = [(c.id, c.name) for c in Category.query.all()]
for value, label in categories:
yield (value, label, self.coerce(value) == self.data)
pre_validate方法用於驗證所選的類別是否有效。
def pre_validate(self, form):
for v, _ in [(c.id, c.name) for c in Category.query.all()]:
if self.data == v:
break
else:
raise ValueError(self.gettext('Not a valid choice'))
修改 views.py
由於我們已經在 CategoryField 中處理了類別選項的擷取,因此我們可以從 create_product 方法中移除以下兩行程式碼:
categories = [(c.id, c.name) for c in Category.query.all()]
form.category.choices = categories
自定義驗證
我們可以定義一個自定義的驗證器來檢查類別名稱是否重複。
def check_duplicate_category(case_sensitive=True):
def _check_duplicate(form, field):
if case_sensitive:
res = Category.query.filter(Category.name.like('%' + field.data + '%')).first()
else:
res = Category.query.filter(Category.name.ilike('%' + field.data + '%')).first()
if res:
raise ValidationError('Category named %s already exists' % field.data)
return _check_duplicate
class CategoryForm(NameForm):
name = StringField('Name', validators=[InputRequired(), check_duplicate_category()])
程式碼解析
def check_duplicate_category(case_sensitive=True):
# ...
- 我們定義了一個
check_duplicate_category函式,用於建立一個驗證器。 - 驗證器會檢查資料函式庫中是否存在相同的類別名稱。
建立自定義元件
我們可以建立自定義的元件來控制欄位在前端的顯示方式。
如何實作
首先,在 models.py 中定義一個自定義的 CustomCategoryInput 類別:
from wtforms.widgets import html_params, Select
from markupsafe import Markup
class CustomCategoryInput(Select):
def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id)
html = []
for val, label, selected in field.iter_choices():
html.append('<input type="radio" %s> %s' % (html_params(name=field.name, value=val, checked=selected, **kwargs), label))
return Markup(' '.join(html))
class CategoryField(SelectField):
widget = CustomCategoryInput()
# ...
程式碼解析
class CustomCategoryInput(Select):
def __call__(self, field, **kwargs):
# ...
- 我們定義了一個
CustomCategoryInput類別,繼承自Select。 __call__方法用於產生欄位的 HTML 程式碼。
上傳檔案
我們可以使用 Flask 和 WTForms 來處理檔案上傳。
如何實作
首先,在 my_app/__init__.py 中設定 UPLOAD_FOLDER:
import os
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])
app.config['UPLOAD_FOLDER'] = os.path.realpath('.') + '/my_app/static/uploads'
程式碼解析
app.config['UPLOAD_FOLDER'] = os.path.realpath('.') + '/my_app/static/uploads'
- 我們設定了
UPLOAD_FOLDER的路徑,用於存放上傳的檔案。
使用 WTForms 處理 Web 表單
上傳檔案至表單
在 my_app/catalog/models.py 檔案中,新增以下程式碼:
from flask_wtf.file import FileField, FileRequired
class Product(db.Model):
image_path = db.Column(db.String(255))
def __init__(self, name, price, category, image_path):
self.image_path = image_path
class ProductForm(NameForm):
image = FileField('產品圖片', validators=[FileRequired()])
內容解密:
FileField用於處理檔案上傳,FileRequired驗證器確保使用者必須上傳檔案。Product模型新增image_path欄位以儲存上傳圖片的路徑。ProductForm新增image欄位,用於接收上傳的圖片。
接著,在 my_app/catalog/views.py 中修改 create_product 方法以儲存上傳的檔案:
import os
from werkzeug.utils import secure_filename
from my_app import ALLOWED_EXTENSIONS
@catalog.route('/product-create', methods=['GET', 'POST'])
def create_product():
form = ProductForm(meta={'csrf': False})
if form.validate_on_submit():
name = form.name.data
price = form.price.data
category = Category.query.get_or_404(form.category.data)
image = form.image.data
if allowed_file(image.filename):
filename = secure_filename(image.filename)
image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
product = Product(name, price, category, filename)
db.session.add(product)
db.session.commit()
flash('產品 %s 已建立' % name, 'success')
return redirect(url_for('catalog.product', id=product.id))
if form.errors:
flash(form.errors, 'danger')
return render_template('product-create.html', form=form)
內容解密:
- 使用
secure_filename確保上傳檔案的檔名安全,避免安全漏洞。 - 將上傳的圖片儲存至組態的
UPLOAD_FOLDER目錄中。 - 將圖片檔名儲存至資料函式庫中的
image_path欄位。
在 templates/product-create.html 中新增圖片上傳欄位:
<form method="POST" action="{{ url_for('catalog.create_product') }}" role="form" enctype="multipart/form-data">
<!-- 其他欄位定義 -->
<div class="form-group">{{ form.image.label }}: {{ form.image(style='display:inline;') }}</div>
<button type="submit" class="btn btn-default">提交</button>
</form>
內容解密:
enctype="multipart/form-data"是必要的,以支援檔案上傳。form.image欄位用於上傳圖片。
在 templates/product.html 中顯示上傳的圖片:
<img src="{{ url_for('static', filename='uploads/' + product.image_path) }}"/>
內容解密:
- 使用
url_for生成圖片的 URL,假設圖片儲存在static/uploads目錄下。
防範 CSRF 攻擊
預設情況下,Flask-WTF 提供 CSRF 保護。只需移除 meta={'csrf': False} 即可啟用:
form = ProductForm()
並在應用組態中設定 CSRF 金鑰:
app.config['WTF_CSRF_SECRET_KEY'] = '隨機金鑰'
在表單中新增 CSRF Token 欄位:
<form method="POST" action="/some-action-like-create-product">
{{ form.csrf_token }}
</form>
對於 AJAX 請求,可以透過以下方式新增 CSRF Token:
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", csrftoken)
}
}
})
內容解密:
- CSRF Token 用於防止跨站請求偽造攻擊。
- AJAX 請求需要在 Header 中加入 CSRF Token。