Formsets in Django

A formset is an abstraction layer that allows you to deal with several forms on a single page. It’s easiest to relate it to a data grid. Let’s imagine you’ve got the following Form:

from django import forms

class BlogForm(forms.Form):
  title = forms.CharField()
  posted_date = forms.DateField()

You could wish to give the customer the option of creating multiple posts at once. As a result, to make a formset out of a BlogForm, follow these steps:

from django.forms import formset_factory
BlogFormSet = formset_factory(BlogForm)

You’ve now created the BlogFormSet formset class. You may now iterate over the forms in the formset and show them as you would a standard form by instantiating the formset:

formset = BlogFormSet()
for form in formset:
  print(form.as_table())

It just presented one empty form, as you can see. The extra parameter determines the number of empty forms that are displayed. formset_factory() creates an extra form by default; the following example creates a formset class that displays two blank forms:

BlogFormSet = formset_factory(BlogForm, extra=2)

When you iterate through a formset, the forms are rendered in the order they were produced. By supplying a substitute implementation for the iter() method, you can vary the order. Formsets can also be indexed into, and the corresponding form is returned. To have matching behavior, if you override iter, you must also override getitem.

Using a formset with some starting data

The central usability of a formset is determined by the initial data. You can specify the number of additional forms as indicated above. It means you’re telling the formset how many more forms it should display in addition to the ones it generates from the original data.

Consider the following scenario:

import datetime
from django.forms import formset_factory
from blogapp.forms import BlogForm

BlogFormSet = formset_factory(BlogForm, extra=2)

formset = BlogFormSet(initial=[
{'title': 'Codeunderscored now covers Django',
'posted_date': datetime.date.today(),}
])

for form in formset:
  print(form.as_table())

Above, there are now three forms to choose from. One for the primary data, as well as two more forms. It’s also worth noting that the initial data is a list of dictionaries.

If you use an initial to show a formset, you should use the same initial to process the formset’s submission so that the formset can recognize which forms the user altered. For example, you might have something like BlogFormSet(request.POST, initial=[…]).

Keeping the maximum number of forms to a minimum

The max_num option to formset_factory() allows you to limit the number of forms displayed by the formset:

from django.forms import formset_factory
from blogapp.forms import BlogForm


BlogFormSet = formset_factory(BlogForm, extra=2, max_num=1)
formset = BlogFormSet()

for form in formset:
  print(form.as_table())

If max_num is more than the number of existing items in the initial data, more blank forms are added to the formset, as long as the overall number of forms does not exceed max_num. If the formset is initialized with one initial item and extra=2 and max_num=2, an initial items’ form and one blank form will be presented.

If the number of items in the beginning data exceeds max_num, regardless of the value of max_num, all initial data forms are displayed, and no extra forms are presented. Also, if the formset is initialized with two initial items and extra=3 and max num=1, two forms containing the initial data are presented.

The default value of max_num is None, which sets a high limit on the number of forms that can be displayed (1000). In practice, this translates to “no restriction.”

By default, max_num affects only the number of forms displayed and does not affect validation. If the formset_factory() is used with validate_max=True, then max_num will have an impact on validation.

Limiting the instantiable number of forms

When submitting POST data, the absolute max option to formset_factory() limits the number of forms that can be instantiated. Memory depletion attacks based on faked POST requests are prevented as a result of this:

from django.forms.formsets import formset_factory
from blogapp.forms import BlogForm

BlogFormSet = formset_factory(BlogForm, absolute_max=1500)

data = {
'form-TOTAL_FORMS': '1501',
'form-INITIAL_FORMS': '0',
}

formset = BlogFormSet(data)
len(formset.forms)
formset.is_valid()
formset.non_form_errors()

When absolute_max is set to None, max_num + 1000 is used as the default value. However, max_num defaults to 2000 if None is specified.

A ValueError will be generated if absolute_max is less than max_num.

Validation of the formset

A formset’s validation is nearly identical to that of a standard Form. The formset has an is_valid method that is used to validate all of the forms in the set:

data = {
  'form-TOTAL_FORMS': '2',
  'form-INITIAL_FORMS': '0',
  'form-0-title': 'Test',
  'form-0-posted_date': '1904-06-16',
  'form-1-title': 'Test',
  'form-1-posted_date': '', 
  # <-- this date is missing but required
}

formset = BlogFormSet(data)
formset.is_valid()
formset.errors

Formset.errors is a list whose elements correlate to the forms in the formset, as we can see. Each of the two forms is validated, and the expected error notice is displayed for the second item.

