---
title: "表单集"
version: 6.1
locale: zh-hans
source: https://docs.djangoproject.com/zh-hans/6.1/topics/forms/formsets/
canonical: https://djangodocs.dev/zh-hans/6.1/topics/forms/formsets/
---
# 表单集

#### `class BaseFormSet`

表单集是一种用于在同一页面上处理多个表单的抽象层。它最适合与数据网格进行比较。假设您有以下表单：

```pycon
>>> from django import forms
>>> class ArticleForm(forms.Form):
...     title = forms.CharField()
...     pub_date = forms.DateField()
...
```

您可能希望允许用户同时创建多篇文章。要从 `ArticleForm` 创建一个表单集，您可以这样做：

```pycon
>>> from django.forms import formset_factory
>>> ArticleFormSet = formset_factory(ArticleForm)
```

现在，您已经创建了一个名为 `ArticleFormSet` 的表单集类。实例化表单集使您能够迭代表单集中的表单，并像普通表单一样显示它们：

```pycon
>>> 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()`](/zh-hans/6.1/ref/forms/formsets/#django.forms.formsets.formset_factory) 定义了一个额外的表单；下面的示例将创建一个表单集类，以显示两个空表单：

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

Formsets can be iterated and indexed, accessing forms in the order they were
created. You can reorder the forms by overriding the default
[`__iter__()`](https://docs.python.org/3/reference/datamodel.html#object.__iter__) and [`__getitem__()`](https://docs.python.org/3/reference/datamodel.html#object.__getitem__) methods if needed.
(For more information on implementing these methods, see the
[Python documentation on sequences](https://docs.python.org/3/glossary.html#term-sequence).)

## 使用formset的初始数据

初始数据是驱动表单集主要可用性的关键。如上所示，您可以定义额外表单的数量。这意味着您告诉表单集要显示多少个额外表单，除了它从初始数据生成的表单数量。让我们看一个示例：

```pycon
>>> 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` 来显示formset，那么您需要在处理formset提交时传入相同的 `initial` ，以便formset检测用户更改了哪些表单。例如，您可能有这样的： `ArticleFormSet(request.POST, initial=[...])` 。

> **See also**
>
> [创建模型表单集](/zh-hans/6.1/topics/forms/modelforms/#model-formsets)。

## 限制表单的最大数量

[`formset_factory()`](/zh-hans/6.1/ref/forms/formsets/#django.forms.formsets.formset_factory) 的 `max_num` 参数允许您限制表单集将显示的表单数量：

```pycon
>>> 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` 并且formset有一个 `initial` 初始化项，则会显示一张初始化表单和一张空白表单。

如果初始数据项的数量超过 `max_num` ，那么 `max_num` 的值会被无视，所有初始数据表单都会显示，并且也不会有额外的表单显示。例如，假设 `extra=3` ， `max_num=1` 并且formset有两个初始化项，那么只会显示两张有初始化数据的表单。

`max_num` 的值 `None` (默认值)，它限制最多显示(1000)张表单，其实这相当于没有限制。

By default, `max_num` only affects how many forms are displayed and does not
affect validation. If `validate_max=True` is passed to the
[`formset_factory()`](/zh-hans/6.1/ref/forms/formsets/#django.forms.formsets.formset_factory), then `max_num` will affect
validation. See [validate\_max](#validate-max).

## 限制实例化表单的最大数量

[`formset_factory()`](/zh-hans/6.1/ref/forms/formsets/#django.forms.formsets.formset_factory) 的 `absolute_max` 参数允许限制在提供 `POST` 数据时可以实例化的表单数量。这可以防止使用伪造的 `POST` 请求进行内存耗尽攻击：

```pycon
>>> 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`。

## Formset验证

使用表单集进行验证与常规 `Form` 几乎相同。表单集上有一个 `is_valid` 方法，提供了一种方便的方式来验证表单集中的所有表单：

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

我们没有向表单集传递任何数据，这导致表单有效。表单集足够智能，可以忽略未更改的额外表单。如果我们提供一个无效的文章：

```pycon
>>> 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` 是一张列表，它的内容对应着formset中表单。两张表都进行了验证，并且第二项中出现了预期的错误消息。

和使用普通 `Form` 一样，formset表单中的每个字段都可能包含HTML属性，例如用于浏览器验证的 `maxlength` 。但是由于表单添加、删除的时候会影响属性 `required` 的验证，表单集中的表单不会包含此属性。

#### `BaseFormSet.total_error_count()`

要检查表单集中有多少个错误，可以使用 `total_error_count` 方法：

```pycon
>>> # Using the previous example
>>> formset.errors
[{}, {'pub_date': ['This field is required.']}]
>>> len(formset.errors)
2
>>> formset.total_error_count()
1
```

我们还可以检查表单数据是否与初始数据不同（即，表单被发送而没有任何数据）：

```pycon
>>> 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` 所必需的。这个表单用于由表单集管理的表单集合。如果不提供这些管理数据，表单集将无效：

```pycon
>>> data = {
...     "form-0-title": "Test",
...     "form-0-pub_date": "",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
```

它被用来跟踪显示了多少个表单实例。如果您通过JavaScript添加新表单，那您同样需要增加相应内容到那些数量字段中，另一方面，如果您允许通过JavaScript来删除已存在对象，那么您需确认被移除的对象已经被标记在 `form-#-DELETE` 中并被放到 `POST` 内。无论如何，所有表单都要确保在 `POST` 数据中。

管理表单以formset的一项属性而存在。在模板中渲染formset时，你可以使用 `{{ my_formset.management_form }}`  (将my\_formset替换为自己的formset名称)渲染出所有管理表单的数据。

> **Note**
>
> 除了这里的例子中显示的 `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` 。

`total_form_count` 返回该formset内表单的总和。 `initial_form_count` 返回该formset内预填充的表单数量，同时用于定义需要多少表单。你可能永远不会重写这两个方法，因此在使用之前请理解它们的用途。

### `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`。

例如，以下是当管理表单丢失时的默认错误消息：

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

以下是一个自定义错误消息的示例：

```pycon
>>> formset = ArticleFormSet(
...     {}, error_messages={"missing_management_form": "Sorry, something went wrong."}
... )
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['Sorry, something went wrong.']
```

### 自定义formset验证

表单集具有类似于 `Form` 类上的 `clean` 方法。这是您在表单集级别定义自己的验证规则的地方：

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

formset的 `clean` 方法会在所有 `Form.clean` 方法调用完之后被调用。可以使用formset的 `non_form_errors()` 方法来查看错误信息。

非表单错误将用一个额外的类 `nonform` 来呈现，以帮助区分它们与表单特定错误。例如，`{{ formset.non_form_errors }}` 将看起来像:

```html+django
<ul class="errorlist nonform">
    <li>Articles in a set must have distinct titles.</li>
</ul>
```

## 验证formset中表单的数量

Django提供了一对方法来验证已提交的表单的最小和最大数量。如果要对应用程序进行更多的可定制验证，那需要使用自定义formset验证。

### `validate_max`

如果方法 [`formset_factory()`](/zh-hans/6.1/ref/forms/formsets/#django.forms.formsets.formset_factory) 有设置参数 `validate_max=True` ，验证还会检查数据集内表单的数量，减去那些被标记为删除的表单数量，剩余数量需小于等于 `max_num` 。

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

即使因为提供的初始数据量过大而超过了 `max_num` 所定义的，`validate_max=True` 还是会严格针对 `max_num` 进行验证。

错误消息可以通过将 `'too_many_forms'` 消息传递给 [error\_messages](#formsets-error-messages) 参数来自定义。

> **Note**
>
> 不管 `validate_max` 如何，如果数据集中的表单数量超过 `absolute_max`，那么表单将无法验证，就像 `validate_max` 被设置一样，另外只有第一个 `absolute_max` 的表单会被验证。其余的将被完全截断。这是为了防止使用伪造的 POST 请求的内存耗尽攻击。参见 [限制实例化表单的最大数量](#formsets-absolute-max)。

### `validate_min`

如果方法 [`formset_factory()`](/zh-hans/6.1/ref/forms/formsets/#django.forms.formsets.formset_factory) 有传参数 `validate_min=True` ，还会验证数据集中的表单的数量减去那些被标记为删除的表单数量是否大于或等于 `min_num` 定义的数量。

```pycon
>>> 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](#formsets-error-messages) 参数来自定义。

> **Note**
>
> 无论 `validate_min` 的值是什么，如果一个 formset 不包含任何数据，那么将显示 `extra + min_num` 空表单。

## 处理表单的排序和删除

方法 [`formset_factory()`](/zh-hans/6.1/ref/forms/formsets/#django.forms.formsets.formset_factory) 提供了两个可选参数 `can_order` 和 `can_delete` 来协助处理formset中表单的排序和删除。

### `can_order`

#### `BaseFormSet.can_order`

默认值： `False`

让你创建一个具有排序功能的表单集：

```pycon
>>> 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`。对于来自初始数据的表单，它会自动分配给它们一个数字值。让我们看看当用户更改这些值时会发生什么：

```pycon
>>> 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)},
...     ],
... )
>>> for form in formset.ordered_forms:
...     print(form.cleaned_data)
...
{'title': 'Article #3', 'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0}
{'title': 'Article #2', 'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1}
{'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2}
```

[`BaseFormSet`](#django.forms.formsets.BaseFormSet) 也提供了 [`ordering_widget`](#django.forms.formsets.BaseFormSet.ordering_widget) 属性和 [`get_ordering_widget()`](#django.forms.formsets.BaseFormSet.get_ordering_widget) 方法，来控制与 [`can_order`](#django.forms.formsets.BaseFormSet.can_order) 一起使用的小部件。

#### `ordering_widget`

#### `BaseFormSet.ordering_widget`

默认: [`NumberInput`](/zh-hans/6.1/ref/forms/widgets/#django.forms.NumberInput)

将 `ordering_widget` 设置为指定用于 `can_order` 的小部件类：

```pycon
>>> 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()`：

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

允许你创建一个具有选择要删除的表单的表单集：

```pycon
>>> 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` 来访问它们：

```pycon
>>> 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]
[{'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10), 'DELETE': True}]
```

如果你使用 [`ModelFormSet`](/zh-hans/6.1/topics/forms/modelforms/#django.forms.models.BaseModelFormSet) ，那些标记为删除的表单模型实例会在调用 `formset.save()` 时被删除。

If you call `formset.save(commit=False)`, objects will not be deleted
automatically. You'll need to call `delete()` on each of the
[`formset.deleted_objects`](/zh-hans/6.1/topics/forms/modelforms/#django.forms.models.BaseModelFormSet.deleted_objects) to actually delete
them:

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

另一方面，如果您使用的是普通的 `FormSet` ，那需要您自己去处理 `formset.deleted_forms` ，可能写在formset的 `save()` 方法中，因为对于阐述删除一张表单还没有一个通用的概念。

[`BaseFormSet`](#django.forms.formsets.BaseFormSet) 也提供了一个 [`deletion_widget`](#django.forms.formsets.BaseFormSet.deletion_widget) 属性和 [`get_deletion_widget()`](#django.forms.formsets.BaseFormSet.get_deletion_widget) 方法，控制用于 [`can_delete`](#django.forms.formsets.BaseFormSet.can_delete) 的部件。

#### `deletion_widget`

#### `BaseFormSet.deletion_widget`

默认： [`CheckboxInput`](/zh-hans/6.1/ref/forms/widgets/#django.forms.CheckboxInput)

将 `deletion_widget` 设置为指定与 `can_delete` 一起使用的小部件类：

```pycon
>>> 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()`：

```pycon
>>> 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` 将移除删除额外表格的选项。

## 给一个formset添加额外字段

如果需要向表单集添加额外的字段，这可以很容易实现。表单集基类提供了一个 `add_fields` 方法。你可以重写这个方法来添加自己的字段，甚至重新定义订单和删除字段的默认字段/属性：

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

## 传递自定义参数到formset表单

有时你的表单类需要接受自定义参数，比如 `MyArticleForm`。你可以在实例化表单集时传递这个参数：

```pycon
>>> 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](#empty-form)，索引为 `None`：

```pycon
>>> 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()
```

## 自定义formset的前缀

在已渲染的HTML页面中，表单集中的每个字段都包含一个前缀。这个前缀默认是 `'form'` ，但可以使用formset的 `prefix` 参数来自定义。

例如，在默认情况下，您可能会看到:

```html
<label for="id_form-0-title">Title:</label>
<input type="text" name="form-0-title" id="id_form-0-title">
```

但使用 `ArticleFormset(prefix='article')` 的话就会变为：

```html
<label for="id_article-0-title">Title:</label>
<input type="text" name="article-0-title" id="id_article-0-title">
```

如果您想 [在视图中使用多个formset](#multiple-formsets-in-view) ，这个参数会很有用。

## 在视图和模板中使用formset

表单集具有以下与渲染相关的属性和方法：

#### `BaseFormSet.renderer`

指定 [渲染器](/zh-hans/6.1/ref/forms/renderers/) 用于表单集。默认为 `FORM_RENDER` 配置所指定的渲染器。

#### `BaseFormSet.template_name`

如果将表单集转换为字符串，例如通过 `print(formset)` 或在模板中使用 `{{ formset }}`，则会呈现模板的名称。

默认情况下，返回渲染器的 [`formset_template_name`](/zh-hans/6.1/ref/forms/renderers/#django.forms.renderers.BaseRenderer.formset_template_name) 值的属性。你可以将其设置为字符串模板名称，以便在特定的表单集类中覆盖它。

这个模板将用于呈现表单集的管理表单，然后根据表单的 [`template_name`](/zh-hans/6.1/ref/forms/api/#django.forms.Form.template_name) 定义的模板来呈现表单集中的每个表单。

#### `BaseFormSet.template_name_div`

在调用 [`as_div()`](#django.forms.formsets.BaseFormSet.as_div) 时使用的模板名称。默认情况下，这是 `"django/forms/formsets/div.html"`。这个模板呈现表单集的管理表单，然后根据表单的 [`as_div()`](/zh-hans/6.1/ref/forms/api/#django.forms.Form.as_div) 方法来呈现表单集中的每个表单。

#### `BaseFormSet.template_name_p`

在调用 [`as_p()`](#django.forms.formsets.BaseFormSet.as_p) 时使用的模板名称。默认情况下，这是 `"django/forms/formsets/p.html"`。这个模板呈现表单集的管理表单，然后根据表单的 [`as_p()`](/zh-hans/6.1/ref/forms/api/#django.forms.Form.as_p) 方法来呈现表单集中的每个表单。

#### `BaseFormSet.template_name_table`

在调用 [`as_table()`](#django.forms.formsets.BaseFormSet.as_table) 时使用的模板名称。默认情况下，这是 `"django/forms/formsets/table.html"`。这个模板呈现表单集的管理表单，然后根据表单的 [`as_table()`](/zh-hans/6.1/ref/forms/api/#django.forms.Form.as_table) 方法来呈现表单集中的每个表单。

#### `BaseFormSet.template_name_ul`

在调用 [`as_ul()`](#django.forms.formsets.BaseFormSet.as_ul) 时使用的模板名称。默认情况下，这是 `"django/forms/formsets/ul.html"`。这个模板呈现表单集的管理表单，然后根据表单的 [`as_ul()`](/zh-hans/6.1/ref/forms/api/#django.forms.Form.as_ul) 方法来呈现表单集中的每个表单。

#### `BaseFormSet.get_context()`

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

可用的上下文：

- `formset`：表单集的实例。

#### `BaseFormSet.render(template_name=None, context=None, renderer=None)`

render 方法被 `__str__` 调用，以及 [`as_div()`](#django.forms.formsets.BaseFormSet.as_div)、[`as_p()`](#django.forms.formsets.BaseFormSet.as_p)、[`as_ul()`](#django.forms.formsets.BaseFormSet.as_ul) 和 [`as_table()`](#django.forms.formsets.BaseFormSet.as_table) 方法。所有参数都是可选的，将默认为：

- `template_name`： [`template_name`](#django.forms.formsets.BaseFormSet.template_name)
- `context`： 由 [`get_context()`](#django.forms.formsets.BaseFormSet.get_context) 返回的值
- `renderer`： 由 [`renderer`](#django.forms.formsets.BaseFormSet.renderer) 返回的值

#### `BaseFormSet.as_div()`

使用 [`template_name_div`](#django.forms.formsets.BaseFormSet.template_name_div) 模板呈现表单集。

#### `BaseFormSet.as_p()`

用 [`template_name_p`](#django.forms.formsets.BaseFormSet.template_name_p) 模板渲染表单集。

#### `BaseFormSet.as_table()`

使用 [`template_name_table`](#django.forms.formsets.BaseFormSet.template_name_table) 模板渲染表单集。

#### `BaseFormSet.as_ul()`

用 [`template_name_ul`](#django.forms.formsets.BaseFormSet.template_name_ul) 模板渲染表单集。

在视图中使用formset与使用常规的 `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` 可能如下所示：

```html+django
<form method="post">
    {{ formset.management_form }}
    <table>
        {% for form in formset %}
        {{ form }}
        {% endfor %}
    </table>
</form>
```

但是对于上面让formset自己处理管理表单，还有个小小的捷径：

```html+django
<form method="post">
    <table>
        {{ formset }}
    </table>
</form>
```

上述操作最终会调用 formset 类上的 [`BaseFormSet.render()`](#django.forms.formsets.BaseFormSet.render) 方法。这会使用 [`template_name`](#django.forms.formsets.BaseFormSet.template_name) 属性指定的模板来渲染 formset。与表单类似，默认情况下 formset 会以 `as_div` 方式渲染，同时提供 `as_p`、`as_ul` 和 `as_table` 等其他辅助方法。可以通过指定 `template_name` 属性来定制 formset 的渲染，或者更一般地通过 [覆盖默认模板](/zh-hans/6.1/ref/forms/renderers/#overriding-built-in-formset-templates) 来实现。

### 手动渲染 `can_delete` 和 `can_order`

如果您在模板中手动渲染字段，您可以用 `{{ form.DELETE }}` 来渲染参数 `can_delete` 。

```html+django
<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>
```

同样，如果formset能排序( `can_order=True` )，可以用 `{{ form.ORDER }}` 来渲染它。

### 在视图中使用多个formset

你可以在视图中使用多个formset。表单集从表单上借鉴了很多行为。像之前说的，你可以使用参数 `prefix` 来给formset中表单的字段附上前缀，以避免多个formset的数据传到同一个视图而引起名称冲突。让我们来看下这是如何实现的：

```
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` ，以便它能被正确渲染和处理。

每个formset的 [prefix](#formset-prefix) 会替换添加到每个字段的 `name` 和 `id` HTML属性的默认 `form` 前缀。
