2009-09-21 14 views
56

Supponiamo che la mia models.py è in questo modo:Valore BooleanField univoco in Django?

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

voglio solo uno dei miei Character casi ad avere is_the_chosen_one == True e tutti gli altri di avere is_the_chosen_one == False. Come posso garantire al meglio che questo vincolo di unicità sia rispettato?

Massimo punteggio alle risposte che tengono conto dell'importanza di rispettare il vincolo a livello di database, modello e (admin) di modulo!

+4

Buona domanda. Sono anche curioso di sapere se è possibile impostare un simile vincolo.So che se hai semplicemente creato un vincolo univoco finirai con solo due possibili righe nel tuo database ;-) –

+0

Non necessariamente: se utilizzi un NullBooleanField, dovresti essere in grado di avere: (a True, a Falso, qualsiasi numero di NULL). –

+0

Secondo [la mia ricerca] (http://stackoverflow.com/a/39374169/2996101), [@semente] (http://stackoverflow.com/a/12269037/2996101) risponde, tiene conto dell'importanza di rispettando il vincolo a livello di database, modello e (admin) mentre fornisce un'ottima soluzione anche per una tabella '' 'through''' di' '' ManyToManyField''' che necessita di un vincolo '' 'unique_together''' . – raratiru

risposta

45

Ogni volta che ho bisogno di eseguire questa operazione, ciò che ho fatto è sovrascrivere il metodo di salvataggio per il modello e fare controllare se qualsiasi altro modello ha già impostato il flag (e disattivarlo).

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) 
+2

Mi piacerebbe solo cambiare 'def save (self):' a: 'def save (self, * args, ** kwargs):' – Marek

+6

Ho provato a modificare questo per cambiare 'save (self)' a ' salva (self, * args, ** kwargs) 'ma la modifica è stata rifiutata. Qualcuno dei revisori potrebbe richiedere del tempo per spiegare perché - poiché ciò sembrerebbe essere coerente con la migliore pratica di Django. – scytale

+9

Ho provato la modifica per rimuovere la necessità di try/except e per rendere il processo più efficiente ma è stato rifiutato .. Invece di 'get()' gendo l'oggetto Character e quindi 'save()' lo ri-ancora, hai solo bisogno filtrare e aggiornamento, che produce solo una query SQL e aiuta a mantenere il DB consistente: 'se self.is_the_chosen_one:' Character.objects.filter (is_the_chosen_one = true) .Update '(is_the_chosen_one = false)' ' super (Carattere, self) .save (* args, ** kwargs) ' –

5
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 

È possibile utilizzare il modulo qui sopra per admin pure, basta usare

class CharacterAdmin(admin.ModelAdmin): 
    form = CharacterForm 
admin.site.register(Character, CharacterAdmin) 
0

ottengo punti per rispondere alla mia domanda?

problema era che si stava trovando nel ciclo, fissato da:

# 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

No, nessun punto per rispondere alla tua domanda e accettare quella risposta. Tuttavia, ci sono punti da fare se qualcuno upvotes la tua risposta. :) – dandan78

+0

Sei sicuro di non voler rispondere alla tua stessa domanda [qui invece] (http://stackoverflow.com/questions/6072349/how-to-enforce-that-object-is-only-with-a -boolean-attr-set-a-vero)? Fondamentalmente tu e @sampablokuper avevi la stessa domanda –

4
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") 

Facendo questo ha reso la convalida disponibile sotto forma di amministrazione di base

20

Invece di usare la pulizia modello personalizzato/salvataggio , Ho creato un custom field ignorando il metodo pre_save su django.db.models.BooleanField. Invece di generare un errore se un altro campo era True, ho creato tutti gli altri campi False se era True. Inoltre, invece di sollevare un errore se il campo era False e nessun altro campo era True, ho salvato il campo come 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) 
+2

Questo sembra molto più pulito degli altri metodi – pistache

+2

