表单集

class BaseFormSet

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

  1. >>> from django import forms
  2. >>> class ArticleForm(forms.Form):
  3. ... title = forms.CharField()
  4. ... pub_date = forms.DateField()
  5. ...

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

  1. >>> from django.forms import formset_factory
  2. >>> ArticleFormSet = formset_factory(ArticleForm)

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

  1. >>> formset = ArticleFormSet()
  2. >>> for form in formset:
  3. ... print(form.as_table())
  4. ...
  5. <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>
  6. <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>

如您所见,它只显示了一个空表单。显示的空表单的数量由 extra 参数控制。默认情况下,formset_factory() 定义了一个额外的表单;下面的示例将创建一个表单集类,以显示两个空表单:

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

遍历 formset 将按照它们创建的顺序渲染表单。你可以通过为 __iter__() 方法提供替代的实现来改变这个顺序。

表单集也可以被索引然后返回对应的表单。如果您已经覆盖了 __iter__ ,则还需覆盖 __getitem__ 让它具备匹配行为。

使用formset的初始数据

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

  1. >>> import datetime
  2. >>> from django.forms import formset_factory
  3. >>> from myapp.forms import ArticleForm
  4. >>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
  5. >>> formset = ArticleFormSet(
  6. ... initial=[
  7. ... {
  8. ... "title": "Django is now open source",
  9. ... "pub_date": datetime.date.today(),
  10. ... }
  11. ... ]
  12. ... )
  13. >>> for form in formset:
  14. ... print(form.as_table())
  15. ...
  16. <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>
  17. <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>
  18. <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>
  19. <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>
  20. <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>
  21. <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>

现在上面显示了三张表单。一张是传了初始数据的,另外两张是额外的。需要注意的是,我们通过传递一个字典列表来作为初始数据。

如果您使用了 initial 来显示formset,那么您需要在处理formset提交时传入相同的 initial ,以便formset检测用户更改了哪些表单。例如,您可能有这样的: ArticleFormSet(request.POST, initial=[...])

参见

创建模型表单集

限制表单的最大数量

formset_factory()max_num 参数允许您限制表单集将显示的表单数量:

  1. >>> from django.forms import formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1)
  4. >>> formset = ArticleFormSet()
  5. >>> for form in formset:
  6. ... print(form.as_table())
  7. ...
  8. <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>
  9. <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 并且formset有一个 initial 初始化项,则会显示一张初始化表单和一张空白表单。

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

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

max_num 默认只影响显示多少数量的表单而不影响验证。如果将 validate_max=True 传给 formset_factory(),那么 max_num 将会影响验证。参见 validate_max

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

formset_factory()absolute_max 参数允许限制在提供 POST 数据时可以实例化的表单数量。这可以防止使用伪造的 POST 请求进行内存耗尽攻击:

  1. >>> from django.forms.formsets import formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> ArticleFormSet = formset_factory(ArticleForm, absolute_max=1500)
  4. >>> data = {
  5. ... "form-TOTAL_FORMS": "1501",
  6. ... "form-INITIAL_FORMS": "0",
  7. ... }
  8. >>> formset = ArticleFormSet(data)
  9. >>> len(formset.forms)
  10. 1500
  11. >>> formset.is_valid()
  12. False
  13. >>> formset.non_form_errors()
  14. ['Please submit at most 1000 forms.']

absolute_maxNone 时,它默认为 max_num + 1000。(如果 max_numNone,则默认为 2000)。

如果 absolute_max 小于 max_num,将引发 ValueError

Formset验证

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

  1. >>> from django.forms import formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> ArticleFormSet = formset_factory(ArticleForm)
  4. >>> data = {
  5. ... "form-TOTAL_FORMS": "1",
  6. ... "form-INITIAL_FORMS": "0",
  7. ... }
  8. >>> formset = ArticleFormSet(data)
  9. >>> formset.is_valid()
  10. True

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

  1. >>> data = {
  2. ... "form-TOTAL_FORMS": "2",
  3. ... "form-INITIAL_FORMS": "0",
  4. ... "form-0-title": "Test",
  5. ... "form-0-pub_date": "1904-06-16",
  6. ... "form-1-title": "Test",
  7. ... "form-1-pub_date": "", # <-- this date is missing but required
  8. ... }
  9. >>> formset = ArticleFormSet(data)
  10. >>> formset.is_valid()
  11. False
  12. >>> formset.errors
  13. [{}, {'pub_date': ['This field is required.']}]

正如我们看到的,formset.errors 是一张列表,它的内容对应着formset中表单。两张表都进行了验证,并且第二项中出现了预期的错误消息。

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

BaseFormSet.total_error_count()

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

  1. >>> # Using the previous example
  2. >>> formset.errors
  3. [{}, {'pub_date': ['This field is required.']}]
  4. >>> len(formset.errors)
  5. 2
  6. >>> formset.total_error_count()
  7. 1

我们还可以检查表单数据是否与初始数据不同(即,表单被发送而没有任何数据):

  1. >>> data = {
  2. ... "form-TOTAL_FORMS": "1",
  3. ... "form-INITIAL_FORMS": "0",
  4. ... "form-0-title": "",
  5. ... "form-0-pub_date": "",
  6. ... }
  7. >>> formset = ArticleFormSet(data)
  8. >>> formset.has_changed()
  9. False

理解 ManagementForm

您可能已经注意到了上面表单集数据中的附加数据(form-TOTAL_FORMSform-INITIAL_FORMS)。这些数据是 ManagementForm 所必需的。这个表单用于由表单集管理的表单集合。如果不提供这些管理数据,表单集将无效:

  1. >>> data = {
  2. ... "form-0-title": "Test",
  3. ... "form-0-pub_date": "",
  4. ... }
  5. >>> formset = ArticleFormSet(data)
  6. >>> formset.is_valid()
  7. False

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

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

备注

除了这里的例子中显示的 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 返回该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_nummax_num

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

  1. >>> formset = ArticleFormSet({})
  2. >>> formset.is_valid()
  3. False
  4. >>> formset.non_form_errors()
  5. ['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.']

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

  1. >>> formset = ArticleFormSet(
  2. ... {}, error_messages={"missing_management_form": "Sorry, something went wrong."}
  3. ... )
  4. >>> formset.is_valid()
  5. False
  6. >>> formset.non_form_errors()
  7. ['Sorry, something went wrong.']

Changed in Django 4.1:

添加了 'too_few_forms''too_many_forms' 键。

自定义formset验证

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

  1. >>> from django.core.exceptions import ValidationError
  2. >>> from django.forms import BaseFormSet
  3. >>> from django.forms import formset_factory
  4. >>> from myapp.forms import ArticleForm
  5. >>> class BaseArticleFormSet(BaseFormSet):
  6. ... def clean(self):
  7. ... """Checks that no two articles have the same title."""
  8. ... if any(self.errors):
  9. ... # Don't bother validating the formset unless each form is valid on its own
  10. ... return
  11. ... titles = set()
  12. ... for form in self.forms:
  13. ... if self.can_delete and self._should_delete_form(form):
  14. ... continue
  15. ... title = form.cleaned_data.get("title")
  16. ... if title in titles:
  17. ... raise ValidationError("Articles in a set must have distinct titles.")
  18. ... titles.add(title)
  19. ...
  20. >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
  21. >>> data = {
  22. ... "form-TOTAL_FORMS": "2",
  23. ... "form-INITIAL_FORMS": "0",
  24. ... "form-0-title": "Test",
  25. ... "form-0-pub_date": "1904-06-16",
  26. ... "form-1-title": "Test",
  27. ... "form-1-pub_date": "1912-06-23",
  28. ... }
  29. >>> formset = ArticleFormSet(data)
  30. >>> formset.is_valid()
  31. False
  32. >>> formset.errors
  33. [{}, {}]
  34. >>> formset.non_form_errors()
  35. ['Articles in a set must have distinct titles.']

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

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

  1. <ul class="errorlist nonform">
  2. <li>Articles in a set must have distinct titles.</li>
  3. </ul>

验证formset中表单的数量

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

validate_max

如果方法 formset_factory() 有设置参数 validate_max=True ,验证还会检查数据集内表单的数量,减去那些被标记为删除的表单数量,剩余数量需小于等于 max_num

  1. >>> from django.forms import formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> ArticleFormSet = formset_factory(ArticleForm, max_num=1, validate_max=True)
  4. >>> data = {
  5. ... 'form-TOTAL_FORMS': '2',
  6. ... 'form-INITIAL_FORMS': '0',
  7. ... 'form-0-title': 'Test',
  8. ... 'form-0-pub_date': '1904-06-16',
  9. ... 'form-1-title': 'Test 2',
  10. ... 'form-1-pub_date': '1912-06-23',
  11. ... }
  12. >>> formset = ArticleFormSet(data)
  13. >>> formset.is_valid()
  14. False
  15. >>> formset.errors
  16. [{}, {}]
  17. >>> formset.non_form_errors()
  18. ['Please submit at most 1 form.']

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

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

备注

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

validate_min

如果方法 formset_factory() 有传参数 validate_min=True ,还会验证数据集中的表单的数量减去那些被标记为删除的表单数量是否大于或等于 min_num 定义的数量。

  1. >>> from django.forms import formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> ArticleFormSet = formset_factory(ArticleForm, min_num=3, validate_min=True)
  4. >>> data = {
  5. ... 'form-TOTAL_FORMS': '2',
  6. ... 'form-INITIAL_FORMS': '0',
  7. ... 'form-0-title': 'Test',
  8. ... 'form-0-pub_date': '1904-06-16',
  9. ... 'form-1-title': 'Test 2',
  10. ... 'form-1-pub_date': '1912-06-23',
  11. ... }
  12. >>> formset = ArticleFormSet(data)
  13. >>> formset.is_valid()
  14. False
  15. >>> formset.errors
  16. [{}, {}]
  17. >>> formset.non_form_errors()
  18. ['Please submit at least 3 forms.']

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

备注

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

处理表单的排序和删除

方法 formset_factory() 提供了两个可选参数 can_ordercan_delete 来协助处理formset中表单的排序和删除。

can_order

BaseFormSet.can_order

默认值: False

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

  1. >>> from django.forms import formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> ArticleFormSet = formset_factory(ArticleForm, can_order=True)
  4. >>> formset = ArticleFormSet(
  5. ... initial=[
  6. ... {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
  7. ... {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
  8. ... ]
  9. ... )
  10. >>> for form in formset:
  11. ... print(form.as_table())
  12. ...
  13. <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>
  14. <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>
  15. <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>
  16. <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>
  17. <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>
  18. <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>
  19. <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>
  20. <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>
  21. <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。对于来自初始数据的表单,它会自动分配给它们一个数字值。让我们看看当用户更改这些值时会发生什么:

  1. >>> data = {
  2. ... "form-TOTAL_FORMS": "3",
  3. ... "form-INITIAL_FORMS": "2",
  4. ... "form-0-title": "Article #1",
  5. ... "form-0-pub_date": "2008-05-10",
  6. ... "form-0-ORDER": "2",
  7. ... "form-1-title": "Article #2",
  8. ... "form-1-pub_date": "2008-05-11",
  9. ... "form-1-ORDER": "1",
  10. ... "form-2-title": "Article #3",
  11. ... "form-2-pub_date": "2008-05-01",
  12. ... "form-2-ORDER": "0",
  13. ... }
  14. >>> formset = ArticleFormSet(
  15. ... data,
  16. ... initial=[
  17. ... {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
  18. ... {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
  19. ... ],
  20. ... )
  21. >>> formset.is_valid()
  22. True
  23. >>> for form in formset.ordered_forms:
  24. ... print(form.cleaned_data)
  25. ...
  26. {'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0, 'title': 'Article #3'}
  27. {'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': 'Article #2'}
  28. {'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 的小部件类:

  1. >>> from django.forms import BaseFormSet, formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> class BaseArticleFormSet(BaseFormSet):
  4. ... ordering_widget = HiddenInput
  5. ...
  6. >>> ArticleFormSet = formset_factory(
  7. ... ArticleForm, formset=BaseArticleFormSet, can_order=True
  8. ... )

get_ordering_widget

BaseFormSet.get_ordering_widget()

如果需要为 can_order 提供一个小部件实例,可以重写 get_ordering_widget()

  1. >>> from django.forms import BaseFormSet, formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> class BaseArticleFormSet(BaseFormSet):
  4. ... def get_ordering_widget(self):
  5. ... return HiddenInput(attrs={"class": "ordering"})
  6. ...
  7. >>> ArticleFormSet = formset_factory(
  8. ... ArticleForm, formset=BaseArticleFormSet, can_order=True
  9. ... )

can_delete

BaseFormSet.can_delete

默认值: False

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

  1. >>> from django.forms import formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True)
  4. >>> formset = ArticleFormSet(
  5. ... initial=[
  6. ... {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
  7. ... {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
  8. ... ]
  9. ... )
  10. >>> for form in formset:
  11. ... print(form.as_table())
  12. ...
  13. <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>
  14. <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>
  15. <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>
  16. <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>
  17. <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>
  18. <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>
  19. <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>
  20. <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>
  21. <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 来访问它们:

  1. >>> data = {
  2. ... "form-TOTAL_FORMS": "3",
  3. ... "form-INITIAL_FORMS": "2",
  4. ... "form-0-title": "Article #1",
  5. ... "form-0-pub_date": "2008-05-10",
  6. ... "form-0-DELETE": "on",
  7. ... "form-1-title": "Article #2",
  8. ... "form-1-pub_date": "2008-05-11",
  9. ... "form-1-DELETE": "",
  10. ... "form-2-title": "",
  11. ... "form-2-pub_date": "",
  12. ... "form-2-DELETE": "",
  13. ... }
  14. >>> formset = ArticleFormSet(
  15. ... data,
  16. ... initial=[
  17. ... {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
  18. ... {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
  19. ... ],
  20. ... )
  21. >>> [form.cleaned_data for form in formset.deleted_forms]
  22. [{'DELETE': True, 'pub_date': datetime.date(2008, 5, 10), 'title': 'Article #1'}]

如果你使用 ModelFormSet ,那些标记为删除的表单模型实例会在调用 formset.save() 时被删除。

如果你调用 formset.save(commit=False),对象将不会被自动删除。你需要在每一个 formset.deleted_objects 上调用 delete() 来实际删除它们:

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

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

BaseFormSet 也提供了一个 deletion_widget 属性和 get_deletion_widget() 方法,控制用于 can_delete 的部件。

deletion_widget

BaseFormSet.deletion_widget

默认: CheckboxInput

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

  1. >>> from django.forms import BaseFormSet, formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> class BaseArticleFormSet(BaseFormSet):
  4. ... deletion_widget = HiddenInput
  5. ...
  6. >>> ArticleFormSet = formset_factory(
  7. ... ArticleForm, formset=BaseArticleFormSet, can_delete=True
  8. ... )

get_deletion_widget

BaseFormSet.get_deletion_widget()

如果需要为 can_delete 提供一个小部件实例,可以重写 get_deletion_widget()

  1. >>> from django.forms import BaseFormSet, formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> class BaseArticleFormSet(BaseFormSet):
  4. ... def get_deletion_widget(self):
  5. ... return HiddenInput(attrs={"class": "deletion"})
  6. ...
  7. >>> ArticleFormSet = formset_factory(
  8. ... ArticleForm, formset=BaseArticleFormSet, can_delete=True
  9. ... )

can_delete_extra

BaseFormSet.can_delete_extra

默认: True

在设置 can_delete=True 的同时,指定 can_delete_extra=False 将移除删除额外表格的选项。

给一个formset添加额外字段

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

  1. >>> from django.forms import BaseFormSet
  2. >>> from django.forms import formset_factory
  3. >>> from myapp.forms import ArticleForm
  4. >>> class BaseArticleFormSet(BaseFormSet):
  5. ... def add_fields(self, form, index):
  6. ... super().add_fields(form, index)
  7. ... form.fields["my_field"] = forms.CharField()
  8. ...
  9. >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
  10. >>> formset = ArticleFormSet()
  11. >>> for form in formset:
  12. ... print(form.as_table())
  13. ...
  14. <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>
  15. <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>
  16. <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>

传递自定义参数到formset表单

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

  1. >>> from django.forms import BaseFormSet
  2. >>> from django.forms import formset_factory
  3. >>> from myapp.forms import ArticleForm
  4. >>> class MyArticleForm(ArticleForm):
  5. ... def __init__(self, *args, user, **kwargs):
  6. ... self.user = user
  7. ... super().__init__(*args, **kwargs)
  8. ...
  9. >>> ArticleFormSet = formset_factory(MyArticleForm)
  10. >>> formset = ArticleFormSet(form_kwargs={"user": request.user})

form_kwargs 也可能取决于特定的表单实例。表单集基类提供了一个 get_form_kwargs 方法。这个方法接受一个参数 - 表单集中表单的索引。对于 empty_form,索引为 None

  1. >>> from django.forms import BaseFormSet
  2. >>> from django.forms import formset_factory
  3. >>> class BaseArticleFormSet(BaseFormSet):
  4. ... def get_form_kwargs(self, index):
  5. ... kwargs = super().get_form_kwargs(index)
  6. ... kwargs["custom_kwarg"] = index
  7. ... return kwargs
  8. ...
  9. >>> ArticleFormSet = formset_factory(MyArticleForm, formset=BaseArticleFormSet)
  10. >>> formset = ArticleFormSet()

自定义formset的前缀

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

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

  1. <label for="id_form-0-title">Title:</label>
  2. <input type="text" name="form-0-title" id="id_form-0-title">

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

  1. <label for="id_article-0-title">Title:</label>
  2. <input type="text" name="article-0-title" id="id_article-0-title">

如果您想 :ref:`在视图中使用多个formset <multiple-formsets-in-view> ` ,这个参数会很有用。

在视图和模板中使用formset

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

BaseFormSet.renderer

指定 渲染器 用于表单集。默认为 FORM_RENDER 配置所指定的渲染器。

BaseFormSet.template_name

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

默认情况下,返回渲染器的 formset_template_name 值的属性。你可以将其设置为字符串模板名称,以便在特定的表单集类中覆盖它。

这个模板将用于呈现表单集的管理表单,然后根据表单的 template_name 定义的模板来呈现表单集中的每个表单。

Changed in Django 4.1:

在旧版本中,template_name 默认为字符串值 'django/forms/formset/default.html'

BaseFormSet.template_name_div

New in Django 4.1.

在调用 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()

New in Django 4.1.

使用 template_name_div 模板呈现表单集。

BaseFormSet.as_p()

template_name_p 模板渲染表单集。

BaseFormSet.as_table()

使用 template_name_table 模板渲染表单集。

BaseFormSet.as_ul()

template_name_ul 模板渲染表单集。

在视图中使用formset与使用常规的 Form 类没有太多不同之处。你唯一需要注意的是确保要在模板中使用管理表单。我们来看一个示例视图:

  1. from django.forms import formset_factory
  2. from django.shortcuts import render
  3. from myapp.forms import ArticleForm
  4. def manage_articles(request):
  5. ArticleFormSet = formset_factory(ArticleForm)
  6. if request.method == "POST":
  7. formset = ArticleFormSet(request.POST, request.FILES)
  8. if formset.is_valid():
  9. # do something with the formset.cleaned_data
  10. pass
  11. else:
  12. formset = ArticleFormSet()
  13. return render(request, "manage_articles.html", {"formset": formset})

模板 manage_articles.html 可能如下所示:

  1. <form method="post">
  2. {{ formset.management_form }}
  3. <table>
  4. {% for form in formset %}
  5. {{ form }}
  6. {% endfor %}
  7. </table>
  8. </form>

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

  1. <form method="post">
  2. <table>
  3. {{ formset }}
  4. </table>
  5. </form>

上面的内容最终会调用表单集类上的 BaseFormSet.render() 方法。这将使用 template_name 属性指定的模板来渲染表单集。与表单类似,默认情况下,表单集将被渲染为 as_table,其他辅助方法 as_pas_ul 可用。表单集的渲染可以通过指定 template_name 属性来定制,或者更普遍的是通过 覆盖默认模板

手动渲染 can_deletecan_order

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

  1. <form method="post">
  2. {{ formset.management_form }}
  3. {% for form in formset %}
  4. <ul>
  5. <li>{{ form.title }}</li>
  6. <li>{{ form.pub_date }}</li>
  7. {% if formset.can_delete %}
  8. <li>{{ form.DELETE }}</li>
  9. {% endif %}
  10. </ul>
  11. {% endfor %}
  12. </form>

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

在视图中使用多个formset

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

  1. from django.forms import formset_factory
  2. from django.shortcuts import render
  3. from myapp.forms import ArticleForm, BookForm
  4. def manage_articles(request):
  5. ArticleFormSet = formset_factory(ArticleForm)
  6. BookFormSet = formset_factory(BookForm)
  7. if request.method == "POST":
  8. article_formset = ArticleFormSet(request.POST, request.FILES, prefix="articles")
  9. book_formset = BookFormSet(request.POST, request.FILES, prefix="books")
  10. if article_formset.is_valid() and book_formset.is_valid():
  11. # do something with the cleaned_data on the formsets.
  12. pass
  13. else:
  14. article_formset = ArticleFormSet(prefix="articles")
  15. book_formset = BookFormSet(prefix="books")
  16. return render(
  17. request,
  18. "manage_articles.html",
  19. {
  20. "article_formset": article_formset,
  21. "book_formset": book_formset,
  22. },
  23. )

然后您就可以像平时那样渲染表单集。需要指出的是,您需要同时在POST和非POST情况下传递 prefix ,以便它能被正确渲染和处理。

每个formset的 prefix 会替换添加到每个字段的 nameid HTML属性的默认 form 前缀。