DjangoでModelChoiceFieldのquerysetを動的に指定するの余談に書いた内容について試してみたので、メモメモ。
前回の宿題
Form.fieldsをいじれば、他のフィールドのいろんな属性をフォーム生成後に変更することができる。
フィールドそのものを追加・削除することもできるかも?
実際にやってみた
Djangoのshellコマンドを起動して、対話型シェル上で試していく
空っぽのForm
オブジェクトを生成する
>>> from django import forms
>>> f = forms.Form()
>>> f.as_p()
''
>>> f.fields
OrderedDict()
この時点では何もフィールドが定義されていないので、as_p()
をコールしても何もレンダリングされない
Form.fields
は空のOrderdDict
オブジェクトが表示されるのみ
…ここで初めて、Form.fields
の型がOrderedDictなるクラスであることを知る。
Form.fields
にフィールドを追加する
>>> f.fields['name'] = forms.CharField(label='名前')
>>> f.as_p()
'<p><label for="id_name">名前:</label> <input id="id_name" name="name" type="text" /></p>'
>>> f.fields
OrderedDict([('name', <django.forms.fields.CharField object at 0x00000000045E5B388>)])
'name'というキーでCharField
を追加。
ちゃんとas_p()
でレンダリングされる。
Form.fields
の中身も、'name'というキーでCharField
のオブジェクトが格納されている。
is_valid()
のコールとerrors
、cleaned_data
の確認
>>> f.is_valid()
Flase
>>> f.errors
{}
>>> f.cleaned_data
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'Form' object has no attribute 'cleaned_data'
追加したnameフィールドはrequired=True
(デフォルト値)なので、is_valid()
がFalseになるのは想定通りだが、なぜかForm.errors
が空っぽ。
調べてみたら、Form
オブジェクトは各フィールドの値が変更されてない場合、または引数にdata
を受け取っていない場合、空のerrors
を作るだけでfull_clean()
の処理を終了するみたい(cleaned_data
も生成されない)。
仕方ないので、Form
オブジェクトにdata
引数を渡して再生成。もっかい試してみる。
>>> f = forms.Form(data={})
>>> f.fields['name'] = forms.CharField(label='名前')
>>> f.is_valid()
False
>>> f.errors
{'name': ['This field is required.']}
>>> f.cleaned_data
{}
想定通り、errors
の中身に'name'の必須チェックエラーが入ってくれた。
cleaned_data
も空のdictが生成されている。
寄り道:is_valid()
とerrors
の挙動
↑の問題で、最初はForm.data
に空のdictを直接代入して対応しようとしたのだが、結果は変わらなかった。
djangoのソースを追いかけてみたところ、Form
クラスの親であるBaseForm
クラスの__init__()
の先頭に、以下の処理があるせいだった。
self.is_bound = data is not None or files is not None
で、is_valid
の実装は以下の1行のみ
def is_valid(self):
"""
Returns True if the form has no errors. Otherwise, False. If errors are
being ignored, returns False.
"""
return self.is_bound and not bool(self.errors)
さらに、errorsはフィールドではなくpropertyデコレータ付きのメソッドとして定義されている。
@property
def errors(self):
"Returns an ErrorDict for the data provided for the form"
if self._errors is None:
self.full_clean()
return self._errors
というわけで、data
を直接編集しただけじゃダメで、以下の2つの処理が必要だった。
-
is_bound
の値をTrueにする -
full_clean()
をコールする
まあ、普通にWebアプリとして使うこと考えたら、1回のリクエストを処理してる最中にパラメータ変わってバリデーションやり直し、なんてことは無いはずだからね…
is_valid()
やerrors
にアクセスするたびにバリデーション(full_clean()
のコール)を実行せず、初回だけ実行、とするのは当然と言えば当然か。
バリデーションがTrueになるようdata
を変更する
>>> f.data = {'name':'hoge'}
>>> f.full_clean()
>>> f.is_valid()
True
>>> f.errors
{}
>>> f.cleaned_data
{'name':'hoge'}
さらにフィールドを追加してみる
>>> f.fields['date'] = forms.DateField(label='日付',required=False)
>>> f.as_p()
'<ul class="errorlist"><li>This field is required.</li></ul>\n<p><label for="id_name">名前:</label> <input id="id_name" name="name" type="text" /></p>\n<p><label for="id_date">日付:</label> <input id="id_date" name="date" type="text" /></p>'
>>> f.fields
OrderedDict([('name', <django.forms.fields.CharField object at 0x00000000045E5B38>), ('date', <django.forms.fields.DateField object at 0x0000000002A3C828>)])
>>> f.full_clean()
>>> f.is_valid()
True
>>> f.errors
{}
>>> f.cleaned_data
{'date':None, 'name':'hoge'}
>>> f.data['date'] = '2014-12-04'
>>> f.full_clean()
>>> f.is_valid()
True
>>> f.errors
{}
>>> f.cleaned_data
{'date':datetime.date(2014, 12, 4), 'name':'hoge'}
Form.fields
からフィールドを削除する
>>> f.fields.popitem()
('date', <django.forms.fields.DateField object at 0x0000000002A3C828>)
>>> f.as_p()
'<p><label for="id_name">名前:</label> <input id="id_name" name="name" type="text" value="hoge" /></p>'
>>> f.fields
OrderedDict([('name', <django.forms.fields.CharField object at 0x00000000045E5B38>)])
>>> f.full_clean()
>>> f.cleaned_data
{'name': 'hoge'}
>>> f.data
{'date': '2014-12-04', 'name': 'hoge'}
Form.fields
はOrderedDict.popitem
でアイテムを削除する。引数last
がTrue(デフォルト)ならLIFO、FalseならLILOになる。
今回はTrueなので、後から追加したdateフィールドが削除された。
まとめ
…というわけで、DjangoのForm
オブジェクトは、後からフィールド自体の追加/削除も可能。
「画面の入力項目をマスタ設定で自由に追加できるようにして!」とかいう要求にも対応できるんじゃないでしょうか。