表单集

优质
小牛编辑
142浏览
2023-12-01

class BaseFormSet[source]

表单集是同一个页面上多个表单的抽象。它非常类似于一个数据表格。假设有下述表单:

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

你可能希望允许用户一次创建多个Article。你可以根据ArticleForm 创建一个表单集:

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

你已经创建一个命名为ArticleFormSet 的表单集。表单集让你能迭代表单集中的表单并显示它们,就和普通的表单一样:

>>> formset = ArticleFormSet()
>>> for form in formset:
...     print(form.as_table())
<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>
<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() 定义一个表单;下面的示例将显示两个空表单:

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

formset 的迭代将以它们创建时的顺序渲染表单。通过提供一个__iter__() 方法,可以改变这个顺序。

表单集还可以索引,它将返回对应的表单。如果覆盖__iter__,你还需要覆盖__getitem__ 以获得一致的行为。

表单集的初始数据

初始数据体现着表单集的主要功能。如上所述,你可以定义表单的数目。它表示除了从初始数据生成的表单之外,还要生成多少个额外的表单。让我们看个例子:

>>> import datetime
>>> from django.forms.formsets 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.as_table())
<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>
<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>
<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>
<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>
<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>
<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>

上面现在一共有三个表单。一个是初始数据生成的,还有两个是额外的表单。还要注意的是,我们传递的初始数据是一个由字典组成的列表。

另见

利用模型表单集从模型中创建表单集

限制表单的最大数量

formset_factory()max_num 参数 ,给予你限制表单集展示表单个数的能力

>>> from django.forms.formsets 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.as_table())
<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>
<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,并且用一个initial项初始化表单集,将显示空白表单。

假如初始化数据的条目超过 max_num的值, 所有初始化数据表单都会被展现并且忽视 max_num值的限定 ,而且不会有额外的表单被呈现。比如, 如果extra=3max_num=1 并且表单集由两个初始化条蜜,那么两个带有初始化数据的表单将被呈现。

max_num 的值为 None (默认值) 等同于限制了一个比较高的展现表单数目(1000个). 实际上就是等同于没限制.

默认的, max_num 只影响了表单的数目展示,但不影响验证. 假如 validate_max=True 传给了 formset_factory(), 然后 max_num才将会影响验证. 请参阅Validating the number of forms in a formset

表单验证

表单集的验证几乎和 一般的Form一样. 表单集里面有一个 is_valid 的方法来提供快捷的验证所有表单的功能。

