I've been discussing this on the Django Developers list and have in fact tabled a method of doing this for consideration in the Django core in one form or another. The method is not fully tested nor finalised but results for now are very encouraging and I'm employing it on a site of mine with success.
In principle it relies on:
Using PostgreSQL as your database engine (we're fairly sure it won't
work on Lightdb or MySQL, but keen for anyone to test this)
Overriding the post() method of your (Class based) view such that it:
- Opens an atomic transaction
- Saves the form
- Saves all the formsets if any
- Calls Model.clean() or something else like Model.full_clean()
In your Model then, in the method called in 2.4 above you will see all your many to many and one to many relations in place. You can validate them and throw a ValidationError to see the whole transaction rolled back and no impact on the database.
This is working wonderfully for me:
def post(self, request, *args, **kwargs):
# The self.object atttribute MUST exist and be None in a CreateView.
self.object = None
self.form = self.get_form()
self.success_url = reverse_lazy('view', kwargs=self.kwargs)
if connection.vendor == 'postgresql':
if self.form.is_valid():
try:
with transaction.atomic():
self.object = self.form.save()
save_related_forms(self) # A separate routine that collects all the formsets in the request and saves them
if (hasattr(self.object, 'full_clean') and callable(self.object.full_clean)):
self.object.full_clean()
except (IntegrityError, ValidationError) as e:
if hasattr(e, 'error_dict') and isinstance(e.error_dict, dict):
for field, errors in e.error_dict.items():
for error in errors:
self.form.add_error(field, error)
return self.form_invalid(self.form)
return self.form_valid(self.form)
else:
return self.form_invalid(self.form)
else:
# The standard Djangop post() method
if self.form.is_valid():
self.object = self.form.save()
save_related_forms(self)
return self.form_valid(self.form)
else:
return self.form_invalid(self.form)
And the conversation on the Developers list is here:
https://groups.google.com/forum/#!topic/django-developers/pQ-8LmFhXFg
if you'd like to contribute any experience you gain from experimenting with this (perhaps with other database backends).
The one big caveat in the above approach is it delegates saving to the post()
method which in the default view is done in the form_valid()
method, so you need to override form_valid()
as well, otherwise a post()
like the one above will see you saving the form twice. Which is just a waste of time on an UpdateView but rather disastrous on a CreateView.