iMind Developers Blog

iMind開発者ブログ

Flask + WTFormsでvalidation

概要

Flask + WTFormsなWebアプリでvalidationを行う。

各種built-inのvalidation利用と、エラーメッセージの表示、カスタムvalidationの作成などを取り扱う。

バージョン情報

  • Flask==1.1.1
  • Flask-WTF==0.14.2

サンプルコード

今回のコードを動かす上でベースにしたサンプルコード。

フォルダ構成。

├── app.py
└── templates
    └── hello.html

app.py

import flask
import flask_wtf
import wtforms
import wtforms.fields.html5 as wtforms5
from wtforms import validators

app = flask.Flask(__name__)
app.secret_key = 'nanika_secret_key_iretoite'

@app.route('/', methods=['GET', 'POST'])
def hello_world():
    form = HelloForm()
    form.validate_on_submit()
    return flask.render_template(
            'hello.html',
            form=form)

class HelloForm(flask_wtf.FlaskForm):
    ''' ログイン画面フォーム
    '''
    mojiretsu = wtforms.StringField('文字列', validators=[
                validators.DataRequired(message='必須です'),
                validators.Length(min=3, max=20, message='3文字以上20文字以内で入力')])

    password1 = wtforms.PasswordField('パスワード1', validators=[
                validators.DataRequired(message='必須です'),
                validators.EqualTo('password2', message='パスワード入力不一致')])

    password2 = wtforms.PasswordField('パスワード2')

templates/hello.html

