フォームセット (Formset)¶
フォームセットとは、同じページで複数のフォームを扱うための抽象化レイヤで、いわばデータグリッドのようなものです。フォームセットを説明するために、まず以下のようなフォームを考えましょう。
>>> from django import forms
>>> class ArticleForm(forms.Form):
... title = forms.CharField()
... pub_date = forms.DateField()
このフォームを使って、ユーザが一度に複数の記事を作成できるようにしたいとします。ArticleForm
からフォームセットを生成するには、次のようにします。
>>> from django.forms 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>
出力を見て分かる通り、空のフォームが 1 つだけ表示されています。 今表示されている空っぽのフォームの合計数は、extra
パラメータによってコントロールされます。formset_factory()
のデフォルトの設定で、「追加のフォーム表示数 (extra)」 を 1 に設定しているからです。次の例は、2 つの空っぽのフォームを出します。
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
formset
をイテレートすると、フォームは作成された順番でレンダリングされます。この順番は、 __iter__()
メソッドで代わりの実装をすることで変更できます。
フォームセットでは、インデックスをつけて、一致するフォームを返すこともできます。__iter__
をオーバーライドした場合、動作を一貫させるため __getitem__
もオーバーライドする必要があります。
フォームセットで初期データを指定する¶
初期データは、フォームセットのユーザビリティに大きく影響します。上述したように、追加するフォーム数を指定できます。 これが意味するのは、初期データから生成するフォーム数に加えて、追加的なフォームをいくつ表示するかをフォームセットに指定している、ということです。以下の例を見てください:
>>> import datetime
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
>>> formset = ArticleFormSet(initial=[
... {'title': '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 つの追加フォームです。初期データとして、辞書のリストを渡していることにも注意してください。
フォームセットを描画するために initial
を使う場合、フォームセットの送信を処理するときに同じ initial
を渡して、どのフォームがユーザによって変更されたかをフォームセットが検出できるようにしてください。例えば、ArticleFormSet(request.POST, initial=[...])
のようになるでしょう。
フォームの最大表示数を制限する¶
formset_factory()
に max_num
パラメータを指定すると、フォームセット中に表示されるフォームの最大数を制限できます。
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> 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
を超えることはできません。例えば、extra=2
と max_num=2
、そしてフォームセットが 1 つの initial
項目で初期化される場合、この初期項目のフォームと 1 つの空のフォームが表示されます。
初期データ内の項目数が max_num
を超える場合、max_num
の値に関わらず全ての初期データのフォームが表示され、追加フォームは 1 つも表示されません。例えば、extra=3
と max_num=1
、そしてフォームセットが 2 つの初期項目で初期化される場合、2 つのフォームが初期データとともに表示されます。
max_num
の値が None
(デフォルト) だった場合、表示されるフォームの上限は大きな数になります (1000)。この数は、実際には制限がないと見なせるでしょう。
デフォルトでは、max_num
はいくつのフォームが表示されるかだけに影響し、バリデーションには影響しません。validate_max=True
が formset_factory()
に渡される場合は、max_num
はバリデーションに影響します。validate_max をご覧ください。
フォームセットのバリデーション¶
フォームセットのバリデーションは、普通の Form
とほぼ同じです。フォームセットにも is_valid
メソッドがあり、フォームセット内の全てのフォームを簡単に検証できます。
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm)
>>> data = {
... 'form-TOTAL_FORMS': '1',
... 'form-INITIAL_FORMS': '0',
... 'form-MAX_NUM_FORMS': '',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
True
この例では、フォームセットにデータを渡さなかったので、有効なフォームを返しています。フォームセットは賢くて、データの変更されなかったフォームを無視してくれます。不適切な記事を提供しようとすると、以下のようになります。
>>> data = {
... 'form-TOTAL_FORMS': '2',
... 'form-INITIAL_FORMS': '0',
... 'form-MAX_NUM_FORMS': '',
... 'form-0-title': 'Test',
... 'form-0-pub_date': '1904-06-16',
... 'form-1-title': 'Test',
... 'form-1-pub_date': '', # <-- this date is missing but required
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {'pub_date': ['This field is required.']}]
見て分かるように、 formset.errors
はリストで、 そのエントリーはフォームセット内のフォームと一致します。 バリデーションは、2 つのフォームそれぞれに働いて、2 つ目の項目にエラーメッセージが表示されています。
通常の Form
を使うときとまったく同じように、フォームセットのフォーム内のそれぞれのフィールドは、ブラウザのバリデーションのための maxlength
のような HTML 属性を含むことができます。ただし、フォームセットのフォームフィールドは、required
属性を含みません。これは、フォームを追加したり削除するときにバリデーションが正しく働かない可能性があるためです。
フォームセット内にいくつのエラーがあるかを確かめるためには、total_error_count
メソッドが使えます。
>>> # Using the previous example
>>> formset.errors
[{}, {'pub_date': ['This field is required.']}]
>>> len(formset.errors)
2
>>> formset.total_error_count()
1
また、フォームに入力されたデータと初期データが異なっているかどうかもチェックできます (たとえば、フォームがデータなしで送信された場合など)。
>>> data = {
... 'form-TOTAL_FORMS': '1',
... 'form-INITIAL_FORMS': '0',
... 'form-MAX_NUM_FORMS': '',
... 'form-0-title': '',
... 'form-0-pub_date': '',
... }
>>> formset = ArticleFormSet(data)
>>> formset.has_changed()
False
ManagementForm
を理解する¶
上記のフォームセットのデータで必要とされた追加のデータ (form-TOTAL_FORMS
, form-INITIAL_FORMS
そして form-MAX_NUM_FORMS
) に気付いているかもしれません。 このデータは ManagementForm
に必要です。 このフォームは、formsetに含まれているフォームのコレクションを管理するためにformsetによって使用されます。 この管理データを提供しない場合、例外が発生します。
>>> data = {
... 'form-0-title': 'Test',
... 'form-0-pub_date': '',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
Traceback (most recent call last):
...
django.forms.utils.ValidationError: ['ManagementForm data is missing or has been tampered with']
これは、表示されているフォームインスタンスの数を追跡するために使用されます。 JavaScriptを使用して新しいフォームを追加する場合は、フォームのカウントフィールドもインクリメントする必要があります。 一方、既存のオブジェクトの削除を許可するためにJavaScriptを使用している場合は、POST
データに form-#-DELETE
を含めることで、削除対象のマークが適切に削除されていることを確認する必要があります。 すべてのフォームがそれにかかわらず POST
データに存在することが期待されます。
ManagementFormは、フォームセット自体の属性として使用できます。 テンプレートでフォームセットをレンダリングするときは、{{ my_formset.management_form }}
(my_formsetは適切な名前に置き換えます)をレンダリングすることで、すべての管理データを含めることができます。
total_form_count
と initial_form_count
¶
BaseFormSet
には、ManagementForm
、total_form_count
、initial_form_count
と密接に関わる 2 つのメソッドがあります。
total_form_count
は、対象のフィールドセット内のフォームの合計数を返します。initial_form_count
は、記入前のフォームセット内のフォームの数を返し、またいくつのフォームが必須なのかを決めるためにも使われます。通常、これらのメソッドをオーバーライドする必要はありませんが、もし必要な場合はメソッドの動作を理解してからオーバーライドしてください。
empty_form
¶
BaseFormSet
には追加の属性 empty_form
があり、__prefix__
というプレフィックスとともにフォームのインスタンスを返します。これにより、JavaScript で動的にフォームを操作することが容易となります。
カスタムフォームセットのバリデーション¶
フォームセットには、Form
クラスと同じような clean
メソッドがあります。フォームセットのレベルで検証するためのバリデーションは、ここに記述します:
>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> 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 form in self.forms:
... title = form.cleaned_data['title']
... if title in titles:
... raise forms.ValidationError("Articles in a set must have distinct titles.")
... titles.append(title)
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> data = {
... 'form-TOTAL_FORMS': '2',
... 'form-INITIAL_FORMS': '0',
... 'form-MAX_NUM_FORMS': '',
... 'form-0-title': 'Test',
... 'form-0-pub_date': '1904-06-16',
... 'form-1-title': 'Test',
... 'form-1-pub_date': '1912-06-23',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Articles in a set must have distinct titles.']
フォームセットの clean
メソッドは、Form.clean
メソッドが呼ばれた後に呼び出されます。エラーを取得するには、フォームセットの non_form_errors()
メソッドを使います。
フォームセット内のフォームの数を検証する¶
送信されたフォームの最小および最大数を検証するために、Django にはいくつかの方法が用意されています。フォームの数のバリデーションをさらにカスタマイズする必要があるときは、カスタムのフォームセットバリデーションを使用する必要があります。
validate_max
¶
If validate_max=True
is passed to
formset_factory()
, validation will also check
that the number of forms in the data set, minus those marked for
deletion, is less than or equal to max_num
.
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, max_num=1, validate_max=True)
>>> data = {
... 'form-TOTAL_FORMS': '2',
... 'form-INITIAL_FORMS': '0',
... 'form-MIN_NUM_FORMS': '',
... 'form-MAX_NUM_FORMS': '',
... 'form-0-title': 'Test',
... 'form-0-pub_date': '1904-06-16',
... 'form-1-title': 'Test 2',
... 'form-1-pub_date': '1912-06-23',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Please submit 1 or fewer forms.']
validate_max=True
validates against max_num
strictly even if
max_num
was exceeded because the amount of initial data supplied was
excessive.
注釈
Regardless of validate_max
, if the number of forms in a data set
exceeds max_num
by more than 1000, then the form will fail to validate
as if validate_max
were set, and additionally only the first 1000
forms above max_num
will be validated. The remainder will be
truncated entirely. This is to protect against memory exhaustion attacks
using forged POST requests.
validate_min
¶
If validate_min=True
is passed to
formset_factory()
, validation will also check
that the number of forms in the data set, minus those marked for
deletion, is greater than or equal to min_num
.
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, min_num=3, validate_min=True)
>>> data = {
... 'form-TOTAL_FORMS': '2',
... 'form-INITIAL_FORMS': '0',
... 'form-MIN_NUM_FORMS': '',
... 'form-MAX_NUM_FORMS': '',
... 'form-0-title': 'Test',
... 'form-0-pub_date': '1904-06-16',
... 'form-1-title': 'Test 2',
... 'form-1-pub_date': '1912-06-23',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Please submit 3 or more forms.']
Dealing with ordering and deletion of forms¶
func:~django.forms.formsets.formset_factory には、フォームセット内のフォームの順序およびフォームセットからのフォームの削除に役立つ 2 つのオプション引数があります。
can_order
¶
-
BaseFormSet.
can_order
¶
デフォルト値: False
並び替えができるフォームセットを作成できるようにします:
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, can_order=True)
>>> formset = ArticleFormSet(initial=[
... {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': '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="number" 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="number" 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="number" name="form-2-ORDER" id="id_form-2-ORDER"></td></tr>
各フィールドに追加フィールドが与えられます。この新しいフィールドは ORDER
という名前の forms.IntegerField
です。初期データから来たフォームに対しては、自動的に数値が与えられます。ユーザーがこれらの値を変更した際に何が起きるかを見てみましょう:
>>> data = {
... 'form-TOTAL_FORMS': '3',
... 'form-INITIAL_FORMS': '2',
... 'form-MAX_NUM_FORMS': '',
... 'form-0-title': 'Article #1',
... 'form-0-pub_date': '2008-05-10',
... 'form-0-ORDER': '2',
... 'form-1-title': 'Article #2',
... 'form-1-pub_date': '2008-05-11',
... 'form-1-ORDER': '1',
... 'form-2-title': 'Article #3',
... 'form-2-pub_date': '2008-05-01',
... 'form-2-ORDER': '0',
... }
>>> formset = ArticleFormSet(data, initial=[
... {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': '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': 'Article #3'}
{'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': 'Article #2'}
{'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': 'Article #1'}
can_delete
¶
-
BaseFormSet.
can_delete
¶
デフォルト値: False
削除対象のフォームを選択できるフォームセットを作成できるようにします:
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True)
>>> formset = ArticleFormSet(initial=[
... {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': '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-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>
Similar to can_order
this adds a new field to each form named DELETE
and is a forms.BooleanField
. When data comes through marking any of the
delete fields you can access them with deleted_forms
:
>>> data = {
... 'form-TOTAL_FORMS': '3',
... 'form-INITIAL_FORMS': '2',
... 'form-MAX_NUM_FORMS': '',
... 'form-0-title': 'Article #1',
... 'form-0-pub_date': '2008-05-10',
... 'form-0-DELETE': 'on',
... 'form-1-title': 'Article #2',
... 'form-1-pub_date': '2008-05-11',
... 'form-1-DELETE': '',
... 'form-2-title': '',
... 'form-2-pub_date': '',
... 'form-2-DELETE': '',
... }
>>> formset = ArticleFormSet(data, initial=[
... {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': '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': 'Article #1'}]
If you are using a ModelFormSet
,
model instances for deleted forms will be deleted when you call
formset.save()
.
If you call formset.save(commit=False)
, objects will not be deleted
automatically. You'll need to call delete()
on each of the
formset.deleted_objects
to actually delete
them:
>>> instances = formset.save(commit=False)
>>> for obj in formset.deleted_objects:
... obj.delete()
On the other hand, if you are using a plain FormSet
, it's up to you to
handle formset.deleted_forms
, perhaps in your formset's save()
method,
as there's no general notion of what it means to delete a form.
Adding additional fields to a formset¶
If you need to add additional fields to the formset this can be easily
accomplished. The formset base class provides an add_fields
method. You
can simply override this method to add your own fields or even redefine the
default fields/attributes of the order and deletion fields:
>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
... def add_fields(self, form, index):
... super().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>
Passing custom parameters to formset forms¶
Sometimes your form class takes custom parameters, like MyArticleForm
.
You can pass this parameter when instantiating the formset:
>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> class MyArticleForm(ArticleForm):
... def __init__(self, *args, user, **kwargs):
... self.user = user
... super().__init__(*args, **kwargs)
>>> ArticleFormSet = formset_factory(MyArticleForm)
>>> formset = ArticleFormSet(form_kwargs={'user': request.user})
The form_kwargs
may also depend on the specific form instance. The formset
base class provides a get_form_kwargs
method. The method takes a single
argument - the index of the form in the formset. The index is None
for the
empty_form:
>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> class BaseArticleFormSet(BaseFormSet):
... def get_form_kwargs(self, index):
... kwargs = super().get_form_kwargs(index)
... kwargs['custom_kwarg'] = index
... return kwargs
Customizing a formset's prefix¶
In the rendered HTML, formsets include a prefix on each field's name. By
default, the prefix is 'form'
, but it can be customized using the formset's
prefix
argument.
For example, in the default case, you might see:
<label for="id_form-0-title">Title:</label>
<input type="text" name="form-0-title" id="id_form-0-title">
But with ArticleFormset(prefix='article')
that becomes:
<label for="id_article-0-title">Title:</label>
<input type="text" name="article-0-title" id="id_article-0-title">
This is useful if you want to use more than one formset in a view.
Using a formset in views and templates¶
Using a formset inside a view is as easy as using a regular Form
class.
The only thing you will want to be aware of is making sure to use the
management form inside the template. Let's look at a sample view:
from django.forms import formset_factory
from django.shortcuts import render
from myapp.forms import ArticleForm
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
pass
else:
formset = ArticleFormSet()
return render(request, 'manage_articles.html', {'formset': formset})
The manage_articles.html
template might look like this:
<form method="post">
{{ formset.management_form }}
<table>
{% for form in formset %}
{{ form }}
{% endfor %}
</table>
</form>
However there's a slight shortcut for the above by letting the formset itself deal with the management form:
<form method="post">
<table>
{{ formset }}
</table>
</form>
The above ends up calling the as_table
method on the formset class.
Manually rendered can_delete
and can_order
¶
If you manually render fields in the template, you can render
can_delete
parameter with {{ form.DELETE }}
:
<form method="post">
{{ formset.management_form }}
{% for form in formset %}
<ul>
<li>{{ form.title }}</li>
<li>{{ form.pub_date }}</li>
{% if formset.can_delete %}
<li>{{ form.DELETE }}</li>
{% endif %}
</ul>
{% endfor %}
</form>
Similarly, if the formset has the ability to order (can_order=True
), it is
possible to render it with {{ form.ORDER }}
.
Using more than one formset in a view¶
You are able to use more than one formset in a view if you like. Formsets
borrow much of its behavior from forms. With that said you are able to use
prefix
to prefix formset form field names with a given value to allow
more than one formset to be sent to a view without name clashing. Lets take
a look at how this might be accomplished:
from django.forms import formset_factory
from django.shortcuts import render
from myapp.forms import ArticleForm, BookForm
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.
pass
else:
article_formset = ArticleFormSet(prefix='articles')
book_formset = BookFormSet(prefix='books')
return render(request, 'manage_articles.html', {
'article_formset': article_formset,
'book_formset': book_formset,
})
You would then render the formsets as normal. It is important to point out
that you need to pass prefix
on both the POST and non-POST cases so that
it is rendered and processed correctly.
Each formset's prefix replaces the default form
prefix that's added to each field's name
and id
HTML attributes.