Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
20
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

@44d

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

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 0x00000000045E5B38>)])

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
20
Help us understand the problem. What are the problem?