LoginSignup
2
0

More than 1 year has passed since last update.

Djangoのform.Formのclean()メソッドでfieldがNoneになる

Last updated at Posted at 2022-03-10

経緯

Djangoのform.Formを使うとき、まずはis_valid()メソッドで中身を確認してからcleaned_dataを取り出す。
is_valid()の中で実行されるself.clean()メソッドをオーバーライドして複数フィールド間のvalidationを行うが、チェックしようとしたフィールドの値がNoneになることがありforms.Formについて実装を読んだ。
(追記)ドキュメントに書いてあった...
2022/3/10時点でのdjango==4.0.3のコードとともに説明していく。

Djangoのforms.Form.is_valid()

is_valid()はフォームにデータが与えられていて、かつまだis_valid()をしていない場合に
validationが行われる。はじめはself.errorsNoneなのでfull_clean()が行われる。

    def is_valid(self):
        """Return True if the form has no errors, or False otherwise."""
        return self.is_bound and not self.errors

    @property
    def errors(self):
        """Return an ErrorDict for the data provided for the form."""
        if self._errors is None:
            self.full_clean()
        return self._errors

is_valid()not self._errorsを返すのでエラーがあった場合はFalseとなりそこで処理を分岐できる。

full_clean()は3つのvalidationを行い最終的に_errorscleaned_dataを生成する。

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

self._clean_fields()は各フィールドについてのvalidation、
self._clean_form()は複数フィールド間でのvalidation、
self._post_clean()はModelFormで利用するvalidationらしく今回は扱わない。

self._clean_fields()をざっと見ていく。

    def _clean_fields(self):
        for name, bf in self._bound_items():
            field = bf.field
            value = bf.initial if field.disabled else bf.data
            try:
                if isinstance(field, FileField):
                    value = field.clean(value, bf.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)

各フィールドについてclean()を行い問題がなければcleaned_dataに追加する。
またFormはclean_<フィールド名>()のメソッドがあるとそれを実行してくれる(if hasattr(self, "clean_%s" % name):の部分)。

from django import forms


class MyForm(forms.Form):
    hoge = forms.IntegerField(min_value=0, required=True)
    
    def clean_hoge(self):
        hoge = self.cleaned_data['hoge']
        ...    

上記の例だと、hogeフィールドのvalidationを行うときclean_hoge()も実行してくれる。ここでは多少複雑なvalidationができる。結果を再びself.cleaned_data[name] = valueで保存するのでreturnで値を返す必要がある。

これらのvalidationで失敗するとself.add_error(name, e)が実行される。コメント等長かったので一部変更した。

    def add_error(self, field, error):
        if not isinstance(error, ValidationError):
            error = ValidationError(error)

        if hasattr(error, "error_dict"):
            if field is not None:
                raise TypeError()
            else:
                error = error.error_dict
        else:
            error = {field or NON_FIELD_ERRORS: error.error_list}

        for field, error_list in error.items():
            if field not in self.errors:
                if field != NON_FIELD_ERRORS and field not in self.fields:
                    raise ValueError(
                        "'%s' has no field named '%s'."
                        % (self.__class__.__name__, field)
                    )
                if field == NON_FIELD_ERRORS:
                    self._errors[field] = self.error_class(
                        error_class="nonfield", renderer=self.renderer
                    )
                else:
                    self._errors[field] = self.error_class(renderer=self.renderer)
            self._errors[field].extend(error_list)
            if field in self.cleaned_data:
                del self.cleaned_data[field]

端的に説明するとエラーがあったフィールドをself._errorsに追加する。
問題は一番最後のdel self.cleaned_data[field]だ。
エラーのあったフィールドはself.cleaned_dataから消されてしまう。これが今回の原因だった。
フィールドの値の不整合があった場合でも処理は止まらない。

self._clean_fields()の次はself._clean_form()が実行される。

    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

ここではself.clean()を実行する。self.clean()実行するときにはすべてのフィールドについてのvalidationが終了していてself.cleaned_dataから各フィールドにアクセスできる。
なのでself.clean()をオーバーライドしてフィールド間のvalidationが行える。

class Order(forms.Form):
    x = forms.IntegerField(min_value=0, required=True)
    y = forms.IntegerField(min_value=0, required=True)
    
    def clean(self):
        cleaned_data = super().clean()
        x, y = cleaned_data.get("x"), cleaned_data.get("y")
        if x < y:
            raise ValidationError()

上記の例のようにフィールドxyについてのチェックができる。
自分はここでフィールドがNone返すことでエラーが起き、その原因がわからないでいた。

フィールドに問題があった場合、そもそもself.clean()が実行されないと思っていたからだ。
実装を読むとどこかのフィールドでvalidationに失敗してもself.clean()は必ず実行され、validationに失敗したフィールドはself.cleaned_dataからdelされているのでNoneになっていた。
以上

所感

原因がわかってスッキリしたのとforms.Formのvalidationの仕組みや手順がわかったので良かった。required=Trueに設定すると空かどうかがフィールドのvalidation時に確認されるが、ここで空だったとしても、やはりself.clean()は実行される。
なんというか、フィールドでのvalidationに失敗したなら実行しなくてもいいんじゃないかという気持ちになる。フロントに伝えるエラーメッセージの都合なんだろうか。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0