Django ModelForm の clean の順番

投稿日: 2022/07/15
更新日: 2024/02/19
シェア:

URL copied!


概要

Django の Form の clean の順番を忘れていると誤っている情報を許容してしまう。
間違えない為の備忘録。

サンプルコード

顧客登録のフォームを想定。
フラグ is_xxxx_flag が ON の場合、 xxxx_val を入力必須としたい場面を考える。

モデル

from django.db import models

class Customer(models.Model):
    first_name = models.CharField("姓", max_length=20)
    last_name = models.CharField("名", max_length=20)
    is_xxxx_flag = models.BooleanField("xxxxに加入する")
    xxxx_val = models.CharField("xxxx特典名", max_length=20, default="", blank=True)

フォーム

from django import forms
from django.core.exceptions import ValidationError

class TestForm(forms.ModelForm):
    class Meta:
        model = Customer
        fields = (      
            "first_name",
            "last_name",
            "is_xxxx_flag",
            "xxxx_val",
        )

    def clean_last_name(self):
        print("clean_last_name is called!")
        return self.cleaned_data.get("last_name")

    def clean_first_name(self):
        print("clean_first_name is called!")
        return self.cleaned_data.get("first_name")

    def clean_xxxx_val(self):
        print("clean_xxxx_val is called!")
        xxxx_val = self.cleaned_data.get("xxxx_val")
        is_xxxx_flag = self.cleaned_data.get("is_xxxx_flag")
        print(f"cleaned_data xxxx_val:{xxxx_val}, is_xxxx_flag:{is_xxxx_flag}")
        if is_xxxx_flag and not xxxx_val:
            raise ValidationError("xxxxに加入される場合にはxxxx特典名を記入してください。")
        return xxxx_val

    def clean_is_xxxx_flag(self):
        print("clean_is_xxxx_flag is called!")
        return self.cleaned_data.get("is_xxxx_flag")

    def clean(self):
        print("clean is called!")
        return self.cleaned_data

投入データ

is_xxxx_flag が ON だが xxxx_val が未記入なので弾きたいデータを想定。

test_data = {
    "first_name": "テスト",
    "last_name": "タロウ",
    "is_xxxx_flag": True,
    "xxxx_val": "",
}

実験

上記のコードのまま実行してみると想定通りの動きをした。

form = TestForm(test_data)
form.is_valid()
# clean_first_name is called!
# clean_last_name is called!
# clean_is_xxxx_flag is called!
# clean_xxxx_val is called!
# cleaned_data xxxx_val:, is_xxxx_flag:True
# clean is called!
# False  <= 想定通り○

form.errors["xxxx_val"]
# ['xxxxに加入される場合にはxxxx特典名を記入してください。']

しかし、fields の順番を下記とすると。。

fields = (      
    "first_name",
    "last_name",
    "xxxx_val",
    "is_xxxx_flag",
)

バリデーションを通過してしまいます。

form = TestForm(test_data)
form.is_valid()
# clean_first_name is called!
# clean_last_name is called!
# clean_xxxx_val is called!
# cleaned_data xxxx_val:, is_xxxx_flag:None
# clean_is_xxxx_flag is called!
# clean is called!
# True  <= おかしい!

これは clean メソッドの呼ばれる順番が fields で指定しているフィールド順になっているためです。(clean メソッドの定義順ではありません)
is_xxxx_flag:None と後に呼ばれるフィールドはまだ値が取れていないことが見て取れます。

このように記載を間違えてしまうとエラーになってしまうので各フィールドの clean で他のフィールド情報を引っ張ってくるのはできるだけ避けた方が良いと思われます。
(わかったうえで利用するのはありだと思います。)
ただ、複数のフィールドを使ったバリデーションは clean に書くと間違いがないと思います。

class TestForm(forms.ModelForm):
    class Meta:
        model = Customer
        fields = (      
            "first_name",
            "last_name",
            "xxxx_val",
            "is_xxxx_flag",
        )

    def clean(self):
        xxxx_val = self.cleaned_data.get("xxxx_val")
        is_xxxx_flag = self.cleaned_data.get("is_xxxx_flag")
        if is_xxxx_flag and not xxxx_val:
            raise ValidationError(
                {"xxxx_val": ["xxxxに加入される場合にはxxxx特典名を記入してください。。"]}
            )
        return self.cleaned_data

ValidationError を上記のように書くと各フィールドにエラーを出すことができます。

form = TestForm(test_data)
form.is_valid()
# False

form.errors["xxxx_val"]
# ['xxxxに加入される場合にはxxxx特典名を記入してください。。']

結果

fields に設定したフィールドの順番で行われ、最後に clean が呼ばれる。

修正

def clean_xxxx_val(self): での print が誤っていましたので出力結果と合わせて修正しました。

print("clean_xxxx_val is called!")  # ○
print("clean_is_xxxx_flag is called!")  # ×