Django Formsets总结
formset
是将多个表单用在同一个页面上的抽象层。
我们有:
from django import forms
class ArticleForm(forms.Form):
title=forms.CharField()
pub_date=forms.DateField()
为允许一次性创建几个articles
,可以创建一个ArticleForm
的formset类ArticleFormSet
:
>>>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()
定义了一个extra form
。
对formset使用初始值
初始值的使用是formset的一大用法,对FormSet
类的实例使用参数initial
,其值为包含有字典的列表,字典的键就是form
种定义的字段名,字典的数量就是初始化的表单的数量。
>>>import datetime
>>>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>
可以看到一共显示了3个表单,1个是初始化的,2个空表单。
限制表单的最大数量
formset_factory
的max_num
参数用来限制显示的表单数。
>>>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>
显示的数量其实是由initial
,max_num
,extra
参数共同决定的:
-
当
initial
和extra
之和(可以理解为应该显示的表单数量),小于或者等于max_num
时,全部显示 -
当
initial
和extra
之和大于max_num
时,保证初始化的都显示,即:- 如果
initial
小于max_num
时,初始化的全部显示,显示max_num
与initial
之差数量的空表单 - 如果
initial
大于max_num
时,初始化的全部显示,没有空表单。
max_num
的值默认为None,即显示1000个表单,在实际应用种,这基本等于没有限制了。默认地,
max_num
仅影响有多少表单显示,而不影响校验。如果validate_max=True
,那么max_num
就影响校验。FormSet校验
formset
的校验与form
的校验基本相同。调用is_valid()
就可以校验formset的所有表单。>>> data={ ... 'form-TOTAL_FORMS':'2', ... 'form-INITIAL_FORMS':'0', ... 'form-MAX_NUM_FORMS':'', ... 'form-0-title':'test', ... 'form-0-pub_date':'1904-06-06', ... 'form-1-title':'TEST', ... 'form-1-pub_date':'', ... } >>> formset=ArticleFormSet(data) >>> formset.errors [{}, {'pub_date': ['This field is required.']}] >>> ss=ArticleFormSet(data) >>> ss.errors [{}, {'pub_date': ['This field is required.']}] >>> ss.is_valid() False
可以看到
formset.errors
是对应于formset表单的列表,列表中每个字典的键是出现异常的字段名,需要注意的是,不需要调用is_valid()
,就有errors
属性。与
Form
用法类似,formset
的每个form
可以包含maxlength
等的HTML使用的属性作为浏览器的校验。然而formset
的form
不能包含required属性,因为添加或者删除表单时,校验有可能出错。为了得到formset的校验错误数量,可以用
total_error_count
:>>> len(ss.errors) 2 >>> formset.total_error_count() 1
还可以用
has_changed()
来判断是否初始值被改变了。>>> data={ ... 'form-TOTAL_FORMS':'1', ... 'form-INITIAL_FORMS':'0', ... 'form-MAX_NUM_FORMS':'', ... 'form-0-title':'', ... 'form-0-pub_date':''} >>> k=ArticleFormSet(data) >>> k.has_changed() #k的初始值是空字符,现在还是空字符 False >>> ss.has_changed() True
理解
ManagementForm
form-TOTAl_FORMS
,form-INITIAL_FORMS
,form-MAX_NUM_FORMS
就是ManagementForm
的字段。这个表单就在专门用来管理formset中的所有表单的,如果不提供这个管理数据,就会抛出错误。
它是用来追踪有多少显示的表单。如果通过JavaScript添加了新表单,那么也应该相应的增加
ManagementForm
中表单的数量。management_form
也是可以作为formset
本身的属性,在template中,可以通过{{ my_formset.management_form }}来包含其管理数据。所以,在写template时候,必须包含{{ my_formset.management }} 否则,
myform=formset(request.POST)
中就缺失ManagementForm
的数据,就会抛出异常:total_form_count,initial_form_count
对于
ManagementForm
,有2个方法与之密切相连:total_form_count
,intial_form_count
。>>> ss.initial_form_count() 0 >>> ss.total_form_count() 2
定制formset 校验
formset
有一个与Form
类似的clean
方法,这就是可以定制校验的地方。可以通过重载#forms.py DRINKS=((None,'Please select a drink type'),(1,'Mocha'),(2,'Espresso'),(3,'Latte')) SIZES=((None,'Please select a drink size'),('s','Small'),('m','Medium'),('1','Large')) class DrinkForm(forms.Form): name=forms.ChoiceField(choices=DRINKS,initial=0) size=forms.ChoiceField(choices=SIZES,initial=0) amount=forms.ChoiceField(choices=[(None,'Amount of drinks')]+[(i,i) for i in range(1,10)]) from django.forms import BaseFormSet class BaseDrinkFormSet(BaseFormSet): def clean(self): if any(self.errors): return None name_size_tuples=[] for form in self.forms: name_size=(form.cleaned_data['name'],form.cleaned_data['size']) if name_size in name_size_tuples: raise forms.ValidationError('UPs! You have multiple %s %s items in your order'%(dict(SIZES)[name_size[1]],dict(DRINKS)[int(name_size[0])])) name_size_tuples.append(name_size)
以上继承并重载了
BaseFormSet
类,在BaseDrinkFormSet
中,重载了类方法clean()
,在该方法中,若发现有某个表单有异常(即any(self.errors),直接结束,如果在所有表单都是正确的情况下,对所有表单的name
,size
字段进行核查,有这两个字段都相同的,抛出异常。>>> from testapp.forms import * >>> from django.forms import formset_factory >>> a=formset_factory(DrinkForm,formset=BaseDrinkFormSet) >>> data={ ... 'form-TOTAL_FORMS':'2', ... 'form-INITIAL_FORMS':'0', ... 'form-MAX_NUM_FORMS':'', ... 'form-0-name':1, ... 'form-0-size':'s', ... 'form-0-amount':1, ... 'form-1-name':1, ... 'form-1-size':'s', ... 'form-1-amount':2,} >>> formset=a(data) >>> formset.errors [{}, {}] >>> formset.non_form_errors() ['UPs! You have multiple Small Mocha items in your order']
formset的
clean()
在所有的Form.clean()
调用后,被调用(所有可以在formset的clean()
方法中遍历formset,并使用每个form的cleaned_data
,用non_form_errors()
来发现找到的错误。校验formset中的表单数量
validate_max
如果
validate_max=True
传入formset_factory()
,也会校验formset
中的表单数减去标记为删减的表单是否超过了max_num
。>>> from django.forms import formset_factory >>> from testapp.forms import * >>> drinkformset=formset_factory(DrinkForm,max_num=1,validate_max=True) >>> data={ ... 'form-TOTAL_FORMS':'2', ... 'form-INITIAL_FORMS':'0', ... 'form-MIN_NUM_FORMS':'', ... 'form-MAX_NUIM_FORMS':'', ... 'form-0-name':2, ... 'form-0-size':'s', ... 'form-0-amount':1, ... 'form-1-name':3, ... 'form-1-size':'m', ... 'form-1-amount':2} >>> formset=drinkformset(data) >>> formset.is_valid() False >>> formset.errors [{}, {}] >>> formset.non_form_errors() ['Please submit 1 or fewer forms.']
validate_min
与
validate_max
类似。>>> drinkformset=formset_factory(DrinkForm,min_num=3,validate_min=True) >>> data={ ... 'form-TOTAL_FORMS':'2', ... 'form-INITIAL_FORMS':'0', ... 'form-MIN_NUM_FORMS':'', ... 'form-MAX_NUIM_FORMS':'', ... 'form-0-name':2, ... 'form-0-size':'s', ... 'form-0-amount':1, ... 'form-1-name':3, ... 'form-1-size':'m', ... 'form-1-amount':2} >>> formset=drinkformset(data) >>> formset.is_valid() False >>> formset.non_form_errors() ['Please submit 3 or more forms.'] >>> form.errors Traceback (most recent call last): File "<console>", line 1, in <module> NameError: name 'form' is not defined >>> formset.errors [{}, {}]
>>> drinkformset=formset_factory(DrinkForm,min_num=3,validate_min=True) >>> data={ ... 'form-TOTAL_FORMS':'2', ... 'form-INITIAL_FORMS':'0', ... 'form-MIN_NUM_FORMS':'', ... 'form-MAX_NUIM_FORMS':'', ... 'form-0-name':2, ... 'form-0-size':'s', ... 'form-0-amount':1, ... 'form-1-name':3, ... 'form-1-size':'m', ... 'form-1-amount':2} >>> formset=drinkformset(data) >>> formset.is_valid() False >>> formset.non_form_errors() ['Please submit 3 or more forms.'] >>> formset.errors [{}, {}]
表单序号(Ordering)和删除(Deletion)
can_order
默认是
False
.>>> articleFormset=formset_factory(ArticleForm,can_order=True) >>> import datetime >>> 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>
可见,自动为每个form添加了
order
字段。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.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_delete=True
为每个form添加了个删除校对框。如果在使用
ModelFormSet
,对于选中的删除的表单,在执行formset.save()
时,该表单的实例将会被删除。但是如果调用formset.save(commit=False)
,将不会删除,需要用formset.deleted_objects
来删除:>>> instances = formset.save(commit=False) >>> for obj in formset.deleted_objects: ... obj.delete()
在formset里添加额外字段
添加额外自定义的字段,只需要继承
BaseFormSet
类,然后重载其函数add_fields(self.form,index)
即可。class ArticleForm(forms.Form): title=forms.CharField() pub_date=forms.DateField() class BaseArticleFormSet(BaseFormSet): def add_fields(self,form,index): super().add_fields(form,index) form.fields['my_field']=forms.CharField()
>>> from django.forms import formset_factory >>> from testapp.forms import * >>> 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>
可以发现已经添加了自定义字段
my field
.自定义formset的前缀
默认前缀都是
form
,比如:<label for="id_form-0-title">Title:</label> <input type="text" name="form-0-title" id="id_form-0-title">
>>> formset=formset_factory(ArticleForm) >>> articleFormset=formset_factory(ArticleForm) >>> formset=articleFormset(prefix='article') >>> for form in formset: ... print(form.as_table()) ... <tr><th><label for="id_article-0-title">Title:</label></th><td><input type="text" name="article-0-title" id="id_article-0-title"></td></tr> <tr><th><label for="id_article-0-pub_date">Pub date:</label></th><td><input type="text" name="article-0-pub_date" id="id_article-0-pub_date"></td></tr>
- 如果
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix