1. 44d

    No comment

    44d
Changes in body
Source | HTML | Preview

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()のコールとerrorscleaned_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__()の先頭に、以下の処理があるせいだった。

forms.py
        self.is_bound = data is not None or files is not None

で、is_validの実装は以下の1行のみ

forms.py
    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デコレータ付きのメソッドとして定義されている。

forms.py
    @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 0x00000000045E5B3
8>)])

>>> f.full_clean()
>>> f.cleaned_data
{'name': 'hoge'}

>>> f.data
{'date': '2014-12-04', 'name': 'hoge'}

Form.fieldsOrderedDict.popitemでアイテムを削除する。引数lastがTrue(デフォルト)ならLIFO、FalseならLILOになる。
今回はTrueなので、後から追加したdateフィールドが削除された。

まとめ

…というわけで、DjangoのFormオブジェクトは、後からフィールド自体の追加/削除も可能。
「画面の入力項目をマスタ設定で自由に追加できるようにして!」とかいう要求にも対応できるんじゃないでしょうか。