Formsets¶
A formset is a layer of abstraction to work with multiple forms on the samepage. It can be best compared to a data grid. Let’s say you have the followingform:
>>>fromdjangoimportforms>>>classArticleForm(forms.Form):...title=forms.CharField()...pub_date=forms.DateField()
You might want to allow the user to create several articles at once. To createa formset out of anArticleForm you would do:
>>>fromdjango.formsimportformset_factory>>>ArticleFormSet=formset_factory(ArticleForm)
You now have created a formset namedArticleFormSet. The formset gives youthe ability to iterate over the forms in the formset and display them as youwould with a regular form:
>>>formset=ArticleFormSet()>>>forforminformset:...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>
As you can see it only displayed one empty form. The number of empty formsthat is displayed is controlled by theextra parameter. By default,formset_factory() defines one extra form; thefollowing example will display two blank forms:
>>>ArticleFormSet=formset_factory(ArticleForm,extra=2)
Iterating over theformset will render the forms in the order they werecreated. You can change this order by providing an alternate implementation forthe__iter__() method.
Formsets can also be indexed into, which returns the corresponding form. If youoverride__iter__, you will need to also override__getitem__ to havematching behavior.
Using initial data with a formset¶
Initial data is what drives the main usability of a formset. As shown aboveyou can define the number of extra forms. What this means is that you aretelling the formset how many additional forms to show in addition to thenumber of forms it generates from the initial data. Let’s take a look at anexample:
>>>importdatetime>>>fromdjango.formsimportformset_factory>>>frommyapp.formsimportArticleForm>>>ArticleFormSet=formset_factory(ArticleForm,extra=2)>>>formset=ArticleFormSet(initial=[...{'title':'Django is now open source',...'pub_date':datetime.date.today(),}...])>>>forforminformset:...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>
There are now a total of three forms showing above. One for the initial datathat was passed in and two extra forms. Also note that we are passing in alist of dictionaries as the initial data.
If you use aninitial for displaying a formset, you should pass the sameinitial when processing that formset’s submission so that the formset candetect which forms were changed by the user. For example, you might havesomething like:ArticleFormSet(request.POST,initial=[...]).
Limiting the maximum number of forms¶
Themax_num parameter toformset_factory()gives you the ability to limit the number of forms the formset will display:
>>>fromdjango.formsimportformset_factory>>>frommyapp.formsimportArticleForm>>>ArticleFormSet=formset_factory(ArticleForm,extra=2,max_num=1)>>>formset=ArticleFormSet()>>>forforminformset:...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>
If the value ofmax_num is greater than the number of existing items in theinitial data, up toextra additional blank forms will be added to theformset, so long as the total number of forms does not exceedmax_num. Forexample, ifextra=2 andmax_num=2 and the formset is initialized withoneinitial item, a form for the initial item and one blank form will bedisplayed.
If the number of items in the initial data exceedsmax_num, all initialdata forms will be displayed regardless of the value ofmax_num and noextra forms will be displayed. For example, ifextra=3 andmax_num=1and the formset is initialized with two initial items, two forms with theinitial data will be displayed.
Amax_num value ofNone (the default) puts a high limit on the numberof forms displayed (1000). In practice this is equivalent to no limit.
By default,max_num only affects how many forms are displayed and does notaffect validation. Ifvalidate_max=True is passed to theformset_factory(), thenmax_num will affectvalidation. Seevalidate_max.
Formset validation¶
Validation with a formset is almost identical to a regularForm. There isanis_valid method on the formset to provide a convenient way to validateall forms in the formset:
>>>fromdjango.formsimportformset_factory>>>frommyapp.formsimportArticleForm>>>ArticleFormSet=formset_factory(ArticleForm)>>>data={...'form-TOTAL_FORMS':'1',...'form-INITIAL_FORMS':'0',...'form-MAX_NUM_FORMS':'',...}>>>formset=ArticleFormSet(data)>>>formset.is_valid()True
We passed in no data to the formset which is resulting in a valid form. Theformset is smart enough to ignore extra forms that were not changed. If weprovide an invalid article:
>>>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.']}]
As we can see,formset.errors is a list whose entries correspond to theforms in the formset. Validation was performed for each of the two forms, andthe expected error message appears for the second item.
Just like when using a normalForm, each field in a formset’s forms mayinclude HTML attributes such asmaxlength for browser validation. However,form fields of formsets won’t include therequired attribute as thatvalidation may be incorrect when adding and deleting forms.
To check how many errors there are in the formset, we can use thetotal_error_count method:
>>># Using the previous example>>>formset.errors[{}, {'pub_date': ['This field is required.']}]>>>len(formset.errors)2>>>formset.total_error_count()1
We can also check if form data differs from the initial data (i.e. the form wassent 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
Understanding theManagementForm¶
You may have noticed the additional data (form-TOTAL_FORMS,form-INITIAL_FORMS andform-MAX_NUM_FORMS) that was requiredin the formset’s data above. This data is required for theManagementForm. This form is used by the formset to manage thecollection of forms contained in the formset. If you don’t providethis management data, an exception will be raised:
>>>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']
It is used to keep track of how many form instances are being displayed. Ifyou are adding new forms via JavaScript, you should increment the count fieldsin this form as well. On the other hand, if you are using JavaScript to allowdeletion of existing objects, then you need to ensure the ones being removedare properly marked for deletion by includingform-#-DELETE in thePOSTdata. It is expected that all forms are present in thePOST data regardless.
The management form is available as an attribute of the formsetitself. When rendering a formset in a template, you can include allthe management data by rendering{{my_formset.management_form}}(substituting the name of your formset as appropriate).
total_form_count andinitial_form_count¶
BaseFormSet has a couple of methods that are closely related to theManagementForm,total_form_count andinitial_form_count.
total_form_count returns the total number of forms in this formset.initial_form_count returns the number of forms in the formset that werepre-filled, and is also used to determine how many forms are required. Youwill probably never need to override either of these methods, so please besure you understand what they do before doing so.
empty_form¶
BaseFormSet provides an additional attributeempty_form which returnsa form instance with a prefix of__prefix__ for easier use in dynamicforms with JavaScript.
Custom formset validation¶
A formset has aclean method similar to the one on aForm class. Thisis where you define your own validation that works at the formset level:
>>>fromdjango.formsimportBaseFormSet>>>fromdjango.formsimportformset_factory>>>frommyapp.formsimportArticleForm>>>classBaseArticleFormSet(BaseFormSet):...defclean(self):..."""Checks that no two articles have the same title."""...ifany(self.errors):...# Don't bother validating the formset unless each form is valid on its own...return...titles=[]...forforminself.forms:...title=form.cleaned_data['title']...iftitleintitles:...raiseforms.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.']
The formsetclean method is called after all theForm.clean methodshave been called. The errors will be found using thenon_form_errors()method on the formset.
Validating the number of forms in a formset¶
Django provides a couple ways to validate the minimum or maximum number ofsubmitted forms. Applications which need more customizable validation of thenumber of forms should use custom formset validation.
validate_max¶
Ifvalidate_max=True is passed toformset_factory(), validation will also checkthat the number of forms in the data set, minus those marked fordeletion, is less than or equal tomax_num.
>>>fromdjango.formsimportformset_factory>>>frommyapp.formsimportArticleForm>>>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 againstmax_num strictly even ifmax_num was exceeded because the amount of initial data supplied wasexcessive.
Note
Regardless ofvalidate_max, if the number of forms in a data setexceedsmax_num by more than 1000, then the form will fail to validateas ifvalidate_max were set, and additionally only the first 1000forms abovemax_num will be validated. The remainder will betruncated entirely. This is to protect against memory exhaustion attacksusing forged POST requests.
validate_min¶
Ifvalidate_min=True is passed toformset_factory(), validation will also checkthat the number of forms in the data set, minus those marked fordeletion, is greater than or equal tomin_num.
>>>fromdjango.formsimportformset_factory>>>frommyapp.formsimportArticleForm>>>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.']
Dealing with ordering and deletion of forms¶
Theformset_factory() provides two optionalparameterscan_order andcan_delete to help with ordering of forms informsets and deletion of forms from a formset.
can_order¶
BaseFormSet.can_order¶
Default:False
Lets you create a formset with the ability to order:
>>>fromdjango.formsimportformset_factory>>>frommyapp.formsimportArticleForm>>>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)},...])>>>forforminformset:...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>
This adds an additional field to each form. This new field is namedORDERand is anforms.IntegerField. For the forms that came from the initialdata it automatically assigned them a numeric value. Let’s look at what willhappen when the user changes these values:
>>>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>>>forforminformset.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¶
Default:False
Lets you create a formset with the ability to select forms for deletion:
>>>fromdjango.formsimportformset_factory>>>frommyapp.formsimportArticleForm>>>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)},...])>>>forforminformset:...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>
Similar tocan_order this adds a new field to each form namedDELETEand is aforms.BooleanField. When data comes through marking any of thedelete fields you can access them withdeleted_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_dataforforminformset.deleted_forms][{'DELETE': True, 'pub_date': datetime.date(2008, 5, 10), 'title': 'Article #1'}]
If you are using aModelFormSet,model instances for deleted forms will be deleted when you callformset.save().
If you callformset.save(commit=False), objects will not be deletedautomatically. You’ll need to calldelete() on each of theformset.deleted_objects to actually deletethem:
>>>instances=formset.save(commit=False)>>>forobjinformset.deleted_objects:...obj.delete()
On the other hand, if you are using a plainFormSet, it’s up to you tohandleformset.deleted_forms, perhaps in your formset’ssave() method,as there’s no general notion of what it means to delete a form.
Adding additional fields to a formset¶
If you need to add additional fields to the formset this can be easilyaccomplished. The formset base class provides anadd_fields method. Youcan simply override this method to add your own fields or even redefine thedefault fields/attributes of the order and deletion fields:
>>>fromdjango.formsimportBaseFormSet>>>fromdjango.formsimportformset_factory>>>frommyapp.formsimportArticleForm>>>classBaseArticleFormSet(BaseFormSet):...defadd_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()>>>forforminformset:...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>
Passing custom parameters to formset forms¶
Sometimes your form class takes custom parameters, likeMyArticleForm.You can pass this parameter when instantiating the formset:
>>>fromdjango.formsimportBaseFormSet>>>fromdjango.formsimportformset_factory>>>frommyapp.formsimportArticleForm>>>classMyArticleForm(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})
Theform_kwargs may also depend on the specific form instance. The formsetbase class provides aget_form_kwargs method. The method takes a singleargument - the index of the form in the formset. The index isNone for theempty_form:
>>>fromdjango.formsimportBaseFormSet>>>fromdjango.formsimportformset_factory>>>classBaseArticleFormSet(BaseFormSet):...defget_form_kwargs(self,index):...kwargs=super(BaseArticleFormSet,self).get_form_kwargs(index)...kwargs['custom_kwarg']=index...returnkwargs
Theform_kwargs argument was added.
Using a formset in views and templates¶
Using a formset inside a view is as easy as using a regularForm class.The only thing you will want to be aware of is making sure to use themanagement form inside the template. Let’s look at a sample view:
fromdjango.formsimportformset_factoryfromdjango.shortcutsimportrenderfrommyapp.formsimportArticleFormdefmanage_articles(request):ArticleFormSet=formset_factory(ArticleForm)ifrequest.method=='POST':formset=ArticleFormSet(request.POST,request.FILES)ifformset.is_valid():# do something with the formset.cleaned_datapasselse:formset=ArticleFormSet()returnrender(request,'manage_articles.html',{'formset':formset})
Themanage_articles.html template might look like this:
<formmethod="post"action="">{{formset.management_form}}<table>{%forforminformset%}{{form}}{%endfor%}</table></form>
However there’s a slight shortcut for the above by letting the formset itselfdeal with the management form:
<formmethod="post"action=""><table>{{formset}}</table></form>
The above ends up calling theas_table method on the formset class.
Manually renderedcan_delete andcan_order¶
If you manually render fields in the template, you can rendercan_delete parameter with{{form.DELETE}}:
<formmethod="post"action="">{{formset.management_form}}{%forforminformset%}<ul><li>{{form.title}}</li><li>{{form.pub_date}}</li>{%ifformset.can_delete%}<li>{{form.DELETE}}</li>{%endif%}</ul>{%endfor%}</form>
Similarly, if the formset has the ability to order (can_order=True), it ispossible to render it with{{form.ORDER}}.
Using more than one formset in a view¶
You are able to use more than one formset in a view if you like. Formsetsborrow much of its behavior from forms. With that said you are able to useprefix to prefix formset form field names with a given value to allowmore than one formset to be sent to a view without name clashing. Lets takea look at how this might be accomplished:
fromdjango.formsimportformset_factoryfromdjango.shortcutsimportrenderfrommyapp.formsimportArticleForm,BookFormdefmanage_articles(request):ArticleFormSet=formset_factory(ArticleForm)BookFormSet=formset_factory(BookForm)ifrequest.method=='POST':article_formset=ArticleFormSet(request.POST,request.FILES,prefix='articles')book_formset=BookFormSet(request.POST,request.FILES,prefix='books')ifarticle_formset.is_valid()andbook_formset.is_valid():# do something with the cleaned_data on the formsets.passelse:article_formset=ArticleFormSet(prefix='articles')book_formset=BookFormSet(prefix='books')returnrender(request,'manage_articles.html',{'article_formset':article_formset,'book_formset':book_formset,})
You would then render the formsets as normal. It is important to point outthat you need to passprefix on both the POST and non-POST cases so thatit is rendered and processed correctly.

