Unique BooleanField value in Django?

Viewed 27174

Suppose my models.py is like so:

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

I want only one of my Character instances to have is_the_chosen_one == True and all others to have is_the_chosen_one == False . How can I best ensure this uniqueness constraint is respected?

Top marks to answers that take into account the importance of respecting the constraint at the database, model and (admin) form levels!

14 Answers

It is simpler to add this kind of constraint to your model after Django version 2.2. You can directly use UniqueConstraint.condition. Django Docs

Just override your models class Meta like this:

class Meta:
    constraints = [
        UniqueConstraint(fields=['is_the_chosen_one'], condition=Q(is_the_chosen_one=True), name='unique_is_the_chosen_one')
    ]
class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

    def clean(self):
        from django.core.exceptions import ValidationError
        c = Character.objects.filter(is_the_chosen_one__exact=True)  
        if c and self.is_the_chosen:
            raise ValidationError("The chosen one is already here! Too late")

Doing this made the validation available in the basic admin form

2020 update to make things less complicated for beginners:

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField(blank=False, null=False, default=False)

    def save(self):
         if self.is_the_chosen_one == True:
              items = Character.objects.filter(is_the_chosen_one = True)
              for x in items:
                   x.is_the_chosen_one = False
                   x.save()
         super().save()

Of course, if you want the unique boolean to be False, you would just swap every instance of True with False and vice versa.

When implementing a solution which overwrites model.save()*, I ran into the issue of Django Admin raising an error before hitting model.save(). The cause seems to be Admin calling model.clean() (or perhaps model.full_clean(), I didn't investigate too carefully) before calling model.save(). model.clean() in turn calls model.validate_unique() which raises a ValidationError before my custom save method can take care of the unique violation. To solve this I overwrote model.validate_unique() as follows:

    def validate_unique(self, exclude=None):
        try:
            super().validate_unique(exclude=exclude)
        except ValidationError as e:
            validation_errors = e.error_dict
            try:
                list_validation_errors = validation_errors["is_the_chosen_one"]
                for validation_error in list_validation_errors:
                    if validation_error.code == "unique":
                        list_validation_errors.remove(validation_error)
                if not list_validation_errors:
                    validation_errors.pop(key)
            except KeyError:
                continue
            if e.error_dict:
                raise e

* the same would be true for a signal solution using pre_save, as pre_save is also not sent before .validate_unique is called

Related