Mi piace anche questa soluzione, anche se sembra potenzialmente pericoloso avere object.update che imposta tutti gli altri oggetti su False nel caso in cui i modelli UniqueBoolean è vero. Sarebbe anche meglio se UniqueBooleanField avesse un argomento facoltativo per indicare se gli altri oggetti dovessero essere impostati su False o se dovesse essere sollevato un errore (l'altra alternativa sensata). Inoltre, dato il tuo commento in elif, dove vuoi impostare l'attributo su true, penso che dovresti cambiare 'Return True' a' setattr (model_instance, self.attname, True) ' –

+1

UniqueBooleanField non è realmente Unico dal momento che puoi avere tutti i falsi valori che vuoi. Non sei sicuro di quale sarebbe il nome migliore ... OneTrueBooleanField? Quello che voglio veramente è poterlo mettere in atto in combinazione con una chiave esterna in modo che io possa avere un campo booleano a cui è stato permesso di essere True una sola volta per relazione (ad es. Una carta di credito ha un campo "primario" e un FK per utente e la combinazione Utente/Primaria è True una volta per l'uso). Per quel caso, penso che la risposta di Adam che sovrascrive il salvataggio sarà più semplice per me. –

4

La seguente soluzione è un po 'brutta ma potrebbe lavoro:

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) 

Se is_the_chosen_one impostata su false o Nessuno sarà sempre NULL. Puoi avere NULL quanto vuoi, ma puoi averne solo uno True.

+0

La prima soluzione che ho pensato anche. NULL è sempre univoco, quindi puoi sempre avere una colonna con più di un NULL. – kaleissin

12

Sostituirei il metodo di salvataggio del modello e se hai impostato il valore booleano su True, assicurati che tutti gli altri siano impostati su False.

from django.db import transaction 

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

    @transaction.atomic 
    def save(self, *args, **kwargs): 
     if self.is_the_chosen_one: 
      Character.objects.filter(
       is_the_chosen_one=True).update(is_the_chosen_one=False) 
     super(Character, self).save(*args, **kwargs) 

Ho provato a modificare la risposta simile da Adam, ma è stato rifiutato per modificare troppo della risposta originale. In questo modo è più succinta ed efficiente, in quanto il controllo di altre voci viene eseguito in una singola query.

+2

Penso che questa sia la risposta migliore, ma suggerirei di inserire 'save' in una transazione' @ transaction.atomic'. Perché potrebbe accadere che tu rimuova tutti i flag, ma poi il salvataggio fallisce e finisci con tutti i caratteri non scelti. – Mitar

+0

Grazie per averlo detto. Hai assolutamente ragione e aggiornerò la risposta. –

1

E questo è tutto.

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

ho provato alcune di queste soluzioni, e si è conclusa con un altro, solo per il gusto di mancanza di codice (non devono ignorare le forme o salvare il metodo). Perché funzioni, il campo non può essere unico nella sua definizione ma il segnale assicura che ciò accada.

# 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

Utilizzando un approccio simile a Saul, ma leggermente scopo diverso:

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 

Questa implementazione alzerà un ValidationError quando si tenta di salvare un altro record con un valore True.

Inoltre, ho aggiunto l'argomento unique_for che può essere impostato su qualsiasi altro campo nel modello, per controllare vera unicità solo per i record con lo stesso valore, come ad esempio:

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

Cercando di fare quadrare il bilancio con le risposte qui, trovo che alcuni di loro affrontare lo stesso problema con successo e ognuno è adatto in diverse situazioni:

sceglierei:

  • @semente: rispetta il vincolo a livello di database, modello e modulo modulo mentre esegue l'override di Django ORM il meno possibile. Inoltre, è possibile utilizzare lo all'interno di una tabella through di uno ManyToManyField in una situazione unique_together. (controllerò e rapporto)

    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) 
    
  • @Flyte: colpisce il database solo una volta in più e accetta la voce corrente come il prescelto. Pulito ed elegante

    from django.db import transaction 
    
    class Character(models.Model): 
        name = models.CharField(max_length=255) 
        is_the_chosen_one = models.BooleanField() 
    
        @transaction.atomic 
        def save(self, *args, **kwargs): 
         if self.is_the_chosen_one: 
          Character.objects.filter(
           is_the_chosen_one=True).update(is_the_chosen_one=False) 
         super(Character, self).save(*args, **kwargs) 
    

altro non soluzioni adatte per il mio caso, ma vitali:

@nemocorp è prevalente il metodo clean per eseguire una convalida. Tuttavia, non riporta quale modello è "quello" e questo non è facile da usare. Nonostante ciò, è un approccio molto carino, specialmente se qualcuno non intende essere aggressivo come @Flyte.

@saul.shanabrook e @Thierry J. creerebbe un campo personalizzato che sia cambiare qualsiasi altra voce "is_the_one" per False o sollevare un ValidationError. Sono solo riluttante nell'impedire nuove funzionalità alla mia installazione Django, a meno che non sia assolutamente necessario.

@daigorocub: utilizza i segnali Django. Lo trovo un approccio unico e dà un suggerimento su come utilizzare Django Signals. Tuttavia non sono sicuro che si tratti di un uso "corretto" dei segnali in senso stretto poiché non posso considerare questa procedura come parte di una "applicazione disaccoppiata".

Problemi correlati