フォームセット—Djangoドキュメント

提供:Dev Guides
< DjangoDjango/docs/3.2.x/topics/forms/formsets
移動先:案内検索

フォームセット

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.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()は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.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=[...])のようなものがあります。

フォームの最大数を制限する

max_numパラメーターを formset_factory()に設定すると、フォームセットに表示されるフォームの数を制限できます。

>>> 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=2max_num=2で、フォームセットが1つのinitialアイテムで初期化されている場合、最初のアイテムのフォームと1つの空白のフォームが表示されます。

初期データの項目数がmax_numを超えると、max_numの値に関係なく、すべての初期データフォームが表示され、追加のフォームは表示されません。 たとえば、extra=3max_num=1で、フォームセットが2つの初期アイテムで初期化されている場合、初期データを含む2つのフォームが表示されます。

max_numの値None(デフォルト)では、表示されるフォームの数に上限があります(1000)。 実際には、これは制限なしと同等です。

デフォルトでは、max_numは表示されるフォームの数にのみ影響し、検証には影響しません。 validate_max=Trueformset_factory()に渡されると、max_numが検証に影響します。 validate_max を参照してください。


インスタンス化されるフォームの最大数を制限する

バージョン3.2の新機能。


absolute_maxパラメーターを formset_factory()に設定すると、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_FORMSform-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データには、関係なくすべてのフォームが存在することが予想されます。

管理フォームは、フォームセット自体の属性として使用できます。 テンプレートでフォームセットをレンダリングする場合、テンプレート:My formset.management formをレンダリングすることですべての管理データを含めることができます(フォームセットの名前を適宜置き換えます)。

ノート

この例に示されているform-TOTAL_FORMSおよびform-INITIAL_FORMSフィールドに加えて、管理フォームにはform-MIN_NUM_FORMSおよびform-MAX_NUM_FORMSフィールドも含まれています。 これらは残りの管理フォームとともに出力されますが、これはクライアント側のコードの便宜のためだけです。 これらのフィールドは必須ではないため、POSTデータの例には示されていません。


バージョン3.2での変更: formset.is_valid()は、管理フォームが見つからないか改ざんされた場合に例外を発生させるのではなく、Falseを返すようになりました。


total_form_countおよびinitial_form_count

BaseFormSetには、ManagementFormtotal_form_count、およびinitial_form_countに密接に関連するいくつかのメソッドがあります。

total_form_countは、このフォームセット内のフォームの総数を返します。 initial_form_countは、事前に入力されたフォームセット内のフォームの数を返します。また、必要なフォームの数を決定するためにも使用されます。 これらのメソッドのいずれかをオーバーライドする必要はおそらくないので、オーバーライドする前に、それらが何をするのかを必ず理解してください。


empty_form

BaseFormSetは、JavaScriptを使用した動的フォームで簡単に使用できるように、プレフィックスが__prefix__のフォームインスタンスを返す追加の属性empty_formを提供します。


error_messages

バージョン3.2の新機能。


error_messages引数を使用すると、フォームセットが生成するデフォルトのメッセージを上書きできます。 オーバーライドするエラーメッセージに一致するキーを使用して辞書を渡します。 たとえば、管理フォームがない場合のデフォルトのエラーメッセージは次のとおりです。

>>> 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 = []
...         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.append(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()メソッドを使用して検出されます。


フォームセット内のフォームの数を検証する

Djangoは、送信されたフォームの最小数または最大数を検証するためのいくつかの方法を提供します。 フォーム数のよりカスタマイズ可能な検証が必要なアプリケーションでは、カスタムフォームセット検証を使用する必要があります。

validate_max

validate_max=Trueformset_factory()に渡された場合、検証では、データセット内のフォームの数から削除のマークが付けられたものを差し引いた数が [以下であることも確認されます。 X185X]。

>>> 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に対して厳密に検証します。

ノート

validate_maxに関係なく、データセット内のフォームの数がabsolute_maxを超えると、フォームはvalidate_maxが設定されているかのように検証に失敗し、さらに最初の[ X175X] フォームが検証されます。 残りは完全に切り捨てられます。 これは、偽造されたPOST要求を使用したメモリ枯渇攻撃から保護するためです。 インスタンス化されたフォームの最大数の制限を参照してください。


validate_min

validate_min=Trueformset_factory()に渡された場合、検証では、データセット内のフォームの数から削除のマークが付けられたものを差し引いた数が [以上であることも確認されます。 X188X]。

>>> 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.']

ノート

validate_minに関係なく、フォームセットにデータが含まれていない場合、extra + min_numの空のフォームが表示されます。


フォームの注文と削除の処理

formset_factory()は、フォームセット内のフォームの順序付けとフォームセットからのフォームの削除に役立つ2つのオプションのパラメーターcan_ordercan_deleteを提供します。

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-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 属性と、 can_order で使用されるウィジェットを制御する get_ordering_widget()メソッドも提供します。

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

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_objectsdelete()を呼び出す必要があります。

>>> instances = formset.save(commit=False)
>>> for obj in formset.deleted_objects:
...     obj.delete()

一方、プレーンなFormSetを使用している場合は、フォームセットのsave()メソッドでformset.deleted_formsを処理するのはあなた次第です。フォームを削除することを意味します。


can_delete_extra

バージョン3.2の新機能。


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

フォームセットフォームへのカスタムパラメータの受け渡し

フォームクラスが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

フォームセットのプレフィックスのカスタマイズ

レンダリングされた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">

これは、ビューで複数のフォームセットを使用する場合に役立ちます


ビューとテンプレートでフォームセットを使用する

ビュー内でフォームセットを使用することは、通常の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>

上記は、フォームセットクラスのas_tableメソッドを呼び出すことになります。

手動でレンダリングされたcan_deleteおよびcan_order

テンプレートのフィールドを手動でレンダリングする場合は、テンプレート:Form.DELETEを使用してcan_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でレンダリングできます。


ビューで複数のフォームセットを使用する

必要に応じて、ビューで複数のフォームセットを使用できます。 フォームセットは、その動作の多くをフォームから借用します。 そうは言っても、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ケースと非POSTケースの両方でprefixを渡す必要があることを指摘することが重要です。

各フォームセットのプレフィックスは、各フィールドのnameおよびid HTML属性に追加されるデフォルトのformプレフィックスを置き換えます。