FlaskFormで簡単にバリデーションする方法

Flaskは、pythonの**軽量な**webフレームワークとして有名です。
そんなFlaskで、formの機能を実装する際によく使用されるのが、「**WTForms**」です。

このWTFormsを使えば、便利な**バリデーション**機能がを使えます。
そのバリデーション機能の紹介と、その機能を使ったサンプルアプリの実装をしていきます。

## 目次
- WTFormsの説明
- validate_on_submitメソッドの使い方
- サンプルアプリの実装
- つまったところ
- おまけ

## 対象
- FlaskFormを使っているがバリデーションのかけ方がわからない人
- flaskでアプリケーションを作っている人
- pythonでwebサービスを作ろうとしている人

### 環境
- python 3.6.0
- Flask 1.0.2

## WTFormsの説明
まず、WTFormsを簡潔に説明します。

[WTFormsの公式ページ](http://flask.pocoo.org/docs/dev/patterns/wtforms/)の文頭には、このように説明があります。
> When you have to work with form data submitted by a browser view, code quickly becomes very hard to read. There are libraries out there designed to make this process easier to manage.

つまり、フォームを作りたいときに、それが複雑になっても、簡単に管理できるようにしてくれます。

### ざっくり実装
フォームの部分だけを実装すると、下記のように書けます。
後ほど(サンプルアプリの実装で)、ブラウザで動かせるように、テンプレートなどを合わせて、実装します。

```python:blog_form.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField

class BlogForm(FlaskForm):
    blog_title = StringField("タイトル")
    description = TextAreaField('本文')
    tags = StringField("タグ")
```

## validate_on_submitメソッドの使い方
ここが、この記事のメインです。
バリデーション!!!
いざ、実際のアプリのフォームを実装しようと思い、バリデーションを考えると、意外と複雑さを求められることがあると思います。
(簡単なバリデーションなら、[こちら](http://flask.pocoo.org/docs/1.0/patterns/wtforms/)を参照ください)

単純な「文字数制限」「必須項目」などだけでなく、特定の文字(「/」「.」「印象の悪い文字」など)を含めたくないとか、日付の制限とか、既存のデータとの重複などなど...場面によって、処理内容が変わります。
そんなときに使うのが、**validate_on_submitメソッド**です。

#### やることは２つ。

- 1. FlaskFormを継承して、フォームの内容を定義したクラスに(先程、ざっくり実装したクラス)メソッドを追加
- 2. フォームがPOSTで送られてきたら、そのフォームに**validate_on_submit()**を実行

#### 守らなければいけないルールは2つ。

- 上記の1を実装する際に、メソッド名は「validate_」+「フィールド名」で定義する
- バリデーション失敗時は、raiseで**wtforms**の**ValidationError**を起こす

### ざっくり実装
では、先程書いたフォームクラスにバリデーション処理を追記しましょう。

```python:blog_form.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, ValidationError


class BlogForm(FlaskForm):
    blog_title = StringField("タイトル")
    description = TextAreaField("本文")
    tags = StringField("タグ (カンマ区切りで入力してください。)")

    def validate_blog_title(self, blog_title):
        """バリデーション内容:
        - 未入力は禁止
        - 文字数が10文字以上は禁止
        - 「/」を含むことは禁止
        """
        if blog_title.data == "":
            raise ValidationError("タイトルを入力してください")

        if len(blog_title.data) > 10:
            raise ValidationError("タイトルは10文字以下にしてください。")

        if "/" in blog_title.data:
            raise ValidationError("タイトルに「/」は含められません。")

    def validate_tags(self, tags):
        """バリデーション内容:
        - 4つ以上は禁止
        """

        tag_list = tags.data.strip().split(",")
        if len(tag_list) > 3:
            raise ValidationError("タグは3つ以下にしてください。")
```

### wtformsの中身を観察
(飛ばしてもいい内容)
validate_on_submitメソッドをたどっていくと、wtformsのform.pyに下記のように書かれています。
ここで、新しく実装されたバリデーションに関するメソッドを見つけてまとめて、呼んでくれています。

```python:wtforms/form.py
def validate(self):
    """
    Validates the form by calling `validate` on each field, passing any
    extra `Form.validate_<fieldname>` validators to the field validator.
    """
    extra = {}
    for name in self._fields:
        inline = getattr(self.__class__, 'validate_%s' % name, None)
        if inline is not None:
            extra[name] = [inline]

    return super(Form, self).validate(extra)
```

## サンプルアプリの実装
では、ここまでの内容を含めて、ブラウザ上で動かせるようにテンプレートや、コントローラー部分も含めて実装します。

### ディレクトリ構成

```
├── app.py
├── blog_form.py
└── templates
    └── index.html
```

### フォームクラスを実装
まずは、フォームクラスを作ろう。
先程の内容にいくつか追記します。

```python:blog_form.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, ValidationError


class BlogForm(FlaskForm):
    blog_title = StringField("タイトル")
    description = TextAreaField("本文")
    tags = StringField("タグ (カンマ区切りで入力してください。)")

    def validate_blog_title(self, blog_title):
        """バリデーション内容:
        - 未入力は禁止
        - 文字数が10文字以上は禁止
        - 「/」を含むことは禁止
        """
        if blog_title.data == "":
            raise ValidationError("タイトルを入力してください")

        if len(blog_title.data) > 10:
            raise ValidationError("タイトルは10文字以下にしてください。")

        if "/" in blog_title.data:
            raise ValidationError("タイトルに「/」は含められません。")

    def validate_description(self, description):
        """バリデーション内容:
        - 未入力は禁止
        - 文字数が10文字未満は禁止
        """

        if description.data == "":
            raise ValidationError("本文を入力してください。")

        if len(description.data) < 10:
            raise ValidationError("本文は10文字以上にしてください。")

    def validate_tags(self, tags):
        """バリデーション内容:
        - 4つ以上は禁止
        """

        tag_list = tags.data.strip().split(",")
        if len(tag_list) > 3:
            raise ValidationError("タグは3つ以下にしてください。")
```

### コントローラーを実装
コントローラー(flaskではviewsって呼ぶこともある)を書いていきます。

```python:app.py
import os
import sys

sys.path.append(os.path.join(os.path.dirname(__file__), "../"))

from flask import Flask, request
from flask import render_template

from blog_form import BlogForm

app = Flask(__name__)
app.config["SECRET_KEY"] = "sample1201"

@app.route("/", methods=["GET", "POST"])
def index():
    form = BlogForm()
    if request.method == "GET":
        message = "フォームを送ってみよう！！"
        return render_template("index.html", form=form, message=message)

    if request.method == "POST":
        if form.validate_on_submit():
            message = "バリデーションを通ったよ＼(^o^)／"
            return render_template("index.html", form=form, message=message)

        message = "バリデーションに失敗したよ(T_T)"
        return render_template("index.html", form=form, message=message)

if __name__ == '__main__':
    app.run(debug=True)
```

### ビューを実装
htmlも書きます。（cssファイルを作るのが面倒だったので、直接書きました。）

FlaskFormを使うと、htmlはかなりスッキリ書けます。
バリデーションに失敗した際のエラーメッセージが、表示されるように

```html:templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>サンプル</title>
</head>
<body>
    <h2>{{ message }}</h2>
    <form method="POST" action="/">
        {{ form.hidden_tag() }}

        <div style="margin: 10px;">
            {% for error in form.blog_title.errors %}
                <span style="color: red;">{{ error }}</span>
            {% endfor %}

            {{ form.blog_title.label }}:
            {{ form.blog_title(placeholder="バリデーションを実装") }}
            </div>

        <div style="margin: 10px;">
            {% for error in form.description.errors %}
                <span style="color: red;">{{ error }}</span>
            {% endfor %}

            {{ form.description.label }}:
            {{ form.description(placeholder="validate_on_submit()を使えば、お手軽だよー") }}
        </div>

        <div style="margin: 10px;">
            {% for error in form.tags.errors %}
                <span style="color: red;">{{ error }}</span>
            {% endfor %}

            {{ form.tags.label }}:
            {{ form.tags(placeholder="python,flask") }}
        </div>

        <div style="margin: 10px;">
            <input type="submit" value="送信">
        </div>
    </form>

</body>
</html>
```

これであとは、app.pyを動かすだけ！

```
$ python app.py
```

## つまったところ
### あれ、バリデーションしてくれない
原因:
メソッド名が適切ではなかった。
上にも書きましたが、バリデーション内容を書くメソッド名は、「validate_」+「フィールド名」である必要があります。

### 全てのバリデーションをクリアしたはずなのに見えないバリデーションにはじかれる

原因:
form内に**{{ form.hidden_tag() }}**の記述を忘れていた。
なんで、これがないと怒られるのかまでは、調べていない。


## おまけ
最後までお付き合いいただき、ありがとうございました。
少しでもお役に立てたのであれば、嬉しいです。

[Renttle](https://renttle.jp)というサービスを開発中です。ぜひ、使ってみて、レビューをください。