Tuesday, July 31, 2018

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.

No comments:

Post a Comment