フォームセット (Formset)

class BaseFormSet

フォームセットは、同じページで複数のフォームを扱うための抽象レイヤーです。データグリッドに例えるとわかりやすいでしょう。次のようなフォームがあるとしましょう:

>>> 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=2max_num=2、そしてフォームセットが 1 つの initial 項目で初期化される場合、この初期項目のフォームと 1 つの空のフォームが表示されます。

初期データ内の項目数が max_num を超える場合、max_num の値に関わらず全ての初期データのフォームが表示され、追加フォームは 1 つも表示されません。例えば、extra=3max_num=1、そしてフォームセットが 2 つの初期項目で初期化される場合、2 つのフォームが初期データとともに表示されます。

max_num の値が None (デフォルト) だった場合、表示されるフォームの上限は大きな数になります (1000)。この数は、実際には制限がないと見なせるでしょう。

デフォルトでは、max_num はいくつのフォームが表示されるかだけに影響し、バリデーションには影響しません。validate_max=Trueformset_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_maxNone の場合、デフォルトは max_num + 1000 になります (max_numNone の場合、デフォルトは 2000 になります)。

absolute_maxmax_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 属性を含みません。これは、フォームを追加したり削除するときにバリデーションが正しく働かない可能性があるためです。

BaseFormSet.total_error_count()

フォームセット内のエラーの数を確認するには、 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_FORMSform-INITIAL_FORMS フィールドの他に、管理フォームには form-MIN_NUM_FORMSform-MAX_NUM_FORMS フィールドがあります。これらは管理フォームの残りの部分と一緒に出力されますが、これはクライアント側のコードの利便性のためだけのものです。これらのフィールドは必須ではないので、例の POST データには表示されていません。

total_form_countinitial_form_count

BaseFormSet には、ManagementFormtotal_form_countinitial_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_nummax_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=Truemax_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

BaseFormSet.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

BaseFormSet.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() メソッドから呼び出されます。すべての引数は省略可能で、デフォルトは下記です:

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_pas_ul が利用可能です。 デフォルトのテンプレートをオーバーライド することで、フォームセットのレンダリングをカスタマイズできます。

can_deletecan_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 は、各フィールドの nameid の HTML 属性に追加されるデフォルトの form プレフィックスを置き換えます。

Back to Top