>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm)
>>> data = {
...     'form-TOTAL_FORMS': '1',
...     'form-INITIAL_FORMS': '0',
...     'form-MAX_NUM_FORMS': '',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
True

我们没有传递任何数据到formset,导致一个有效的形式。表单集足够聪明,可以忽略未更改的其他表单。如果我们提供无效的文章:

>>> data = {
...     'form-TOTAL_FORMS': '2',
...     'form-INITIAL_FORMS': '0',
...     'form-MAX_NUM_FORMS': '',
...     '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 是一个列表, 他包含的错误信息正好与表单集内的表单一一对应错误检查会在两个表单中分别执行,被预见的错误出现错误列表的第二项

BaseFormSet.``total_error_count()[source]

想知道表单集内有多少个错误可以使用total_error_count方法

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

我们也可以检查表单数据是否从初始值发生了变化 (i.e. the form was sent without any data):

>>> data = {
...     'form-TOTAL_FORMS': '1',
...     'form-INITIAL_FORMS': '0',
...     'form-MAX_NUM_FORMS': '',
...     'form-0-title': '',
...     'form-0-pub_date': '',
... }
>>> formset = ArticleFormSet(data)
>>> formset.has_changed()
False

了解ManagementForm

你也许已经注意到了那些附加的数据 (form-TOTAL_FORMS, form-INITIAL_FORMS and form-MAX_NUM_FORMS) 他们是必要的,且必须位于表单集数据的最上方 这些必须传递给ManagementForm. ManagementFormThis 用于管理表单集中的表单. 如果你不提供这些数据,将会触发异常

>>> data = {
...     'form-0-title': 'Test',
...     'form-0-pub_date': '',
... }
>>> formset = ArticleFormSet(data)
Traceback (most recent call last):
...
django.forms.utils.ValidationError: ['ManagementForm data is missing or has been tampered with']

也同样用于记录多少的表单实例将被展示如果您通过JavaScript添加新表单,则应该增加此表单中的计数字段。On the other hand, if you are using JavaScript to allow deletion of existing objects, then you need to ensure the ones being removed are properly marked for deletion by including form-#-DELETE in the POST data. 期望所有形式存在于POST数据中。

管理表单可用作表单集本身的属性。在模板中呈现表单集时,您可以通过呈现{{ my_formset.management_form }} t0&gt;(替换您的formset的名称适当)。

total_form_count and

BaseFormSet有一些与ManagementFormtotal_form_countinitial_form_count密切相关的方法。

total_form_count返回此表单集中的表单总数。initial_form_count返回Formset中预填充的表单数,也用于确定需要多少表单。你可能永远不需要重写这些方法,所以请确保你明白他们做什么之前这样做。

empty_form

BaseFormSet提供了一个附加属性empty_form,它返回一个前缀为__prefix__的表单实例,以便于使用JavaScript的动态表单。

自定义表单集验证

A formset has a clean method similar to the one on a Form class. 这是您定义自己的验证,在formset级别工作:

>>> from django.forms.formsets import BaseFormSet
>>> from django.forms.formsets 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 = []
...         for form in self.forms:
...             title = form.cleaned_data['title']
...             if title in titles:
...                 raise forms.ValidationError("Articles in a set must have distinct titles.")
...             titles.append(title)

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> data = {
...     'form-TOTAL_FORMS': '2',
...     'form-INITIAL_FORMS': '0',
...     'form-MAX_NUM_FORMS': '',
...     '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方法被调用后,调用formset clean方法。将使用表单集上的non_form_errors()方法找到错误。

验证表单集中的表单数

Django 提供了两种方法去检查表单能够提交的最大数和最小数,应用如果需要更多的关于提交数量的自定义验证逻辑,应该使用自定义表单击验证

validate_max

I如果validate_max=True 被提交给 formset_factory(), validation 将在数据集中检查被提交表单的数量, 减去被标记删除的, 必须小于等于max_num.

>>> from django.forms.formsets 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-MIN_NUM_FORMS': '',
...     'form-MAX_NUM_FORMS': '',
...     '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 1 or fewer forms.']

validate_max=True validates 将会对max_num 严格限制,即使提供的初始数据超过 max_num 而导致其无效

注意

Regardless of validate_max, if the number of forms in a data set exceeds max_num by more than 1000, then the form will fail to validate as if validate_max were set, and additionally only the first 1000 forms above max_num will be validated. 剩余部分将被完全截断。这是为了防止使用伪造的POST请求的内存耗尽攻击。

validate_min

New in Django 1.7.

如果validate_min=True被传递到formset_factory(),验证也将检查数据集中的表格数量减去那些被标记为删除的表格数量大于或等于到min_num

>>> from django.forms.formsets 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-MIN_NUM_FORMS': '',
...     'form-MAX_NUM_FORMS': '',
...     '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 3 or more forms.']

Changed in Django 1.7:

min_numvalidate_min参数添加到formset_factory()中。

表单的排序和删除行为

formset_factory()提供两个可选参数can_ordercan_delete 来实现表单集中表单的排序和删除。

can_order

BaseFormSet.``can_order

默认值:False

使你创建能排序的表单集。

>>> from django.forms.formsets 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.as_table())
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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类型。它根据初始数据,为这些表单自动生成数值。下面让我们看一下,如果用户改变这个值会发生什么变化:

>>> data = {
...     'form-TOTAL_FORMS': '3',
...     'form-INITIAL_FORMS': '2',
...     'form-MAX_NUM_FORMS': '',
...     'form-0-title': 'Article #1',
...     'form-0-pub_date': '2008-05-10',
...     'form-0-ORDER': '2',
...     'form-1-title': 'Article #2',
...     'form-1-pub_date': '2008-05-11',
...     'form-1-ORDER': '1',
...     'form-2-title': 'Article #3',
...     'form-2-pub_date': '2008-05-01',
...     'form-2-ORDER': '0',
... }

