Form and Formset
-
Comments:
- here.
Sometimes, you’ll have an object that you want to save, and, at the same time, some related objects that should also be updated, created and/or deleted.
Django has really nice tools for doing both of these operations (ModelForm for the individual instance, and InlineFormSet for the group of related objects). Both of these are really well documented. However, it is nice to be able to encapsulate these operations into a single functional unit.
We can leverage the fact that all request data is passed to a form class when it is instantiated, along with some nice use of the django cached_property
decorator to make this really quite neat.
Let’s consider this model structure: we have a Person, and each Person may have zero or more Addresses. Every Person has a name, and an optional date of birth. All of the fields for the address are required:
class Person(models.Model):
name = models.TextField()
date_of_birth = models.DateField(null=True, blank=True)
class Address(models.Model):
person = models.ForeignKey(Person, related_name='addresses')
street = models.TextField()
suburb = models.TextField()
postcode = models.TextField()
country = django_countries.fields.CountryField()
We can have a view for updating the Person model instance that is very simple:
class PersonForm(forms.ModelForm):
name = forms.TextInput()
date_of_birth = forms.DateInput()
class Meta:
model = Person
fields = ('name', 'date_of_birth')
class UpdatePerson(UpdateView):
form_class = PersonForm
Likewise, we can have a view for updating a person’s addresses:
AddressFormSet = inlineformset_factory(
Person,
Address,
fields=('street', 'suburb', 'postcode', 'country'),
)
class UpdateAddresses(UpdateView):
form_class = AddressFormSet
As mentioned above, we’d like to have a page where a Person’s name, date of birth and addresses may be modified in one go, rather than having to have two seperate pages.
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
class PersonForm(forms.ModelForm):
name = forms.TextInput()
date_of_birth = forms.DateInput()
class Meta:
model = Person
fields = ('name', 'date_of_birth')
@cached_property
def addresses(self):
return inlineformset_factory(
Person, Address, fields=('street', 'suburb', 'postcode', 'country')
)(
data=self.data,
files=self.files,
instance=self.instance,
prefix='address',
)
def clean(self):
# Just in case we are subclassing some other form that does something in `clean`.
super().clean()
if not self.addresses.is_valid():
self.add_error(None, _('Please check the addresses'))
def save(self, commit=True):
result = super().save(commit=commit)
self.addresses.save(commit=commit)
return result
class UpdatePerson(UpdateView):
form_class = PersonForm
So, how does this work?
When the form.addresses
attribute is accessed, the decorator looks up to see if it has been accessed within this request-response cycle. On the first access, a new formset class is generated from the factory, which is then instantiated with the arguments as shown. Every other access will result in the cached value from the instantiation being used, keeping everything working.
Within our template, we can just render the formset normally, however, we may want to use some fancy javascript to make it dynamic. In this case, I’ll just use the default rendering as seen in the django formset documentation.
<form action="{% url 'person:update' form.instance.pk %}"
method="POST">
{% csrf_token %}
{{ form }}
{{ form.addresses }}
<button type="submit">
{% trans 'Save' %}
</button>
</form>