【Django】012. フォームのバリデーション

前回はデータベースのレコードの並べ替え、集計等を行いました。
[前回記事：【Django】011. データベースの機能を少し深堀る](https://qiita.com/ys_dirard/items/99e353fc6bd6fe0a3d6f)

今回はフォームで入力された値に対するバリデーションを行ってみます。

今回も以下の本を参考にしています。

https://www.shuwasystem.co.jp/book/9784798062419.html


## バリデーションとは？
「値のチェック」のこと！

値に問題があった場合、そのまま使用したり保存したりするとエラーのもとになったりします。

そうならないように入力された値が問題ないかをチェックする機能です。

チェックして問題がなければ通常の処理を行い、問題があった場合は再入力してもらう等の処理を行います。

## forms.Formのバリデーション
まずは一般的なフォームのバリデーションについて見ていきます。

Djangoではフォームはforms.Formクラスを継承して作成しました。

今回は以下のCheckFormクラスを使用します。
```python
class CheckForm(forms.Form):
    txt = forms.CharField(label='Name', widget=forms.TextInput(attrs={'class': 'form-control'}))
```

テンプレートは以下のcheck.htmlを作成します。
```html:check.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">

</head>
<body class="container">
    <h1 class="display-4 text-primary mb-4">{{ title }}</h1>
    <p>{{ message | safe }}</p>
    
    <form action="{% url 'check' %}" method="post">
        {% csrf_token %}
        {{ form.as_table }}
        <input type="submit" value="click" class="btn btn-primary">
    </form>
</body>
</html>

```

最後にビュー関数は以下のcheck関数を作成します。
```python
from .forms import CheckForm

def check(request):
    params = {
        'title': 'Hello',
        'message': 'check validation',
        'form': CheckForm(),
    }
    if (request.method == 'POST'):
        form = CheckForm(request.POST)
        params['form'] = form
        if (form.is_valid()):
            params['message'] = 'OK!'
        else:
            params['message'] = 'no good.'
    return render(request, 'hello/check.html', params)

```

バリデーションは
```python
if (<Form>.is_valid()):
    ...正常時の処理...
else:
    ...エラー時の処理... 
```
のように行います。

Formクラスのis_validメソッドを使用し、戻り値 (正常ならTrue) によってその後の処理を変えます。

アクセスすると以下の画面になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/00c5ce12-41b0-775d-05ee-203fffc9f6d1.png)

正常な値なら正常な場合の処理の「OK!」が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/9558f265-eb38-9e09-cbcc-cc35bbcf538e.png)

空白を入力して送るとエラー時の処理の「no good.」が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/c67d386f-4b7c-84d0-39e5-52b5f3da4830.png)

ちなみにですが、未入力で送信しようとするとブラウザの機能により以下のような表示になりました。

※簡単なバリデーションはブラウザにも組み込まれているとわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/97c0938c-455f-9577-f188-543eae373fd1.png)

## バリデーションの種類
これまでとりあえず一つのCharFieldを用意してそのままis_valid関数を呼び出しました。

勝手にいい感じにバリデーションを行ってくれているように見えますがバリデーションにはいったいどんな種類があるのでしょうか？

### CharFieldのバリデーション

CharFieldには以下のような設定が可能でした。

|name|detail|
|-|-|
|label|ラベルの表示テキストの指定|
|widget|ウィジェットを設定できる|
|required|必須項目かどうかを指定 (True/False)|
|min_length, max_length|最小/最大文字数を指定(整数値)|
|empty_value|空の入力を許可するか (True/False)|

is_valid関数では上記のような条件が満たされているかを確認しているということになります。(widgetは条件ではないですが…)

その他のテキスト入力を行うためのEmailFieldやURLFieldも同様です。

## IntegerField / FloatFieldのバリデーション
IntegerField / FloatFieldには以下のような設定が可能でした。

|name|detail|
|-|-|
|label|ラベルの表示テキストの指定|
|widget|ウィジェットを設定できる|
|required|必須項目かどうかを指定 (True/False)|
|min_value, max_value|最小/最大値を指定(整数値)、この範囲外の入力を受け付けなくできる。|

上記の中でrequired、min_value、max_valueがis_valid関数での確認対象ということです。

## 日時関連のバリデーション
DateField、TimeField、DateTimeFieldなどの日時関連のフィールドはrequired以外にもフォーマットに関するバリデーションが設定されています。

指定のフォーマットにあわないものが入力されるとエラーとなります。

