DjangoでFormオブジェクトに動的にフィールドを追加する

[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()`をコールする

まあ、普通に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`オブジェクトは、後からフィールド自体の追加/削除も可能。
「画面の入力項目をマスタ設定で自由に追加できるようにして！」とかいう要求にも対応できるんじゃないでしょうか。