Each field in a formset may include HTML characteristics such as max length for browser validation, much like when using a regular Form. Form fields in formsets, on the other hand, will not include the must property because the validation may be inaccurate while adding and deleting forms.

BaseFormSet.total error count()

The total error count method is used to determine how many errors are present in the formset:

formset.errors
len(formset.errors)
formset.total_error_count()

We may also see if the form data is different from the starting data (i.e., the form was sent empty):

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

The ManagementForm: An Overview

You may have observed that the formset’s data required more data including form-TOTAL FORMS, and form-INITIAL_FORMS.

The ManagementForm requires this information. The formset uses this form to keep track of the forms that make up the formset. In fact, the formset will be invalid if you do not give this management data:

'form-0-title': 'Test',
'form-0-posted_date': '',
}
formset = BlogFormSet(data)
formset.is_valid()

It’s used to keep track of how many form instances are on screen at any given time. If you’re using JavaScript to create new forms, you should also increment the count fields in this form. Suppose you’re using JavaScript to allow for the deletion of existing objects. In that case, you’ll need to include form-#-DELETE in the POST data to guarantee that the deleted ones are appropriately marked for deletion.


Regardless, all forms are anticipated to be present in the POST data.

The management form can be found as a property of the formset. You can include all the management data when displaying a formset in a template by rendering {{ my_formset.management_form}}. In the last-mentioned, substitute the name of your formset as appropriate.

total_form_count and initial_form_count are two variables to consider. They are two methods in BaseFormSet closely related to the ManagementForm.The formset’s total count of forms is returned by total_form_count. On the other hand, initial_form_count returns the number of pre-filled forms in the formset, and it’s also used to figure out how many forms are needed.You’ll almost certainly never need to override either of these methods, so make sure you know what they do first.

empty_form

For more straightforward use with dynamic forms with JavaScript, BaseFormSet adds a new attribute empty form, which returns a form instance with prefix.

error messages

The error messages option allows you to override the formset’s default messages. Override error messages by passing in a dictionary with entries that match the error messages you want to override. When the management form is absent, for example, the following is the default error message:

formset = BlogFormSet({})
formset.is_valid()
formset.non_form_errors()

Subsequently, here is an example of a custom error message.

formset = ArticleFormSet({}, error_messages={'missing_management_form': 'Sorry, error encountered..'})
formset.is_valid()
formset.non_form_errors()

Validation of custom formsets

A clean method on a formset is comparable to a Form class. It is where you can define your own formset-level validation rules:

from django.core.exceptions import ValidationError
from django.forms import BaseFormSet
from django.forms import formset_factory
from blogapp.forms import ArticleForm

class BaseBlogFormSet(BaseFormSet):
  def clean(self):
    """Checks that no two posts have the same heading."""
    if any(self.errors):
      # avoid validating the formset unless each form is valid on its own
      return
      titles = []
      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("Posts in a set must have distinct headings.")
          titles.append(title)

 
BlogFormSet = formset_factory(BlogForm, formset=BaseBlogFormSet)
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 = BlogFormSet(data)
formset.is_valid()
formset.errors
formset.non_form_errors()

After all of the Form.clean methods have been called, the formset clean method is invoked. The non_form_errors() is the method responsible for finding errors on the formset.To identify non-form errors from form-specific mistakes, they will be presented with an additional nonform class. For instance, {{ formset.non_form_errors }} would look like this:

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

Validating forms in a formset

Django offers a couple of options for validating the minimum and the maximum number of forms submitted. Custom formset validation is recommended for applications that require more flexible validation of the number of forms.

validate_max

Validation will additionally check that the number of forms in the data set, less those marked for deletion is fewer than or equal to max_num if validate_max=True is provided to formset_factory().

from django.forms import formset_factory
from blogapp.forms import BlogForm

BlogFormSet = formset_factory(BlogForm, 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-posted_date': '1912-06-23',
}

formset = BlogFormSet(data)
formset.is_valid()
formset.errors
formset.non_form_errors()

validate_max=True aggressively validates against max_num, even if max_num was exceeded due to an excessive quantity of initial data supplied. On the off chance that the absolute_max is exceeded by the forms’ count in a dataset, regardless of validate_max, the form will fail to validate as if validate_max were set, and only the first absolute_max forms are validated.
The remaining will be wholly omitted. It is to prevent fake POST requests from causing memory exhaustion.

validate_min

Validation will additionally check that the number of forms in the data set, less those marked for deletion, is higher than or equal to min num if validate_min=True is provided to formset_factory(). If a formset includes no data, extra + min_num, blank forms are presented regardless of validate_min.

