madasionka
madasionka

Reputation: 922

Django model validate a ManyToMany field before adding

I have a model that looks somewhat like this:

class Passenger(models.Model):
    name = models.CharField(max_length=50)
    surname = models.CharField(max_length=50)


class Flight(models.Model):
    capacity = models.IntegerField()
    passengers = models.ManyToManyField(Passenger)

Before adding a new passenger to the flight I would like to validate whether the number of passengers is not going to exceed the capacity. I was wondering what would be the best way to go about this.

Obviously I could manually check the number of passengers before adding a new one, but maybe there is some support in django? I tried writing a validator, but wasn't sure how to do it.

Upvotes: 6

Views: 3263

Answers (3)

mka
mka

Reputation: 173

According to @M.Void answer – Code Example:

from django.db import models
from django.db.models.signals import m2m_changed
from django.core.exceptions import ValidationError

class MyModel(models.Model):
    m2mField = models.ManyToManyField('self')
    m2mFieldLimit = 2

def m2mField_changed(sender,**kwargs):
    instance = kwargs['instance']
    if len(instance.m2mField.all()) >= instance.m2mFieldLimit :
        raise ValidationError(f'Max number of records is {instance.m2mFieldLimi}')

m2m_changed.connect(m2mField_changed,sender=MyModel.m2mField.through)

Upvotes: 3

M.Void
M.Void

Reputation: 2894

According to Django docs you can listen to the m2m_changed signal, which will trigger pre_add and post_add actions.

Using add() with a many-to-many relationship, however, will not call any save() methods (the bulk argument doesn’t exist), but rather create the relationships using QuerySet.bulk_create(). If you need to execute some custom logic when a relationship is created, listen to the m2m_changed signal, which will trigger pre_add and post_add actions.

Upvotes: 4

fiveclubs
fiveclubs

Reputation: 2431

Override the clean method on the model to do the check you want:

class Passenger(models.Model):
    name = models.CharField(max_length=50)
    surname = models.CharField(max_length=50)

    def clean(self, *args, **kwargs):
        # clean gets called automatically by other things, so we can't always
        # expect flight_id to be provided
        if 'flight_id' in kwargs:
            flight = Flight.objects.get(pk=kwargs['flight_id'])
            if flight.passengers.all().count() >= flight.capacity:
                # flight is full!
                raise ValidationError
        super(Passenger, self).clean()

class Flight(models.Model):
    capacity = models.IntegerField()
    passengers = models.ManyToManyField(Passenger)

Note that to do this, you will have to pass in the flight ID when validating the passenger:

f = Flight.objects.get(...)
p = Passenger(name='First', surname='Last')
try:
    p.clean(flight_id=f.id) # full_clean calls clean, among other validations
    p.save()
except ValidationError as e:
    # do something to handle the error

Note that it is possible in multi-threaded applications for something to get validated successfully, but still fail to save in a race condition. You would need to add additional code to handle that.

See here for details on model validation.

Upvotes: 0

Related Questions