フォームセット (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)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>
見てのとおり、空のフォームがひとつだけ表示されました。表示される空のフォームの数は extra
パラメータで制御できます。デフォルトでは、 formset_factory()
は空のフォームを1つ定義します。次の例では、フォームセットクラスを作成して空のフォームを2つ表示します:
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
フォームセットをイテレートすると、フォームが作成された順にレンダリングされます。この順序を変更するには、 __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)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Django is now open source" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2023-02-11" id="id_form-0-pub_date"></div>
<div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" id="id_form-1-title"></div>
<div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" id="id_form-1-pub_date"></div>
<div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
<div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>
上の例では、今度は 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)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>
もし、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 をご覧ください。
インスタンス化できるフォームの数を制限する¶
formset_factory()
の absolute_max
パラメータによって、 POST
データを与えたときにインスタンス化できるフォームの数を制限できます。これにより、偽造された POST
リクエストを使用したメモリ枯渇攻撃から守ることができます:
>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, absolute_max=1500)
>>> data = {
... "form-TOTAL_FORMS": "1501",
... "form-INITIAL_FORMS": "0",
... }
>>> formset = ArticleFormSet(data)
>>> len(formset.forms)
1500
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['Please submit at most 1000 forms.']
absolute_max
が None
の場合、デフォルトは max_num + 1000
になります (max_num
が None
の場合、デフォルトは 2000
になります)。
absolute_max
が max_num
よりも小さい場合、 ValueError
が発生します。
フォームセットのバリデーション¶
フォームセットでのバリデーションは通常の 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",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
True
フォームセットに何もデータを渡さなかったので、有効なフォームとみなされます。フォームセットは十分に賢いので、変更されていない余分なフォームは無視します。無効な記事を提供すると、:
>>> data = {
... "form-TOTAL_FORMS": "2",
... "form-INITIAL_FORMS": "0",
... "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-0-title": "",
... "form-0-pub_date": "",
... }
>>> formset = ArticleFormSet(data)
>>> formset.has_changed()
False
ManagementForm
を理解する¶
上のフォームセットのデータに追加のデータ (form-TOTAL_FORMS
, form-INITIAL_FORMS
) が必要なことに気づいたかもしれません。このデータは ManagementForm
に必要です。このフォームは、フォームセットに含まれるフォームのコレクションを管理するために使用されます。この管理データを渡さないと、フォームセットは無効になります:
>>> data = {
... "form-0-title": "Test",
... "form-0-pub_date": "",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
これは、表示されているフォームインスタンスの数を追跡するために使用されます。 JavaScriptを使用して新しいフォームを追加する場合は、フォームのカウントフィールドもインクリメントする必要があります。 一方、既存のオブジェクトの削除を許可するためにJavaScriptを使用している場合は、POST
データに form-#-DELETE
を含めることで、削除対象のマークが適切に削除されていることを確認する必要があります。 すべてのフォームがそれにかかわらず POST
データに存在することが期待されます。
ManagementFormは、フォームセット自体の属性として使用できます。 テンプレートでフォームセットをレンダリングするときは、{{ my_formset.management_form }}
(my_formsetは適切な名前に置き換えます)をレンダリングすることで、すべての管理データを含めることができます。
注釈
ここで例として示した form-TOTAL_FORMS
と form-INITIAL_FORMS
フィールドの他に、管理フォームには form-MIN_NUM_FORMS
と form-MAX_NUM_FORMS
フィールドがあります。これらは管理フォームの残りの部分と一緒に出力されますが、これはクライアント側のコードの利便性のためだけのものです。これらのフィールドは必須ではないので、例の POST
データには表示されていません。
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 で動的にフォームを操作することが容易となります。
error_messages
¶
引数 error_messages
で、フォームセットが表示するデフォルトのメッセージを上書きできます。上書きしたいエラーメッセージにマッチするキーを持つ辞書を渡します。エラーメッセージのキーには 'too_few_forms'
, 'too_many_forms'
, 'missing_management_form'
があります。 too_few_forms'
と too_many_forms'
のエラーメッセージには %(num)d
を含めることができ、それぞれ min_num
と max_num
に置き換えられます。
例えば、管理フォームがない場合のデフォルトのエラーメッセージは以下の通りです:
>>> formset = ArticleFormSet({})
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['ManagementForm data is missing or has been tampered with. Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS. You may need to file a bug report if the issue persists.']
そしてこれはカスタムエラーメッセージです:
>>> formset = ArticleFormSet(
... {}, error_messages={"missing_management_form": "Sorry, something went wrong."}
... )
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['Sorry, something went wrong.']
フォームセットのバリデーションをカスタマイズする¶
フォームセットには Form
クラスと同じような clean
メソッドがあります。ここで、フォームセットレベルで動作する独自のバリデーションを定義します:
>>> from django.core.exceptions import ValidationError
>>> 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 = set()
... for form in self.forms:
... if self.can_delete and self._should_delete_form(form):
... continue
... title = form.cleaned_data.get("title")
... if title in titles:
... raise ValidationError("Articles in a set must have distinct titles.")
... titles.add(title)
...
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> data = {
... "form-TOTAL_FORMS": "2",
... "form-INITIAL_FORMS": "0",
... "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()
メソッドを使います。
フォーム以外のエラーは、フォーム固有のエラーと区別するために nonform
という追加のクラスでレンダリングされます。たとえば、 {{ formset.non_form_errors }}
は次のようになります:
<ul class="errorlist nonform">
<li>Articles in a set must have distinct titles.</li>
</ul>
フォームセット内のフォームの数を検証する¶
送信されたフォームの最小および最大数を検証するために、Django にはいくつかの方法が用意されています。フォームの数のバリデーションをさらにカスタマイズする必要があるときは、カスタムのフォームセットバリデーションを使用する必要があります。
validate_max
¶
formset_factory()
に validate_max=True
が渡された場合、データセット内のフォーム数から削除マークが付けられたフォーム数を除いた数が 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-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 at most 1 form.']
validate_max=True
は max_num
に対して厳密にバリデーションを行います。たとえ max_num
を超過した原因が初期データの量が多すぎたことだったとしてもです。
エラーメッセージは error_messages 引数に 'too_many_forms'
メッセージを渡すことでカスタマイズできます。
注釈
validate_max
に関係なく、データセット内のフォームの数が absolute_max
を超えた場合、 validate_max
が指定されている場合と同様にフォームのバリデーションは失敗し、さらに最初の absolute_max
内のフォームのみがバリデートされます。残りは完全に切り捨てられます。これは POST リクエストを偽装してメモリを使い果たす攻撃から守るためです。詳細は インスタンス化できるフォームの数を制限する を参照してください。
validate_min
¶
formset_factory()
に validate_min=True
が渡された場合、バリデーションはデータセット内のフォームの数から削除マークが付けられたものを除いた数が 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-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 at least 3 forms.']
エラーメッセージは error_messages 引数に 'too_few_forms'
メッセージを渡すことでカスタマイズできます。
注釈
validate_min
に関係なく、フォームセットにデータが含まれていない場合、 extra + min_num
空のフォームが表示されます。
フォームのソートと削除に対応させる¶
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)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date"></div>
<div><label for="id_form-0-ORDER">Order:</label><input type="number" name="form-0-ORDER" value="1" id="id_form-0-ORDER"></div>
<div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title"></div>
<div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date"></div>
<div><label for="id_form-1-ORDER">Order:</label><input type="number" name="form-1-ORDER" value="2" id="id_form-1-ORDER"></div>
<div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
<div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>
<div><label for="id_form-2-ORDER">Order:</label><input type="number" name="form-2-ORDER" id="id_form-2-ORDER"></div>
これで、各フォームにフィールドが追加されます。この新しいフィールドは ORDER
という名前で、forms.IntegerField
です。初期データのフォームには自動的に数値が割り当てられます。ユーザーがこれらの値を変更すると何が起こるかを見てみましょう:
>>> data = {
... "form-TOTAL_FORMS": "3",
... "form-INITIAL_FORMS": "2",
... "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'}
BaseFormSet
には ordering_widget
属性と get_ordering_widget()
メソッドがあり、 can_order
で使用するウィジェットを制御します。
ordering_widget
¶
- BaseFormSet.ordering_widget¶
デフォルト値: NumberInput
ordering_widget
をセットすると、 can_order
で使用するウィジェットクラスを指定できます:
>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
... ordering_widget = HiddenInput
...
>>> ArticleFormSet = formset_factory(
... ArticleForm, formset=BaseArticleFormSet, can_order=True
... )
get_ordering_widget
¶
can_order
で使用するウィジェットのインスタンスを指定する必要がある場合は、 get_ordering_widget()
をオーバーライドします:
>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
... def get_ordering_widget(self):
... return HiddenInput(attrs={"class": "ordering"})
...
>>> ArticleFormSet = formset_factory(
... ArticleForm, formset=BaseArticleFormSet, can_order=True
... )
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)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date"></div>
<div><label for="id_form-0-DELETE">Delete:</label><input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE"></div>
<div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title"></div>
<div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date"></div>
<div><label for="id_form-1-DELETE">Delete:</label><input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE"></div>
<div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
<div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>
<div><label for="id_form-2-DELETE">Delete:</label><input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE"></div>
can_order
と同様に、各フォームに DELETE
という名前の新しいフィールドが追加されます。このフィールドは forms.BooleanField
です。削除フィールドのいずれかをマークしてデータが渡されると、それらに deleted_forms
でアクセスできます。
>>> data = {
... "form-TOTAL_FORMS": "3",
... "form-INITIAL_FORMS": "2",
... "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'}]
ModelFormSet
を使っている場合、 formset.save()
を呼び出すと、削除されたフォームのモデルインスタンスは削除されます。
formset.save(commit=False)
を呼び出すと、オブジェクトは自動的に削除されません。 実際に削除するには、 formset.deleted_objects
のそれぞれに対して delete()
を呼び出す必要があります:
>>> instances = formset.save(commit=False)
>>> for obj in formset.deleted_objects:
... obj.delete()
...
一方、普通の FormSet
を使用している場合は、フォームを削除するという一般的な概念がないため、あなた自身でフォームセットの save()
メソッドで formset.deleted_forms
を処理することになります。
BaseFormSet
には deletion_widget
属性と get_deletion_widget()
メソッドもあり、 can_delete
で使用するウィジェットを制御します。
deletion_widget
¶
- BaseFormSet.deletion_widget¶
デフォルト値: CheckboxInput
delete_widget
をセットすると、 can_delete
で使用するウィジェットクラスを指定できます:
>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
... deletion_widget = HiddenInput
...
>>> ArticleFormSet = formset_factory(
... ArticleForm, formset=BaseArticleFormSet, can_delete=True
... )
get_deletion_widget
¶
can_delete
で使用するウィジェットのインスタンスを指定する必要がある場合は、 get_deletion_widget()
をオーバーライドします:
>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
... def get_deletion_widget(self):
... return HiddenInput(attrs={"class": "deletion"})
...
>>> ArticleFormSet = formset_factory(
... ArticleForm, formset=BaseArticleFormSet, can_delete=True
... )
can_delete_extra
¶
- BaseFormSet.can_delete_extra¶
デフォルト値: True
can_delete=True
を設定しているときに can_delete_extra=False
を指定すると、余分なフォームを削除するオプションがなくなります。
フォームセットにフィールドを追加する¶
フォームセットに追加のフィールドを追加する必要がある場合、これは簡単に実現できます。フォームセットの基底クラスには add_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)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>
<div><label for="id_form-0-my_field">My field:</label><input type="text" name="form-0-my_field" id="id_form-0-my_field"></div>
フォームセットのフォームにカスタムパラメータを渡す¶
フォームクラスには、MyArticleForm
のようなカスタムパラメータを指定することがあります。フォームセットをインスタンス化するときに、このパラメータを渡すことができます:
>>> 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})
form_kwargs
はフォームのインスタンスによって異なります。フォームセットの基底クラスには get_form_kwargs
メソッドがあります。このメソッドは引数としてフォームセット内のフォームのインデックスを受け取ります。 empty_form の場合は None
です:
>>> 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
...
>>> ArticleFormSet = formset_factory(MyArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet()
フォームセットのプレフィックスをカスタマイズする¶
レンダリングされた HTML では、フォームセットは各フィールド名のプレフィックスを含みます。デフォルトではプレフィックスは 'form'
ですが、フォームセットの prefix
引数を使用してカスタマイズすることもできます。
たとえば、デフォルトの場合は次のように表示されます:
<label for="id_form-0-title">Title:</label>
<input type="text" name="form-0-title" id="id_form-0-title">
しかし、ArticleFormset(prefix='article')
を使うと、以下のようになります:
<label for="id_article-0-title">Title:</label>
<input type="text" name="article-0-title" id="id_article-0-title">
これは ビュー の中で複数のフォームセットを使いたいときに便利です。
ビューとテンプレートでフォームセットを使う¶
フォームセットには、レンダリングに関連する以下の属性とメソッドがあります:
- BaseFormSet.renderer¶
フォームセットに使用する レンダラー を指定します。デフォルトは
FORM_RENDERER
設定で指定されたレンダラーです。
- BaseFormSet.template_name[ソース]¶
print(formset)
やテンプレート内で{{ formset }}
を使ってフォームセットを文字列にキャストした場合にレンダリングされるテンプレートの名前です。デフォルトでは、レンダラーの
formset_template_name
の値を返すプロパティです。テンプレート名に文字列を指定することで、特定のフォームセットクラスのテンプレート名を上書きできます。このテンプレートは、フォームセットの管理フォームをレンダリングするために使用され、フォームセット内の各フォームは、フォームの
template_name
で定義されたテンプレートに従ってレンダリングされます。
- BaseFormSet.template_name_div¶
as_div()
を呼び出す際に使用するテンプレートの名前です。デフォルトでは"django/forms/formsets/div.html"
です。このテンプレートはフォームセットの管理フォームをレンダリングし、 フォームセット内の各フォームをフォームのas_div()
メソッドに従ってレンダリングします。
- BaseFormSet.template_name_p¶
as_p()
を呼び出す際に使用するテンプレートの名前です。デフォルトでは"django/forms/formsets/p.html"
です。このテンプレートはフォームセットの管理フォームをレンダリングし、 フォームセット内の各フォームをフォームのas_p()
メソッドに従ってレンダリングします。
- BaseFormSet.template_name_table¶
as_table()
を呼び出す際に使用するテンプレートの名前です。デフォルトでは"django/forms/formsets/table.html"
です。このテンプレートはフォームセットの管理フォームをレンダリングし、 フォームセット内の各フォームをフォームのas_table()
メソッドに従ってレンダリングします。
- BaseFormSet.template_name_ul¶
as_ul()
を呼び出す際に使用するテンプレートの名前です。デフォルトでは"django/forms/formsets/ul.html"
です。このテンプレートはフォームセットの管理フォームをレンダリングし、 フォームセット内の各フォームをフォームのas_ul()
メソッドに従ってレンダリングします。
- BaseFormSet.get_context()[ソース]¶
テンプレート内のフォームセットをレンダリングするためのコンテキストを返します。
利用可能なコンテキストは次のとおりです。
formset
: フォームセットのインスタンス。
- BaseFormSet.render(template_name=None, context=None, renderer=None)¶
render メソッドは
str__
メソッド、as_div()
メソッド、as_p()
メソッド、as_ul()
メソッド、as_table()
メソッドから呼び出されます。すべての引数は省略可能で、デフォルトは下記です:template_name
:template_name
context
:get_context()
が返す値renderer
:renderer
が返す値
- BaseFormSet.as_div()¶
フォームセットを
template_name_div
テンプレートでレンダリングします。
- BaseFormSet.as_p()¶
フォームセットを
template_name_p
テンプレートでレンダリングします。
- BaseFormSet.as_table()¶
フォームセットを
template_name_table
テンプレートでレンダリングします。
- BaseFormSet.as_ul()¶
フォームセットを
template_name_ul
テンプレートでレンダリングします。
ビュー内でフォームセットを使用するのは、通常の Form
クラスを使用するのとあまり変わりません。ただひとつだけ注意したいのが、テンプレート内で管理フォームを使用することです。ビューのサンプルを見てみましょう:
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})
manage_articles.html
テンプレートは次のようになります:
<form method="post">
{{ formset.management_form }}
<table>
{% for form in formset %}
{{ form }}
{% endfor %}
</table>
</form>
ただし、フォームセット自体が管理フォームを処理するためのちょっとしたショートカットもあります。
<form method="post">
<table>
{{ formset }}
</table>
</form>
上記は、フォームセットクラスの BaseFormSet.render()
メソッドを呼び出すことになります。これは template_name
属性で指定されたテンプレートを使用してフォームセットをレンダリングします。フォームと同様に、デフォルトではフォームセットは as_table
でレンダリングされ、その他のヘルパーメソッドとして as_p
と as_ul
が利用可能です。 デフォルトのテンプレートをオーバーライド することで、フォームセットのレンダリングをカスタマイズできます。
can_delete
や can_order
のときに手動でレンダリングする¶
テンプレート内で手動でフィールドをレンダリングする場合、 can_delete
パラメータを {{ 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>
同様に、フォームセットに並び替えの機能 (can_order=True
) がある場合, {{ form.ORDER }}
でレンダリングできます。
ビューで複数のフォームセットを使う¶
必要に応じて、1 つのビューで複数のフォームセットを使用できます。フォームセットはその動作の多くをフォームから借用しています。そのため、prefix
を使用して、フォームセットのフォームフィールド名の前に指定した値を付与することで、 名前の衝突を避けて複数のフォームセットをビューに送信できます。これをどのように実現するか見てみましょう:
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,
},
)
フォームセットは通常どおりレンダリングされます。ここで重要なのは、POST の場合とそうでない場合の両方で prefix
を渡す必要があるということです。
各フォームセットの prefix は、各フィールドの name
と id
の HTML 属性に追加されるデフォルトの form
プレフィックスを置き換えます。