Dealing with form ordering and deletion

To aid in ordering forms in formsets and the deletion of forms from a formset, the formset_factory() includes two optional options can_order and can_delete.

can_order (BaseFormSet.can_order)

False is the default value, and it allows you to create a formset that may be ordered:

from django.forms import formset_factory
from blogapp.forms import BlogForm

BlogFormSet = formset_factory(BlogForm, can_order=True)

formset = BlogFormSet(initial=[
{'title': 'Post #1', 'pub_date': datetime.date(2008, 5, 10)},
{'title': 'Post #2', 'pub_date': datetime.date(2008, 5, 11)},
])
for form in formset:
  print(form.as_table())

Each form gets an extra field as a result of this.In fact, ORDER is the name of the new field, which is a form.

IntegerField

It automatically allocated a numeric value to the forms from the initial data. Let’s see what happens if the user modifies these values:

data = {
  'form-TOTAL_FORMS': '3',
  'form-INITIAL_FORMS': '2',
  'form-0-title': 'Post #1',
  'form-0-posted_date': '2008-05-10',
  'form-0-ORDER': '2',
  'form-1-title': 'Post #2',
  'form-1-posted_date': '2008-05-11',
  'form-1-ORDER': '1',
  'form-2-title': 'Post #3',
  'form-2-posted_date': '2008-05-01',
  'form-2-ORDER': '0',
}

formset = BlogFormSet(data, initial=[
{'title': 'Post #1', 'posted_date': datetime.date(2008, 5, 10)},
{'title': 'Post #2', 'posted_date': datetime.date(2008, 5, 11)},
])

formset.is_valid()
for form in formset.ordered_forms:
  print(form.cleaned_data)

BaseFormSet also has an ordering_widget attribute, and a get_ordering_widget() method for controlling the widget is used with can_order.

ordering_widget(BaseFormSet.ordering_widget)

NumberInput is the default value that defines the widget class to use with can_order, set ordering_ widget to:

from django.forms import BaseFormSet, formset_factory
from blogapp.forms import BlogForm

class BaseBlogFormSet(BaseFormSet):
  ordering_widget = HiddenInput
  
BlogFormSet = formset_factory(BlogForm, formset=BaseBlogFormSet, can_order=True)
get_ordering_widget (BaseFormSet.get_ordering_widget())

On the off chance that you provide an instance of a widget for use with can_order, then you need to override get_ordering_widget() as follows:

from django.forms import BaseFormSet, formset_factory
from myapp.forms import BlogForm

class BaseBlogFormSet(BaseFormSet):
  def get_ordering_widget(self):
    return HiddenInput(attrs={'class': 'ordering'})
  
BlogFormSet = formset_factory(BlogForm, formset=BaseBlogFormSet, can_order=True)

can_delete (BaseFormSet.can_delete)

It allows you to construct a formset with the option to delete specific forms. False is the default value. Below is an illustration:

from django.forms import formset_factory
from blogapp.forms import BlogForm

BlogFormSet = formset_factory(BlogForm, can_delete=True)

formset = BlogFormSet(initial=[
{'title': 'Post #1', 'posted_date': datetime.date(2008, 5, 10)},
{'title': 'Post #2', 'posted_date': datetime.date(2008, 5, 11)},
])
for form in formset:
  print(form.as_table())

It adds a new field to each form called DELETE and forms.BooleanField, similar to can_order. If you mark any of the remove fields as deleted, you can access them with deleted forms:

data = {
'form-TOTAL_FORMS': '3',
'form-INITIAL_FORMS': '2',
'form-0-title': 'Post #1',
'form-0-posted_date': '2008-05-10',
'form-0-DELETE': 'on',
'form-1-title': 'Post #2',
'form-1-posted_date': '2008-05-11',
'form-1-DELETE': '',
'form-2-title': '',
'form-2-posted_date': '',
'form-2-DELETE': '',
}

formset = BlogFormSet(data, initial=[
{'title': 'Post #1', 'posted_date': datetime.date(2008, 5, 10)},
{'title': 'Post #2', 'posted_date': datetime.date(2008, 5, 11)},
])

[form.cleaned_data for form in formset.deleted_forms]

When you call formset.save() with a ModelFormSet, model instances removed forms will be deleted. Objects will not be destroyed automatically if you execute formset.save(commit=False). To delete the formset.deleted_objects, you’ll need to use_delete() on each of them:

