Created
December 5, 2017 19:04
-
-
Save wolever/376ee976c2769230203ad709685bf047 to your computer and use it in GitHub Desktop.
A Django model base class that hides models instead of deleting them.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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