1. 44d

    No comment

    44d
Changes in body
Source | HTML | Preview
@@ -1,194 +1,195 @@
[DjangoでModelChoiceFieldのquerysetを動的に指定する](http://qiita.com/44d/items/897e5bb20113315af006)の余談に書いた内容について試してみたので、メモメモ。
# 前回の宿題
> Form.fieldsをいじれば、他のフィールドのいろんな属性をフォーム生成後に変更することができる。
> フィールドそのものを追加・削除することもできるかも?
# 実際にやってみた
Djangoのshellコマンドを起動して、対話型シェル上で試していく
## 空っぽの`Form`オブジェクトを生成する
```py3
>>> from django import forms
>>> f = forms.Form()
>>> f.as_p()
''
>>> f.fields
OrderedDict()
```
この時点では何もフィールドが定義されていないので、`as_p()`をコールしても何もレンダリングされない
`Form.fields`は空の`OrderdDict`オブジェクトが表示されるのみ
…ここで初めて、`Form.fields`の型が[OrderedDict](https://docs.python.org/3/library/collections.html#collections.OrderedDict)なるクラスであることを知る。
## `Form.fields`にフィールドを追加する
```py3
>>> 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`の確認
```py3
>>> 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`引数を渡して再生成。もっかい試してみる。
```py3
>>> 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__()`の先頭に、以下の処理があるせいだった。
```py3:forms.py
self.is_bound = data is not None or files is not None
```
で、`is_valid`の実装は以下の1行のみ
```py3: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デコレータ付きのメソッドとして定義されている。
```py3: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()`をコールする
+
+* `is_bound`の値をTrueにする
+* `full_clean()`をコールする
まあ、普通にWebアプリとして使うこと考えたら、1回のリクエストを処理してる最中にパラメータ変わってバリデーションやり直し、なんてことは無いはずだからね…
`is_valid()`や`errors`にアクセスするたびにバリデーション(`full_clean()`のコール)を実行せず、初回だけ実行、とするのは当然と言えば当然か。
## バリデーションがTrueになるよう`data`を変更する
```py3
>>> f.data = {'name':'hoge'}
>>> f.full_clean()
>>> f.is_valid()
True
>>> f.errors
{}
>>> f.cleaned_data
{'name':'hoge'}
```
## さらにフィールドを追加してみる
```py3
>>> 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`からフィールドを削除する
```py3
>>> 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`オブジェクトは、後からフィールド自体の追加/削除も可能。
「画面の入力項目をマスタ設定で自由に追加できるようにして!」とかいう要求にも対応できるんじゃないでしょうか。