Flask-WTFはWTFormsの操作を簡略化するためのサードパーティーライブラリです。WTFormsの主な機能には、ユーザー入力データの検証とテンプレートのレンダリングがあります。その他の機能としては、CSRF保護やファイルアップロードなどがあります。インストール手順は以下の通りです:
pip3 install flask-wtf
ユーザー認証サンプル
- ログイン画面
ユーザーがログインする際には、ユーザー名とパスワードについて複数の形式検証を行う必要があります。例えば:
ユーザー名は空であってはならない;ユーザー名の長さは6文字以上であること; パスワードは空であってはならない;パスワードの長さは12文字以上であること;パスワードにはアルファベット、数字、特殊文字を含む必要がある(カスタム正規表現);
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from flask import Flask, render_template, request, redirect
from wtforms import Form
from wtforms.fields import simple
from wtforms import validators
from wtforms import widgets
app = Flask(__name__, template_folder='templates')
app.debug = True
class LoginForm(Form):
# フィールド(内部に正規表現を含む)
name = simple.StringField(
label='ユーザー名',
validators=[
validators.DataRequired(message='ユーザー名は必須です。'),
validators.Length(min=6, max=18, message='ユーザー名の長さは%(min)d文字以上%(max)d文字以下にしてください。')
],
widget=widgets.TextInput(), # ページ表示用のコンポーネント
render_kw={'class': 'form-control'}
)
# フィールド(内部に正規表現を含む)
pwd = simple.PasswordField(
label='パスワード',
validators=[
validators.DataRequired(message='パスワードは必須です。'),
validators.Length(min=8, message='パスワードは%(min)d文字以上にしてください。'),
validators.Regexp(regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,}",
message='パスワードは最低8文字、大文字1つ、小文字1つ、数字1つ、特殊文字1つを含んでいなければなりません。')
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
form = LoginForm()
return render_template('login.html', form=form)
else:
form = LoginForm(formdata=request.form)
if form.validate():
print('ユーザー入力値が正当性検証を通過しました。送信された値:', form.data)
else:
print(form.errors)
return render_template('login.html', form=form)
if __name__ == '__main__':
app.run()
app.py```
<p>{{form.pwd.label}} {{form.pwd}} {{form.pwd.errors[0] }}</p>
<input type="submit" value="送信">
templates/login.html2. ユーザー登録
登録ページでは、ユーザー名、パスワード、パスワード確認、性別、趣味などを入力してもらいます。
from flask import Flask, render_template, request, redirect
from wtforms import Form
from wtforms.fields import core
from wtforms.fields import html5
from wtforms.fields import simple
from wtforms import validators
from wtforms import widgets
app = Flask(__name__, template_folder='templates')
app.debug = True
class RegisterForm(Form):
name = simple.StringField(
label='ユーザー名',
validators=[
validators.DataRequired()
],
widget=widgets.TextInput(),
render_kw={'class': 'form-control'},
default='alex'
)
pwd = simple.PasswordField(
label='パスワード',
validators=[
validators.DataRequired(message='パスワードは必須です。')
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)
pwd_confirm = simple.PasswordField(
label='パスワード確認',
validators=[
validators.DataRequired(message='パスワード確認は必須です。'),
validators.EqualTo('pwd', message="パスワードが一致しません。")
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)
email = html5.EmailField(
label='メールアドレス',
validators=[
validators.DataRequired(message='メールアドレスは必須です。'),
validators.Email(message='メールアドレスの形式が違います。')
],
widget=widgets.TextInput(input_type='email'),
render_kw={'class': 'form-control'}
)
gender = core.RadioField(
label='性別',
choices=(
(1, '男性'),
(2, '女性'),
),
coerce=int # 「1」 「2」
)
city = core.SelectField(
label='都市',
choices=(
('bj', '北京'),
('sh', '上海'),
)
)
hobby = core.SelectMultipleField(
label='趣味',
choices=(
(1, 'バスケットボール'),
(2, 'サッカー'),
),
coerce=int
)
favor = core.SelectMultipleField(
label='好み',
choices=(
(1, 'バスケットボール'),
(2, 'サッカー'),
),
widget=widgets.ListWidget(prefix_label=False),
option_widget=widgets.CheckboxInput(),
coerce=int,
default=[1, 2]
)
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
self.favor.choices = ((1, 'バスケットボール'), (2, 'サッカー'), (3, 'バドミントン'))
def validate_pwd_confirm(self, field):
"""
pwd_confirmフィールドのカスタム検証ルール。例:pwdフィールドとの一致確認
:param field:
:return:
"""
# 初期化時、self.dataにはすべての値が含まれる
if field.data != self.data['pwd']:
# raise validators.ValidationError("パスワードが一致しません") # 次の検証を継続
raise validators.StopValidation("パスワードが一致しません") # 検証を停止
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'GET':
form = RegisterForm(data={'gender': 2,'hobby':[1,]}) # 初期値設定
return render_template('register.html', form=form)
else:
form = RegisterForm(formdata=request.form)
if form.validate():
print('ユーザー入力値が正当性検証を通過しました。送信された値:', form.data)
else:
print(form.errors)
return render_template('register.html', form=form)
if __name__ == '__main__':
app.run()
app01.py```
templates/register.htmlソースコード解析
処理フロー図
クラス作成プロセスの分析
class LoginForm(Form):
# フィールド(内部に正規表現を含む)
name = simple.StringField(
label='ユーザー名',
validators=[
validators.DataRequired(message='ユーザー名は必須です。'),
validators.Length(min=6, max=18, message='ユーザー名の長さは%(min)d文字以上%(max)d文字以下にしてください。')
],
widget=widgets.TextInput(), # ページ表示用のコンポーネント
render_kw={'class': 'form-control'}
)
LoginFormはFormを継承しています
class Form(with_metaclass(FormMeta, BaseForm)):
"""
宣言型フォームベースクラス。Formサブクラスでフィールドをクラス属性として定義できるようにし、
BaseFormの基本動作を拡張します。
さらに、フォームおよびインスタンスの入力データは構築時に取得され、process()メソッドに渡されます。
"""
Meta = DefaultMeta
def with_metaclass(meta, base=object):
return meta("NewBase", (base,), {})
with_metaclassはFormMeta(typeを継承)を通じて動的にクラスを作成し、この際にFormMetaの__init__メソッドが実行されます。
class FormMeta(type):<br></br> # clsはLoginFormクラス<br></br>
def __init__(cls, name, bases, attrs):
type.__init__(cls, name, bases, attrs)
cls._unbound_fields = None
cls._wtforms_meta = None
上記のクラス作成により、独自クラスに2つの属性が追加されます。
LoginForm._unbound_fields = None
LoginForm._wtforms_meta = None
LoginFormのフィールドnameの追跡
name = simple.StringField(
label='ユーザー名',
validators=[
validators.DataRequired(message='ユーザー名は必須です。'),
validators.Length(min=6, max=18, message='ユーザー名の長さは%(min)d文字以上%(max)d文字以下にしてください。')
],
widget=widgets.TextInput(), # ページ表示用のコンポーネント
render_kw={'class': 'form-control'}
)
nameはオブジェクトであることがわかります。まずは__new__メソッドを追跡します。
def __new__(cls, *args, **kwargs):
if '_form' in kwargs and '_name' in kwargs:
return super(Field, cls).__new__(cls)
else:
return UnboundField(cls, *args, **kwargs)
上記のソースコードから、属性に_formと_nameが含まれていないため、UnboundField(cls, *args, **kwargs)オブジェクトが返されることを確認できます。
class UnboundField(object):
_formfield = True
creation_counter = 0
def __init__(self, field_class, *args, **kwargs):
UnboundField.creation_counter += 1
self.field_class = field_class
self.args = args
self.kwargs = kwargs
self.creation_counter = UnboundField.creation_counter
結果
LoginForm.name = UnboundField(simple.StringField, StringFieldのすべての引数)LoginForm.pwd = UnboundField(simple.PasswordField, PasswordFieldのすべての引数)
HTMLの自動生成
フロントエンドでLoginForm.nameを呼び出すとinputボックスがどのように生成されるかを確認します。LoginFormのnameはオブジェクトであることがわかります。
class StringField(Field):
"""
このフィールドは複雑なフィールドの基底であり、``<input type="text">``を表します。
"""
widget = widgets.TextInput()
def process_formdata(self, valuelist):
if valuelist:
self.data = valuelist[0]
elif self.data is None:
self.data = ''
def _value(self):
return text_type(self.data) if self.data is not None else ''
LoginForm.nameを実行すると内部の__str__メソッドが呼び出されます。
def __str__(self):
"""
フィールドのHTML表現を返します。より強力なレンダリングについては、`__call__`メソッドを参照してください。
"""
return self()
返り値は自分自身であり、__call__メソッドが呼び出されます。
def __call__(self, **kwargs):
return self.meta.render_field(self, kwargs)
render_fieldメソッドは以下の通りです。
def render_field(self, field, render_kw):
"""
render_fieldはウィジェットレンダリング方法のカスタマイズを可能にします。
デフォルトの実装では ``field.widget(field, **render_kw)`` を呼び出します。
"""
other_kw = getattr(field, 'render_kw', None)
if other_kw is not None:
render_kw = dict(other_kw, **render_kw)
return field.widget(field, **render_kw)
widgetはオブジェクトであり、内部の__call__メソッドが呼び出されます。
class StringField(Field):
"""
このフィールドは複雑なフィールドの基底であり、``<input type="text">``を表します。
"""
widget = widgets.TextInput()
TextInputオブジェクト
class TextInput(Input):
"""
単一行テキスト入力のレンダリング。
"""
input_type = 'text'
その__call__メソッドは以下の通りです。
def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id)
kwargs.setdefault('type', self.input_type)
if 'value' not in kwargs:
kwargs['value'] = field._value()
if 'required' not in kwargs and 'required' in getattr(field, 'flags', []):
kwargs['required'] = True
return HTMLString('<input %s>' % self.html_params(name=field.name, **kwargs))