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)!
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)
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.
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)
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".
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.
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)
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
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)
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)
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)
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()