表单集

class BaseFormSet[source]

表单集是一个用于在同一页面上处理多个表单的抽象层。它可以最好地比作数据网格。假设您有以下表单

>>> 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() 定义了一个额外的表单;以下示例将创建一个表单集类,以显示两个空白表单

>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)

表单集可以被迭代和索引,并按创建顺序访问表单。如果需要,您可以通过覆盖默认的 迭代索引 行为来重新排序表单。

在表单集中使用初始数据

初始数据是驱动表单集主要可用性的因素。如上所示,您可以定义额外表单的数量。这意味着您告诉表单集除了从初始数据生成的表单数量之外,还要显示多少个附加表单。让我们看一个例子

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

现在总共显示了三个表单。一个用于传入的初始数据,两个额外的表单。还要注意,我们正在传入一个字典列表作为初始数据。

如果您使用 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,并且表单集用一个 initial 项目初始化,则将显示一个用于初始项目的表单和一个空白表单。

如果初始数据中的项目数量超过 max_num,则将显示所有初始数据表单,而不管 max_num 的值如何,并且不会显示任何额外表单。例如,如果 extra=3max_num=1,并且表单集用两个初始项目初始化,则将显示两个包含初始数据的表单。

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_maxNone 时,它默认为 max_num + 1000。(如果 max_numNone,则它默认为 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 是一个列表,其条目对应于表单集中的表单。对两个表单中的每一个都执行了验证,并且第二个项目的预期错误消息出现。

就像使用普通 Form 一样,表单集表单中的每个字段都可以包含 HTML 属性,例如 maxlength 用于浏览器验证。但是,表单集的表单字段将不包含 required 属性,因为在添加和删除表单时,该验证可能不正确。

BaseFormSet.total_error_count()[source]

要检查表单集中有多少个错误,我们可以使用 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_FORMSform-INITIAL_FORMS 字段外,管理表单还包含 form-MIN_NUM_FORMSform-MAX_NUM_FORMS 字段。它们与管理表单的其余部分一起输出,但仅为了客户端代码的方便。这些字段不是必需的,因此在示例 POST 数据中未显示。

total_form_countinitial_form_count

BaseFormSet 有几个与 ManagementForm 密切相关的方法,total_form_countinitial_form_count

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

自定义表单集验证

表单集有一个 clean 方法,类似于 Form 类上的方法。在这里,您可以定义在表单集级别工作的自定义验证

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

在所有 Form.clean 方法调用之后,将调用表单集的 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

如果将 validate_max=True 传递给 formset_factory(),验证还将检查数据集中的表单数量减去标记为删除的表单数量是否小于或等于 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

可以通过将 'too_many_forms' 消息传递到 error_messages 参数来自定义错误消息。

注意

无论 validate_max 如何,如果数据集中的表单数量超过 absolute_max,则表单将无法验证,就像设置了 validate_max 一样,并且此外,只有前 absolute_max 个表单将被验证。其余部分将被完全截断。这是为了防止使用伪造的 POST 请求进行内存耗尽攻击。请参阅 限制实例化表单的最大数量

validate_min

如果将 validate_min=True 传递给 formset_factory(),验证还将检查数据集中的表单数量减去标记为删除的表单数量是否大于或等于 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.']

可以通过将 'too_few_forms' 消息传递到 error_messages 参数来自定义错误消息。

注意

无论 validate_min 如何,如果表单集不包含任何数据,则将显示 extra + min_num 个空表单。

处理表单的排序和删除

formset_factory() 提供了两个可选参数 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)
...
<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 一起使用的 widget。

ordering_widget

BaseFormSet.ordering_widget

默认值:NumberInput

设置 ordering_widget 以指定要与 can_order 一起使用的 widget 类

>>> 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()[source]

如果需要为 can_order 提供一个 widget 实例,请覆盖 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,你需要自己处理 formset.deleted_forms,可能在你的表单集的 save() 方法中,因为没有关于删除表单的通用概念。

BaseFormSet 还提供了一个 deletion_widget 属性和 get_deletion_widget() 方法,它们控制与 can_delete 一起使用的 widget。

deletion_widget

BaseFormSet.deletion_widget

默认值:CheckboxInput

设置 deletion_widget 以指定要与 can_delete 一起使用的 widget 类。

>>> 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()[source]

如果需要为 can_delete 提供一个 widget 实例,请覆盖 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[source]

如果表单集转换为字符串(例如,通过 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()[source]

返回在模板中渲染表单集的上下文。

可用的上下文为

  • 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。可以通过指定 template_name 属性或更普遍地 覆盖默认模板 来自定义表单集的渲染。

手动渲染的 can_deletecan_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,以便正确渲染和处理它。

每个表单集的 prefix 将替换添加到每个字段的 nameid HTML 属性的默认 form 前缀。

返回顶部