フォーマットはinput_formatsという引数でリストとして指定します。
```python
input_format = [format_1, format_2, ...]
```

また、フォーマットは以下の形式で作成します。
|format|detail|
|-|-|
|%y|年を表す数字|
|%m|月を表す数字|
|%d|日を表す数字|
|%H|時を表す数字|
|%M|分を表す数字|
|%S|秒を表す数字|

具体的には以下のようになります。
※CheckFormを変更しています。

```python
class CheckForm(forms.Form):
    date = forms.DateField(label='Date', input_formats=['%d'], widget=forms.DateInput(attrs={'class': 'form-control'}))
    time = forms.TimeField(label='Time', input_formats=['%H'], widget=forms.TimeInput(attrs={'class': 'form-control'}))
    datetime = forms.DateTimeField(label='DateTime', input_formats=['%y/%m/%d/%H/%M/%S'], widget=forms.DateTimeInput(attrs={'class': 'form-control'}))
```

アクセスして入力して送信してみるとちゃんとバリデーションが働いていそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/f4e9fbde-99c5-421f-7b6b-0186c4ed6662.png)

input_formatsで指定したフォーマットにするとちゃんと「OK!」と表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/2fe0d68c-d6f5-0ce3-d517-ea752d180880.png)


## バリデーションを追加する
これまではデフォルトで用意されているバリデーションを見てみましたが、それほど数は多くありません。

自作のバリデーションを追加したいときはどうすればよいでしょうか？

そういう時はFormクラスにメソッドを追加すればよいです。
```python
class クラス名(forms.Form):
    ...項目の用意...

    def clean(self):
        変数 = super().clean()
        ...値の処理...
```
「clean」というメソッドは酔いされた値の検証を行う際に呼び出されます。

親クラスのcleanを呼び出し、その後に独自のチェックを行えばよいということです。

エラーを発生させたい場合は
```python
raise ValidationError(エラーメッセージ)
```
のようにValidationErrorを発生させればOKです。

今回は以下のように実装しました。
```python
class CheckForm(forms.Form):
    txt = forms.CharField(label='Name', widget=forms.TextInput(attrs={'class': 'form-control'}))
    
    def clean(self):
        cleaned_data = super().clean()
        txt = cleaned_data['txt']
        if (txt.lower().startswith('no')):
            raise forms.ValidationError('You input "NO"!')
            
```

アクセスして試しにNoから始まるテキストを送ると無事にエラーが出ました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/da6295c7-5a50-8b43-2a13-5e9b9d0cf7dd.png)


## ModelFormでのバリデーション
Djangoのフォームはforms.Form以外にもforms.ModelFormもありました。

※データベースと連携するときに便利なやつでした

この前作ったModelFormであるFriendFormは以下でした。
```python
class FriendForm(forms.ModelForm):
    class Meta:
        model = Friend
        fields = ['name', 'mail', 'gender', 'age', 'birthday']
```

ModelFormのバリデーションの設定はModelFormクラスではなく、元のModelクラスに用意されます。

ということで、元のFriendモデルは以下です。
```python
class Friend(models.Model):
    name = models.CharField(max_length=100)
    mail = models.EmailField(max_length=200)
    gender = models.BooleanField()
    age = models.IntegerField(default=0)
    birthday = models.DateField()
```

forms.Formの時のようなmax_lengthのようなものが見られます。

これらの値によりバリデーションが実行されます。

また、バリデーションが実行されるのはsave関数が呼び出されたタイミングです。

save関数が呼び出されると
* フォームに入力された値が問題ないか？
* モデルインスタンスの項目に問題がないか？
を順にチェックし、問題なければ保存を実行します。

ちなみに、forms.Form同様にModelFormの方もis_valid関数が使えます。


## モデルのバリデーション設定
先ほど```name = models.CharField(max_length=100)```のmax_lengthでバリデーションが実行できているとなりました。

しかし、モデルで使用できるバリデーションはforms.Formとは違うらしいです。

同じものはmax_length程度で、require、min_length、min_value、max_valueとかはモデルでは使えないようです。

ではどうやってmax_value等のバリデーションを設定すればよいのでしょうか？

以下のようにすると良いです。

```python
from django.core.validators import MinValueValidator, MaxValueValidator

class Friend(models.Model):
    name = models.CharField(max_length=100)
    mail = models.EmailField(max_length=200)
    gender = models.BooleanField()
    age = models.IntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(150)])
    birthday = models.DateField()

```