{# エラーメッセージ表示用のマクロ #}
{% macro render_error(field) %}
  <ul class=errors>
    {% for error in field.errors %}
      <li class="error">{{ error }}</li>
    {% endfor %}
  </ul>
{% endmacro %}

<form method="post">
  {{ form.csrf_token }}

  <p>
  {{ form.mojiretsu(placeholder="必須文字列", size=20, required=False) }}
  {{ render_error(form.mojiretsu) }}

  <p>
  {{ form.password1(size=20) }}
  {{ render_error(form.password1) }}

  <p>
  {{ form.password2(size=20) }}
  {{ render_error(form.password2) }}

  <button>submit</button>
</form>

こんな感じのコードを元に、各種validationの動きを書き加えていきます。

built-inのvalidatorたち

WTFormsには下記のvalidatorが用意されている。

validator description
DataRequired 必須チェック
InputRequired 入力必須チェック
EqualTo 入力内容が同じかどうか(パスワード2つ入力させて双方が同じかチェックするとか)
AnyOf 指定された値のいずれかと一致するか
NoneOf AnyOfの逆で指定された値のいずれとも一致しない
Optional 空白を許容する(空白の場合は他のチェックをスキップ)
Length 文字列長チェック
Regexp 正規表現チェック
NumberRange 数値範囲チェック
Email メールアドレスチェック
IPAddress IPアドレス(v4,v6どっちもいけます)
MacAddress mac addressチェック
URL URLチェック
UUID UUIDチェック

DataRequiredとInputRequired

DataRequiredとInputRequiredは必須チェックをします。

Form

class HelloForm(flask_wtf.FlaskForm):
    ''' ログイン画面フォーム
    '''
    mojiretsu = wtforms.StringField('文字列', validators=[
                validators.DataRequired(message='必須です')])

template

  {{ form.mojiretsu.field }}
  {{ form.mojiretsu(placeholder="必須文字列", size=20, required=False) }}
  {{ render_error(form.mojiretsu) }}

こんな感じで記述すると、空で入力した際にmessageで指定した「必須です」という文字列が画面に表示されます。

template側で required=False をしているのは、DataRequired/InputRequiredが指定されると自動でhtml form側にrequiredが指定されてsubmitできなくなるからです。普段実装する時はこの指定は入れません。

両者の違いはDataRequiredは値がFalseの場合はエラーになるけど、InputRequiredは入力さえあればセーフになるあたり。

例えば下記のようにIntegerFieldでそれぞれフィールドを指定します。

class HelloForm(flask_wtf.FlaskForm):
    ''' ログイン画面フォーム
    '''
    data_req = wtforms.IntegerField('DataRequiredの例', validators=[
                validators.DataRequired(message='必須です')])

    input_req = wtforms.IntegerField('InputRequiredの例', validators=[
                validators.InputRequired(message='必須です')])

template

  <p>
  {{ form.data_req.label }}
  {{ form.data_req(placeholder="必須文字列", size=20, required=False) }}
  {{ render_error(form.data_req) }}

  <p>
  {{ form.input_req.label }}
  {{ form.input_req(placeholder="必須文字列", size=20, required=False) }}
  {{ render_error(form.input_req) }}

このフォームに0を入れると、DataRequiredは0 == Falseなのでチェックに引っかかります。

対してInputeRequiredはエラーになりません。

EqualTo

2つのフィールドの値が一致しているかチェック。

例としてパスワードフィールドを2つ用意し、validators.EqualToをpassword1に設定する。

    password1 = wtforms.PasswordField('パスワード1', validators=[
                validators.DataRequired(message='必須です'),
                validators.EqualTo('password2', message='パスワード入力不一致')])

    password2 = wtforms.PasswordField('パスワード2')

template

  <p>
  {{ form.password1(size=20) }}
  {{ render_error(form.password1) }}

  <p>
  {{ form.password2(size=20) }}
  {{ render_error(form.password2) }}

これでそれぞれに違う値を入力すると「パスワード入力不一致」とメッセージが表示される。

AnyOf

AnyOfは指定した値のどれかに一致しているかをチェックする。

    mojiretsu = wtforms.StringField('文字列', validators=[
                validators.AnyOf(values=['A', 'B', 'C'], message='A〜Cのいずれかであること')])

これでvaluesに指定した以外の文字列が来るとエラーになります。

Optional

Optionalは空白であればvalidationを通過できます。

上で実行したAnyOfは空白を許容しませんでしたが、下記のように記述すれば空白も許可されます。

    mojiretsu = wtforms.StringField('文字列', validators=[
                validators.Optional(),
                validators.AnyOf(values=['A', 'B', 'C'], message='A〜Cのいずれかであること')])

Optionalの引数はデフォルトで strip_whitespace=True になっています。

strip_whitespace=Falseを指定すると空白文字もチェックで引っかかるようになります。

    mojiretsu = wtforms.StringField('文字列', validators=[
                validators.Optional(strip_whitespace=False),
                validators.AnyOf(values=['A', 'B', 'C'], message='A〜Cのいずれかであること')])

NoneOf

NoneOfは指定した値のいずれとも一致しない条件です。

    mojiretsu = wtforms.StringField('文字列', validators=[
                validators.NoneOf(values=['A', 'B', 'C'], message='A〜Cのいずれでもないこと')])

Length

文字列の長さをチェックします。

    mojiretsu = wtforms.StringField('文字列', validators=[
                validators.Length(min=4, max=8, message='4〜8文字で入力')])

Regexp

正規表現チェックをします。

下記は数値とアルファベット小文字のみを許可する例。

    mojiretsu = wtforms.StringField('文字列', validators=[
                validators.Regexp('^[0-9a-z]+$', message='ルール違反')])

NumberRange

数値の範囲チェックをします。

    kazu = wtforms.FloatField('数値', validators=[
                validators.NumberRange(min=1, max=9, message='1〜9の数値を入力してください')])

template

  {{ form.kazu() }}
  {{ render_error(form.kazu) }}

これで1.0〜9.0の範囲を外れる数値が入力されるとエラーになります。

ところで上記のサンプルで数値以外を入力する、エラーメッセージが下記のようになりました。

  • Not a valid float value
  • 1〜9の数値を入力してください

Not a valid float valueはFloat Fieldのデフォルトのエラーメッセージです。

このFieldが持つデフォルトメッセージをカスタマイズする方法がわかりませんでした。

参考ページ

https://github.com/wtforms/wtforms/issues/39

https://stackoverflow.com/questions/34422803/number-validation-between-range-0-and-100

独自クラス作っちゃえば一応できますが。

class MyFloatField(wtforms.FloatField):
    def __init__(self, label=None, validators=None, message="Not a valid float value", **kwargs):
        super(MyFloatField, self).__init__(label, validators, **kwargs)
        self.message = message

    def process_formdata(self, valuelist):
        if valuelist:
            try:
                self.data = float(valuelist[0])
            except ValueError:
                self.data = None
                raise ValueError(self.gettext(self.message))

class HelloForm(flask_wtf.FlaskForm):
    ''' ログイン画面フォーム
    '''
    kazu = MyFloatField('数値', message="数値じゃない", validators=[
                validators.NumberRange(min=1, max=9, message='1〜9の数値を入力してください')])

custom validation

独自にバリデーションを定義してみます。

下記はtrueを示してるっぽい文字列であれば通過するvalidationをdefで定義した例。

from wtforms.validators import ValidationError

def is_true(form, field):
    text = field.data.lower()
    if not text in ['true', 'ok', 'yes']:
        raise ValidationError('OKじゃない')

class HelloForm(Form):
    mojiretsu = wtforms.StringField('文字列', validators=[is_true])

続いてClassで定義する場合。

書き方は本家wtformsのvalidators.pyを参考。

https://github.com/wtforms/wtforms/blob/2.2.1/wtforms/validators.py#L39

例として指定されたチェックボックス(BooleanField)のフィールド名を複数指定して、「すべてチェックされている」 or 「すべてチェックされていない」場合は通る処理を書いてみる。

class CheckMultipleField(object):
    def __init__(self, fieldnames, message):
        self.fieldnames = fieldnames
        self.message = message

    def __call__(self, form, field):
        fieldnames = self.fieldnames + [field.name]
        true_count = sum([form[name].data for name in fieldnames])
        if true_count != 0 and true_count != len(fieldnames):
            raise ValidationError(self.message)

class HelloForm(flask_wtf.FlaskForm):
    ''' ログイン画面フォーム
    '''
    f1 = wtforms.BooleanField('F1', validators=[
        CheckMultipleField(['f1', 'f2', 'f3'], message='全部チェックか全部未チェックのみ許可')])

    f2 = wtforms.BooleanField('F2')
    f3 = wtforms.BooleanField('F3')

template

  <p>
  {{ form.f1.label }}
  {{ form.f1() }}
  {{ render_error(form.f1) }}

  <p>
  {{ form.f2.label }}
  {{ form.f2() }}
  {{ render_error(form.f2) }}

  <p>
  {{ form.f3.label }}
  {{ form.f3() }}
  {{ render_error(form.f3) }}

改定履歴

Author: Masato Watanabe, Date: 2020-02-29, 記事投稿