兄貴ふたたび

おまとめ三行

エラーチェックを行ってみたいと思います
便利なんだけど何かめんどくさいのよね
Have a nice validation!
前回、前々回と二回にわたってフォームの作り方を見てきました。今回は仕上げとして作成したフォームのエラーチェックを行ってみたいと思います。CakePHPでもだいたいめんどくさくて進みが遅くなるところ。それがエラーチェック。

ちなみに前回と前々回の記事はこちら。
フォームを作ってみよう
フォームを作ってみよう2



簡単なおさらい

サイト名を入力するテキストボックスはこんな感じで作れます。

#forms.py
class SampleForm(forms.Form) :
  site = forms.CharField(label = 'サイト名')

#index.html
{{form.site.label}}:{{form.site}}

今回はこのフォームをPOSTする前提でエラーチェックなどを行なっていきます。



エラーがあるかどうか判定

どんなエラーか分からなくてもいいからとりあえずエラーがあるかどうかだけを知りたい場合。

#views.py
class index(TemplateView) :
  post(self, request, args, kwagrs) :
    form = SampleForm(request.POST)
    valid = form.is_valid()

    if not valid :
      #エラーあり

フォームのインスタンスを生成する時にPOSTデータを渡せばエラーチェックまで一気に処理が走ります。「is_valid()」というメソッドは、エラーが一つもなければ「True」、一つでもエラーがあれば「False」が返ってくるので、それでエラーがあるかどうか判定できます。



エラーの内容を取得

エラーの内容は「form.errors」の中に入ってきます。

#views.py
class index(TemplateView) :
  post(self, request, args, kwagrs) :
    form = SampleForm(request.POST)
    print(form.errors)

#出力結果
<ul class="errorlist"><li>site<ul class="errorlist"><li>このフィールドは必須です。</li></ul></li></ul>

デフォルトの設定だとエラー内容はリストのHTMLになるようです。テーブルレイアウトなどに変えることもできるみたいですが今回はパス。

forms.pyでフィールドの設定をする場合、デフォルトの設定では必須入力がONになっているので、テキストボックスの中身を空の状態で送信すると「このフィールドは必須です。」というデフォルトのエラーメッセージをDjangoがセットしてくれます。

HTMLではなくこのエラーのメッセージだけを取得したい場合。

#views.py
class index(TemplateView) :
  post(self, request, args, kwagrs) :
    form = SampleForm(request.POST)
    print(form.errors.items())

#出力結果
dict_items([('site', ['このフィールドは必須です。']))

「form.errors」の中にはエラー内容が辞書データでも格納されているみたいなので、「form.errors.items()」などでエラーメッセージを取得できます。「form.errors.values()」で取得するとメッセージだけでキーが取れないので、複数のテキストボックスがあるフォームなどでエラーになった場合にどのフォームのエラーなのかが判別できません。

#forms.py
class SampleForm(forms.Form) :
  site = forms.CharField(label = 'サイト名')
  email = forms.Email(label = 'アドレス')

#index.html
{{form.site.label}}:{{form.site}}
{{form.email.label}}:{{form.email}}

#views.py
class index(TemplateView) :
  post(self, request, args, kwagrs) :
    form = SampleForm(request.POST)
    print(form.is_valid())
    print(form.errors.items())
    print(form.errors.values())

#出力結果
False
dict_items([('site', ['このフィールドは必須です。']), (['email', ['このフィールドは必須です。'])])
dict_values([['このフィールドは必須です。'], ['このフィールドは必須です。']])

だからitems()を使うのが良いと思います。メッセージの方は平文ではなくリストになっているので注意。



デフォルトのバリデーションルールについて

必須入力以外にも、使用するフィールドによってDjangoがいろいろとエラーチェックを行なってくれます。例えば先ほどEmailFieldでメールアドレスの入力フォームを設定しましたが、EmailFieldはメールアドレスの形式チェックも行なってくれますし、IntegerFieldだと入力値が数字がどうかのチェックも行なってくれます。

#forms.py
class SampleForm(forms.Form) :
  site = forms.CharField(label = 'サイト名')
  email = forms.Email(label = 'アドレス')
  number = forms.IntegerField(label = 'ナンバー')

#index.html
サイト名:<input type="text" name="site" required value="" />
アドレス:<input type="text" name="email" required value="abcde" />
ナンバー:<input type="text" name="site" required value="aiueo" />

#views.py
class index(TemplateView) :
  post(self, request, args, kwagrs) :
    form = SampleForm(request.POST)
    print(form.errors.items())

#出力結果
dict_items([('site', ['このフィールドは必須です。']), (['email', ['有効なメールアドレスを入力してください。']), (['number', ['整数を入力してください。'])])

メッセージもエラーの内容に合わせたものを返してくれるので便利ですね。



独自のバリデーションルール

自分でバリデーションルールを追加したい場合は「clean_変数名」という関数を自作します。

例えば今、ナンバーの項目は必ず「49」という数字から始まらないとエラーになるというルールを追加してみましょう。

#forms.py
import re
class SampleForm(forms.Form) :
  site = forms.CharField(label = 'サイト名')
  email = forms.Email(label = 'アドレス')
  number = forms.IntegerField(label = 'ナンバー')

  def clean_number(self) :
    #入力されたデータを取得
    data = self.cleaned_data.get('number')

    #データの先頭が49かチェック
    match = re.match('49', data)

    if not match :
      raise forms.ValidationError('先頭が49ではありません。')

    return data    

変数名が「number」なので関数名も「clean_number」になります。またフォームの値は「cleaned_data[変数名]」「cleaned_data.get(変数名)」などで取得できます。日本のバーコードは49から始まるという決まりがあるので、このルールを追加すれば入力されたバーコードが日本のものかどうかチェックできますね。どの国のバーコードかチェックするシステムなんて作る日あるかしら?



ちょっとした落とし穴的な

先ほどの応用で複数の値を使って複合的にエラーチェックを行いたいこともあるかと思うのですが、上記の方法で二つ以上の値を同時にチェックすることはできません。

どういうことかというと、例えば今サイト名とメールアドレスを同じにすることはできないというルールを追加したいとします。

#forms.py
class SampleForm(forms.Form) :
  site = forms.CharField(label = 'サイト名')
  email = forms.Email(label = 'アドレス')
  number = forms.IntegerField(label = 'ナンバー')

  def clean_site(self) :
    #エラーチェック
    if self.cleaned_data.get('site') == self.cleaned_data.get('email')
      raise forms.ValidationError('サイト名とメールアドレスに同じものは使用できません')

    return data    

パッと見はこんなんでも良さそうですが、こうすると「self.cleaned_data.get(‘email’)」の方は必ず「None」になってしまいます。

ちょっとややこしいんですけど「cleaned_data」で取れる値には以下の条件があるようです。

1. 自分自身のデータ
2. 自分より上にあるフィールドでエラーになっていないデータ

自分より上というのは、今回の場合だとemailから見たsite、numberから見たsiteとemailが該当します。

#forms.py
class SampleForm(forms.Form) :
  site = forms.CharField(label = 'サイト名')
  email = forms.Email(label = 'アドレス')
  number = forms.IntegerField(label = 'ナンバー')

  def clean_site(self) :
    print('site')
    print(self.cleaned_data) #サイト名しか取れない
    return data  

  def clean_email(self) :
    print('email')
    print(self.cleaned_data) #サイト名とメールアドレスしか取れない
    return data    

  def clean_number(self) :
    print('number')
    print(self.cleaned_data) #全部取れる
    return data

#出力結果
site
{'site': 'あかつきのお宿'}
email
{'site': 'あかつきのお宿', 'email': 'akatsuki@norm-nois.com'}
number
{'site': 'あかつきのお宿', 'email': 'akatsuki@norm-nois.com', 'number': '491234'}

下に行くほど他のフィールドの値も一緒に取れていますね。ただしこれは全てのフィールドがエラーではなかった場合の話です。今ここでメールアドレスの入力にエラーがあったとしたら先ほどの2つ目の条件を満たしていないことになるので、clean_number()でもemailの値は取れません。

#メールアドレスがエラーだった場合
site
{'site': 'あかつきのお宿'}
email
{'site': 'あかつきのお宿', 'email': 'abcde'}
number
{'site': 'あかつきのお宿', 'number': '491234'}

「お前の説明は回りくどいから何言ってるか分からん!」って場合は「複数のフィールドでエラー判定をする場合はとにかく下にあるフィールドでやれ」ってことだけ認識してもらえば大丈夫です。先ほどのサイト名とメールアドレスの例でいくと、メールアドレスの方(clean_email)でやれば良いってことですね。

たぶんDjangoが上から順番にエラーのチェックを行ない、エラーがないものから順番にcleaned_dataに入れてくという動きをしてるんだと思います。だから自分より下にあるデータや自分より上でエラーになったデータはcleaned_dataで取れないと。何せcleanedですからね。不正に入力された汚らわしいデータなどいらぬってことなんでしょうね。さすがはジャンゴさん。きっと海賊をやめて海軍の一員になってからは賄賂を受け取ったり一切汚職などしていないクリーンな身体でフルボディとの友情を深めているのでしょう。



複数のフィールドを使ったバリデーション

一応さっきのやり方でもできますが、「clean_〇〇」ではなく「clean」という関数を自作すれば常に全部のフィールドの値を取り扱えます。

#forms.py
class SampleForm(forms.Form) :
  site = forms.CharField(label = 'サイト名')
  email = forms.Email(label = 'アドレス')
  number = forms.IntegerField(label = 'ナンバー')

  def clean(self) :
    cleaned = self.clean()
    
    if cleaned.get('site') == cleaned.get('email') :
      self.add_error('site', 'サイト名とメールアドレスに同じものは使用できません')

    return cleaned  

さっきは「forms.ValidationError()」という関数を使ってエラーメッセージをセットしましたが、clean()の場合はどのフィールドにエラーをセットするかも設定しないといけないので「add.error(フィールド, メッセージ)」という関数を使います。例外は発生させなくて大丈夫です。

このエラーチェックは他のエラーチェックを全部行なった後に発動します。clean_site()やclean_email()で引っかかった場合はそっちのエラーが優先されます。

複数のフィールドでエラーチェックをする場合はこちらのclean()を使う方が推奨されているみたいです。公式のドキュメントもこっちのやり方が書いてある。



cleanのおまけ

エラーがなかった場合、ビュー側でPOSTデータをまとめて取得する場合にもclean()を使うことができます。

#views.py
form = SampleForm(request.POST)
valid = form.is_valid()

#エラーがなかった場合
if valid :
  data = form.clean()
  print(data)

#出力結果
{'site': 'あかつきのお宿', 'email': 'akatsuki@norm-nois.com', 'number': '491234'}






CakePHPでもそうでしたけど、バリデーションって便利なんだけど何かめんどくさいのよね。独自のルールを追加できる反面、いまいちすんなりといかないと言うか。

まあでもユーザーさんが意図的に不正なデータを入力してきたりうっかりミスったりするのは確実に起こり得ることですから、エラーチェックは怠るわけにはいかんのですよ。だから何とかここは乗り越えて少しでサービスの質を上げたいところですね。頑張ろう。

Have a nice validation!