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().
Maybe you can use django-paranoid
is similar to acts_as_paranoid for rails and is easy to use.
You only need extend to model with ParanoidModel.
For see the deleted object you can use objects_with_deleted:
MyModel.objects_with_deleted.last()
and if do you want do hard delete an object you only should use True param:
m = MyModel.objects.last()
m.delete(True)
django-softdelete is a library implementing soft delete for Django, with cascading. It also stores the changesets, and allows reverting the deletions (e.g. reverting the whole cascade).
I'm not sure what is it's maintenance status and quality, but it can at least serve as inspiration, if not a solution by itself.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With