mirror of
https://github.com/Hopiu/django-model-utils.git
synced 2026-03-16 20:00:23 +00:00
Add SoftDeletableModel
This commit is contained in:
parent
9231617d39
commit
9e90dde2e8
8 changed files with 123 additions and 3 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
------
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue