djangoのバリデーションについてまとめる（第1回〜エラーメッセージの最適化）

今回のお題
-
今回はdjangoアプリのバリデーションについて話します。

一口にバリデーションといっても範囲がものすごく広いのですが、今回は第1回と称して「エラーメッセージの最適化」を行ってみたいと思います。

目次
-

- そもそもエラーメッセージをどう改良したいのか
- 改良その１〜フォームのオプションの無効化
- 改良その２〜メッセージの変更

そもそもエラーメッセージをどう改良したいのか
-
そもそも、エラーメッセージをどう改良したいのかを話します。

現在、書籍のレビュー投稿アプリを作成しているのですが、複数の入力項目に不備がある場合でも一つ目のエラーメッセージしか表示されない、という問題を抱えています。

以下がコードと実物です。

```python:models.py
class Review(models.Model):
  book = models.ForeignKey(
    Book,
    on_delete=models.CASCADE
  )
  name = models.CharField(
    max_length=20,
    verbose_name="名前",
  )
  body = models.TextField(verbose_name="本文", max_length=255)

  def __str__(self) -> str:
      return f"{self.name}: {self.body[:10]}"
```
```python:forms.py
class ReviewForm(ModelForm):
  
  def __init__(self, *args, **kwargs):
      super().__init__(*args, **kwargs)
      self.fields["book"].disabled = True
  
  class Meta:
    model = Review
    fields = "__all__"
    labels = {
      "book": "書籍名",
      "name": "投稿者名",
      "body": "レビュー本文",
    }
```
nameとbodyに関してはデフォルトで`null=False`と`blank=False`が設定されています。

実物
[![Image from Gyazo](https://i.gyazo.com/186671f8091307e045ede61a567ac087.png)](https://gyazo.com/186671f8091307e045ede61a567ac087)

これの何が問題かというと、何も入力せずにそのまま登録ボタンを押すと一旦は投稿者名の入力を促されるのですが（写真１）、入力後に再度登録を試みると今度はレビュー本文の入力を求められます（写真２）。

写真１
[![Image from Gyazo](https://i.gyazo.com/196d49102291db510a0404fcf72520cb.png)](https://gyazo.com/196d49102291db510a0404fcf72520cb)

写真２
[![Image from Gyazo](https://i.gyazo.com/91ae8f4b1df02f66cd0b67d202c12ca9.png)](https://gyazo.com/91ae8f4b1df02f66cd0b67d202c12ca9)

エラー内容が一度に全て確認できないせいで、２度手間になっていますよね。

なのでまずはこれをなんとかしていきます。

改良その１〜フォームのオプションの無効化
-
いきなり結論に入るのですが、今までに出てきたエラーメッセージは（モデルの）バリデーションエラーではありません。

djangoではmodels.pyにおいてフィールドに`blank=False`が設定されているとinput要素に`required`オプションが設定される仕様になっており、それによってメッセージが表示されているというわけです。

これに関しては`form`要素に`novaldate`属性を設定してあげることで解決されます(`required`オプションは無効化されますが、`maxlength`オプションは有効なままです)。

```django
<form method="xxx" action="xxx" novalidate>
  {{ form }}
</form>
```
で、その結果がこちら。

[![Image from Gyazo](https://i.gyazo.com/45b7f38138fae39c43e66c5e605c57ba.png)](https://gyazo.com/45b7f38138fae39c43e66c5e605c57ba)

エラーメッセージが両方とも表示されていますね。

とりあえずはこれで大丈夫なのですが、メッセージが不親切な感もあるので、ここを改良していきます。

改良その２〜メッセージの変更
-
メッセージの編集は、`models.py`ではなく`forms.py`で行います。

```python:forms.py
class ReviewForm(ModelForm):
  
  def __init__(self, *args, **kwargs):
      super().__init__(*args, **kwargs)
      self.fields["book"].disabled = True
  
  class Meta:
    model = Review
    fields = "__all__"
    labels = {
      "book": "書籍名",
      "name": "投稿者名",
      "body": "レビュー本文",
    }

    # エラーメッセージを追加
    error_messages = {
      "name": {
        "required": "投稿者名が入力されていません",
      },
      "body": {
        "required": "本文が入力されていません",
      },
    }

    help_texts = {
      "book": "書籍名は変更できません",
    }
```
上記のように`error_messages = {}`として辞書形式でメッセージを記述します。

フィールド名 > バリデーション名 > メッセージと指定するので辞書が二重になっていることに注意してください。

結果
[![Image from Gyazo](https://i.gyazo.com/4479a78a63fe7b211c65baa7978fca94.png)](https://gyazo.com/4479a78a63fe7b211c65baa7978fca94)

無事に表示できました。

まとめ
-
- models.pyで`null=False`(フィールドの種類によっては`blank=False`)が設定されるとテンプレートには`requiredオプション`が、モデルには`requiredという名前のバリデーション`が設定される。
- 両者ともに有効な場合、html要素の`requiredオプション`が優先される。
- `requiredオプション`は、フォームに`novalidate属性`を付与することで無効化できる。
- バリデーションメッセージの変更は、`forms.py`から行う。

以上になります。
バリデーションには他にも`cleanedメソッド`や`自作のバリデーション`などいくつか方法があるので、そちらについても適宜まとめていく予定です。