I am trying to implement a general soft deletion pattern for Django models.
Models are given a is_deleted field, which keeps deleted objects in DB, but hides them for all practical purposes: all normal rules for cascading etc. should be followed, except for actual removal. The admin app, however, should still be able to work with deleted objects, for the purpose of either erasing (definitely throw them out) o restoring them. (See code below)
Problem: this breaks cascading. What I had expected to happen was cascading to occur through the methods I overrode on models and custom queryset. What actually happens is that they are instead bypassed by the default queryset/manager which also happens to be using a fast _raw_delete internal API. So either cascaded delete does not happen, or if I call the super().delete() method on my model (and save() after that), standard delete is performed on related objects.
I have tried what is suggested in Cascading Delete w/ Custom Model Delete Method, but this breaks things horribly - besides it advocates usage of the deprecated use_for_related_fields manager attribute.
I am beginning to think that what I want to achieve is not possible without effecting major dismemeberments of Django's privates - weird, since this soft deletion behavior is a standard pattern in many DBMS situations.
This is where I am at now:
I created a custom manager and query set for objects with a is_deleted field:
from django.db import models from django.db.models.query import QuerySet class SoftDeleteQuerySet(QuerySet): #https://stackoverflow.com/questions/28896237/override-djangos-model-delete-method-for-bulk-deletion def __init__(self,*args,**kwargs): return super(self.__class__,self).__init__(*args,**kwargs) def delete(self,*args,**kwargs): for obj in self: obj.delete() #http://codespatter.com/2009/07/01/django-model-manager-soft-delete-how-to-customize-admin/ # but use get_queryset, not get_query_set !!! class SoftDeleteManager(models.Manager): """ Use this manager to get objects that have a is_deleted field """ def get_queryset(self,*args,**kwargs): return SoftDeleteQuerySet(model=self.model, using=self._db, hints=self._hints).filter(is_deleted=False) def all_with_deleted(self,*args,**kwargs): return SoftDeleteQuerySet(model=self.model, using=self._db, hints=self._hints).filter() def deleted_set(self,*args,**kwargs): return SoftDeleteQuerySet(model=self.model, using=self._db, hints=self._hints).filter(is_deleted=True) def get(self, *args, **kwargs): """ if a specific record was requested, return it even if it's deleted """ return self.all_with_deleted().get(*args, **kwargs) def filter(self, *args, **kwargs): """ if pk was specified as a kwarg, return even if it's deleted """ if 'pk' in kwargs: return self.all_with_deleted().filter(*args, **kwargs) return self.get_queryset().filter(*args, **kwargs)
Added a base model to use it:
class SoftDeleteModel(models.Model): objects=SoftDeleteManager() is_deleted = models.BooleanField(default=False, verbose_name="Is Deleted") def delete(self,*args,**kwargs): if self.is_deleted : return self.is_deleted=True self.save() def erase(self,*args,**kwargs): """ Actually delete from database. """ super(SoftDeleteModel,self).delete(*args,**kwargs) def restore(self,*args,**kwargs): if not self.deleted: return self.is_deleted=False self.save() def __unicode__(self): return "%r %s of %s"%(self.__class__,str(self.id)) class Meta: abstract = True
And admin classes to handle the erasure, restore, etc:
# for definitive deletion of models in admin def erase_model(modeladmin,request,queryset): """ Completely remove models from db """ for obj in queryset: obj.erase(user=request.user) def restore_model(modeladmin,request,queryset): """ Restore a softdeletd model set """ for obj in queryset: obj.restore(user=request.user) #http://codespatter.com/2009/07/01/django-model-manager-soft-delete-how-to-customize-admin/ # but the method is now get_queryset. class SoftDeleteAdmin(admin.ModelAdmin): list_display = ('pk', '__unicode__', 'is_deleted',) list_filter = ('is_deleted',) actions=[erase_model, restore_model] def get_queryset(self, request): """ Returns a QuerySet of all model instances that can be edited by the admin site. This is used by changelist_view. """ # Default: qs = self.model._default_manager.get_query_set() qs = self.model._default_manager.all_with_deleted() #TR() # TODO: this should be handled by some parameter to the ChangeList. ordering = self.ordering or () # otherwise we might try to *None, which is bad ;) if ordering: qs = qs.order_by(*ordering) return qs queryset=get_queryset
Ideas?
EDIT: The takeaway of all this (other than searching more thouroughly for packaged solutions :-) ) is that overriding delete and getting it right can be done, but it ain't easy, of for the faint of hart. The package I am going to use - django-softdelete, an evolution of my starting point, ripped from http://codespatter.com/2009/07/01/django-model-manager-soft-delete-how-to-customize-admin/ - uses a ChangeSet computed through the Contenttype API.
Short of all that, there are several situations where the overriden delete() is not called at all (basically, every time a group deletion occurs, django takes shortcuts that jump over the head of model.delete()).
Which is, in my humble opinion, a design blunder. If overriding it takes such quantities of brain explodium, model.delete() should actually be model._delete().