FlaskでWTFormsを使ってみよう
WTFromsについて
"FlaskのHTTPメソッド処理を理解しよう" の例のように、FlaskのFormの機能は貧弱なため、フォームはHTMLのINPUTタグを使ってフォーム画面を作りましたが、フィールドが増えてくると記述やデータ検証が面倒になってきます。 WTForms はフォーム画面を構築するときに便利なフィールドとバリデータを提供するパッケージで、Flask-WTF 拡張機能は、WTForms をFlaskから利用できるようにするものです
WTFormsでログイン画面を作成
フォームの定義
Formクラスは、フォームのフィールドをクラス変数として定義するだけです。
これをapp/forms.py として作成します。
code: forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):
submit = SubmitField('Sign In')
フォームで使用されるフィールドタイプを表すクラスは、WTFormsパッケージからインポートします。フィールドタイプに与える第1引数が、フィールド名になります。
フィールドごとに、オブジェクトをLoginFormクラスのクラス変数として保存します。
オプションのvalidatorsキーワード引数を与えると、そのフィールドのデータを検証させることができます。この例にある、DataRequired()は、フィールドが空でないかをチェックします。
テンプレートの定義
次に、フォームをHTMLテンプレートに追加することです。
まず、ベーステンプレートファイルを作成しましょう。
code: templates/base.html
<html>
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
これを継承するチャイルド・テンプレートを作成しましょう。
code: 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.submit() }}</p>
</form>
{% endblock %}
このテンプレートは、LoginFormクラスからインスタンス化されたformオブジェクトが引数として与えられることを想定しています。
form.hidden_tag() は、悪意のあるアクセスからフォームを保護するために使用されるトークンを含む非表示フィールドを生成します。しかし、定義はこれだけで、実際にはWTForms が残りの処理をすべて行ってくれます。
この処理は、実際には form.csrf_token() がこれを行っていて、form.hidden_tag() はユーザが HiddenField() で定義したフィールドもヘッダに含めて返してくれるようになります。
{{ form.フィールド名.label }} でFormクラスで定義したフィールド名に展開されます。
ビューの定義
次の、URLにマップされるビュー関数を定義します。
code: Python
from flask import render_template
from forms import LoginForm
# ...
@app.route('/')
@app.route('/login')
def login():
form = LoginForm()
return render_template('login.html', title='Sign In', form=form)
ナビゲーションメニューが base.html にあると便利かもしれません。
code: templates/base.html
<html>
<head>
<title>{{ title }}</title>
</head>
<div>
Navigation:
<a href="{{ url_for('') }}">Home</a>
<a href="{{ url_for('login') }}">Login</a>
</div>
<body>
{% block content %}{% endblock %}
</body>
</html>
ログイン画面を確認
いま時点での app.py には、ほとんどコードがありません。
code: app.py
import flask
from forms import LoginForm
app = flask.Flask(__name__)
@app.route('/')
@app.route('/login')
def login():
form = LoginForm()
return flask.render_template('login.html', form=form)
https://gyazo.com/26c18b7322f16afa643d72109a7e81e5
フォームデータを受信
このままでは、フォームをブラウザに表示するだけで、Sign In のボタンをクリックするとエラーになってしまいます。
https://gyazo.com/5cb6e525ce4010b6ba81b18e797fe8ab
これは、@app.route() がデフォルトでは、HTTPのGETメソッドしか処理しないからです。
POSTメソッドも受け取れるようにします。
code: app2.py
from flask import render_template, flash, redirect
# ...
def login():
form = LoginForm()
if form.validate_on_submit():
flash(f'Login requested for user {form.username.data}'
return redirect('/index')
return render_template('login.html', title='Sign In', form=form)
@app.route('/index')
def index():
return 'index'
form.validate_on_submit() メソッドは、GETリクエストのときはFalseを返すので、
ログインフォームが表示されます。POSTリクエストでFormクラスで指定したデータ検証が行われ異常がなかった(つまり、この例ではフィールドが空でない)ときは、/index へリダイレクトします。
ユーザがフォームに入力したデータは form.username.data としてアクセスできることに注目してください。
/index のビュー関数 index() も、今は文字列を返すだけですが、一緒に定義しておきます。
flash()関数を呼び出すと、Flaskはメッセージを保存しますがメッセージはWebページに表示されません。テンプレートで、フラッシュされたメッセージレンダリングする必要があります。
ベーステンプレートを次のように修正します。
code: templates/base2.html
<html>
<head>
{% if title %}
<title>{{ title }} - Flask Example </title>
{% else %}
<title>Flask Example</title>
{% endif %}
</head>
<body>
<div>
Navigation:
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('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>
フラッシュメッセージで注意する点は、get_flashed_messages()関数で要求されると、メッセージリストから削除されることです。flash()関数が実行された後に、一度だけしか表示されないので、Webページがリロードされると消えてしまいます。
入力フォームの改善
LoginFormクラスで定義している、フォームフィールドに与えているバリデーターは、無効なデータがアプリケーションに伝わるのを防いでくれます。データに問題があると、フォームを再表示して、ユーザーが必要な修正を行えるようにすることが大切です。
ここまでのサンプルでは、ユーザが無効なデータを送信しようとしたとき、検証メカニズムは正常に機能しますが、フォームに何か問題があることはユーザーに示されません。
ユーザーにとってはフォームが再表示されるだけです。
データに問題があるフィールドの横にエラーメッセージを追加してみましょう。
code: templates/index2.html
{% extends "base2.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.submit() }}</p>
</form>
https://gyazo.com/32d1b80274c1ff58d1f72f22a9170751
WTForms のフィールドタイプとバリデータ
WTForms では次のフィールドタイプを使用することができます。
table: WTFormsで使用できるフィールドタイプ
フィールドタイプ 説明
BooleanField True/False のブール値をもつチェックボックス
DateField 特定の形式で日付(datetime.date値)を入力するテキストフィールド
DecimalField 10進数を入力するテキストフィールド
FileField ファイルアップロードフィールド
MultipleFileField 複数ファイルのアップロードフィールド
HiddenField 隠しテキストフィールド
FieldList 指定されたタイプのフィールドのリスト
FloatField 浮動小数点値を入力するテキストフィールド
FormField フィールドが埋め込まれたコンテナフォーム
IntegerField 整数値を入力するテキストフィールド
PasswordField パスワードを入力するテキストフィールド
RadioField ラジオボタンのリスト
SelectField 選択肢をリストするドロップダウンフィールド
SelectMultipleField 選択肢を複数選択可能なドロップダウンフィールド
SubmitField フォームの送信ボタン
StringField テキストフィールド
TextAreaField 複数行のテキストフィールド
WTForms では次のバリデータを使用することができます。
table: WTForms で使用できるバリデータ
バリデータ 説明
DataRequired 型変換後にフィールドにデータが含まれていることを検証
Email メールアドレスとして妥当性を検証
EqualTo 2つのフィールドの値を比較
確認のためにパスワードの2回の入力を要求する場合に役立ちます
InputRequired 型変換の前にフィールドにデータが含まれていることを検証
IPAddress IPv4ネットワークアドレスとしての妥当性を検証
Length 入力された文字列の長さを検証
MacAddress MACアドレスとしての妥当性を検証
NumberRange 入力された値が指定した数値の範囲内であることを検証
Optional 検証をスキップしてフィールドに空の入力を許可する
Regexp 正規表現に合致する入力かを検証
URL URLとしての妥当性を検証
UUID UUIDとしての妥当性を検証
AnyOf 入力データが可能な値のリストにあることを検証
NoneOf 入力が可能な値のリストのどれでもないことを検証
ここまでは、ログイン画面のレンダリングまわりを説明してきました。
実際には、ユーザデータやコンテンツはデータベースに格納されていることが多いはずです。
つぎからは、データベースをFlask でどう使うのかを見ていきましょう。
参考: