表单集

class BaseFormSet

formset是一个抽象层,它可以在同一页面上处理多个表单的。它最适合被比喻成网格数据。我们假设您有以下表单:

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

您可能想允许用户一次创建多篇文章。 要创建一个 ArticleForm 的formset,您可以这样做:

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

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

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

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

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

使用formset的初始数据

初始数据驱动着formset的主要能力。如上所示,您可以定义额外表单的数量。也就是说,您告诉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(initial=[
  6. ... {'title': 'Django is now open source',
  7. ... 'pub_date': datetime.date.today(),}
  8. ... ])
  9. >>> for form in formset:
  10. ... print(form.as_table())
  11. <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>
  12. <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>
  13. <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>
  14. <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>
  15. <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>
  16. <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. <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>
  8. <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

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

formet_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验证

formset的验证与常规 Form 几乎相同。formset提供了一个 is_valid 方法以便验证formset内所有表单:

  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

我们传了空数据给formset,并被给了一个有效的结果。formset足够聪明去忽略那些没有变动的额外表单。如果我们提供了一篇无效的文章:

  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 方法来检查formset中有多少错误:

  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

The error_messages argument lets you override the default messages that the formset will raise. Pass in a dictionary with keys matching the error messages you want to override. Error message keys include 'too_few_forms', 'too_many_forms', and 'missing_management_form'. The 'too_few_forms' and 'too_many_forms' error messages may contain %(num)d, which will be replaced with min_num and max_num, respectively.

For example, here is the default error message when the management form is missing:

  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({}, error_messages={'missing_management_form': 'Sorry, something went wrong.'})
  2. >>> formset.is_valid()
  3. False
  4. >>> formset.non_form_errors()
  5. ['Sorry, something went wrong.']

Changed in Django 4.1:

The 'too_few_forms' and 'too_many_forms' keys were added.

自定义formset验证

formset有个与 Form 类相似的 clean 方法。您可以在这里定义自己的验证规则,它会在formset层面进行验证。

  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 = []
  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.append(title)
  19. >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
  20. >>> data = {
  21. ... 'form-TOTAL_FORMS': '2',
  22. ... 'form-INITIAL_FORMS': '0',
  23. ... 'form-0-title': 'Test',
  24. ... 'form-0-pub_date': '1904-06-16',
  25. ... 'form-1-title': 'Test',
  26. ... 'form-1-pub_date': '1912-06-23',
  27. ... }
  28. >>> formset = ArticleFormSet(data)
  29. >>> formset.is_valid()
  30. False
  31. >>> formset.errors
  32. [{}, {}]
  33. >>> formset.non_form_errors()
  34. ['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>

Changed in Django 4.0:

增加了额外的 nonform 类。

验证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 进行验证。

The error message can be customized by passing the 'too_many_forms' message to the error_messages argument.

备注

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

The error message can be customized by passing the 'too_few_forms' message to the error_messages argument.

备注

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

处理表单的排序和删除

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

can_order

BaseFormSet.can_order

默认值: False

让你创建能排序的formset:

  1. >>> from django.forms import formset_factory
  2. >>> from myapp.forms import ArticleForm
  3. >>> ArticleFormSet = formset_factory(ArticleForm, can_order=True)
  4. >>> formset = ArticleFormSet(initial=[
  5. ... {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
  6. ... {'title': 'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
  7. ... ])
  8. >>> for form in formset:
  9. ... print(form.as_table())
  10. <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>
  11. <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>
  12. <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>
  13. <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>
  14. <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>
  15. <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>
  16. <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>
  17. <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>
  18. <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(data, initial=[
  15. ... {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
  16. ... {'title': 'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
  17. ... ])
  18. >>> formset.is_valid()
  19. True
  20. >>> for form in formset.ordered_forms:
  21. ... print(form.cleaned_data)
  22. {'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0, 'title': 'Article #3'}
  23. {'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': 'Article #2'}
  24. {'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. >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet, can_order=True)

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. >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet, can_order=True)

can_delete

BaseFormSet.can_delete

默认值: False

让你创建能删除指定表单的formset:

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

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

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

deletion_widget

New in Django 4.0.

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. >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet, can_delete=True)

get_deletion_widget

New in Django 4.0.

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. >>> 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添加额外字段

如果你想在formset中添加额外的字段,这相当简单。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. >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
  9. >>> formset = ArticleFormSet()
  10. >>> for form in formset:
  11. ... print(form.as_table())
  12. <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>
  13. <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>
  14. <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 。您可以在formset实例化的时候传递这个参数:

  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. >>> ArticleFormSet = formset_factory(MyArticleForm)
  9. >>> formset = ArticleFormSet(form_kwargs={'user': request.user})

form_kwargs 也可能依赖于特定的表单实例。formset基类提供了一个 get_form_kwargs 方法。该方法只接收一个参数——formset中表单的序列。对于 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

自定义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

Formsets have the following attributes and methods associated with rendering:

BaseFormSet.renderer

New in Django 4.0.

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

BaseFormSet.template_name

New in Django 4.0.

The name of the template rendered if the formset is cast into a string, e.g. via print(formset) or in a template via {{ formset }}.

By default, a property returning the value of the renderer’s formset_template_name. You may set it as a string template name in order to override that for a particular formset class.

This template will be used to render the formset’s management form, and then each form in the formset as per the template defined by the form’s template_name.

Changed in Django 4.1:

In older versions template_name defaulted to the string value 'django/forms/formset/default.html'.

BaseFormSet.template_name_div

New in Django 4.1.

The name of the template used when calling as_div(). By default this is "django/forms/formsets/div.html". This template renders the formset’s management form and then each form in the formset as per the form’s as_div() method.

BaseFormSet.template_name_p

New in Django 4.0.

The name of the template used when calling as_p(). By default this is "django/forms/formsets/p.html". This template renders the formset’s management form and then each form in the formset as per the form’s as_p() method.

BaseFormSet.template_name_table

New in Django 4.0.

The name of the template used when calling as_table(). By default this is "django/forms/formsets/table.html". This template renders the formset’s management form and then each form in the formset as per the form’s as_table() method.

BaseFormSet.template_name_ul

New in Django 4.0.

The name of the template used when calling as_ul(). By default this is "django/forms/formsets/ul.html". This template renders the formset’s management form and then each form in the formset as per the form’s as_ul() method.

BaseFormSet.get_context()

New in Django 4.0.

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

可用的上下文:

  • formset:表单集的实例。

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

New in Django 4.0.

渲染方法被 __str__ 以及 as_p()as_ul()as_table() 方法所调用。所有的参数都是可选的,并将默认为:

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 属性来定制,或者更普遍的是通过 覆盖默认模板

Changed in Django 4.0:

表单集的渲染被转移到模板引擎。

手动渲染 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(request, 'manage_articles.html', {
  17. 'article_formset': article_formset,
  18. 'book_formset': book_formset,
  19. })

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

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