ManyToMany Field data validation

I’m having some difficulties to add some data validation to a manyTomany field I have in my model. The code I have until now is the following:

#models.py
class Cargo(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self) -> str:
        return self.name

class Committee(models.Model):
    MAX_CARGOS = 3

    name = models.CharField(max_length=100)
    image = models.ImageField(upload_to='committee/', blank=True)
    cargos = models.ManyToManyField(Cargo, blank=True, related_name='cargos')

    def __str__(self) -> str:
        return self.name

#admin.py
class CommitteeCargoInline(admin.TabularInline):
    model = Committee.cargos.through
    max_num = 5

class CommitteeAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['name']}),
    ]
    inlines = [CommitteeCargoInline]

admin.site.register(Committee, CommitteeAdmin)
admin.site.register(Cargo)

The aim here is to ensure that when admin user clicks on one of the “Save” buttons in admin panel Committee page, it is checked that a Committee has no more than MAX_CARGOS.

At the moment I’m kinda lost because I couldn’t find anything that says what is the correct and standard way to achieve this, I’d like feedback on this point.

I already tried multiple things:

  • Started by trying to add a field validator to my cargos attribute but then I found at Django Docs that field validators are not supported for ManyToMany fields (“ManyToManyField does not support validators.”)

  • Then I tried implement Committe save() method but it also didn’t work:

 def save(self, force_insert, force_update, using, update_fields):
      if self.cargos.count() > self.MAX_CARGOS:
          raise ValidationError(f"A Committee can have at most {self.MAX_CARGOS} cargos.")
      super().save(force_insert, force_update, using, update_fields)

Anyway this is not what I wanted since this implementation will only take into account the old model values and not the updated ones.

  • Then I tried to implement it through signals, it seems many people use this solution. Here, I tried to listen m2m_changed, but no signal of this type seems to be triggered when data is modified through admin panel. I was able to get a signal pre_save, but with this signal I don’t know how to get the new values:
def validate_committee_cargos(sender, instance, **kwargs):
    logging.info("We got a signal")
    logging.info(sender)
    logging.info(sender.objects.get(pk=instance.pk))

    updated = sender.objects.get(pk=instance.pk).cargos.all()

    for item in updated:
        logging.info(item)

    logging.info(kwargs)

    my_m2m_field = instance.cargos.all()

    for item in my_m2m_field:
        logging.info(item)
   
pre_save.connect(validate_committee_cargos, sender=Committee, weak=False)
  • Also saw some people implementing these validations only at forms level. IMO this is good to make some pre checks and to save some round-trips, but at backend level this validation should be done too, since it is part of business / model logic.

Are signals the standard way to do this? If yes, how can I achieve this data validation? Should I use managers, since Django Docs say “Signals can make your code harder to maintain. Consider implementing a helper method on a custom manager, to both update your models and perform additional logic, or else overriding model methods before using model signals.”?

Using Django 4.1. Thanks in advance

Right idea, but wrong location.

As you may have identified, the saving of the inlines is the second step of a two-part process.

The admin first saves the base model, then saves the related objects.

The method you’re looking for here is save_related.

(And yes, I always recommend against using signals in any situation other than those in which they are truly necessary.)

1 Like