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.

Reply via email to