Skip to content

Instantly share code, notes, and snippets.

@wolever
Created December 5, 2017 19:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wolever/376ee976c2769230203ad709685bf047 to your computer and use it in GitHub Desktop.
Save wolever/376ee976c2769230203ad709685bf047 to your computer and use it in GitHub Desktop.
A Django model base class that hides models instead of deleting them.
from django.utils import timezone
from django.db import models as m
from django.db.models.base import ModelBase
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields.related import SingleRelatedObjectDescriptor
from model_utils.managers import PassThroughManagerMixin
class DeletableQuerySetMixin(object):
def __init__(self, *args, **kwargs):
if "deleted_filter" in kwargs:
self.deleted_filter = kwargs.pop("deleted_filter")
super(DeletableQuerySetMixin, self).__init__(*args, **kwargs)
def _clone(self, *args, **kwargs):
qs = super(DeletableQuerySetMixin, self)._clone(*args, **kwargs)
qs.deleted_filter = self.deleted_filter
return qs
def delete(self):
self.delete_soft()
def delete_soft(self):
return self.update(deleted_on=timezone.now())
def delete_hard(self):
super(DeletableQuerySetMixin, self).delete()
def active(self):
return self.filter(**self.deleted_filter)
def deleted(self):
return self.exclude(**self.deleted_filter)
class DeletableQuerySet(DeletableQuerySetMixin, m.query.QuerySet):
pass
class DeletableManager(PassThroughManagerMixin, m.Manager):
filter_fields = ["deleted_on"]
queryset_cls = DeletableQuerySet
def __init__(self, queryset_cls=None, include_deleted=False,
*args, **kwargs):
self.include_deleted = include_deleted
self._deleted_filter = dict(
(name, None) for name in self.filter_fields
)
super(DeletableManager, self).__init__(
queryset_cls=(queryset_cls or self.queryset_cls), *args, **kwargs
)
def get_queryset(self):
qs = self._queryset_cls(
model=self.model,
using=self.db,
deleted_filter=self._deleted_filter,
)
if not self.include_deleted:
qs = qs.active()
return qs
get_query_set = get_queryset
class DeletableModelBase(ModelBase):
def __new__(cls, name, bases, attrs):
model = super(DeletableModelBase, cls).__new__(cls, name, bases, attrs)
unique_fields = [
f for f in model._meta.fields
if f.unique and f != model._meta.pk
]
if unique_fields:
raise AssertionError(
"%r is a DeletableModel but has unique fields: %s"
%(model, ", ".join(f.name for f in unique_fields))
)
return model
class DeletableModel(m.Model):
__metaclass__ = DeletableModelBase
# Note: created_on is NULLABLE to make migrations easy; in practice it
# should probably never be NULL.
created_on = m.DateTimeField(auto_now_add=True, null=True, editable=False)
deleted_on = m.DateTimeField(blank=True, null=True, editable=False)
objects = DeletableManager()
objects_all = DeletableManager(include_deleted=True)
class Meta:
abstract = True
def mark_deleted(self):
self.deleted_on = timezone.now()
def delete(self):
self.delete_soft()
def delete_soft(self):
self.mark_deleted()
self.save()
def delete_hard(self):
super(DeletableModel, self).delete()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment