Ouch, I am not believing my eyes, but somewhat overjoyed with what I have
found in my explorations over recent evenings empirically exploring
transactions. The TLDR is this: I am puzzled that I haven't tested and
found this to date, and that all the folk I read online asking for m2m ORM
validation haven't, So puzzled I had to do a second take, a thorough retake
and lots of note taking on the whole process. But in a nutshell it IS
possible to do ORM validation of 2many relations - at least with Django 2.1
and Postgresql. My findings are empirical and a dive down into the code
again to back it with understanding is on my todo list. But I want to share
the findings so far.
Essentially if in your form you override the post() method and use a
transaction and add an explicit clean inside of that transaction then the
objects clean() is called twice, once via form.is_valid() and the second
time explicitly in our post() method. And stunningly on a CreateForm, the
first time, with no id and no visible 2many relation data, but on the
second all with an id and with all 2many relation data visible and there is
at this point no change to the database yet!
In code form finding is clear and tested twice with thorough notes taken
and here it is using a Book model straight out of the tutorials with the
relations I want tested added:
class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author, related_name="books")
lead_author = models.ForeignKey(Author, on_delete=models.CASCADE)
def clean(self):
if (self.id is None):
# Validate all the model fields that are not One2Many or
Many2Many
pass
else:
# Validate all the One2Many or Many2Many fields
pass
class Chapter(models.Model):
title = models.CharField(max_length=100)
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name=
"chapters")
author = models.ForeignKey(Author, on_delete=models.CASCADE,
related_name="chapters")
class BookCreate(CreateView):
model = Book
fields = '__all__'
ChapterFormSet = inlineformset_factory(Book, Chapter, fields="__all__")
def post(self, request, *args, **kwargs):
self.form = self.get_form()
if self.form.is_valid():
try:
with transaction.atomic():
self.object = self.form.save(commit=True)
self.formset = self.ChapterFormSet(request.POST, request
.FILES, instance=self.object)
self.formset.is_valid():
self.formset.save()
self.object.clean()
except IntegrityError:
transaction.set_rollback(True)
return self.form_invalid(self.form)
return self.form_valid(self.form)
else:
return self.form_invalid(self.form)
I have yet to nut out the nitty gritty of exceptions I throw in clean() and
returning a form with error messages, but wanted to report this finding now
to see who can shoot it down ;-).
I have not tested form errors with it yet, and I want to dive into the
save() code to see how and why it's building a complete ORM with no sign of
it in the database yet. It's GREAT that it does.
I have noticed that if the transaction on a Create is rolled back that in
Postgresql at least this consumes the id that was provisioned to the ORM in
the transaction. By which I mean the next Book created has the next id up,
and the one in the baile dtransaction is gap in the id sequence of the
table in the database. An interesting observation and of consequence only
if ids are in short supply, or for some reason one wishes them to be
consecutive (which is hard anyhow if you can delete records).
Still, I admit I am blown away by this discovery, and there it is. We
don't, it seems need to do anything to clean our 2many relations in the
model itself, only override post() and use a transaction (which is sensible
anyhow).
Comments welcome.
Regards,
Bernd.
--
You received this message because you are subscribed to the Google Groups
"Django developers (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/django-developers.
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-developers/449720b1-b794-4445-bdb3-0dcc5e99b199%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.