Django 表单集

class BaseFormSet

表单集是用于在同一页面处理多个表单的一层抽象,可以把它理解为数据网格。假设已有如下表单:

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

要让用户一次创建多篇文章,可以从 ArticleForm 创建表单集:

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

现在得到名为 ArticleFormSet 的表单集类。实例化后可迭代其中的表单,像普通表单一样显示:

>>> formset = ArticleFormSet()
>>> for form in formset:
...     print(form)
...
<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)

表单集支持迭代和索引,按创建顺序访问表单。必要时可以覆盖默认的 __iter__() 与 __getitem__(),改变顺序;实现方法见 Python 序列协议文档。

使用初始数据

初始数据是表单集实用性的关键。extra 指在初始数据生成的表单之外,再显示多少空表单:

>>> 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 个空表单。例如 extra=2、max_num=2 且有一个初始条目时,显示一个初始表单和一个空表单。

若初始条目数已超过 max_num,仍显示全部初始表单,不显示额外表单。例如 extra=3、max_num=1、两个初始条目,仍显示两个初始表单。

默认 max_num=None 表示1000这个较高上限,实践中相当于不限制。默认它只影响显示,不影响验证;传入 validate_max=True 才将其用于验证,见后文。

限制实例化的表单数量

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_max=None 时默认 max_num + 1000;若 max_num 也是 None,默认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 一样,字段可带 maxlength 等浏览器验证属性;但不会包含 required,因为动态增删表单时该验证可能不正确。

BaseFormSet.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_FORMS 和 form-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 }} 即可包含全部管理数据,其中变量名应按实际表单集替换。

total_form_count 与 initial_form_count

BaseFormSet 的这两个方法与 ManagementForm 密切相关。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。前两类可包含 %(num)d,分别替换成 min_num 和 max_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.']

自定义表单集验证

表单集有类似 Form 的 clean(),可在此定义跨表单验证:

>>> from django.core.exceptions import ValidationError
>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm

>>> class BaseArticleFormSet(BaseFormSet):
...     def clean(self):
...         """Checks that no two articles have the same title."""
...         if any(self.errors):
...             # Don't bother validating the formset unless each form is valid on its own
...             return
...         titles = 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 CSS 类,以便与单个表单错误区分。例如 {{ formset.non_form_errors }}:

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

验证表单数量

Django 提供提交表单数的最小、最大值验证。需要更灵活的数量规则时,应使用自定义表单集验证。

validate_max

给 formset_factory() 传 validate_max=True 后,还会检查提交表单数减去标记删除的数量,是否不超过 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.']

这一验证严格执行,即使超过 max_num 是因初始数据过多。可在 error_messages 设置 too_many_forms 自定义消息。

validate_min

传入 validate_min=True 后,还会检查提交表单数减去标记删除的数量,是否至少为 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.']

可通过 error_messages 的 too_few_forms 自定义消息。

排序和删除表单

formset_factory() 提供 can_order 与 can_delete 两个可选参数,支持排序和删除。

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)},
...     ],
... )
>>> 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 还提供 ordering_widget 和 get_ordering_widget(),控制排序字段部件。

BaseFormSet.ordering_widget

默认 NumberInput。设置所用部件类:

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

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

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>

每个表单增加名为 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]
[{'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10), 'DELETE': True}]

使用 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() 控制删除字段部件。

BaseFormSet.deletion_widget

默认 CheckboxInput。设置部件类:

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

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

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

表单集转成字符串时使用的模板名,例如 print(formset) 或模板里的 {{ formset }}。默认属性返回渲染器的 formset_template_name;可以设置字符串模板名,为特定表单集类覆盖。模板先渲染管理表单,再按照各表单的 Form.template_name 渲染每个表单。

各布局模板

BaseFormSet.get_context()

返回模板渲染上下文,其中 formset 为当前表单集实例。

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

__str__、as_div()、as_p()、as_ul() 和 as_table() 都调用它。参数均可选,默认 template_name 来自同名属性,context 来自 get_context(),renderer 来自同名属性。

BaseFormSet.as_div()、as_p()、as_table()、as_ul() 分别使用 template_name_div、template_name_p、template_name_table、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_div 渲染,也支持 as_p、as_ul、as_table。可以指定 template_name,或覆盖内置模板来自定义渲染。

手动渲染 can_delete 与 can_order

若手动渲染字段,可用 {{ form.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 两条路径都必须传入相同前缀,才能正确渲染和处理。每个表单集的前缀会替换所有字段 HTML name 和 id 属性中的默认 form 前缀。

来源:Django 文档贡献者,Django 6.1:表单集。中文整理并补译官方页中的英文段落,示例源码保持原文。Copyright (c) Django Software Foundation and individual contributors,BSD 三条款许可。

原项目许可文本
Copyright (c) Django Software Foundation and individual contributors.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

    1. Redistributions of source code must retain the above copyright notice,
       this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in the
       documentation and/or other materials provided with the distribution.

    3. Neither the name of Django nor the names of its contributors may be used
       to endorse or promote products derived from this software without
       specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容