Djangoのフォームまとめー

フォームセット

フォームセットはフォームの繰り返しを表現します。
クラスの生成に「formset_factory」関数を使います。これがフォームセットの肝です。

試しに最初に作ったユーザ登録用のフォーム「RegistrationForm」を使います。

>>> from django.forms.formsets import formset_factory
>>> # 空フォーム数の繰り返し回数をextraで指定できる
>>> FormSet = formset_factory(RegistrationForm, extra=3)
>>> # formsetに対する初期データはinitial引数に配列で指定する(ここで指定したフォームはextraとは別に作成される点に注意)
>>> formset1 = FormSet(initial=[{'username': 'test'}, {'username': 'test2'}])
 
>>> # forms属性にフォームインスタンスが入っている
>>> formset1.forms
[
 <RegistrationForm bound=False, valid=Unknown, fields=(username;email;password)>, <RegistrationForm bound=False, valid=Unknown, fields=(username;email;password)>,
 <RegistrationForm bound=False, valid=Unknown, fields=(username;email;password)>, <RegistrationForm bound=False, valid=Unknown, fields=(username;email;password)>,
 <RegistrationForm bound=False, valid=Unknown, fields=(username;email;password)>
]
>>> len(formset1.forms)
5
 
>>> # フォームと同じようにHTML出力できる
>>> print(formset1.as_ul())
<input id="id_form-TOTAL_FORMS" name="form-TOTAL_FORMS" type="hidden" value="5" /><input id="id_form-INITIAL_FORMS" name="form-INITIAL_FORMS" type="hidden" value="2" /><input id="id_form-MIN_NUM_FORMS" name="form-MIN_NUM_FORMS" type="hidden" value="0" /><input id="id_form-MAX_NUM_FORMS" name="form-MAX_NUM_FORMS" type="hidden" value="1000" />
<li><label for="id_form-0-username">Username:</label> <input id="id_form-0-username" name="form-0-username" type="text" value="test" /></li>
<li><label for="id_form-0-email">Email:</label> <input id="id_form-0-email" name="form-0-email" type="email" /></li>
<li><label for="id_form-0-password">Password:</label> <input id="id_form-0-password" name="form-0-password" type="password" /></li> <li><label for="id_form-1-username">Username:</label> <input id="id_form-1-username" name="form-1-username" type="text" value="test2" /></li>
<li><label for="id_form-1-email">Email:</label> <input id="id_form-1-email" name="form-1-email" type="email" /></li>
<li><label for="id_form-1-password">Password:</label> <input id="id_form-1-password" name="form-1-password" type="password" /></li> <li><label for="id_form-2-username">Username:</label> <input id="id_form-2-username" name="form-2-username" type="text" /></li>
<li><label for="id_form-2-email">Email:</label> <input id="id_form-2-email" name="form-2-email" type="email" /></li>
<li><label for="id_form-2-password">Password:</label> <input id="id_form-2-password" name="form-2-password" type="password" /></li> <li><label for="id_form-3-username">Username:</label> <input id="id_form-3-username" name="form-3-username" type="text" /></li>
<li><label for="id_form-3-email">Email:</label> <input id="id_form-3-email" name="form-3-email" type="email" /></li>
<li><label for="id_form-3-password">Password:</label> <input id="id_form-3-password" name="form-3-password" type="password" /></li> <li><label for="id_form-4-username">Username:</label> <input id="id_form-4-username" name="form-4-username" type="text" /></li>
<li><label for="id_form-4-email">Email:</label> <input id="id_form-4-email" name="form-4-email" type="email" /></li>
<li><label for="id_form-4-password">Password:</label> <input id="id_form-4-password" name="form-4-password" type="password" /></li>

フォームセットではパラメータの種類や形式が単体のフォーム出力とは異なることが分かるでしょうか。

自動的に生成されるパラメータを手動検査するのはあまりよい方法とは言えません。
フォームセットによって出力されたパラメータは、フォームセットによってバリデーションするのが妥当でしょう。(タブンネ)

ManagementForm

こういった繰り返し以外の特殊なパラメータを扱うのがManagementFormと呼ばれるものです。
具体的なパラメータは「form-TOTAL_FORMS」「form-INITIAL_FORMS」です。(form-MIN_NUM_FORMSとform-MAX_NUM_FORMSは放置)
それぞれ「総フォーム数」と「初期入力済フォーム数」を示しています。

未入力フォームセットの場合

総フォーム数は3(extra引数で指定した)+2(initial)で5となります。

>>> formset1.total_form_count()
5
>>> formset1.initial_form_count()
2
>>> # management_form属性からも取得できる
>>> formset1.management_form
<ManagementForm bound=False, valid=Unknown, fields=(TOTAL_FORMS;INITIAL_FORMS;MIN_NUM_FORMS;MAX_NUM_FORMS)>
入力済フォームセットの場合

フォームセットにパラメータを与えた場合、それぞれのフォームインスタンスは入力済フォーム(束縛フォーム)として扱われます。
束縛フォームセットとでも言うのでしょうか。

入力済フォームセットでは繰り返し回数はパラメータ(form-TOTAL_FORMS)によって決定されます。

>>> # 擬似的にform-TOTAL_FORMS=10のパラメータを作成して
>>> qd = QueryDict('form-TOTAL_FORMS=10&form-INITIAL_FORMS=1&form-MIN_NUM_FORMS=0&form-0-username=test0&form-0-email=test0@example.com&form-0-password=testtest&form-1-username=test1&form-1-email=test1@example.com&form-1-password=testtest&form-2-username=test2&form-2-email=test2@example.com&form-2-password=testtest', mutable=True)
 
>>> # formset2にパラメータを与えてみる
>>> formset2 = FormSet(qd)
>>> formset2.total_form_count()
10
>>> formset2.initial_form_count()
1
>>> # フォーム10個分のパラメータはないけどバリデーションは通る
>>> formset2.is_valid()
True
>>> # 10個ともvalid=Trueとなる(ここがよくわからん)
>>> formset2.forms
[
 <RegistrationForm bound=True, valid=True, fields=(username;email;password)>, <RegistrationForm bound=True, valid=True, fields=(username;email;password)>,
 <RegistrationForm bound=True, valid=True, fields=(username;email;password)>, <RegistrationForm bound=True, valid=True, fields=(username;email;password)>,
 <RegistrationForm bound=True, valid=True, fields=(username;email;password)>, <RegistrationForm bound=True, valid=True, fields=(username;email;password)>,
 <RegistrationForm bound=True, valid=True, fields=(username;email;password)>, <RegistrationForm bound=True, valid=True, fields=(username;email;password)>,
 <RegistrationForm bound=True, valid=True, fields=(username;email;password)>, <RegistrationForm bound=True, valid=True, fields=(username;email;password)>
]
>>> # 入力されたパラメータを取り出すにはフォームと同じようにcleaned_dataとする
>>> formset2.cleaned_data
[
 {'username': u'test0', 'password': u'testtest', 'email': u'test0@example.com'},
 {'username': u'test1', 'password': u'testtest', 'email': u'test1@example.com'},
 {'username': u'test2', 'password': u'testtest', 'email': u'test2@example.com'},
 {}, {}, {}, {}, {}, {}, {}
]

フォームセットのバリデーションはフォーム単位で行われ、パラメータの入力が不十分なフォームが存在する場合バリデーションはエラーとなるようです。
言い換えるとトータルのパラメータ数が足りなくとも、フォーム単位で不整合が生じなければエラーとはなりません。

>>> # 上の続き
>>> # form-2の省略不可パラメータを消してみる
>>> del qd['form-2-email']
>>> formset2 = FormSet(qd)
>>> formset2.is_valid()
False
 
>>> # エラー内容はerrorsに入ってる
>>> formset2.errors
[{}, {}, {'email': [u'This field is required.']}, {}, {}, {}, {}, {}, {}, {}]
 
>>> # バリデーションに通らないとcleaned_data属性がとれない
>>> formset2.cleaned_data
Traceback (most recent call last):
File "<console>", line 1, in <module>
    File "/home/user/Tests/djangoform/local/lib/python2.7/site-packages/django/forms/formsets.py", line 197, in cleaned_data
    raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__)
    AttributeError: 'RegistrationFormFormSet' object has no attribute 'cleaned_data'

max_num

formset_factory関数のmax_num引数にてフォーム数の上限を設定することができますが、これは空の初期フォーム数の話です。

>>> FormSet = formset_factory(RegistrationForm, extra=3, max_num=1)
>>> formset3 = FormSet(initial=[{'username': 'test'}, {'username': 'test2'}])
>>> formset3.total_form_count()
2
>>> formset3.initial_form_count()
2
>>> print(formset3.management_form)
<input id="id_form-TOTAL_FORMS" name="form-TOTAL_FORMS" type="hidden" value="2" /><input id="id_form-INITIAL_FORMS" name="form-INITIAL_FORMS" type="hidden" value="2" /><input id="id_form-MIN_NUM_FORMS" name="form-MIN_NUM_FORMS" type="hidden" value="0" /><input id="id_form-MAX_NUM_FORMS" name="form-MAX_NUM_FORMS" type="hidden" value="1" />
 
>>> formset4 = FormSet(qd)
>>> formset4.total_form_count()
10
>>> formset4.initial_form_count()
1

初期値があるフォームとユーザが入力したフォームの数はmax_num引数では制限できません。パラメータでも指定できるし、これ何に使えるんですかね?(困惑)
入力フォーム数を制限したい場合はユーザの入力値(form-TOTAL_FORMS)を検査するか、単純にループ数に制限をかける等の対策をするのがいいかもしれませんね。

can_order

パラメータの入力順を制御するのに利用します。
formset_factory関数のcan_order引数に「True」を指定することでHTMLに出力され、パラメータとして受け取ることが可能となります。
デフォルトはFalseです。

>>> FormSet = formset_factory(RegistrationForm, extra=3, can_order=True)
>>> formset5 = FormSet()
>>> # ORDERパラメータが追加された
>>> print(formset5)
<input id="id_form-TOTAL_FORMS" name="form-TOTAL_FORMS" type="hidden" value="3" /><input id="id_form-INITIAL_FORMS" name="form-INITIAL_FORMS" type="hidden" value="0" /><input id="id_form-MIN_NUM_FORMS" name="form-MIN_NUM_FORMS" type="hidden" value="0" /><input id="id_form-MAX_NUM_FORMS" name="form-MAX_NUM_FORMS" type="hidden" value="1000" />
<tr><th><label for="id_form-0-username">Username:</label></th><td><input id="id_form-0-username" name="form-0-username" type="text" /></td></tr>
<tr><th><label for="id_form-0-email">Email:</label></th><td><input id="id_form-0-email" name="form-0-email" type="email" /></td></tr>
<tr><th><label for="id_form-0-password">Password:</label></th><td><input id="id_form-0-password" name="form-0-password" type="password" /></td></tr>
<tr><th><label for="id_form-0-ORDER">Order:</label></th><td><input id="id_form-0-ORDER" name="form-0-ORDER" type="number" /></td></tr> <tr><th><label for="id_form-1-username">Username:</label></th><td><input id="id_form-1-username" name="form-1-username" type="text" /></td></tr>
<tr><th><label for="id_form-1-email">Email:</label></th><td><input id="id_form-1-email" name="form-1-email" type="email" /></td></tr>
<tr><th><label for="id_form-1-password">Password:</label></th><td><input id="id_form-1-password" name="form-1-password" type="password" /></td></tr>
<tr><th><label for="id_form-1-ORDER">Order:</label></th><td><input id="id_form-1-ORDER" name="form-1-ORDER" type="number" /></td></tr> <tr><th><label for="id_form-2-username">Username:</label></th><td><input id="id_form-2-username" name="form-2-username" type="text" /></td></tr>
<tr><th><label for="id_form-2-email">Email:</label></th><td><input id="id_form-2-email" name="form-2-email" type="email" /></td></tr>
<tr><th><label for="id_form-2-password">Password:</label></th><td><input id="id_form-2-password" name="form-2-password" type="password" /></td></tr>
<tr><th><label for="id_form-2-ORDER">Order:</label></th><td><input id="id_form-2-ORDER" name="form-2-ORDER" type="number" /></td></tr>
 
 
>>> # パラメータにORDERを加える
>>> # form-1,form-2のORDERを指定する。わざとform-0は指定しない
>>> qd.update({'form-1-ORDER': 1, 'form-2-ORDER': 0})
>>> formset6 = FormSet(qd)
 
>>> # cleaned_dataは整列されていない
>>> formset6.cleaned_data
[{'username': u'test0', 'password': u'testtest', 'email': u'test0@example.com', u'ORDER': None}, {'username': u'test1', 'password': u'testtest', 'email': u'test1@example.com', u'ORDER': 1}, {'username': u'test2', 'password': u'testtest', 'email': u'test2@example.com', u'ORDER': 0}]
 
>>> # formsも整列されていない
>>> for form in formset6.forms:
...     form.cleaned_data
...
{'username': u'test0', 'password': u'testtest', 'email': u'test0@example.com', u'ORDER': None}
{'username': u'test1', 'password': u'testtest', 'email': u'test1@example.com', u'ORDER': 1}
{'username': u'test2', 'password': u'testtest', 'email': u'test2@example.com', u'ORDER': 0}
 
>>> # 整列されたformはordered_formsに入っている
>>> for form in formset6.ordered_forms:
...     form.cleaned_data
...
{'username': u'test2', 'password': u'testtest', 'email': u'test2@example.com', u'ORDER': 0}
{'username': u'test1', 'password': u'testtest', 'email': u'test1@example.com', u'ORDER': 1}
{'username': u'test0', 'password': u'testtest', 'email': u'test0@example.com', u'ORDER': None}

指定されない場合、ORDERはNoneとなり順番は最後となるようです。

can_delete

削除対象の入力値を受け取るのに利用します。
formset_factory関数のcan_order引数に「True」を指定することでHTMLに出力され、パラメータとして受け取ることが可能となります。
デフォルトはFalseです。

>>> FormSet = formset_factory(RegistrationForm, extra=3, can_delete=True)
>>> formset7 = FormSet()
 
>>> # 削除フラグはチェックボックスとして出力される
>>> print(formset7)
<input id="id_form-TOTAL_FORMS" name="form-TOTAL_FORMS" type="hidden" value="3" /><input id="id_form-INITIAL_FORMS" name="form-INITIAL_FORMS" type="hidden" value="0" /><input id="id_form-MIN_NUM_FORMS" name="form-MIN_NUM_FORMS" type="hidden" value="0" /><input id="id_form-MAX_NUM_FORMS" name="form-MAX_NUM_FORMS" type="hidden" value="1000" />
<tr><th><label for="id_form-0-username">Username:</label></th><td><input id="id_form-0-username" name="form-0-username" type="text" /></td></tr>
<tr><th><label for="id_form-0-email">Email:</label></th><td><input id="id_form-0-email" name="form-0-email" type="email" /></td></tr>
<tr><th><label for="id_form-0-password">Password:</label></th><td><input id="id_form-0-password" name="form-0-password" type="password" /></td></tr>
<tr><th><label for="id_form-0-DELETE">Delete:</label></th><td><input id="id_form-0-DELETE" name="form-0-DELETE" type="checkbox" /></td></tr> <tr><th><label for="id_form-1-username">Username:</label></th><td><input id="id_form-1-username" name="form-1-username" type="text" /></td></tr>
<tr><th><label for="id_form-1-email">Email:</label></th><td><input id="id_form-1-email" name="form-1-email" type="email" /></td></tr>
<tr><th><label for="id_form-1-password">Password:</label></th><td><input id="id_form-1-password" name="form-1-password" type="password" /></td></tr>
<tr><th><label for="id_form-1-DELETE">Delete:</label></th><td><input id="id_form-1-DELETE" name="form-1-DELETE" type="checkbox" /></td></tr> <tr><th><label for="id_form-2-username">Username:</label></th><td><input id="id_form-2-username" name="form-2-username" type="text" /></td></tr>
<tr><th><label for="id_form-2-email">Email:</label></th><td><input id="id_form-2-email" name="form-2-email" type="email" /></td></tr>
<tr><th><label for="id_form-2-password">Password:</label></th><td><input id="id_form-2-password" name="form-2-password" type="password" /></td></tr>
<tr><th><label for="id_form-2-DELETE">Delete:</label></th><td><input id="id_form-2-DELETE" name="form-2-DELETE" type="checkbox" /></td></tr>
 
>>> # form-0,form-1に値を入れてみる。form-0には文字列の0を指定してみた
>>> qd.update({'form-0-DELETE': '0', 'form-1-DELETE': '1'})
>>> formset8 = FormSet(qd)
>>> formset8.cleaned_data
[
 {'username': u'test0', 'password': u'testtest', 'email': u'test0@example.com', u'DELETE': True},
 {'username': u'test1', 'password': u'testtest', 'email': u'test1@example.com', u'DELETE': True},
 {'username': u'test2', 'password': u'testtest', 'email': u'test2@example.com', u'DELETE': False}
]
 
>>> # 削除対象のフォームはdeleted_formsに入ってる。あとは煮るなり焼くなり
>>> for form in formset8.deleted_forms:
...     form.cleaned_data
 
{'username': u'test0', 'password': u'testtest', 'email': u'test0@example.com', u'DELETE': True}
{'username': u'test1', 'password': u'testtest', 'email': u'test1@example.com', u'DELETE': True}

文字列の「’0’」はTrueと判断されるため、削除対象となったようです。
数値型の「0」オブジェクトを指定するとdeleted_formsには入りません。
通常チェックボックスであれば指定しないとDELETEパラメータ自体がとばないはずですが、ひねくれた使い方をするときは気をつけましょう^^;

フォームセットのカスタマイズ

フォームセットの挙動を変更したい場合、「BaseFormSet」を継承したクラスを「formset_factory」のformset引数に与えます。

>>> from django.forms.formsets import BaseFormSet
>>> class BaseRegistrationFormSet(BaseFormSet):
...     def add_fields(self, form, index):
...         """フォームセットの時だけreasonフィールドを追加する(ユーザを追加した理由というどうでもいい設定)
...         """
...         super(BaseRegistrationFormSet, self).add_fields(form, index)
...         form.fields["reason"] = forms.CharField()
...
...     def clean(self):
...         """重複したusernameがないことを確認するバリデータ"""
...         if any(self.errors):
...             # いずれかのフォームセットでバリデーションエラーがあった場合、フォームセットのバリデーションは行わない
...             return
...
...         names = []
...         for i in range(self.total_form_count()):
...             form = self.forms[i]
...             name = form.cleaned_data['username']
...             if name in names:
...                 raise forms.ValidationError(u'ユーザ名が重複しています')
...             names.append(name)
 
>>> FormSet = formset_factory(RegistrationForm, formset=BaseRegistrationFormSet)
>>> qd = QueryDict('form-TOTAL_FORMS=3&form-INITIAL_FORMS=1&form-MIN_NUM_FORMS=0&form-0-reason=test0&form-0-username=test0&form-0-email=test0@example.com&form-0-password=testtest&form-1-reason=test1&form-1-username=test1&form-1-email=test1@example.com&form-1-password=testtest&form-2-reason=test1&form-2-username=test2&form-2-email=test2@example.com&form-2-password=testtest', mutable=True)
>>> formset = FormSet(qd)
>>> formset.is_valid()
True
 
>>> # form-2のreasonパラメータ(省略不可)を消す
>>> qd2 = qd.copy()
>>> del qd2['form-2-reason']
>>> formset2 = FormSet(qd2)
>>> formset2.is_valid()
False
 
>>> # わざとユーザIDを重複させる
>>> qd3 = qd.copy()
>>> qd3['form-1-username'] = 'test0'
>>> formset3 = FormSet(qd3)
>>> formset3.is_valid()
False

応用してなにかやろうかと思ったけど、もう触りたくないのでこれで終わり。

1 2