2011-01-28 16 views
12

Ho una messa a punto come questo (semplificato per questa domanda):Prevenire eliminare nel modello di Django

class Employee(models.Model): 
    name = models.CharField(name, unique=True) 

class Project(models.Model): 
    name = models.CharField(name, unique=True) 
    employees = models.ManyToManyField(Employee) 

Quando un impiegato sta per essere cancellato, voglio controllare se o non è collegato a tutti i progetti . In tal caso, la cancellazione dovrebbe essere impossibile.

Conosco i segnali e come lavorarli. Posso collegarmi al segnale pre_delete e lanciare un'eccezione come ValidationError. Questo impedisce la cancellazione ma non è gestito con garbo dalle forme e così via.

Questa sembra una situazione in cui altri si sono imbattuti. Spero che qualcuno possa indicare una soluzione più elegante.

+1

Questo non è possibile solo utilizzando il codice Python; anche il database stesso dovrà essere modificato. –

+0

Grazie per il tuo commento. Sto cercando prima la parte Python/Django e vedo quanto mi arriva nella mia app. – dyve

risposta

2

Ho un suggerimento ma non sono sicuro che sia meglio della vostra idea attuale. Date un'occhiata alla risposta here per un problema remoto ma non correlato, è possibile ignorare le varie azioni dell'amministratore di django eliminandole essenzialmente e utilizzando il proprio. Così, per esempio, dove hanno:

def really_delete_selected(self, request, queryset): 
    deleted = 0 
    notdeleted = 0 
    for obj in queryset: 
     if obj.project_set.all().count() > 0: 
      # set status to fail 
      notdeleted = notdeleted + 1 
      pass 
     else: 
      obj.delete() 
      deleted = deleted + 1 
    # ... 

Se non stai usando django amministratore come me, allora semplicemente costruire che il check nella logica dell'interfaccia utente prima di consentire all'utente di eliminare l'oggetto.

+0

Grazie. Non sto usando l'admin di Django per questo, anche se una soluzione che includesse sia l'amministratore di Django sia il codice di interfaccia utente personalizzata sarebbe fantastico. Se fosse solo Django admin, la tua soluzione e il tuo riferimento sarebbero eccellenti. +1 per quello. – dyve

5

Se sai che non ci saranno mai tentativi di cancellazione dei dipendenti di massa, potresti semplicemente sostituire lo delete sul tuo modello e chiamare solo super se è un'operazione legale.

Purtroppo, tutto ciò che potrebbe chiamare queryset.delete() andrà direttamente a SQL: http://docs.djangoproject.com/en/dev/topics/db/queries/#deleting-objects

Ma io non vedo come un grosso problema, perché tu sei quello di scrivere questo codice e possibile garantire non ci sono mai qualsiasi queryset.delete() sui dipendenti. Chiama il numero delete() manualmente.

Spero che la cancellazione dei dipendenti sia relativamente rara.

def delete(self, *args, **kwargs): 
    if not self.related_query.all(): 
     super(MyModel, self).delete(*args, **kwargs) 
+0

Grazie. So di questo, e sarà probabilmente la soluzione che cerco se il segnale pre_delete non funziona. +1 per descriverlo con pro e contro. – dyve

+0

+1 per buone vibrazioni. +1 in casa! –

+3

È possibile gestire le eliminazioni di massa scrivendo 2 classi: una che eredita modelli.Manager e un altro inheriting models.query.QuerySet Il primo sovrascrive get_query_set, restituendo un'istanza della seconda classe. La classe derivata da QuerySet sovrascriverà il metodo delete(). Questo metodo di eliminazione esegue iterazioni sull'istanza della classe e chiama delete() su ciascun elemento. Spero che questo sia chiaro. –

15

ero alla ricerca di una risposta a questo problema, non era in grado di trovare un buon compromesso, che avrebbe funzionato per entrambi models.Model.delete() e QuerySet.delete(). Sono andato avanti e, in un certo senso, implementando la soluzione di Steve K. Ho usato questa soluzione per assicurarmi che un oggetto (Dipendente in questo esempio) non possa essere cancellato dal database, in alcun modo, ma è impostato su inattivo.

È una risposta tardiva .. solo per il gusto di altre persone che guardano sto mettendo la mia soluzione qui.

Ecco il codice:

class CustomQuerySet(QuerySet): 
    def delete(self): 
     self.update(active=False) 


class ActiveManager(models.Manager): 
    def active(self): 
     return self.model.objects.filter(active=True) 

    def get_queryset(self): 
     return CustomQuerySet(self.model, using=self._db) 


class Employee(models.Model): 
    name = models.CharField(name, unique=True) 
    active = models.BooleanField(default=True, editable=False) 

    objects = ActiveManager() 

    def delete(self): 
     self.active = False 
     self.save() 

Usage:

Employee.objects.active() # use it just like you would .all() 

o nel admin:

class Employee(admin.ModelAdmin): 

    def queryset(self, request): 
     return super(Employee, self).queryset(request).filter(active=True) 
+0

Non capisco come si cancella un dipendente poiché ho capito che si imposta una flag senza alcun controllo sui progetti, ma la domanda vuole eliminare (o disabilitare) un dipendente se non coinvolge alcun progetto su cui non si è verificato alcun controllo quella. – MohsenTamiz

+1

@MohsenTamiz Questa soluzione riguarda il principio di base (ed elegante) di prevenire l'eliminazione in Django. Ignorare il metodo di cancellazione rende più semplice soddisfare il caso d'uso del richiedente. – Elwin

+0

Grazie per la tua risposta, è un punto di partenza, ma l'ho fatto in un altro modo e ho qualche domanda al riguardo. Sarei grato se tu potessi controllare la mia [domanda] (http://stackoverflow.com/questions/36998620/writing-custom-assignment-operator-for-django-manytomany-field-intermediate-tabl) e portarmi un feedback. – MohsenTamiz

3

Questo sarebbe avvolgere soluzione dall'implementazione nella mia app.Qualche codice è forma LWN's answer.

Ci sono 4 le situazioni che i dati vengono cancellati:

  • query SQL
  • Calling delete() su istanza modello: project.delete()
  • Calling delete() su QuerySet innstance: Project.objects.all().delete()
  • Eliminato dal campo ForeignKey su altro Modello

Mentre non c'è nulla che puoi fare con il primo caso, gli altri tre possono essere controllati a grana fine. Un consiglio è che, nella maggior parte dei casi, non si dovrebbero mai cancellare i dati stessi, poiché tali dati riflettono la cronologia e l'utilizzo della nostra applicazione. Impostazione su active Il campo booleano viene invece preferito.

Per evitare delete() su istanza del modello, sottoclasse delete() nella dichiarazione Modello:

def delete(self): 
     self.active = False 
     self.save(update_fields=('active',)) 

Mentre delete() su istanza QuerySet ha bisogno di un po 'di messa a punto con un gestore di oggetto personalizzato come in LWN's answer.

Wrap questo fino a un'implementazione riutilizzabile:

class ActiveQuerySet(models.QuerySet): 
    def delete(self): 
     self.save(update_fields=('active',)) 


class ActiveManager(models.Manager): 
    def active(self): 
     return self.model.objects.filter(active=True) 

    def get_queryset(self): 
     return ActiveQuerySet(self.model, using=self._db) 


class ActiveModel(models.Model): 
    """ Use `active` state of model instead of delete it 
    """ 
    active = models.BooleanField(default=True, editable=False) 
    class Meta: 
     abstract = True 

    def delete(self): 
     self.active = False 
     self.save() 

    objects = ActiveManager() 

Uso, solo su bclass ActiveModel classe:

class Project(ActiveModel): 
    ... 

Comunque il nostro oggetto può ancora essere cancellato se uno qualsiasi dei suoi campi ForeignKey vengono eliminati:

class Employee(models.Model): 
    name = models.CharField(name, unique=True) 

class Project(models.Model): 
    name = models.CharField(name, unique=True) 
    manager = purchaser = models.ForeignKey(
     Employee, related_name='project_as_manager') 

>>> manager.delete() # this would cause `project` deleted as well 

Questo può essere prevenuta con l'aggiunta di on_delete argument di campo Modello:

class Project(models.Model): 
    name = models.CharField(name, unique=True) 
    manager = purchaser = models.ForeignKey(
     Employee, related_name='project_as_manager', 
     on_delete=models.PROTECT) 

L'impostazione predefinita di on_delete è CASCADE che causerà l'eliminazione dell'istanza utilizzando PROTECT invece che genererà un ProtectedError (una sottoclasse di IntegrityError). Un altro scopo di questo è che la ForeignKey dei dati dovrebbe essere tenuta come riferimento.

+0

questo è un buon riassunto, ma cosa succede quando viene generato questo errore? L'eliminazione di un dipendente non andrà a buon fine? Come permettiamo la cancellazione, ma lasciamo che la dipendenza cada e proteggi il Progetto – strangetimes

1

desidero proporre più una variazione LWN e anhdat's risposte in cui si usa un campo anziché un campo activedeleted e escludiamo oggetti "eliminati" dal queryset predefinita, in modo da trattare tali oggetti come non più presente a meno che non li includiamo specificamente.

class SoftDeleteQuerySet(models.QuerySet): 
    def delete(self): 
     self.update(deleted=True) 


class SoftDeleteManager(models.Manager): 
    use_for_related_fields = True 

    def with_deleted(self): 
     return SoftDeleteQuerySet(self.model, using=self._db) 

    def deleted(self): 
     return self.with_deleted().filter(deleted=True) 

    def get_queryset(self): 
     return self.with_deleted().exclude(deleted=True) 


class SoftDeleteModel(models.Model): 
    """ 
    Sets `deleted` state of model instead of deleting it 
    """ 
    deleted = models.NullBooleanField(editable=False) # NullBooleanField for faster migrations with Postgres if changing existing models 
    class Meta: 
     abstract = True 

    def delete(self): 
     self.deleted = True 
     self.save() 

    objects = SoftDeleteManager() 


class Employee(SoftDeleteModel): 
    ... 

Usage:

Employee.objects.all()   # will only return objects that haven't been 'deleted' 
Employee.objects.with_deleted() # gives you all, including deleted 
Employee.objects.deleted()  # gives you only deleted objects 

Come indicato nella risposta di anhdat, assicurarsi di impostare il on_delete property sul ForeignKeys del modello al fine di evitare comportamenti a cascata, per esempio

class Employee(SoftDeleteModel): 
    latest_project = models.ForeignKey(Project, on_delete=models.PROTECT) 

Nota:

Una funzionalità simile è incluso nel django-model-utils s' SoftDeletableModel come ho appena scoperto. Vale la pena dare un'occhiata. Viene fornito con alcune altre cose utili.

0

Per coloro che fanno riferimento a questa domanda con lo stesso problema con una relazione ForeignKey, la risposta corretta sarebbe utilizzare il campo on_delete=models.PROTECT di Djago sulla relazione ForeignKey. Ciò impedirà la cancellazione di qualsiasi oggetto che abbia collegamenti a chiave esterna ad esso. Ciò non funzionerà per le relazioni ManyToManyField (come discusso nella domanda this), ma funzionerà perfettamente per i campi ForeignKey.

Quindi, se i modelli erano come questo, questo dovrebbe funzionare per impedire la cancellazione di qualsiasi Employee oggetto che ha una o più Project oggetto (s) ad esso associata:

class Employee(models.Model): 
    name = models.CharField(name, unique=True) 

class Project(models.Model): 
    name = models.CharField(name, unique=True) 
    employees = models.ForeignKey(Employee, on_delete=models.PROTECT) 

La documentazione può essere trovato HERE .

Problemi correlati