diff --git a/AUTHORS.rst b/AUTHORS.rst index b8cfc59..b601622 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -46,6 +46,7 @@ | Jannis Leidel | Javier Garcia Sogo | Jeff Elmore +| Joe Riddle | John Vandenberg | Jonathan Sundqvist | João Amaro @@ -104,3 +105,4 @@ | Éric Araujo | Őry Máté | Nafees Anwar +| meanmail diff --git a/CHANGES.rst b/CHANGES.rst index d964b9f..87422fd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,15 +2,23 @@ Changelog ========= To be released --------- +-------------- +- Add Brazilian Portuguese translation (GH-#578) +- Don't use `post_init` signal for initialize tracker + +4.4.0 (2024-02-10) +------------------ -- Confirm support for `Django 4.2` - Add support for `Python 3.11` (GH-#545) - Add support for `Python 3.12` (GH-#545) - Drop support for `Python 3.7` (GH-#545) -- Swedish translation (GH-#561) +- Add support for `Django 4.2` +- Add support for `Django 5.0` +- Remove ``SaveSignalHandlingModel``. This model used a modified copy of the internal Django method `Model.save_base()` + and had not been updated for upstream bug fixes changes since its addition. +- Add Swedish translation - Use proper column name instead of attname (GH-#573) -- Add Brazilian Portuguese translation (GH-#578) +- Fix ValueError when calling prefetch_related for tracked ForeignKey fields 4.3.1 (2022-11-15) ------------------ diff --git a/docs/models.rst b/docs/models.rst index 8ecf6bc..89f2cff 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -89,8 +89,8 @@ 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. - +release. Until then, the recommended course of action is to use the manager +``all_objects`` when you want to include all instances. UUIDModel ------------------ @@ -112,21 +112,3 @@ Also you can override the default uuid version. Versions 1,3,4 and 5 are now sup .. _`UUIDField`: https://github.com/jazzband/django-model-utils/blob/master/docs/fields.rst#uuidfield - - -SaveSignalHandlingModel ------------------------ - -An abstract base class model to pass a parameter ``signals_to_disable`` -to ``save`` method in order to disable signals - -.. code-block:: python - - from model_utils.models import SaveSignalHandlingModel - - class SaveSignalTestModel(SaveSignalHandlingModel): - name = models.CharField(max_length=20) - - obj = SaveSignalTestModel(name='Test') - # Note: If you use `Model.objects.create`, the signals can't be disabled - obj.save(signals_to_disable=['pre_save'] # disable `pre_save` signal diff --git a/docs/utilities.rst b/docs/utilities.rst index a1d16e5..99ee560 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -328,9 +328,9 @@ FieldTracker implementation details This is how ``FieldTracker`` tracks field changes on ``instance.save`` call. -1. In ``class_prepared`` handler ``FieldTracker`` patches ``save_base`` and - ``refresh_from_db`` methods to reset initial state for tracked fields. -2. In ``post_init`` handler ``FieldTracker`` saves initial values for tracked +1. In ``class_prepared`` handler ``FieldTracker`` patches ``save_base``, + ``refresh_from_db`` and ``__init__`` methods to reset initial state for tracked fields. +2. In the patched ``__init__`` method ``FieldTracker`` saves initial values for tracked fields. 3. ``MyModel.save`` changes ``update_fields`` in order to store auto updated ``modified`` timestamp. Complete list of saved fields is now known. diff --git a/model_utils/models.py b/model_utils/models.py index 268db8c..c816b46 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -1,7 +1,6 @@ from django.core.exceptions import ImproperlyConfigured -from django.db import models, router, transaction +from django.db import models from django.db.models.functions import Now -from django.db.models.signals import post_save, pre_save from django.utils.translation import gettext_lazy as _ from model_utils.fields import ( @@ -172,60 +171,3 @@ class UUIDModel(models.Model): class Meta: abstract = True - - -class SaveSignalHandlingModel(models.Model): - """ - An abstract base class model to pass a parameter ``signals_to_disable`` - to ``save`` method in order to disable signals - """ - class Meta: - abstract = True - - def save(self, signals_to_disable=None, *args, **kwargs): - """ - Add an extra parameters to hold which signals to disable - If empty, nothing will change - """ - - self.signals_to_disable = signals_to_disable or [] - - super().save(*args, **kwargs) - - def save_base(self, raw=False, force_insert=False, - force_update=False, using=None, update_fields=None): - """ - Copied from base class for a minor change. - This is an ugly overwriting but since Django's ``save_base`` method - does not differ between versions 1.8 and 1.10, - that way of implementing wouldn't harm the flow - """ - using = using or router.db_for_write(self.__class__, instance=self) - assert not (force_insert and (force_update or update_fields)) - assert update_fields is None or len(update_fields) > 0 - cls = origin = self.__class__ - - if cls._meta.proxy: - cls = cls._meta.concrete_model - meta = cls._meta - if not meta.auto_created and 'pre_save' not in self.signals_to_disable: - pre_save.send( - sender=origin, instance=self, raw=raw, using=using, - update_fields=update_fields, - ) - with transaction.atomic(using=using, savepoint=False): - if not raw: - self._save_parents(cls, using, update_fields) - updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields) - - self._state.db = using - self._state.adding = False - - if not meta.auto_created and 'post_save' not in self.signals_to_disable: - post_save.send( - sender=origin, instance=self, created=(not updated), - update_fields=update_fields, raw=raw, using=using, - ) - - # Empty the signals in case it might be used somewhere else in future - self.signals_to_disable = [] diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 682ed29..e2a96a7 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -103,6 +103,9 @@ class DescriptorWrapper: else: instance.__dict__[self.field_name] = value + def __getattr__(self, attr): + return getattr(self.descriptor, attr) + @staticmethod def cls_for_descriptor(descriptor): if hasattr(descriptor, '__delete__'): @@ -340,7 +343,7 @@ class FieldTracker: wrapped_descriptor = wrapper_cls(field_name, descriptor, self.attname) setattr(sender, field_name, wrapped_descriptor) self.field_map = self.get_field_map(sender) - models.signals.post_init.connect(self.initialize_tracker) + self.patch_init(sender) self.model_class = sender setattr(sender, self.name, self) self.patch_save(sender) @@ -353,6 +356,16 @@ class FieldTracker: tracker.set_saved_fields() instance._instance_initialized = True + def patch_init(self, model): + original = getattr(model, '__init__') + + @wraps(original) + def inner(instance, *args, **kwargs): + original(instance, *args, **kwargs) + self.initialize_tracker(model, instance) + + setattr(model, '__init__', inner) + def patch_save(self, model): self._patch(model, 'save_base', 'update_fields') self._patch(model, 'refresh_from_db', 'fields') diff --git a/requirements-test.txt b/requirements-test.txt index 138b0de..ee53835 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -pytest==6.2.5 -pytest-django==3.10.0 -psycopg2-binary==2.9.5 -pytest-cov==2.10.1 +pytest==7.4.3 +pytest-django==4.5.2 +psycopg2-binary==2.9.9 +pytest-cov==4.1.0 diff --git a/setup.py b/setup.py index 85834df..b9babe0 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ setup( 'Framework :: Django :: 4.0', 'Framework :: Django :: 4.1', 'Framework :: Django :: 4.2', + 'Framework :: Django :: 5.0', ], zip_safe=False, package_data={ diff --git a/tests/models.py b/tests/models.py index 6d3f23f..b5efe55 100644 --- a/tests/models.py +++ b/tests/models.py @@ -7,7 +7,6 @@ from model_utils import Choices from model_utils.fields import MonitorField, SplitField, StatusField, UUIDField from model_utils.managers import InheritanceManager, JoinManagerMixin, QueryManager from model_utils.models import ( - SaveSignalHandlingModel, SoftDeletableModel, StatusModel, TimeFramedModel, @@ -446,10 +445,6 @@ class CustomNotPrimaryUUIDModel(models.Model): uuid = UUIDField(primary_key=False) -class SaveSignalHandlingTestModel(SaveSignalHandlingModel): - name = models.CharField(max_length=20) - - class TimeStampWithStatusModel(TimeStampedModel, StatusModel): STATUS = Choices( ("active", _("active")), diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index 670e587..90f2370 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -538,6 +538,29 @@ class FieldTrackerForeignKeyTests(FieldTrackerTestCase): self.assertCurrent(fk=self.instance.fk_id) +class FieldTrackerForeignKeyPrefetchRelatedTests(FieldTrackerTestCase): + """Test that using `prefetch_related` on a tracked field does not raise a ValueError.""" + + fk_class = Tracked + tracked_class = TrackedFK + + def setUp(self): + model_tracked = self.fk_class.objects.create(name="", number=0) + self.instance = self.tracked_class.objects.create(fk=model_tracked) + + def test_default(self): + self.tracker = self.instance.tracker + self.assertIsNotNone(list(self.tracked_class.objects.prefetch_related("fk"))) + + def test_custom(self): + self.tracker = self.instance.custom_tracker + self.assertIsNotNone(list(self.tracked_class.objects.prefetch_related("fk"))) + + def test_custom_without_id(self): + self.tracker = self.instance.custom_tracker_without_id + self.assertIsNotNone(list(self.tracked_class.objects.prefetch_related("fk"))) + + class FieldTrackerTimeStampedTests(FieldTrackerTestCase): fk_class = Tracked diff --git a/tests/test_models/test_savesignalhandling_model.py b/tests/test_models/test_savesignalhandling_model.py deleted file mode 100644 index 946da36..0000000 --- a/tests/test_models/test_savesignalhandling_model.py +++ /dev/null @@ -1,42 +0,0 @@ -from django.db.models.signals import post_save, pre_save -from django.test import TestCase - -from tests.models import SaveSignalHandlingTestModel -from tests.signals import post_save_test, pre_save_test - - -class SaveSignalHandlingModelTests(TestCase): - - def test_pre_save(self): - pre_save.connect(pre_save_test, sender=SaveSignalHandlingTestModel) - - obj = SaveSignalHandlingTestModel.objects.create(name='Test') - delattr(obj, 'pre_save_runned') - obj.name = 'Test A' - obj.save() - self.assertEqual(obj.name, 'Test A') - self.assertTrue(hasattr(obj, 'pre_save_runned')) - - obj = SaveSignalHandlingTestModel.objects.create(name='Test') - delattr(obj, 'pre_save_runned') - obj.name = 'Test B' - obj.save(signals_to_disable=['pre_save']) - self.assertEqual(obj.name, 'Test B') - self.assertFalse(hasattr(obj, 'pre_save_runned')) - - def test_post_save(self): - post_save.connect(post_save_test, sender=SaveSignalHandlingTestModel) - - obj = SaveSignalHandlingTestModel.objects.create(name='Test') - delattr(obj, 'post_save_runned') - obj.name = 'Test A' - obj.save() - self.assertEqual(obj.name, 'Test A') - self.assertTrue(hasattr(obj, 'post_save_runned')) - - obj = SaveSignalHandlingTestModel.objects.create(name='Test') - delattr(obj, 'post_save_runned') - obj.name = 'Test B' - obj.save(signals_to_disable=['post_save']) - self.assertEqual(obj.name, 'Test B') - self.assertFalse(hasattr(obj, 'post_save_runned')) diff --git a/tox.ini b/tox.ini index eb0c961..39c219e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,11 @@ [tox] envlist = py{37,38,39,310}-dj32 - py{38,39,310,311,312}-dj{40,41,42,main} + py{38,39,310}-dj{40} + py{38,39,310,311}-dj{41} + py{38,39,310,311}-dj{42} + py{310,311,312}-dj{50} + py{310,311,312}-dj{main} flake8 isort @@ -22,6 +26,7 @@ deps = dj40: Django==4.0.* dj41: Django==4.1.* dj42: Django==4.2.* + dj50: Django==5.0.* djmain: https://github.com/django/django/archive/main.tar.gz ignore_outcome = djmain: True