Add SoftDeletableModel

This commit is contained in:
Radosław Ganczarek 2016-09-12 13:50:03 +02:00
parent 9231617d39
commit 9e90dde2e8
8 changed files with 123 additions and 3 deletions

View file

@ -40,3 +40,4 @@ Travis Swicegood <travis@domain51.com>
Trey Hunner <trey@treyhunner.com>
Karl Wan Nan Wo <karl.wnw@gmail.com>
zyegfryed
Radosław Jan Ganczarek <radoslaw@ganczarek.in>

View file

@ -4,6 +4,9 @@ CHANGES
master (unreleased)
-------------------
* Added `SoftDeletableModel` abstract class, its manageer
`SoftDeletableManager` and queryset `SoftDeletableQuerySet`.
* Fix issue with field tracker and deferred FileField for Django 1.10.

View file

@ -132,6 +132,14 @@ PassThroughManager
built-in `QuerySet.as_manager()` and/or `Manager.from_queryset()` utilities
instead.
SoftDeletableManager
--------------------
Returns only model instances that have the ``is_removed`` field set
to False. Uses ``SoftDeletableQuerySet``, which ensures model instances
won't be removed in bulk, but they will be marked as removed instead.
Mixins
------

View file

@ -47,3 +47,11 @@ returns objects with that status only:
# this query will only return published articles:
Article.published.all()
SoftDeletableModel
------------------
This abstract base class just provides field ``is_removed`` which is
set to True instead of removing the instance. Entities returned in
default manager are limited to not-deleted instances.

View file

@ -244,3 +244,37 @@ class QueryManagerMixin(object):
class QueryManager(QueryManagerMixin, models.Manager):
pass
class SoftDeletableQuerySet(QuerySet):
"""
QuerySet for SoftDeletableModel. Instead of removing instance sets
its ``is_removed`` field to True.
"""
def delete(self):
"""
Soft delete objects from queryset (set their ``is_removed``
field to True)
"""
self.update(is_removed=True)
class SoftDeletableManager(models.Manager):
"""
Manager that limits the queryset by default to show only not removed
instances of model.
"""
_queryset_class = SoftDeletableQuerySet
def get_queryset(self):
"""
Return queryset limited to not removed entries.
"""
kwargs = {'model': self.model, 'using': self._db}
if hasattr(self, '_hints'):
kwargs['hints'] = self._hints
return SoftDeletableQuerySet(**kwargs).filter(is_removed=False)
get_query_set = get_queryset

View file

@ -10,7 +10,7 @@ if django.VERSION >= (1, 9, 0):
else:
from django.utils.timezone import now
from model_utils.managers import QueryManager
from model_utils.managers import QueryManager, SoftDeletableManager
from model_utils.fields import AutoCreatedField, AutoLastModifiedField, \
StatusField, MonitorField
@ -99,3 +99,25 @@ models.signals.class_prepared.connect(add_timeframed_query_manager)
def _field_exists(model_class, field_name):
return field_name in [f.attname for f in model_class._meta.local_fields]
class SoftDeletableModel(models.Model):
"""
An abstract base class model with a ``is_removed`` field that
marks entries that are not going to be used anymore, but are
kept in db for any reason.
Default manager returns only not-removed entries.
"""
is_removed = models.BooleanField(default=False)
class Meta:
abstract = True
objects = SoftDeletableManager()
def delete(self, using=None, keep_parents=False):
"""
Soft delete object (set its ``is_removed`` field to True)
"""
self.is_removed = True
self.save()

View file

@ -4,7 +4,12 @@ from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from model_utils.models import TimeStampedModel, StatusModel, TimeFramedModel
from model_utils.models import (
SoftDeletableModel,
StatusModel,
TimeFramedModel,
TimeStampedModel,
)
from model_utils.tracker import FieldTracker, ModelTracker
from model_utils.managers import QueryManager, InheritanceManager
from model_utils.fields import SplitField, MonitorField, StatusField
@ -305,3 +310,13 @@ class StatusFieldDefaultNotFilled(models.Model):
class StatusFieldChoicesName(models.Model):
NAMED_STATUS = Choices((0, "no", "No"), (1, "yes", "Yes"))
status = StatusField(choices_name='NAMED_STATUS')
class SoftDeletable(SoftDeletableModel):
"""
Test model with additional manager for full access to model
instances.
"""
name = models.CharField(max_length=20)
all_objects = models.Manager()

View file

@ -28,7 +28,8 @@ from model_utils.tests.models import (
ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple, InheritedModelTracked,
Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple,
InheritedTracked, TrackedFileField, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled,
InheritanceManagerTestChild3, StatusFieldChoicesName)
InheritanceManagerTestChild3, StatusFieldChoicesName,
SoftDeletable)
class MigrationsTests(TestCase):
@ -1975,3 +1976,31 @@ class InheritedModelTrackerTests(ModelTrackerTests):
self.name2 = 'test'
self.assertEqual(self.tracker.previous('name2'), None)
self.assertTrue(self.tracker.has_changed('name2'))
class SoftDeletableModelTests(TestCase):
def test_can_only_see_not_removed_entries(self):
SoftDeletable.objects.create(name='a', is_removed=True)
SoftDeletable.objects.create(name='b', is_removed=False)
queryset = SoftDeletable.objects.all()
self.assertEqual(queryset.count(), 1)
self.assertEqual(queryset[0].name, 'b')
def test_instance_cannot_be_fully_deleted(self):
instance = SoftDeletable.objects.create(name='a')
instance.delete()
self.assertEqual(SoftDeletable.objects.count(), 0)
self.assertEqual(SoftDeletable.all_objects.count(), 1)
def test_instance_cannot_be_fully_deleted_via_queryset(self):
SoftDeletable.objects.create(name='a')
SoftDeletable.objects.all().delete()
self.assertEqual(SoftDeletable.objects.count(), 0)
self.assertEqual(SoftDeletable.all_objects.count(), 1)