Django formset handling with class based views, custom errors and validation
Overview
In Django, forms and formsets are very similar, but by default it supports just regular forms in class based views, this is a guide to easily use Formsets with Class Based Views (CBVs).
Basic
The most basic view for handling forms is one that:
- displays a form. 1.1. On error, redisplays the form with validation errors; 1.2. on success, redirects to a new URL.
That behaviour is already handled at django.views.generic.edit.FormView.
What if we want to take advantage of the above view but display an arbitrary number of forms for the same model?
Let’s assume we have the following in myapp/models.py
:
from django.db import models
from django.urls import reverse
class Author(models.Model):
name = models.CharField(max_length=200)
def get_absolute_url(self):
return reverse('author-detail', kwargs={'pk': self.pk})
To use a regular FormView CBV that displays a single form, we would
have to define it with a form in myapp/forms.py
:
from django import forms
class AuthorForm(forms.Form):
name = forms.CharField()
and a view at myapp/views.py
using the above form specifying in at
form_class
attribute:
from myapp.forms import AuthorForm
from django.views.generic.edit import FormView
class AuthorFormView(FormView):
template_name = author.html
form_class = AuthorForm
success_url = '/'
with a template like:
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Add">
</form>
Using a formset
To display two author forms in our template with a single submit button, we slightly change the above code.
Create formset
First of all, we create a new formset in myapp/forms.py
:
AuthorFormSet = formset_factory(
AuthorForm,
extra=2,
max_num=2,
min_num=1
)
We are using formset_factory
which is
defined at from django.forms import formset_factory
like:
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
can_delete=False, max_num=None, validate_max=False,
min_num=None, validate_min=False):
...
Use formset in view
Then use the above formset in the view, instead of a single
AuthorForm
we use AuthorFormSet
:
from myapp.forms import AuthorFormSet
from django.views.generic.edit import FormView
class AuthorFormView(FormView):
template_name = author.html
form_class = AuthorFormSet
success_url = '/'
Now the template would display two forms, a simple test to be sure
that the name is required (this test isn’t complete but I left here
to give an idea how to use TestCase.assertFormsetError
):
from django.test import TestCase
from django.urls import reverse
from myapp.views import
class AuthorFormView(TestCase):
def test_language_is_required(self):
name = 'Richard Morales'
data = {
'form-TOTAL_FORMS': '2',
'form-INITIAL_FORMS': '1',
'form-MAX_NUM_FORMS': '2',
#'form-0-name':name, not sent to raise the error
}
response = self.client.post(reverse('create-two-authors'), data)
self.assertFormsetError(response,
formset='form',
form_index=0,
field='name',
errors='Please specify the name of the
author.')
Adding custom validation to formset
To further customize the validation process, i.e.: overriding
full_clean()
, we need to inherit from BaseFormSet
instead of just
defining the method in the view, because it is the formset who does
the proper validation for each of its forms and the collect all of
those forms cleaned data at self.cleaned_data
.
Then in the myapp/forms.py
we explicitely create a new
formset=BaseFormSet
to extend from, called AuthorBaseFormSet
and
then use it in formset_factory
.
When defining our custom full_clean
function, we can access all
forms data in the self.cleaned_data
list, which contains the data
for each form in a dictionary like: [{'name': 'entered name in first form'},{'name': 'entered name in second form'}]
.
Finally, we add a specific error message and attach it to the right
form field like self.forms[0].add_error('name', "the name of the first form should contain vowels")
.
Summarizing:
class AuthorBaseFormSet(BaseFormSet):
def full_clean(self, *args, **kwargs):
"""
Cleaning and validating fields that depend on each other
"""
super().full_clean(*args, **kwargs)
# if basic clean has any problem stop further processing
# if not is_valid() it won't have cleaned_data
if(not self.is_valid()):
return
# now we can have access to self.cleaned_data
# and be able to raise any error or attach an error to a form field
# after some validation we add the errors
...
msg0 = "First author name should contain vowels"
msg1 = "Second author name should contain consonants"
self.forms[0].add_error('rawtext', msg0)
self.forms[1].add_error('rawtext', msg1)
AuthorFormSet = formset_factory(
AuthorForm,
formset=AuthorBaseFormSet, # added to handle validation and error management
extra=2,
max_num=2,
min_num=1
)
Now to test the above expected behaviour:
from django.test import TestCase
from django.urls import reverse
from myapp.views import
class AuthorFormView(TestCase):
def test_view_names_have_vowels_and_consonants(self):
name0 = 'ZZZZZ'
name1 = 'AAAAA'
data = {
'form-TOTAL_FORMS': '2',
'form-INITIAL_FORMS': '2',
'form-MIN_NUM_FORMS': '1',
'form-MAX_NUM_FORMS': '2',
'form-0-name': name0,
'form-1-name': name1,
}
response = self.client.post(reverse('create-two-authors'), data)
self.assertEqual(response.status_code, 200)
# first form
self.assertFormsetError(response,
formset='form',
form_index=0,
field='name',
errors='First author name should contain vowels')
# second form
self.assertFormsetError(response,
formset='form',
form_index=1,
field='name',
errors='Second author name should contain consonants')
Conclusion
In this way we can get all the benefits of Class Based Views using them with Formsets without having to repeat code for common task already well tested.
References
- Code example based in https://docs.djangoproject.com/en/2.2/ref/class-based-views/generic-editing/
- https://docs.djangoproject.com/en/2.1/topics/class-based-views/generic-editing/
- Adding a Cancel button in Django class-based views, editing views and formsJuly 15, 2019
- Using Django Model Primary Key in Custom Forms THE RIGHT WAYJuly 13, 2019
- Django formset handling with class based views, custom errors and validation
- How To Use Bootstrap 4 In Django FormsMay 25, 2018
- Understanding Django FormsApril 30, 2018
- How To Create A Form In DjangoJuly 29, 2016
Articles
Except as otherwise noted, the content of this page is licensed under CC BY-NC-ND 4.0 . Terms and Policy.
Powered by SimpleIT Hugo Theme
·