>>> formset = ArticleFormSet(data, initial=[
...     {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
...     {'title': 'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> formset.is_valid()
True
>>> for form in formset.ordered_forms:
...     print(form.cleaned_data)
{'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0, 'title': 'Article #3'}
{'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': 'Article #2'}
{'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': 'Article #1'}

can_delete

BaseFormSet.``can_delete

默认值:False

使你创建一个表单集,可以选择删除一些表单。

>>> from django.forms.formsets 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.as_table())
<input type="hidden" name="form-TOTAL_FORMS" value="3" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="2" id="id_form-INITIAL_FORMS" /><input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS" />
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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来获取标记删除字段的数据:

>>> data = {
...     'form-TOTAL_FORMS': '3',
...     'form-INITIAL_FORMS': '2',
...     'form-MAX_NUM_FORMS': '',
...     'form-0-title': 'Article #1',
...     'form-0-pub_date': '2008-05-10',
...     'form-0-DELETE': 'on',
...     'form-1-title': 'Article #2',
...     'form-1-pub_date': '2008-05-11',
...     'form-1-DELETE': '',
...     'form-2-title': '',
...     'form-2-pub_date': '',
...     'form-2-DELETE': '',
... }

>>> formset = ArticleFormSet(data, initial=[
...     {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
...     {'title': 'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> [form.cleaned_data for form in formset.deleted_forms]
[{'DELETE': True, 'pub_date': datetime.date(2008, 5, 10), 'title': 'Article #1'}]

如果你使用 ModelFormSet,调用 formset.save() 将删除那些有删除标记的表单的模型实例。

Changed in Django 1.7:

如果你调用formset.save(commit=False), 对像将不会被自动删除。你需要调用formset.deleted_objects每个对像的 delete() 来真正删除他们。

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

如果你想保持向前兼容 Django 1.6 或更早的版本,你需要这样做:

>>> try:
>>>     # For Django 1.7+
>>>     for obj in formset.deleted_objects:
>>>         obj.delete()
>>> except AssertionError:
>>>     # Django 1.6 and earlier already deletes the objects, trying to
>>>     # delete them a second time raises an AssertionError.
>>>     pass

On the other hand, if you are using a plain FormSet, it’s up to you to handle formset.deleted_forms, perhaps in your formset’s save() method, as there’s no general notion of what it means to delete a form.

给表单集添加一个额外的字段

如果你想往表单集中添加额外的字段,是十分容易完成的,表单集的基类(BaseFormSet)提供了 add_fields 方法。可以简单的通过重写这个方法来添加你自己的字段,甚至重新定义order和deletion字段的方法和属性:

>>> from django.forms.formsets import BaseFormSet
>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     def add_fields(self, form, index):
...         super(BaseArticleFormSet, self).add_fields(form, index)
...         form.fields["my_field"] = forms.CharField()

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet()
>>> for form in formset:
...     print(form.as_table())
<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>
<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>
<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>

在视图和模板中使用表单集

在视图中使用表单集就像使用标准的Form 类一样简单,唯一要做的就是确信你在模板中处理表单。让我们看一个简单视图:

from django.forms.formsets import formset_factory
from django.shortcuts import render_to_response
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_to_response('manage_articles.html', {'formset': formset})

manage_articles.html 模板也可以像这样:

<form method="post" action="">
    {{ formset.management_form }}
    <table>
        {% for form in formset 
        {{ form }}
        {% endfor </table>
</form>

不过,上面可以用一个快捷写法,让表单集来分发管理表单:

<form method="post" action="">
    <table>
        {{ formset }}
    </table>
</form>

上面表单集调用 as_table 方法。

手动渲染can_delete

如果你在模板中手工渲染字段,那么渲染 can_delete 参数用{{ form.DELETE }}:

<form method="post" action="">
    {{ 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.formsets import formset_factory
from django.shortcuts import render_to_response
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_to_response('manage_articles.html', {
        'article_formset': article_formset,
        'book_formset': book_formset,
    })

你可以以正常的方式渲染模板。记住 prefix 在POST请求和非POST 请求中均需设置,以便他能渲染和执行正确