web-dev-qa-db-fra.com

Valeur BooleanField unique dans Django?

Supposons que mon models.py ressemble à ceci:

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

Je souhaite qu'une seule de mes Character instances ait is_the_chosen_one == True et tous les autres d'avoir is_the_chosen_one == False. Comment puis-je garantir au mieux que cette contrainte d'unicité est respectée?

Excellentes notes aux réponses qui tiennent compte de l'importance de respecter la contrainte au niveau de la base de données, du modèle et du formulaire (admin)!

80
sampablokuper

Chaque fois que j'ai eu besoin d'accomplir cette tâche, ce que j'ai fait, c'est remplacer la méthode de sauvegarde du modèle et lui faire vérifier si un autre modèle a déjà le drapeau (et le désactiver).

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

    def save(self, *args, **kwargs):
        if self.is_the_chosen_one:
            try:
                temp = Character.objects.get(is_the_chosen_one=True)
                if self != temp:
                    temp.is_the_chosen_one = False
                    temp.save()
            except Character.DoesNotExist:
                pass
        super(Character, self).save(*args, **kwargs)
59
Adam

Je remplacerais la méthode de sauvegarde du modèle et si vous avez défini le booléen sur True, assurez-vous que tous les autres sont définis sur False.

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:
            return super(Character, self).save(*args, **kwargs)
        with transaction.atomic():
            Character.objects.filter(
                is_the_chosen_one=True).update(is_the_chosen_one=False)
            return super(Character, self).save(*args, **kwargs)

J'ai essayé de modifier la réponse similaire d'Adam, mais elle a été rejetée pour avoir trop changé la réponse d'origine. Cette méthode est plus succincte et efficace car la vérification des autres entrées se fait en une seule requête.

29
Ellis Percival

Au lieu d'utiliser le nettoyage/enregistrement de modèle personnalisé, j'ai créé un champ personnalisé remplaçant le pre_save méthode sur Django.db.models.BooleanField. Au lieu de générer une erreur si un autre champ était True, j'ai créé tous les autres champs False s'il s'agissait de True. De plus, au lieu de générer une erreur si le champ était False et qu'aucun autre champ n'était True, je l'ai enregistré sous 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)
27
saul.shanabrook

En essayant de joindre les deux bouts avec les réponses ici, je trouve que certains d'entre eux résolvent le même problème avec succès et chacun convient dans des situations différentes:

Je choisirais:

  • @ semente : respecte la contrainte au niveau de la base de données, du modèle et du formulaire d'administration tout en remplaçant Django ORM le moins possible. De plus, il peut probablement être utilisé dans une table through d'une ManyToManyField dans uneunique_together situation. (Je vais vérifier et rapporter)

    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 : ne frappe la base de données qu'une seule fois de plus et accepte l'entrée actuelle comme celle choisie. Propre et élégant.

    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)  
    

Autres solutions non adaptées à mon cas mais viables:

@ nemocorp remplace la méthode clean pour effectuer une validation. Cependant, il ne rapporte pas quel modèle est "celui" et ce n'est pas convivial. Malgré cela, c'est une approche très agréable, surtout si quelqu'un n'a pas l'intention d'être aussi agressif que @Flyte.

@ saul.shanabrook et @ Thierry J. créerait un champ personnalisé qui changerait toute autre entrée "is_the_one" en False ou augmenterait un ValidationError. Je suis juste réticent à implémenter de nouvelles fonctionnalités dans mon installation Django sauf si cela est absolument nécessaire.

@ daigorocub : Utilise Django signaux. Je trouve que c'est une approche unique et donne un indice sur la façon d'utiliser Django Signals . Cependant je suis Je ne sais pas s'il s'agit d'une utilisation "stricte" des signaux, à proprement parler, car je ne peux pas considérer cette procédure comme faisant partie d'une "application découplée".

9
raratiru

La solution suivante est un peu moche mais pourrait fonctionner:

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)

Si vous définissez is_the_chosen_one sur False ou None, il sera toujours NULL. Vous pouvez avoir NULL autant que vous le souhaitez, mais vous ne pouvez avoir qu'un seul True.

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

    def save(self, *args, **kwargs):
        if self.is_the_chosen_one:
            qs = Character.objects.filter(is_the_chosen_one=True)
            if self.pk:
                qs = qs.exclude(pk=self.pk)
            if qs.count() != 0:
                # choose ONE of the next two lines
                self.is_the_chosen_one = False # keep the existing "chosen one"
                #qs.update(is_the_chosen_one=False) # make this obj "the chosen one"
        super(Character, self).save(*args, **kwargs)

class CharacterForm(forms.ModelForm):
    class Meta:
        model = Character

    # if you want to use the new obj as the chosen one and remove others, then
    # be sure to use the second line in the model save() above and DO NOT USE
    # the following clean method
    def clean_is_the_chosen_one(self):
        chosen = self.cleaned_data.get('is_the_chosen_one')
        if chosen:
            qs = Character.objects.filter(is_the_chosen_one=True)
            if self.instance.pk:
                qs = qs.exclude(pk=self.instance.pk)
            if qs.count() != 0:
                raise forms.ValidationError("A Chosen One already exists! You will pay for your insolence!")
        return chosen

Vous pouvez également utiliser le formulaire ci-dessus pour l'administrateur, utilisez simplement

class CharacterAdmin(admin.ModelAdmin):
    form = CharacterForm
admin.site.register(Character, CharacterAdmin)
6
shadfc
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")

Cela a rendu la validation disponible dans le formulaire d'administration de base

4
nemocorp

En utilisant une approche similaire à Saul, mais un objectif légèrement différent:

class TrueUniqueBooleanField(BooleanField):

    def __init__(self, unique_for=None, *args, **kwargs):
        self.unique_for = unique_for
        super(BooleanField, self).__init__(*args, **kwargs)

    def pre_save(self, model_instance, add):
        value = super(TrueUniqueBooleanField, self).pre_save(model_instance, add)

        objects = model_instance.__class__.objects

        if self.unique_for:
            objects = objects.filter(**{self.unique_for: getattr(model_instance, self.unique_for)})

        if value and objects.exclude(id=model_instance.id).filter(**{self.attname: True}):
            msg = 'Only one instance of {} can have its field {} set to True'.format(model_instance.__class__, self.attname)
            if self.unique_for:
                msg += ' for each different {}'.format(self.unique_for)
            raise ValidationError(msg)

        return value

Cette implémentation lèvera un ValidationError lorsque vous tenterez d'enregistrer un autre enregistrement avec une valeur True.

J'ai également ajouté le unique_for argument qui peut être défini sur n'importe quel autre champ du modèle, pour vérifier l'unicité uniquement pour les enregistrements ayant la même valeur, tels que:

class Phone(models.Model):
    user = models.ForeignKey(User)
    main = TrueUniqueBooleanField(unique_for='user', default=False)
2
Thierry J.

Et c'est tout.

def save(self, *args, **kwargs):
    if self.default_dp:
        DownloadPageOrder.objects.all().update(**{'default_dp': False})
    super(DownloadPageOrder, self).save(*args, **kwargs)
2
palestamp

J'ai essayé certaines de ces solutions et je me suis retrouvé avec une autre, juste pour des raisons de brièveté du code (pas besoin de remplacer les formulaires ou de sauvegarder la méthode). Pour que cela fonctionne, le champ ne peut pas être unique dans sa définition, mais le signal s'assure que cela se produit.

# 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)
1
daigorocub

Dois-je obtenir des points pour répondre à ma question?

le problème était qu'il se retrouvait dans la boucle, corrigé par:

    # 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()
1
bytejunkie