If you’re using a standard FormSet, on the other hand, you’ll have to handle formset.deleted_forms yourself, possibly in your formset’s save() method, because there’s no general understanding of what it means to remove a form.

The deletion_widget attribute and get_deletion_widget() method in BaseFormSet are used to control the widget used with can_delete.

deletion_widget(BaseFormSet.deletion_widget)

CheckboxInput is the default value. Thus, to define the widget class to use with can_delete, set deletion_widget to:

from django.forms import BaseFormSet, formset_factory
from blogapp.forms import BlogForm

class BaseBlogFormSet(BaseFormSet):
  deletion_widget = HiddenInput

BlogFormSet = formset_factory(BlogForm, formset=BaseBlogFormSet, can_delete=True)
get_deletion_widget(BaseFormSet.get_deletion_widget())

Override get_deletion_widget() if your intention is to provide an instance of a widget for use with can_delete:

from django.forms import BaseFormSet, formset_factory
from blogapp.forms import BlogForm

class BaseBlogFormSet(BaseFormSet):
  def get_deletion_widget(self):
    return HiddenInput(attrs={'class': 'deletion'})
  
BlogFormSet = formset_factory(BlogForm, formset=BaseBlogFormSet, can_delete=True)

can_delete_extra (BaseFormSet.can_delete_extra)

True by default.Specifying can_delete_extra=False while setting can_delete=True disables the ability to delete extra forms.

Increasing the number of fields in a formset

It is a simple process if you add more fields to the formset. The add fields method is available in the formset base class. You can override this function to add your fields or even alter the order and deletion fields’ default fields/attributes:

from django.forms import BaseFormSet
from django.forms import formset_factory
from blogapp.forms import BlogForm

class BaseBlogFormSet(BaseFormSet):

  def add_fields(self, form, index):
      super().add_fields(form, index)
      form.fields["my_field"] = forms.CharField()
      
BlogFormSet = formset_factory(BlogForm, formset=BaseBlogFormSet)
formset = BlogFormSet()
for form in formset:
print(form.as_table())

Are custom parameters are passed to formset forms?

Your form class, such as MyBlogForm, may take custom parameters. This argument can be passed to the formset when it is created:

from django.forms import BaseFormSet
from django.forms import formset_factory
from blogapp.forms import BlogForm

class MyBlogForm(BlogForm):
  def init(self, *args, user, *kwargs): self.user = user super().init(args, **kwargs)
    
BlogFormSet = formset_factory(MyBlogForm)
formset = BlogFormSet(form_kwargs={'user': request.user})

The form_kwargs may also be affected by the form instance. The get_form_kwargs method is available in the formset base class. The method only accepts one argument: the form’s index in the formset. For the empty_form, the index is None:

Changing the prefix of a formset

On every produced HTML, every field name is given a prefix by Formsets. The prefix is set to ‘form’ by default, but it can be changed using the formset’s prefix option.

In the default case, for example, you might see:

<label for="id_form-0-title">Name:</label>
<input type="text" name="form-0-title" id="id_form-0-title">

But with BlogFormset(prefix=’post’) that becomes:

<label for="id_post-0-title">Name:</label>
<input type="text" name="post-0-title" id="id_post-0-title">

Using many formsets in a view

If you like, you can utilize more than one formset in a view. Formsets take a lot of their behavior from forms. As a result, you may use a prefix to prefix formset form field names with a given value, allowing multiple formsets to be submitted to a view without causing name conflicts.

Let’s have a look at how we may do this:

from django.forms import formset_factory
from django.shortcuts import render
from myapp.forms import BlogForm, WriterForm

def manage_posts(request):
  BlogFormSet = formset_factory(BlogForm)
  WriterFormSet = formset_factory(WriterForm)
  if request.method == 'POST':
    blog_formset = BlogFormSet(request.POST, request.FILES, prefix='posts')
    writer_formset = WriterFormSet(request.POST, request.FILES, prefix='writers')
    if blog_formset.is_valid() and writer_formset.is_valid():
      # perform executions with the cleaned_data on the formsets.
      pass
    else:
      blog_formset = BlogFormSet(prefix='posts')
      book_formset = WriterFormSet(prefix='writers')
  
  return render(request, 'manage_posts.html', {
    'blog_formset': blog_formset,
    'book_formset': book_formset,
  })

The formsets would then be rendered typically. It’s worth noting that prefix is given in both POST and non-POST scenarios to be rendered and processed correctly. The prefix is given to each field’s name, and id in HTML attributes by each formset substitute the default form prefix.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *