revision-up-to: | 17812 (1.4) |
---|
フォームセットとは、同じページで複数のフォームを扱うための抽象化レイヤで、 いわばデータグリッドのようなものです。フォームセットを説明するために、まず 以下のようなフォームを考えましょう:
>>> from django import forms
>>> class ArticleForm(forms.Form):
... title = forms.CharField()
... pub_date = forms.DateField()
このフォームを使って、ユーザが一度に複数の記事を作成できるようにしたい場合
があったとします。そのために、 ArticleForm
からフォームセットを生成しま
す:
>>> from django.forms.formsets import formset_factory
>>> ArticleFormSet = formset_factory(ArticleForm)
ArticleFormSet
という名前のフォームセットクラスができました。このフォー
ムセットには、フォームセットに入っているフォームを一つ一つ取り出して、それ
ぞれを普通のフォームとして表示する機能があります:
>>> formset = ArticleFormSet()
>>> for form in formset:
... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
出力を見て分かる通り、空のフォームが一つだけ表示されています。
今表示されている空っぽのフォームの合計は、 extra
パラメータ
によってコントロールされます。
formset_factory
のデフォルトの設定で、「追加のフォーム表示数 (extra)」
を 1 に設定しているからです。二つの空っぽのフォームを出す例は:
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
リリースノートを参照してください
Django 1.3 で重要視されるのは、 フォームセットのインスタンスがイテレート
(iterate)できないということです。フォームセットの表示をするために、
forms
アトリビュート(attribute)をイテレートします。:
>>> formset = ArticleFormSet()
>>> for form in formset.forms:
... print form.as_table()
formset.forms
をイテレートすることは、フォームが作成された順番で
フォームを並べるのと同じことです。通常、フォームセットのイテレータは
この順番でフォームを表示します、しかしこの順番を __iter__()
メソッドを使って別の順番へと変更することができます。
フォームセットは、インデックスを入れこむこともできます。フォームセットは
組み込まれたフォームを返します。もし、 __iter__
を上書きしたならば、
__getitem__
も、フォームとのマッチングのために上書きする必要があります。
初期データは、フォームセットのユーザビリティに影響する大きな要素です。上に
示したように、 formset_factory
には追加のフォーム表示数を指定できます。
この「追加」とは、初期データを渡したときに表示されるフォームの中で、追加で
表示されている空のフォーム数という意味です。以下の例をよく見てください:
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
>>> formset = ArticleFormSet(initial=[
... {'title': u'Django is now open source',
... 'pub_date': datetime.date.today(),}
... ])
>>> for form in formset:
... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Django is now open source" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-12" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" id="id_form-1-title" /></td></tr>
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" id="id_form-1-pub_date" /></td></tr>
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
上の例では、今度は 3 つのフォームが表示されました。初期データとして渡した 1 つと、 2 つの追加フォームです。初期データとして、辞書のリストを渡しているこ とにも注意してください。
formset_factory
に max_num
パラメタを指定すると、フォームセット中に
表示されるフォームの最大数を制御できます:
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1)
>>> formset = ArticleFormset()
>>> for form in formset:
... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
リリースノートを参照してください
もし、 max_num
の値が存在するオブジェクトの合計より大きい場合、
extra
次第で、空のフォームがフォームセットに加えられます。
フォームの合計の長さは max_num
を超えることはできません。
max_num
の値が None
(通常です)であった場合、表示されるフォームの数には
制限がありません。 max_num
の値が 0
から None
になったことを
覚えておいてください。 version 1.2 から 0
は有効な値となりました。
フォームセットのバリデーションは、普通の Form
とほぼ同じです。フォーム
セットにも is_valid
メソッドがあり、フォームセット中の全てのフォームを
簡単に検証できます:
>>> ArticleFormSet = formset_factory(ArticleForm)
>>> data = {
... 'form-TOTAL_FORMS': u'1',
... 'form-INITIAL_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
True
この例では、フォームセットにデータを渡さなかったので、有効なフォームを返し ています。フォームセットは賢くて、データの変更されなかったフォームを無視し てくれます。あるフォーム上の記事を変更しようとして、失敗した場合の挙動を以 下に示します:
>>> data = {
... 'form-TOTAL_FORMS': u'2',
... 'form-INITIAL_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Test',
... 'form-0-pub_date': u'1904-06-16',
... 'form-1-title': u'Test',
... 'form-1-pub_date': u'', # <-- this date is missing but required
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {'pub_date': [u'This field is required.']}]
確認できるように、 formset.errors
はリストで、
そのエントリーは、フォームセットの中のフォームのものです。
バリデーションは、二つのフォームそれぞれに働いて、
リストの二つ目のアイテムにエラーメッセージが表示されています。
また、初めのデータと入力されたデータが異なっているかどうかも チェックできます。(すなわち、フォームは何のデータもなしに送信されない) ということです。
>>> data = {
... 'form-TOTAL_FORMS': u'1',
... 'form-INITIAL_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'',
... 'form-0-pub_date': u'',
... }
>>> formset = ArticleFormSet(data)
>>> formset.has_changed()
False
ManagementForm
を理解する¶上の例でフォームセットに与えた初期値には、追加のデータ( form-TOTAL_FORMS
, form-INITIAL_FORMS
, form-MAX_NUM_FORMS
)が入っていたことに気
付いたでしょうか。これはフォームセットで内部的に処理されているフォーム、
ManagementForm
で扱うためのデータです。追加のデータ抜きでフォームセット
を使おうとすると、例外が送出されます:
>>> data = {
... 'form-0-title': u'Test',
... 'form-0-pub_date': u'',
... }
>>> formset = ArticleFormSet(data)
Traceback (most recent call last):
...
django.forms.util.ValidationError: [u'ManagementForm data is missing or has been tampered with']
ManagementForm
のデータは、表示するフォームインスタンスの数を追跡するた
めに使われます。 JavaScript でフォームを動的に追加する場合、
ManagementForm
データのカウントも増やさねばなりません。
マネジメントフォームはフォームセット自身のアトリビュートとして使用可能です。
テンプレートにフォームセットをレンダリングする際、
{{ my_formset.management_form }}
をレンダリングすることでマネジメント
データを含むことができます。(フォームセットのところは適切な名前に変えて
使ってください)
total_form_count
と initial_form_count
¶BaseFormSet
は二つの ManagementForm
に類似したメソッドを持っています、
total_form_count
と initial_form_count
です。
total_form_count
は、フォームセットの中のフォームの合計の数を返します。
initial_form_count
はフォームセットの中のフォームのうち、データが
入力されているものだけを返します、また幾つのフォームが必要かを判断するのに
使えます。おそらくこれらのメソッドを上書きする必要に迫られることはないでしょ
う。まずは、実行する前に何を実行するのかを理解しておいたほうがよいでしょう。
リリースノートを参照してください
empty_form
¶BaseFormSet
は empty_form
という追加のアトリビュートを提供します。
これは、 JavaScript を使った動的なフォーム生成を簡単にするために
__prefix__
のプレフィクスをフォームインスタンスとともに返します。
Form
クラスと同様、フォームセットには clean
メソッドがあります。こ
のメソッドには、フォームセットレベルで扱う独自のバリデーションを定義します:
>>> from django.forms.formsets import BaseFormSet
>>> class BaseArticleFormSet(BaseFormSet):
... def clean(self):
... """Checks that no two articles have the same title."""
... if any(self.errors):
... # Don't bother validating the formset unless each form is valid on its own
... return
... titles = []
... for i in range(0, self.total_form_count()):
... form = self.forms[i]
... title = form.cleaned_data['title']
... if title in titles:
... raise forms.ValidationError("Articles in a set musthave distinct titles.")
... titles.append(title)
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> data = {
... 'form-TOTAL_FORMS': u'2',
... 'form-INITIAL_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Test',
... 'form-0-pub_date': u'1904-06-16',
... 'form-1-title': u'Test',
... 'form-1-pub_date': u'1912-06-23',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
[u'Articles in a set must have distinct titles.']
フォームセットの clean
メソッドは、全てのフォームに対して
Form.clean
メソッドが呼出された後に実行されます。フォームセットに関する
エラーは、フォームセットの non_form_errors()
メソッドで取り出せます。
フォームセットを扱う上でよくあるユースケースは、フォームインスタンスの並び
順や削除の処理です。フォームセットはこれらの処理を実行してくれます。
formset_factory
には can_order
および can_delete
という二つの
パラメタがあり、指定するとフォームにフィールドを追加して、並び順や削除フラ
グを操作する簡単な手段を提供します。
can_order
¶デフォルト値: False
並べ替え機能とともに、フォームセットを作ってみましょう:
>>> ArticleFormSet = formset_factory(ArticleForm, can_order=True)
>>> formset = ArticleFormSet(initial=[
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> for form in formset:
... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-0-ORDER">Order:</label></th><td><input type="text" name="form-0-ORDER" value="1" id="id_form-0-ORDER" /></td></tr>
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title" /></td></tr>
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date" /></td></tr>
<tr><th><label for="id_form-1-ORDER">Order:</label></th><td><input type="text" name="form-1-ORDER" value="2" id="id_form-1-ORDER" /></td></tr>
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
<tr><th><label for="id_form-2-ORDER">Order:</label></th><td><input type="text" name="form-2-ORDER" id="id_form-2-ORDER" /></td></tr>
このオプションを指定すると、各フォームにフィールドが追加されます。フィール
ドの名前は ORDER
で、型は forms.IntegerField
です。初期データを使っ
てフォームを生成した場合、 ORDER
フィールドには自動的に数値が割り当てら
れます。ユーザがこの値を変更するとどうなるか見てみましょう:
>>> data = {
... 'form-TOTAL_FORMS': u'3',
... 'form-INITIAL_FORMS': u'2',
... 'form-0-title': u'Article #1',
... 'form-0-pub_date': u'2008-05-10',
... 'form-0-ORDER': u'2',
... 'form-1-title': u'Article #2',
... 'form-1-pub_date': u'2008-05-11',
... 'form-1-ORDER': u'1',
... 'form-2-title': u'Article #3',
... 'form-2-pub_date': u'2008-05-01',
... 'form-2-ORDER': u'0',
... }
>>> formset = ArticleFormSet(data, initial=[
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> formset.is_valid()
True
>>> for form in formset.ordered_forms:
... print form.cleaned_data
{'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0, 'title': u'Article #3'}
{'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': u'Article #2'}
{'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': u'Article #1'}
can_delete
¶デフォルト値: False
削除機能とともに、フォームセットを作ってみましょう:
>>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True)
>>> formset = ArticleFormSet(initial=[
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> for form in formset:
.... print form.as_table()
<input type="hidden" name="form-TOTAL_FORMS" value="3" id="id_form-TOTAL_FORMS" />
<input type="hidden" name="form-INITIAL_FORMS" value="2" id="id_form-INITIAL_FORMS" />
<input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS" />
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-0-DELETE">Delete:</label></th><td><input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /></td></tr>
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title" /></td></tr>
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date" /></td></tr>
<tr><th><label for="id_form-1-DELETE">Delete:</label></th><td><input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE" /></td></tr>
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
<tr><th><label for="id_form-2-DELETE">Delete:</label></th><td><input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE" /></td></tr>
can_order
と同様、 DELETE
という名前のついたフィールドが追加されま
す。このフィールドは forms.BooleanField
です。削除フラグフィールドをマー
クしたデータを投入すると、削除フラグの立っているフォームに
deleted_forms
でアクセスできます:
>>> data = {
... 'form-TOTAL_FORMS': u'3',
... 'form-INITIAL_FORMS': u'2',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Article #1',
... 'form-0-pub_date': u'2008-05-10',
... 'form-0-DELETE': u'on',
... 'form-1-title': u'Article #2',
... 'form-1-pub_date': u'2008-05-11',
... 'form-1-DELETE': u'',
... 'form-2-title': u'',
... 'form-2-pub_date': u'',
... 'form-2-DELETE': u'',
... }
>>> formset = ArticleFormSet(data, initial=[
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> [form.cleaned_data for form in formset.deleted_forms]
[{'DELETE': True, 'pub_date': datetime.date(2008, 5, 10), 'title': u'Article #1'}]
フォームセットには、追加のフィールドを簡単に追加できます。フォームセットの
ベースクラスは add_fields
メソッドを提供しています。このメソッドをオー
バライドして、独自にフィールドを追加したり、デフォルトのフィールドや属性を
再定義したり、フィールドを削除したりできます:
>>> class BaseArticleFormSet(BaseFormSet):
... def add_fields(self, form, index):
... super(BaseArticleFormSet, self).add_fields(form, index)
... form.fields["my_field"] = forms.CharField()
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet()
>>> for form in formset:
... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-0-my_field">My field:</label></th><td><input type="text" name="form-0-my_field" id="id_form-0-my_field" /></td></tr>
ビュー内でのフォームセットの扱いは簡単で、普通の Form
クラスと同じよう
に使うだけです。気をつけておかねばならないのは、テンプレート内で
management_form
を使うという点です。ビューの例を以下に示します:
def manage_articles(request):
ArticleFormSet = formset_factory(ArticleForm)
if request.method == 'POST':
formset = ArticleFormSet(request.POST, request.FILES)
if formset.is_valid():
# do something with the formset.cleaned_data
else:
formset = ArticleFormSet()
return render_to_response('manage_articles.html', {'formset': formset})
manage_articles.html
テンプレートは以下のようになります:
<form method="post" action="">
{{ formset.management_form }}
<table>
{% for form in formset %}
{{ form }}
{% endfor %}
</table>
</form>
ただし、下記のようなショートカットを使えば、フォームセット自体に管理フォー ムを扱わせられます:
<form method="post" action="">
<table>
{{ formset }}
</table>
</form>
このショートカットは、フォームセットの as_table
を呼び出した時と同じ内
容を出力します。
can_delete
と can_order
を手動で出力する。¶もし、テンプレートの中で手動でフィールドを出力するなら、
can_delete
パラメータは {{ form.DELETE }}
で出力できます:
<form method="post" action="">
{{ formset.management_form }}
{% for form in formset %}
{{ form.id }}
<ul>
<li>{{ form.title }}</li>
{% if formset.can_delete %}
<li>{{ form.DELETE }}</li>
{% endif %}
</ul>
{% endfor %}
</form>
同じように、もしフォームセットが並べ替え機能を所持するなら( can_order=True
)、これもまた {{ form.ORDER }}
で呼び出すことができます。
お望みなら、複数のフォームセットを一つのビューで扱えます。フォームセットは
フォームとほとんど同じく動作します。つまり、 prefix
を使ってフォームセッ
トのフォームフィールド名にプレフィクスをつければ、複数のフォームセットを
一つのビューに入れても問題なく動作するのです。複数のフォームセットがどのよ
うに動作するか、以下の例で示しましょう:
def manage_articles(request):
ArticleFormSet = formset_factory(ArticleForm)
BookFormSet = formset_factory(BookForm)
if request.method == 'POST':
article_formset = ArticleFormSet(request.POST, request.FILES, prefix='articles')
book_formset = BookFormSet(request.POST, request.FILES, prefix='books')
if article_formset.is_valid() and book_formset.is_valid():
# do something with the cleaned_data on the formsets.
else:
article_formset = ArticleFormSet(prefix='articles')
book_formset = BookFormSet(prefix='books')
return render_to_response('manage_articles.html', {
'article_formset': article_formset,
'book_formset': book_formset,
})
これで、フォームセットは通常通りレンダできます。重要なのは、 prefix
を
POST リクエストの場合とそうでない場合の両方に指定して、正しくレンダさせるこ
とです。
Oct 26, 2017