diff --git a/AUTHORS.rst b/AUTHORS.rst index 64f3b0e..c86a15d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -15,6 +15,7 @@ | Bo Marchman | Bojan Mihelac | Bruno Alla +| Craig Anderson | Daniel Andrlik | Daniel Stanton | Den Lesnov diff --git a/CHANGES.rst b/CHANGES.rst index cd1bb7d..8cb8fda 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,8 @@ CHANGES - `FieldTracker` now respects `update_fields` changed in overridden `save()` method - Replace ugettext_lazy with gettext_lazy to satisfy Django deprecation warning +- Add available_objects manager to SoftDeletableModel and add deprecation + warning to objects manager. 4.0.0 (2019-12-11) ------------------ diff --git a/docs/models.rst b/docs/models.rst index 79783e9..fa40381 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -83,9 +83,13 @@ returns objects with that status only: SoftDeletableModel ------------------ -This abstract base class just provides field ``is_removed`` which is +This abstract base class just provides a field ``is_removed`` which is set to True instead of removing the instance. Entities returned in -default manager are limited to not-deleted instances. +manager ``available_objects`` are limited to not-deleted instances. + +Note that relying on the default ``objects`` manager to filter out not-deleted +instances is deprecated. ``objects`` will include deleted objects in a future +release. UUIDModel diff --git a/model_utils/managers.py b/model_utils/managers.py index 3725eab..09c2be7 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -1,3 +1,5 @@ +import warnings + import django from django.core.exceptions import ObjectDoesNotExist from django.db import connection @@ -282,10 +284,25 @@ class SoftDeletableManagerMixin: """ _queryset_class = SoftDeletableQuerySet + def __init__(self, *args, _emit_deprecation_warnings=False, **kwargs): + self.emit_deprecation_warnings = _emit_deprecation_warnings + super().__init__(*args, **kwargs) + def get_queryset(self): """ Return queryset limited to not removed entries. """ + + if self.emit_deprecation_warnings: + warning_message = ( + "{0}.objects model manager will include soft-deleted objects in an " + "upcoming release; please use {0}.available_objects to continue " + "excluding soft-deleted objects. See " + "https://django-model-utils.readthedocs.io/en/stable/models.html" + "#softdeletablemodel for more information." + ).format(self.model.__class__.__name__) + warnings.warn(warning_message, DeprecationWarning) + kwargs = {'model': self.model, 'using': self._db} if hasattr(self, '_hints'): kwargs['hints'] = self._hints diff --git a/model_utils/models.py b/model_utils/models.py index 38cdd4d..afec145 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -37,7 +37,7 @@ class TimeStampedModel(models.Model): if 'update_fields' in kwargs and 'modified' not in kwargs['update_fields']: kwargs['update_fields'] += ['modified'] super().save(*args, **kwargs) - + class Meta: abstract = True @@ -132,7 +132,8 @@ class SoftDeletableModel(models.Model): class Meta: abstract = True - objects = SoftDeletableManager() + objects = SoftDeletableManager(_emit_deprecation_warnings=True) + available_objects = SoftDeletableManager() all_objects = models.Manager() def delete(self, using=None, soft=True, *args, **kwargs): diff --git a/tests/test_models/test_softdeletable_model.py b/tests/test_models/test_softdeletable_model.py index 6cb8682..7124318 100644 --- a/tests/test_models/test_softdeletable_model.py +++ b/tests/test_models/test_softdeletable_model.py @@ -6,45 +6,48 @@ from tests.models import SoftDeletable 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) + SoftDeletable.available_objects.create(name='a', is_removed=True) + SoftDeletable.available_objects.create(name='b', is_removed=False) - queryset = SoftDeletable.objects.all() + queryset = SoftDeletable.available_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 = SoftDeletable.available_objects.create(name='a') instance.delete() - self.assertEqual(SoftDeletable.objects.count(), 0) + self.assertEqual(SoftDeletable.available_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.available_objects.create(name='a') - SoftDeletable.objects.all().delete() + SoftDeletable.available_objects.all().delete() - self.assertEqual(SoftDeletable.objects.count(), 0) + self.assertEqual(SoftDeletable.available_objects.count(), 0) self.assertEqual(SoftDeletable.all_objects.count(), 1) def test_delete_instance_no_connection(self): - obj = SoftDeletable.objects.create(name='a') + obj = SoftDeletable.available_objects.create(name='a') self.assertRaises(ConnectionDoesNotExist, obj.delete, using='other') def test_instance_purge(self): - instance = SoftDeletable.objects.create(name='a') + instance = SoftDeletable.available_objects.create(name='a') instance.delete(soft=False) - self.assertEqual(SoftDeletable.objects.count(), 0) + self.assertEqual(SoftDeletable.available_objects.count(), 0) self.assertEqual(SoftDeletable.all_objects.count(), 0) def test_instance_purge_no_connection(self): - instance = SoftDeletable.objects.create(name='a') + instance = SoftDeletable.available_objects.create(name='a') self.assertRaises(ConnectionDoesNotExist, instance.delete, using='other', soft=False) + + def test_deprecation_warning(self): + self.assertWarns(DeprecationWarning, SoftDeletable.objects.all)