表单集是同一个页面上多个表单的抽象。 它非常类似于一个数据表格。 假设有下述表单:
>>> from django import forms
>>> class ArticleForm(forms.Form):
... title = forms.CharField()
... pub_date = forms.DateField()
你可能希望允许用户一次创建多个Article。 你可以根据ArticleForm
创建一个表单集:
>>> from django.forms 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 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>
上面现在一共有三个表单。 一个是初始数据生成的,还有两个是额外的表单。 还要注意的是,我们传递的初始数据是一个由字典组成的列表。
如果您使用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.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
的值 比已经在初始化数据中存在的条目数目多的话, max_num
对应个数的额外空表单将会被添加到表单集, 只要表单总数不超过 extra
. 例如,如果initial
和extra=2
,并且用一个max_num=2
项初始化表单集,将显示空白表单。
假如初始化数据的条目超过 max_num
的值, 所有初始化数据表单都会被展现并且忽视 max_num
值的限定 ,而且不会有额外的表单被呈现。 比如, 如果extra=3
,max_num=1
并且表单集由两个初始化条蜜,那么两个带有初始化数据的表单将被呈现。
max_num
的值为 None
(默认值) 等同于限制了一个比较高的展现表单数目(1000个). 实际上就是等同于没限制.
默认的, max_num
只影响了表单的数目展示,但不影响验证. 假如 max_num
传给了 formset_factory()
, 然后 validate_max=True
才将会影响验证. 见validate_max。
表单集的验证几乎和 一般的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',
... '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
是一个列表, 他包含的错误信息正好与表单集内的表单一一对应 错误检查会在两个表单中分别执行,被预见的错误出现错误列表的第二项
就像使用正常的Form
一样,表单集的表单中的每个字段都可能包含HTML属性,例如用于浏览器验证的maxlength
。 但是,formets的表单域不会包含required
属性,因为添加和删除表单时验证可能不正确。
想知道表单集内有多少个错误可以使用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-MAX_NUM_FORMS
, form-TOTAL_FORMS
and form-INITIAL_FORMS
) 他们是必要的,且必须位于表单集数据的最上方 这些必须传递给ManagementForm
. ManagementFormThis 用于管理表单集中的表单. 如果你不提供这些数据,将会触发异常
>>> data = {
... 'form-0-title': 'Test',
... 'form-0-pub_date': '',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
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>(替换您的formset的名称适当)。
total_form_count
和initial_form_count
¶initial_form_count
有一些与total_form_count
,BaseFormSet
和ManagementForm
密切相关的方法。
total_form_count
返回此表单集中的表单总数。
initial_form_count
返回Formset中预填充的表单数,也用于确定需要多少表单。 你可能永远不需要重写这些方法,所以请确保你明白他们做什么之前这样做。
一个formset有一个类似于Form
类的clean
方法。 这是您定义自己的验证,在formset级别工作:
>>> 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 = []
... 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.']
在所有clean
方法被调用后,调用formset Form.clean
方法。 将使用表单集上的non_form_errors()
方法找到错误。
Django 提供了两种方法去检查表单能够提交的最大数和最小数, 应用如果需要更多的关于提交数量的自定义验证逻辑,应该使用自定义表单击验证
validate_max
¶I如果max_num
被提交给 formset_factory()
, validation 将在数据集中检查被提交表单的数量, 减去被标记删除的, 必须小于等于validate_max=True
.
>>> 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-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.']
max_num
validates 将会对validate_max=True
严格限制,即使提供的初始数据超过 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
¶如果min_num
被传递到formset_factory()
,验证也将检查数据集中的表格数量减去那些被标记为删除的表格数量大于或等于到validate_min=True
。
>>> 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-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.']
formset_factory()
提供两个可选参数can_order
和can_delete
来实现表单集中表单的排序和删除。
can_order
¶BaseFormSet。
can_order T0> ¶ T1>
默认值: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.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 T0> ¶ T1>
默认值: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.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-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()
将删除那些有删除标记的表单的模型实例。
如果你调用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
,也许在你的formset的save()
如果你想往表单集中添加额外的字段,是十分容易完成的, 表单集的基类(BaseFormSet)提供了 add_fields
方法。 可以简单的通过重写这个方法来添加你自己的字段,甚至重新定义order和deletion字段的方法和属性:
>>> 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(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>
有时,您的表单类会使用自定义参数,例如MyArticleForm
。
在实例化表单集时可以传递此参数:
>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> class MyArticleForm(ArticleForm):
... def __init__(self, *args, **kwargs):
... self.user = kwargs.pop('user')
... super(MyArticleForm, self).__init__(*args, **kwargs)
>>> ArticleFormSet = formset_factory(MyArticleForm)
>>> formset = ArticleFormSet(form_kwargs={'user': request.user})
form_kwargs
也可能取决于具体的窗体实例。 formset基类提供了一个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(BaseArticleFormSet, self).get_form_kwargs(index)
... kwargs['custom_kwarg'] = index
... return kwargs
在视图中使用表单集就像使用标准的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" 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_order
¶如果手动在模板中渲染字段,则可以使用{{ form.DELETE }}呈现
can_delete
参数/ T5> T2>:
<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 t4> }}
。
可以在视图中使用多个表单集, 表单集从表单中借鉴了很多方法 你可以使用 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,
})
你可以以正常的方式渲染模板。 记住 prefix
在POST请求和非POST 请求中均需设置,以便他能渲染和执行正确
2017年9月6日