Unique BooleanField value in Django?

前端 未结 13 790
清酒与你
清酒与你 2020-12-07 14:11

Suppose my models.py is like so:

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


        
相关标签:
13条回答
  • 2020-12-07 14:33

    Instead of using custom model cleaning/saving, I created a custom field overriding the pre_save method on django.db.models.BooleanField. Instead of raising an error if another field was True, I made all other fields False if it was True. Also instead of raising an error if the field was False and no other field was True, I saved it the field as True

    fields.py

    from django.db.models import BooleanField
    
    
    class UniqueBooleanField(BooleanField):
        def pre_save(self, model_instance, add):
            objects = model_instance.__class__.objects
            # If True then set all others as False
            if getattr(model_instance, self.attname):
                objects.update(**{self.attname: False})
            # If no true object exists that isnt saved model, save as True
            elif not objects.exclude(id=model_instance.id)\
                            .filter(**{self.attname: True}):
                return True
            return getattr(model_instance, self.attname)
    
    # To use with South
    from south.modelsinspector import add_introspection_rules
    add_introspection_rules([], ["^project\.apps\.fields\.UniqueBooleanField"])
    

    models.py

    from django.db import models
    
    from project.apps.fields import UniqueBooleanField
    
    
    class UniqueBooleanModel(models.Model):
        unique_boolean = UniqueBooleanField()
    
        def __unicode__(self):
            return str(self.unique_boolean)
    
    0 讨论(0)
  • 2020-12-07 14:33

    Do I get points for answering my question?

    problem was it was finding itself in the loop, fixed by:

        # is this the testimonial image, if so, unselect other images
        if self.testimonial_image is True:
            others = Photograph.objects.filter(project=self.project).filter(testimonial_image=True)
            pdb.set_trace()
            for o in others:
                if o != self: ### important line
                    o.testimonial_image = False
                    o.save()
    
    0 讨论(0)
  • 2020-12-07 14:35

    Trying to make ends meet with the answers here, I find that some of them address the same issue successfully and each one is suitable in different situations:

    I would choose:

    • @semente: Respects the constraint at the database, model and admin form levels while it overrides Django ORM the least possible. Moreover it can probably be used inside a through table of a ManyToManyField in aunique_together situation. (I will check it and report)

      class MyModel(models.Model):
          is_the_chosen_one = models.NullBooleanField(default=None, unique=True)
      
          def save(self, *args, **kwargs):
              if self.is_the_chosen_one is False:
                  self.is_the_chosen_one = None
              super(MyModel, self).save(*args, **kwargs)
      
    • @Ellis Percival: Hits the database only one extra time and accepts the current entry as the chosen one. Clean and elegant.

      from django.db import transaction
      
      class Character(models.Model):
          name = models.CharField(max_length=255)
          is_the_chosen_one = models.BooleanField()
      
      def save(self, *args, **kwargs):
          if not self.is_the_chosen_one:
              # The use of return is explained in the comments
              return super(Character, self).save(*args, **kwargs)  
          with transaction.atomic():
              Character.objects.filter(
                  is_the_chosen_one=True).update(is_the_chosen_one=False)
              # The use of return is explained in the comments
              return super(Character, self).save(*args, **kwargs)  
      

    Other solutions not suitable for my case but viable:

    @nemocorp is overriding the clean method to perform a validation. However, it does not report back which model is "the one" and this is not user friendly. Despite that, it is a very nice approach especially if someone does not intend to be as aggressive as @Flyte.

    @saul.shanabrook and @Thierry J. would create a custom field which would either change any other "is_the_one" entry to False or raise a ValidationError. I am just reluctant to impement new features to my Django installation unless it is absoletuly necessary.

    @daigorocub: Uses Django signals. I find it a unique approach and gives a hint of how to use Django Signals. However I am not sure whether this is a -strictly speaking- "proper" use of signals since I cannot consider this procedure as part of a "decoupled application".

    0 讨论(0)
  • 2020-12-07 14:36

    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.

    0 讨论(0)
  • 2020-12-07 14:37

    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')
        ]
    
    0 讨论(0)
  • 2020-12-07 14:39

    I tried some of these solutions, and ended up with another one, just for the sake of code shortness (don't have to override forms or save method). For this to work, the field can't be unique in it's definition but the signal makes sure that happens.

    # making default_number True unique
    @receiver(post_save, sender=Character)
    def unique_is_the_chosen_one(sender, instance, **kwargs):
        if instance.is_the_chosen_one:
            Character.objects.all().exclude(pk=instance.pk).update(is_the_chosen_one=False)
    
    0 讨论(0)
提交回复
热议问题