上記のageにvalidatorsという引数を追加しています。

その中で、MinValueValidator、MaxValueValidatorを使用しています。

このようにvalidatorsにバリデータをリストとして渡すことでバリデーションが可能になります。

### モデルで使えるバリデータ

#### MinValueValidator / MaxValueValidator
最小値、最大値のバリデータです。
```
MinValueValidator(値)
MaxValueValidator(値)
```

#### MinLengthValidator / MaxLengthValidator
長さについてのバリデータです。
```
MinLengthValidator(値)
MaxLengthValidator(値)
```

#### EvailValidator / URLValidator
Email、URLかどうかをチェックします。

以下のように使用します。(他と同様です)
```python
from django.core.validators import URLValidator

models.CharField(validators=[URLValidator()])
```

#### ProhibitNullCharactersValidator
null文字を禁止します。

#### RegaxValidator
正規表現を使用してパターンに合致するかをチェックするためのものです。

以下のように使用します。
```python
from django.core.validators import RegaxValidator

models.CharField(validators=[RegaxValidator(r'^[a-x]*$')])
```
引数にはチェックしたい正規表現を準備します。

## バリデータ関数を作成する
バリデータの使い方はわかりましたが、準備されているバリデータの数はそれほど多くはないです。

自作のバリデータを使いたくなった場合は以下のバリデータ関数を準備します。
```python
def 関数名(value):
    ...処理...
```

引数のvalueがチェックする値で、処理の部分でチェックを行い、問題があれば「raise ValidationError」でエラーを発生させます。

### 数字バリデータ関数
例として数字の入力のみを許可するバリデータ関数を作ってみます。

```python
import re
from django.db import models
from django.core.validators import ValidationError

def number_only(value):
    if (re.match(r'^[0-9]*$', value) == None):
        raise ValidationError('%(value)s is not Number!', params={'value': value})

class Friend(models.Model):
    name = models.CharField(max_length=100, validators=[number_only])
    mail = models.EmailField(max_length=200)
    gender = models.BooleanField()
    age = models.IntegerField(default=0)
    birthday = models.DateField()
    
```

number_only関数で値が数値かを判定して、数値以外ならValidationErrorを発生左折用にしています。

## フォームとエラーメッセージを個別に表示する
これまではforms.Formやforms.ModelFormを使用してフォームを自動生成していました。

これらを利用するとエラー時のメッセージ表示も自動で行ってくれて便利です。

表示も {{form.as_table}} のように簡単で便利でした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/f4e9fbde-99c5-421f-7b6b-0186c4ed6662.png)

しかし、フォームをカスタマイズしたい場合は個々のフィールドやエラーメッセージを個別に表示する必要があります。

```html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">

</head>
<body class="container">
    <h1 class="display-4 text-primary mb-4">{{ title }}</h1>
    <p>{{ message | safe }}</p>
    <ol class="list-group">
        {% for item in form %}
        <li class="list-group-item py-2">
            {{ item.name }} ({{ item.value }}): {{ item.errors.as_text }}
        </li>
        {% endfor %}
    </ol>
    
    <table class="table mt-4">
        <form action="{% url 'check' %}" method="post">
            {% csrf_token %}
            <tr><th>名前</th><td>{{ form.name }}</td></tr>
            <tr><th>メール</th><td>{{ form.mail }}</td></tr>
            <tr><th>性別</th><td>{{ form.gender }}</td></tr>
            <tr><th>年齢</th><td>{{ form.age }}</td></tr>
            <tr><th>誕生日</th><td>{{ form.birthday }}</td></tr>
            <tr><td></td><td><input type="submit" value="click" class="btn btn-primary">
            </td></tr>
        </form>
    </table>
</body>
</html>

```

formという変数でModelFormをビュー関数から送っています。

formをfor分で回すことで送信された各項目の値にアクセスできます。

errorsという名前でエラー情報も取得が可能です。

フォームの値はform.項目名で取得可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/95bec8a3-bbd8-55bf-9318-90614759095a.png)

無事に個々のフィールドやエラーメッセージを個別に表示することができました。

## まとめ
今回はフォームのバリデーションを行いました。

forms.Formとforms.ModelFormではバリデーションのやり方が異なりました。

カスタムする場合は
* forms.Form → clear関数
* forms.ModelForm → バリデート関数
で行う。

フォームをカスタマイズしたい場合は個々のフィールドやエラーメッセージを個別に表示して行う。