Form・clean()・field・creaned_dataの関係
DjangoのFormについて(clean()・cleaned_data・メソッド呼び出し順)まとめました
- is_valid()が呼ばれてからどのようにメソッドが呼ばれるか
- clean()・cleaned_dataはどこで呼び出し・生成されるか
記事を書いた理由
- Form・clean()・field・creaned_dataの関係、呼び出し順がさっぱり分からなかったから
- その問題に出会ったきっかけは以下
- 業務でDjangoのFormの改修に取り組むことがあった
- 既存のロジックでは、clean()をオーバーライドしたり、cleand_data変数をごにょごにょしていた
djangoのソースコード(記事中はDjango 2.0.1です)
※ field... Fieldのインスタンス, form... Formのインスタンス
結論
呼び出しの順番
form.is_valid()が呼ばれてから ↓
- form.errors()
- form.full_clean()
- form.cleaned_data生成
- form._clean_fields()
- field.clean()
- form.clean<field名>()
- form._clean_form()
- form.clean()
- form._post_clean()
- form.full_clean()
clean()
- field.clean()とform.clean()の二種類がある
field.clean()
- 各fieldの値に対してvalidationチェックを行う
- 返り値はfieldの値
form.clean()
- デフォルトではcleaned_dataを返すだけ
- cleaned_dataを扱えるので、各fieldを跨いだvalidationチェックが可能
- 返り値はself.cleaned_data
form.clean_<field名>()
- 各fieldの値に対してvalidationチェックを行う
- 返り値はfieldの値
form.cleaned_data
- dict型の変数
- form.full_clean()の中で生成される
- 最初は field.clean()が返した値が格納される
- form_clean_<field名>(), form.clean()などで値を扱うことができ、返した値が格納される
詳細
is_valid()
- is_valid()が呼ばれると、下記の条件でbool値を返す
- self.is_bound == True and self.errors == False
def is_valid(self): """Return True if the form has no errors, or False otherwise.""" return self.is_bound and not self.errors
self.is_bound
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, label_suffix=None, empty_permitted=False, field_order=None, use_required_attribute=None, renderer=None): self.is_bound = data is not None or files is not None self._errors = None
self.errors
- propertyデコレータによってerrors()が実行される
- self._errorsを参照し、Noneでなければis_valid() == False となる
- form.full_clean()を呼び出す
@property def errors(self): """Return an ErrorDict for the data provided for the form.""" if self._errors is None: self.form.full_clean() return self._errors
今回の記事のポイント↓↓
- errors()の内部でform.full_clean()が呼ばれており、その中でclean()やcreaned_dataが扱われる
form.full_clean()
- dict型のself.clearned_dataが生成される
form.full_clean()の中では以下の3つのメソッドが呼び出される
- _clean_fields()
- _clean_form()
- _post_clean()
上記メソッドの中でvalidationチェックを行い、各メソッドの条件によってValidationErrorを発生させる
def full_clean(self): """ Clean all of self.data and populate self._errors and self.cleaned_data. """ self._errors = ErrorDict() if not self.is_bound: # Stop further processing. return self.cleaned_data = {} # If the form is permitted to be empty, and none of the form data has # changed from the initial data, short circuit any validation. if self.empty_permitted and not self.has_changed(): return self._clean_fields() self._clean_form() self._post_clean()
3つのメソッドを見ていきましょう
_clean_fields()
- 各fieldに対してfield.clean()が呼び出される
- field.clean()が返した値がcleaned_dataに格納される
- form.clean_<field_name>()があった場合は呼び出し、返した値でcleaned_dataを上書く
def _clean_fields(self): for name, field in self.fields.items(): # value_from_datadict() gets the data from the data dictionaries. # Each widget type knows how to retrieve its own data, because some # widgets split data over several HTML fields. if field.disabled: value = self.get_initial_for_field(field, name) else: value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) try: if isinstance(field, FileField): initial = self.get_initial_for_field(field, name) value = field.clean(value, initial) else: value = field.clean(value) self.cleaned_data[name] = value if hasattr(self, 'clean_%s' % name): value = getattr(self, 'clean_%s' % name)() self.cleaned_data[name] = value except ValidationError as e: self.add_error(name, e)
Fieldのclean()
- validate()によってrequiredのvalidationチェックのみ行われる
- run_validators()によって、他のvalidationチェック(max, min...)が行われる
def clean(self, value): """ Validate the given value and return its "cleaned" value as an appropriate Python object. Raise ValidationError for any errors. """ value = self.to_python(value) self.validate(value) self.run_validators(value) return value
ValidationErrorが発生する条件
- データの型がfield指定の型と異なっている場合
- field.clean()でエラーを吐いた場合
- clean_<field_name>でエラーが吐かれている場合
_clean_form()
- form.clean()が呼び出される
- clean()はデフォルトではcleaned_dataを返すだけですが、ここでオーバーライドしてカスタマイズすることがある
def clean(self): """ Hook for doing any extra form-wide cleaning after Field.clean() has been called on every field. Any ValidationError raised by this method will not be associated with a particular field; it will have a special-case association with the field named '__all__'. """ return self.cleaned_data .... def _clean_form(self): try: cleaned_data = self.clean() except ValidationError as e: self.add_error(None, e) else: if cleaned_data is not None: self.cleaned_data = cleaned_data
ValidationErrorが発生する条件
- form.clean()でエラーを吐く場合
_post_clean()
- Formのインスタンスメソッドとしては何も行わない
- BaseModelForm(BaseForm)でオーバーライドされており、validationチェックが行われる
def _post_clean(self): """ An internal hook for performing additional cleaning after form cleaning is complete. Used for model validation in model forms. """ pass
最後に
- どこにvalidationを置くのがベストか考えたい
- プロジェクトのアーキテクチャに沿う
- validationの存在目的の整理
読んでいただきありがとうございました。
(要リライト)