フォームとフィールドのバリデーション¶
フォームのバリデーションは、データがクリーニング (clean) されるときに実行されます。このプロセスをカスタムしたい場合は、様々な箇所に変更を加えることができ、それぞれが違う目的を持っています。クリーニング方法のうち 3 タイプは、フォームのプロセス中に実行されます。これらは、通常フォーム上の is_valid()
メソッドを呼び出したときに実行されます。このほかにも、クリーニングとバリデーションのトリガーとなる処理 (直接 errors
属性にアクセスしたり、 full_clean()
を直接呼び出す) がありますが、通常必要とはなりません。
一般的に、あらゆるクリーニング方法は ValidationError
を投げる可能性があります。処理されるデータに問題がある場合、関連情報を ValidationError
コンストラクタに渡します。下記項目 に、ValidationError
を投げる際のベストプラクティスがあります。ValidationError
が投げられない場合、メソッドはクリーニングされた (標準化された) データを Python オブジェクトとして返すはずです。
ほとんどのバリデーションは validators を使用して行うことができます。これは再利用できるヘルパーです。バリデーション用の関数(または呼び出し可能オブジェクト)は、単一の引数を取り、無効な入力の場合に ValidationError
を発生させます。バリデーション用の関数は、フィールドの to_python
メソッドと validate
メソッドが呼び出された後に実行されます。
フォームのバリデーションは複数のステップに分割されます。カスタムやオーバーライドもできます:
Field
のto_python()
メソッドはすべてのバリデーションの最初のステップとなります。これは、値をデータ型に正すことを強制し、それができない場合はValidationError
を投げます。このメソッドはウィジェットからそのままの値を受け取り、変換した値を返します。たとえば、FloatField
は Python のfloat
に変換されるか、もしくはValidationError
を投げます。Field
のvalidate()
メソッドは、フィールド特有のバリデーションを行います。これはバリデータとしては不適です。正しいデータ型を強制された値を取り、エラーの際はValidationError
を投げます。このメソッドは何も返さず、また値の変更も行わないはずです。バリデータに記述したくないバリデーションロジックを実行するためには、このメソッドをオーバーライドしてください。Field
のrun_validators()
メソッドはフィールドのバリデータをすべて実行し、すべてのエラーを単一のValidationError
に統合します。このメソッドをオーバーライドする必要はないはずです。Field
サブクラスのclean()
メソッドは、to_python()
、validate()
、run_validators()
を正しい順序で実行し、エラーを伝達する役目を負っています。もしこの過程のどこかでValidationError
が投げられた場合、バリデーションは停止し、そのエラーを投げます。このメソッドはクリーニングされたデータを返し、フォームのcleaned_data
ディクショナリに格納します。clean_<fieldname>()
メソッドは、フォームサブクラス上で呼び出されます --<fieldname>
がフォームのフィールド属性の名前と置き換えられます。このメソッドは、フィールドのタイプにかかわらず、その特定の属性に対するクリーニングを実行します。 このメソッドはパラメータを受け取りません。self.cleaned_data
内のフィールドの値を検索する必要があり、またこの時点ではフォーム上で元々送信された文字列ではなく Python オブジェクトであること覚えておいてください (これはcleaned_data
内にあります。上記に出てきた一般フィールドのclean()
メソッドがすでに一度データをクリーニングしているからです)。例えば、
serialnumber
というCharField
の内容が一意であることを検証したい場合、clean_serialnumber()
が適切な場所になります。特定のフィールドは必要ありません (CharField
です) が、フォームフィールド固有のバリデーションや、データのクリーニング/正規化が必要です。このメソッドの戻り値は
cleaned_data
内の既存の値を置き換えるため、(たとえこのメソッドが変更しなかったとしても)cleaned_data
からのフィールドの値、ないし新しくクリーニングされた値になるはずです。フォームのサブクラスの
clean()
メソッドは、複数のフォームフィールドにまたがるバリデーションにも使用できます。この場所には、たとえば「フィールドA
が入力されたときフィールドB
は有効なメールアドレスのはず」といったチェックを記述できます。このメソッドは、必要な場合はまったく違うディクショナリを返すこともでき、その場合はcleaned_data
として使用されます。フィールドバリデーションのメソッドは
clean()
が呼ばれる際に実行されるので、フォームのerrors
属性にもアクセスできるようになります。これは各フィールドのクリーニングによって発生した例外をすべて含みます。Form.clean()
をオーバーライドして発生させた例外は、特定のフィールドに結びつかない点に注意してください。これらは特別な "フィールド" (__all__
と呼ばれます) に格納され、必要に応じてnon_field_errors()
メソッドを通じてアクセスできます。特定のフィールドにエラーを紐付けて格納したい場合は、add_error()
を呼び出す必要があります。ModelForm
サブクラスのclean()
メソッドをオーバーライドする際、特に考慮する点がまだあります (詳しくは ModelForm ドキュメント を参照してください)。
これらのメソッドは、一度に一つのフィールドに対し、先述した通りの順番で実行されます。フォーム内の各フィールドに対して (フォームの定義時に宣言された順序で)、Field.clean()
メソッド (ないしそのオーバーライド) が実行され、その後 clean_<fieldname>()
が実行されます。最後に、この 2 つのメソッドが全フィールドに対して実行された後、これらのメソッドでエラーが発生したかどうかにかかわらず Form.clean()
メソッド (ないしそのオーバーライド) が実行されます。
各メソッドの例は後述します。
上述の通り、どのメソッドでも ValidationError
が発生する可能性があります。どのフィールドに対しても、Field.clean()
メソッドが ValidationError
を発生させた場合、フィールド特有のクリーニングメソッドは呼ばれません。一方で、すべての残りのフィールドに対するクリーニングメソッドは呼び出されます。
ValidationError
を発生させる¶
エラーメッセージを柔軟かつ簡単にオーバーライドできるようにするため、以下のガイドラインを検討してください:
説明のための
code
をコンストラクタに渡します:# Good ValidationError(_("Invalid value"), code="invalid") # Bad ValidationError(_("Invalid value"))
メッセージには変数を強制しません; プレースホルダとコンストラクタの
params
引数を使用します:# Good ValidationError( _("Invalid value: %(value)s"), params={"value": "42"}, ) # Bad ValidationError(_("Invalid value: %s") % value)
位置指定ではなく、マッピングキーを使います。これにより、メッセージを書き直すときに、変数を任意の順序で配置したり、すべて省略したりすることができます。
# Good ValidationError( _("Invalid value: %(value)s"), params={"value": "42"}, ) # Bad ValidationError( _("Invalid value: %s"), params=("42",), )
メッセージを
gettext
でラップし、翻訳できるようにします:# Good ValidationError(_("Invalid value")) # Bad ValidationError("Invalid value")
全てを一緒に記述すると以下のようになります:
raise ValidationError(
_("Invalid value: %(value)s"),
code="invalid",
params={"value": "42"},
)
再利用可能なフォーム、フォームフィールド、モデルフィールドを記述した場合、特にこのガイドラインの遵守が必要となります。
推奨はされませんが、バリデーションチェーンの最後で (たとえばフォームの clean()
メソッド) エラーメッセージのオーバーライドを 決してしない ことが確かな場合は、より簡潔に記述することもできます:
ValidationError(_("Invalid value: %s") % value)
Form.errors.as_data()
や Form.errors.as_json()
メソッドは、十分な機能を有する (code
名と params
ディクショナリを持つ) ValidationError
により大きな恩恵を受けます。
複数のエラーを起こす¶
クリーニングメソッド内で複数のエラーを検証し、すべてのエラーをフォーム送信者に知らせたい場合、ValidationError
コンストラクタにエラーのリストを渡すことができます。
上述の通り、ValidationError
インスタンスには code
と params
を渡すことが推奨されていますが、文字列のリストも使うことができます:
# Good
raise ValidationError(
[
ValidationError(_("Error 1"), code="error1"),
ValidationError(_("Error 2"), code="error2"),
]
)
# Bad
raise ValidationError(
[
_("Error 1"),
_("Error 2"),
]
)
実際にバリデーションを使用する¶
前のセクションでは、フォームに対する検証が一般にどのように働くかを説明しました。実際の使われ方を見た方が機能をよく理解できるということが往々にしてあります。ここでは、説明した各機能を使った一連の小さな使用例を説明します。
バリデータを使う¶
Django のフォーム(とモデル)フィールドは、バリデータとして知られるユーティリティ関数やクラスの使用をサポートしています。バリデータは呼び出し可能なオブジェクトや関数で、値を受け取り、その値が有効であれば何も返さず、有効でなければ ValidationError
を発生させます。バリデータはフィールドのコンストラクタに渡すか、フィールドの validators
引数で渡すか、 Field
クラスの default_validators
属性で定義します。
バリデータはフィールド内の値を検証するために使用できます。Django の SlugField
を見てみましょう:
from django.core import validators
from django.forms import CharField
class SlugField(CharField):
default_validators = [validators.validate_slug]
SlugField
は、特定の文字規則に従うテキストを受け付けるように検証するカスタムバリデータを持つ CharField
です。これは、フィールド定義時にも行うことができます。
slug = forms.SlugField()
これは以下と同じです:
slug = forms.CharField(validators=[validators.validate_slug])
メールアドレスや正規表現に対する検証などの一般的なケースは、Django に既存のバリデーションクラスを使用して処理できます。例えば、validators.validate_slug
は、最初の引数としてパターン ^[-a-zA-Z0-9_]+\Z
を指定して構築された RegexValidator
のインスタンスです。利用可能なバリデータの一覧や、バリデータの書き方の例については、バリデータの作成 のセクションを参照してください。
フォームフィールドのデフォルトのクリーニング¶
まず、カンマで区切られたメールアドレスを含むかどうか検証するカスタムフォームフィールドを作成しましょう。クラスの全体像は以下のようになります:
from django import forms
from django.core.validators import validate_email
class MultiEmailField(forms.Field):
def to_python(self, value):
"""Normalize data to a list of strings."""
# Return an empty list if no input was given.
if not value:
return []
return value.split(",")
def validate(self, value):
"""Check if value consists only of valid emails."""
# Use the parent's handling of required fields, etc.
super().validate(value)
for email in value:
validate_email(email)
このフィールドを使うすべてのフォームでは、フィールドのデータを使えるようになる前に、これらのメソッドが実行されます。これはこのタイプのフィールドに特有であり、それはこの後の使われ方には関係ありません。
このフィールドの使い方を示すために、ContactForm
を作成してみましょう:
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField()
sender = forms.EmailField()
recipients = MultiEmailField()
cc_myself = forms.BooleanField(required=False)
MultiEmailField
は他のフォームフィールドと同様に使用します。フォームに対して is_valid()
メソッドが呼び出されると、クリーニングプロセスの一環として MultiEmailField.clean()
メソッドが実行され、それがさらにカスタムの to_python()
および validate()
メソッドを呼び出します。
特定のフィールド属性をクリーニングする¶
上の例を引き続き使用して、ContactForm
の recipients
フィールドが常に "fred@example.com"
を含むようにしたいとします。これはフォームに特有のバリデーションなので、MultiEmailField
クラスには記述したくありません。代わりに recipients
フィールドで動作するクリーニングメソッドを記述します:
from django import forms
from django.core.exceptions import ValidationError
class ContactForm(forms.Form):
# Everything as before.
...
def clean_recipients(self):
data = self.cleaned_data["recipients"]
if "fred@example.com" not in data:
raise ValidationError("You have forgotten about Fred!")
# Always return a value to use as the new cleaned data, even if
# this method didn't change it.
return data
互いに依存するフィールドをクリーニングして検証する¶
コンタクトフォームに新たな要件を追加することを考えます: cc_myself
フィールドが True
の場合、subject
には必ず "help"
という言葉が含まれるという要件です。この場合、複数のフィールドにまたがったバリデーションを行うので、フォームの clean()
メソッドで行うのが適切です。ここで注意すべきなのは、今扱っているのはフォームの clean()
メソッドであり、上記で扱ってきたフィールドの clean()
ではないということです。バリデーションを記述すする場所を決める際は、フィールドとフォームの違いを明確にすることが重要です。 フィールドは単一のデータポイントであり、フォームはフィールドの集まりです。
フォームの clean()
メソッドが呼び出されるまでに、個別のフィールドのクリーンメソッドが呼び出されているので (上記 2 つのセクションで見た通りです)、self.cleaned_data
にはこれまでのクリーニングで生き残ったデータが格納されています。したがって、検証しようとしているフィールドが、この個別フィールドのチェックを生き残っていない可能性を考慮する必要があります。
この段階でのエラーを通知するには 2 つの方法があります。おそらく最も一般的な方法は、フォームのトップでエラーを表示する方法です。このようなエラーを生成するには、clean()
メソッドで ValidationError
を発生させてください。次に例を示します。
from django import forms
from django.core.exceptions import ValidationError
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super().clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject:
# Only do something if both fields are valid so far.
if "help" not in subject:
raise ValidationError(
"Did not send for 'help' in the subject despite CC'ing yourself."
)
このコードでは、検証エラーが発生した場合、通常、問題を説明するエラーメッセージがフォームの上部に表示されます。このようなエラーは、フィールド以外のエラーとされ、テンプレート内で {{ form.non_field_errors }}
として表示されます。
例のコードにある super().clean()
の呼び出しは、親クラスのバリデーションロジックも維持されることを保証します。clean()
メソッドで (必須ではないので) cleaned_data
ディクショナリを返さないクラスを継承している場合、cleaned_data
を super()
の結果にアサインせず、代わりに self.cleaned_data
を使用してください:
def clean(self):
super().clean()
cc_myself = self.cleaned_data.get("cc_myself")
...
バリデーションエラーを通知するもう 1 つのアプローチは、エラーメッセージをフィールドの 1 つにアサインすることです。この場合、エラーメッセージはフォーム表示の "subject" と "cc_myself" の両方の行にアサインしましょう。この方法をとるときは、フォームの出力に混乱を招かないように注意してください。ここでは何が可能なのかを示しますが、実際の状況で効果的に実装するのはあなたとあなたのデザイナー次第です。(上の例を置き換えた) 新しいコードは以下のようになります:
from django import forms
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super().clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject and "help" not in subject:
msg = "Must put 'help' in subject when cc'ing yourself."
self.add_error("cc_myself", msg)
self.add_error("subject", msg)
add_error()
の第2引数は、文字列でもよいですが、できれば ValidationError
のインスタンスが最適です。詳細は、ValidationError を発生させる を参照してください。なお、add_error()
は自動的にフィールドを cleaned_data
から削除します。