2

I have these two models:

class Test(models.Model):
    problems = models.ManyToManyField('Problem')
    ...

class Problem(models.Model):
    type = models.CharField(max_length=3, choices=SOME_CHOICES)
    ...

Now, while adding Problems to a Test, I need to limit the number of particular type of problems in the Test. E.g. a Test can contain only 3 Problems of type A, and so on.

The only way to validate this seems to be by using m2m_changed signal on Test.problems.through table. However, to do the validation, I need to access the current Problem being added AND the existing Problems - which doesn't seem to be possible somehow.

What is the correct way to do something like this? M2M validation seems to be a topic untouched in the docs. What am I missing?

TeknasVaruas
  • 1,480
  • 3
  • 15
  • 28
  • When exactly are you validating? Where do you have your validation code? – gitaarik Apr 13 '15 at 22:15
  • Possible duplicate of [Django: how to validate m2m relationships?](https://stackoverflow.com/questions/46362251/django-how-to-validate-m2m-relationships) – Bernd Wechner Feb 25 '19 at 11:15

2 Answers2

2

You are right on the part that you have to register an m2m_changed signal function like the following:

def my_callback(sender, instance, action, reverse, model, pk_set, **kwargs)

If you read the documentation you 'll see that sender is the object-model that triggers the change and model is the object-model that will change. pk_set will give you the pkeys that will be the new reference for your model. So in your Test model you have to do something like this:

@receiver(m2m_changed)
def my_callback(sender, instance, action, reverse, model, pk_set, **kwargs):
    if action == "pre_add":
        problem_types = [x.type for x in model.objects.filter(id__in=pk_set)]
        if problem_types.count("A") > some_number:
            raise SomeException

Mind though that an Exception at that level will not be caught if you're entering fields from Django admin site. To be able to provide user friendly errors for django admin data entry, you'll have to register your own form as admin form. In your case, you need to do the following:

class ProblemTypeValidatorForm(ModelForm):
    def clean(self):
        super(ProblemTypeValidatorForm, self).clean()
        problem_types = [x.type for x in self.cleaned_data.get("problems") if x]
        if problem_types.count("A") > some_number:
            raise ValidationError("Cannot have more than {0} problems of type {1}"
                                  .format(len(problem_types), "A")

then in your admin.py

@admin.register(Test)
class TestAdmin(admin.ModelAdmin):
    form = ProblemTypeValidatorForm

Now keep in mind that these are two different level implementations. None will protect you from someone doing manually this:

one_test_object.problems.add(*Problem.objects.all())
one_test_object.save()

Personal opinion:

So keeping in mind the above, I suggest you go with the ModelForm & ModelAdmin approach and if you're providing an API for CRUD operations, make your validations there as well. Nothing can protect you from someone entering stuff in your db through django shell. If you want such solution types you should go directly to your db and write some kind of magic trigger script. But keep in mind that your db is actually data. Your backend is the one with the business logic. So you shouldn't really try to impose business rules down to the db level. Keep the rules in your backend by validating your data at the spots where create/update happens.

0

You can't override save for a M2M I'm afraid, but you can achieve what you want.

Use the m2m_changed signal where the action is pre_add.
The 'instance' kwarg will be the Test model the problem is being added to.
The 'pk_id' kwarg will be the primary key of the Problems being added (1 or more).
The validation logic will be something like this:

p_type = Problem.objects.get(id=kwargs['pk_id']).type
type_count = kwargs['instance'].problems.filter(type=p_type).count()

if p_type == 'A' and type_count == 3:
  raise Exception("cannot have more than 3 Problems of type A")

[sorry don't have django on hand to verify the query]

Daniel
  • 1,994
  • 15
  • 36
  • Well, there's the problem. During `pre_add`, the `problem_set` is cleared. So `kwargs['instance'].problems.filter(type=p_type).count()` would be 0. – TeknasVaruas Apr 14 '15 at 10:00