いわむぶろぐ

Webエンジニア@スタートアップ@のんびり綴ってます。

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です)

github.com

※ 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()

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

  • インスタンス生成時にコンストラクタによってis_bound変数が生成される
    • dataもしくはfileがNoneではないかどうかが判断される
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を置くのがベストか考えたい

読んでいただきありがとうございました。

(要リライト)