Tuesday, July 31, 2018

RE: Validating count + property of a Many2Many with a through table

Hi Sanjay,
You may want to try signals.
https://docs.djangoproject.com/en/2.0/topics/signals/


-----Original Message-----
From: django-users@googlegroups.com [mailto:django-users@googlegroups.com] On Behalf Of Sanjay Bhangar
Sent: Tuesday, July 31, 2018 1:09 PM
To: django-users@googlegroups.com
Subject: Validating count + property of a Many2Many with a through table

Hello!

This is more of a code organization question than a "something is not working" question - it might get slightly rambling, so if this kind of question is not your cup of tea, you have been warned :-)

So - I have a set of models like this (highly simplified):

class Item(models.Model):
title = models.CharField(...)
price = models.IntegerField(...)
tags = models.ManyToManyField(Tag, through=ItemTag)

class Tag(models.Model):
name = models.CharField(...)

class ItemTag(models.Model):
item = models.ForeignKey(Item)
tag = models.ForeignKey(Tag)
is_primary = models.BooleanField()


So, I have an Item model that has some properties - it has a ManyToMany relationship with Tag, and one or more tags can be selected as "is_primary" tags for the model.

Now, to validate properties on the Item model, I call validation methods inside a "clean" method on the Item model, something like:

def clean(self):
if self.price > 10000:
raise ValidationError('Price cannot be so high!")

This is simplified, but you get the idea - this works great, errors are propagated in the Admin, as well as raised when I import data via a CSV file for example. I know there's a few different places one can define validations - I personally like to have them in the model, but I would love to hear alternate view-points if this is not the best place to put them.

So, coming to the problem: there is now a requirement to validate that a model cannot have more than 3 primary tags associated with it - i.e.
cannot be associated with more than 3 tags with the through table specifying is_primary=True.

This, of course, cannot really go in the `clean` method of the Item model. The ItemTag relationships don't exist yet, and I don't have the data I need to be able to validate a maximum of 3 tags.

The problem is, I cannot seem to be able to do this kind of validation in the ItemTag model either, since it depends on knowing how many existing ItemTag relationships to Item there are - and before the entire transaction is committed to the database, a single ItemTag doesn't know whether the total exceeds the allowed 3.

The only place to put it that worked (and made sense) was in the Formset definition for the inline form in the admin.

So, we landed up over-riding the formset used for the ItemTag inline in the admin, adding our validations to the `clean` method - roughly
like:

class ItemImageInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
image_count = 0
for form in self.forms:
if form.cleaned_data.get('primary') == True:
image_count += 1
if image_count > 3:
raise ValidationError('Cannot have more than 3 primary tags')


This works. However, it seems a bit strange to have validations split between a model method, and a subclass of a formset of an admin form.
It seems confusing in terms of maintainability - also, writing tests for the validations in the model seemed a lot more straightforward when validations were just in the model's `clean` method.

I can see why this is so. Am just wondering if I'm getting something wrong in terms of patterns here / if there is a way other folks solve this, to, ideally, be able to have all the validation that pertains to a particular model in one place, and be able to test all model validations in a consistent manner.

Thank you to whoever read this far :-)

-Sanjay

--
You received this message because you are subscribed to the Google Groups "Django users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-users+unsubscribe@googlegroups.com.
To post to this group, send email to django-users@googlegroups.com.
Visit this group at https://groups.google.com/group/django-users.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-users/CAG3W7ZFZnemOSOwDRjDN--JLKdF94QEJNrbuTw2MCPoK1QxP7Q%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "Django users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-users+unsubscribe@googlegroups.com.
To post to this group, send email to django-users@googlegroups.com.
Visit this group at https://groups.google.com/group/django-users.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-users/8888981a20654ff4b41e32c600464ddd%40ISS1.ISS.LOCAL.
For more options, visit https://groups.google.com/d/optout.

No comments:

Post a Comment