【requiredが効かない?】FormSetのhas_changed()の深堀り
DjangoのFormsetを使っている時、
Formの各fieldにrequiredオプションを設定しているのに、
空文字列を入力してもFormset.is_valid()=>True
になってしまった。
(詳しくは下記「問題の状況
」)
そこで、条件を変更した検証も交え、ボトルネックの調査をした。
今回使用するFormSetとForm
from django import forms from django.forms.formsets import formset_factory class PeriodForm(forms.Form): start = forms.DateTimeField( required=True, ) end = forms.DateTimeField( required=True, ) PeriodFormSet = formset_factory( PeriodForm)
● 問題の状況
startとendどちらも空文字
→ FormSetを生成
data = { 'form-TOTAL_FORMS': '1', 'form-INITIAL_FORMS': '0', 'form-MIN_NUM_FORMS': '', 'form-MAX_NUM_FORMS': '', 'form-0-start': '', # <-- this date is missing but required 'form-0-end': '', # <-- this date is missing but required }
>>> formset = PeriodFormSet(data) >>> formset.is_valid() True # <-- False expected
start,end共に空文字列
の場合、requiredオプションを設定していても
is_valid()=True
になってしまう。
○ 検証:どちらかに値が入っている場合
startのみdatetime
、endは空文字 → FormSetを生成
from datetime import datetime data = data.update({'form-0-start':datetime.now()}) # <-- end is missing but required >>> formset = PeriodFormSet(data) >>> formset.is_valid() False # <-- False expected
想定通りis_valid()=False
になる。
問題のボトルネック
今回のボトルネックとなった部分(Django1.9.xのsourceより)
● 問題の状況の場合
# formset[0] : start="",end=""... >>> formset[0].empty_permitted True >>> formset[0].has_changed() False
if self.empty_permitted and not self.has_changed(): return # <-- called in this situation _clean_fields(self) # <-- not called
has_changed() = False
のため、
Form._clean_fields()が呼ばれず、fieldごとのvalidationを行っていなかった
○ どちらかに値が入っている場合
# formset[0] : start=datetime.now(),end=""... >>> formset[0].empty_permitted True >>> formset[0].has_changed() True >>> form.empty_permitted False >>> form.has_changed() False
has_changed() = True
になり、
Form._clean_fields()が呼ばれ、fieldごとのvalidationも行う
なぜ Formset.has_changed() の値が変わるのか
Formset.has_changed()(djangoのsourceより)
return: any(form.has_changed() for form in self) (FormSetの中の各Formに対してhas_changed()を呼び出し、boolで返している)
Form.has_changed()(djangoのsourceより)
return: bool(changed_data)
Form.changed_data()(djangoのsourceより)
return: data # 各Fieldのchanged_data(initial, data)を呼び出し、Trueの場合はdataに値を追加し、dataを返り値としている ※ initial : Fieldの初期値。特に定義されていない場合はNone ※ data : widgetから値を取り出している(今回は ""(空文字列))
Field.has_changed(initial, data)(djangoのsourceより)
注目箇所
以下の3行でinitial,dataに対して、Noneだった場合は""(空文字)に変換する処理を加える
initial_value = initial if initial is not None else '' data_value = data if data is not None else '' return initial_value != data_value # <-- "" != "" # => False
return: initial_value != data_value (=> False)
まとめ
今回のようにstart="", end=""
の場合は、
Field.has_changed(initial, data) """ return: initial_value != data_value => False """ ↓ Form.changed_data() """ return: data => [] """ ↓ Form.has_changed() """ return: Form.changed_data() => False """ ↓ Formset.has_changed[f:id:kohei_iwamura:20180204185721j:plain]<figure class="figure-image figure-image-fotolife" title="【requiredが効かない?】FormSetのhas_changed()の深堀り">[f:id:kohei_iwamura:20180204185721j:plain]<figcaption>【requiredが効かない?】FormSetのhas_changed()の深堀り</figcaption></figure>() """ return: any(form.has_changed() for form in self) => False """
となり、各fieldごとのvalidationが行われず、
Formset.is_valid() => True
となる