From a0624f1492cee18174ddb9ad6e56101d23250509 Mon Sep 17 00:00:00 2001 From: joeriddles Date: Thu, 26 Jan 2023 13:17:49 -0800 Subject: [PATCH 01/11] For error on prefetch_related for tracked field --- model_utils/tracker.py | 3 +++ tests/models.py | 8 ++++++++ tests/test_fields/test_field_tracker.py | 24 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 682ed29..c86b27e 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__'): diff --git a/tests/models.py b/tests/models.py index d785e88..adbf344 100644 --- a/tests/models.py +++ b/tests/models.py @@ -324,6 +324,14 @@ class ModelTrackedFK(models.Model): custom_tracker_without_id = ModelTracker(fields=['fk']) +class ModelTrackedPrefetchRelatedFK(models.Model): + fk = models.ForeignKey('ModelTrackedFK', on_delete=models.CASCADE) + + tracker = ModelTracked() + custom_tracker = ModelTracker(fields=['fk_id']) + custom_tracker_without_id = ModelTracker(fields=['fk']) + + class ModelTrackedNotDefault(models.Model): name = models.CharField(max_length=20) number = models.IntegerField() diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index 670e587..77f3ea0 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -15,6 +15,7 @@ from tests.models import ( ModelTrackedFK, ModelTrackedMultiple, ModelTrackedNotDefault, + ModelTrackedPrefetchRelatedFK, Tracked, TrackedAbstract, TrackedFileField, @@ -538,6 +539,29 @@ class FieldTrackerForeignKeyTests(FieldTrackerTestCase): self.assertCurrent(fk=self.instance.fk_id) +class ModelTrackedPrefetchRelatedFKTests(FieldTrackerTestCase): + """Test that using `prefetch_related` on a tracked field does raise a ValueError.""" + + tracked_class = ModelTrackedPrefetchRelatedFK + + def setUp(self): + model_tracked = ModelTracked.objects.create(name="", number=0) + model_tracked_fk = ModelTrackedFK.objects.create(fk=model_tracked) + self.instance = ModelTrackedPrefetchRelatedFK.objects.create(fk=model_tracked_fk) + + def test_default(self): + self.tracker = self.instance.tracker + self.assertIsNotNone(list(self.tracked_class.objects.prefetch_related("fk__fk"))) + + def test_custom(self): + self.tracker = self.instance.custom_tracker + self.assertIsNotNone(list(self.tracked_class.objects.prefetch_related("fk__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__fk"))) + + class FieldTrackerTimeStampedTests(FieldTrackerTestCase): fk_class = Tracked From 7e1437b18a06e0a71d15282c3fa89ba99b90d1a8 Mon Sep 17 00:00:00 2001 From: joeriddles Date: Thu, 26 Jan 2023 13:25:51 -0800 Subject: [PATCH 02/11] Add joeriddles to AUTHORS.rst --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 21c822d..2c875fe 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -45,6 +45,7 @@ | Jannis Leidel | Javier Garcia Sogo | Jeff Elmore +| Joe Riddle | John Vandenberg | Jonathan Sundqvist | João Amaro From 9d466ffa742592557c04b06a5193d7da16a069fe Mon Sep 17 00:00:00 2001 From: joeriddles Date: Thu, 26 Jan 2023 14:16:29 -0800 Subject: [PATCH 03/11] Add prefetch_related fix to CHANGES.rst --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 09d39ca..3ac443a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ Changelog ========= +4.3.2 +----- +- Fix `ValueError` when calling `prefetch_related` for tracked `ForeignKey` fields (Fixes GH-433) + 4.3.1 (2022-11-15) ------------------ From 604c6f0b622f095945f068a5582d350bf9bf609d Mon Sep 17 00:00:00 2001 From: joeriddles Date: Thu, 26 Jan 2023 14:25:09 -0800 Subject: [PATCH 04/11] Remove unnecessary model and simplify tests for prefetch_related --- tests/models.py | 8 -------- tests/test_fields/test_field_tracker.py | 17 ++++++++--------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/tests/models.py b/tests/models.py index adbf344..d785e88 100644 --- a/tests/models.py +++ b/tests/models.py @@ -324,14 +324,6 @@ class ModelTrackedFK(models.Model): custom_tracker_without_id = ModelTracker(fields=['fk']) -class ModelTrackedPrefetchRelatedFK(models.Model): - fk = models.ForeignKey('ModelTrackedFK', on_delete=models.CASCADE) - - tracker = ModelTracked() - custom_tracker = ModelTracker(fields=['fk_id']) - custom_tracker_without_id = ModelTracker(fields=['fk']) - - class ModelTrackedNotDefault(models.Model): name = models.CharField(max_length=20) number = models.IntegerField() diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index 77f3ea0..e69807e 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -15,7 +15,6 @@ from tests.models import ( ModelTrackedFK, ModelTrackedMultiple, ModelTrackedNotDefault, - ModelTrackedPrefetchRelatedFK, Tracked, TrackedAbstract, TrackedFileField, @@ -539,27 +538,27 @@ class FieldTrackerForeignKeyTests(FieldTrackerTestCase): self.assertCurrent(fk=self.instance.fk_id) -class ModelTrackedPrefetchRelatedFKTests(FieldTrackerTestCase): +class FieldTrackerForeignKeyPrefetchRelatedTests(FieldTrackerTestCase): """Test that using `prefetch_related` on a tracked field does raise a ValueError.""" - tracked_class = ModelTrackedPrefetchRelatedFK + fk_class = Tracked + tracked_class = TrackedFK def setUp(self): - model_tracked = ModelTracked.objects.create(name="", number=0) - model_tracked_fk = ModelTrackedFK.objects.create(fk=model_tracked) - self.instance = ModelTrackedPrefetchRelatedFK.objects.create(fk=model_tracked_fk) + 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__fk"))) + 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__fk"))) + 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__fk"))) + self.assertIsNotNone(list(self.tracked_class.objects.prefetch_related("fk"))) class FieldTrackerTimeStampedTests(FieldTrackerTestCase): From 103c942ca5a6d41a9e0bdb35d8b5c3f5dcf77f41 Mon Sep 17 00:00:00 2001 From: joeriddles Date: Thu, 26 Jan 2023 14:25:43 -0800 Subject: [PATCH 05/11] Fix docstring for prefetch_related test class --- tests/test_fields/test_field_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index e69807e..90f2370 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -539,7 +539,7 @@ class FieldTrackerForeignKeyTests(FieldTrackerTestCase): class FieldTrackerForeignKeyPrefetchRelatedTests(FieldTrackerTestCase): - """Test that using `prefetch_related` on a tracked field does raise a ValueError.""" + """Test that using `prefetch_related` on a tracked field does not raise a ValueError.""" fk_class = Tracked tracked_class = TrackedFK From 11f3a53b0f8a5fbaa90149d14745fd0cf84a1c84 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 26 Oct 2023 16:53:16 +0100 Subject: [PATCH 06/11] Remove SaveSignalHandlingModel --- CHANGES.rst | 2 + docs/models.rst | 18 ------ model_utils/models.py | 60 +------------------ tests/models.py | 5 -- .../test_savesignalhandling_model.py | 42 ------------- 5 files changed, 3 insertions(+), 124 deletions(-) delete mode 100644 tests/test_models/test_savesignalhandling_model.py diff --git a/CHANGES.rst b/CHANGES.rst index cf71733..3a0033d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ Changelog To be released -------- +- 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. (GH-#582) - Confirm support for `Django 4.2` - Add support for `Python 3.11` (GH-#545) - Add support for `Python 3.12` (GH-#545) diff --git a/docs/models.rst b/docs/models.rst index 8ecf6bc..2c5996f 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -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/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/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_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')) From 270802357f5c7b20ac0fc1d7efccabd1c7120a7d Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 26 Oct 2023 17:00:43 +0100 Subject: [PATCH 07/11] Test Django 5.0 --- CHANGES.rst | 2 +- requirements-test.txt | 8 ++++---- setup.py | 1 + tox.ini | 7 ++++++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4cb100e..f27bac1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,7 @@ To be released - 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. (GH-#582) -- Confirm support for `Django 4.2` +- Confirm support for `Django 4.2` and `Django 5.0`. - Add support for `Python 3.11` (GH-#545) - Add support for `Python 3.12` (GH-#545) - Drop support for `Python 3.7` (GH-#545) 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/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 From fb9f9c647d20773cd20cd74a8b33e61afd2d0f88 Mon Sep 17 00:00:00 2001 From: Remco Wendt Date: Sat, 10 Feb 2024 17:09:14 +0100 Subject: [PATCH 08/11] Update CHANGES.rst --- CHANGES.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f27bac1..11d2c72 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,18 +1,19 @@ Changelog ========= -To be released --------- +4.4.0 (2024-02-10) +------------------ +- Add support for `Python 3.11` +- Add support for `Python 3.12` +- Drop support for `Python 3.7` +- 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. (GH-#582) -- Confirm support for `Django 4.2` and `Django 5.0`. -- 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) -- Use proper column name instead of attname (GH-#573) -- Fix `ValueError` when calling `prefetch_related` for tracked `ForeignKey` fields (Fixes GH-433) + and had not been updated for upstream bug fixes changes since its addition. +- Add Swedish translation +- Use proper column name instead of attname +- Fix ValueError when calling prefetch_related for tracked ForeignKey fields 4.3.1 (2022-11-15) ------------------ From 8b5f1f52137c0aabd5fd9d07d82eeca7785af1d0 Mon Sep 17 00:00:00 2001 From: Tom Adamczewski Date: Mon, 12 Feb 2024 10:35:00 +0000 Subject: [PATCH 09/11] clarify docs for managers of SoftDeletableModel follow-up to https://github.com/jazzband/django-model-utils/pull/438/ --- docs/models.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/models.rst b/docs/models.rst index 2c5996f..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 ------------------ From 5663374890988209c72fccbe50f920a6ce37c7d1 Mon Sep 17 00:00:00 2001 From: meanmail Date: Sun, 5 Feb 2023 13:28:20 +0400 Subject: [PATCH 10/11] Don't use `post_init` signal for initialize tracker --- AUTHORS.rst | 1 + docs/utilities.rst | 6 +++--- model_utils/tracker.py | 12 +++++++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 2c875fe..557f7cb 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -104,3 +104,4 @@ | Éric Araujo | Őry Máté | Nafees Anwar +| meanmail 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/tracker.py b/model_utils/tracker.py index c86b27e..e2a96a7 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -343,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) @@ -356,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') From 2bbbfcbfcacec380fb07f52a7175393d4cf28329 Mon Sep 17 00:00:00 2001 From: meanmail Date: Sun, 5 Feb 2023 13:41:14 +0400 Subject: [PATCH 11/11] Update changelog --- CHANGES.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 11d2c72..d4ad635 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,10 +1,15 @@ Changelog ========= +4.5.0 +----- + +- Don't use `post_init` signal for initialize tracker + 4.4.0 (2024-02-10) ------------------ -- Add support for `Python 3.11` +- Add support for `Python 3.11` - Add support for `Python 3.12` - Drop support for `Python 3.7` - Add support for `Django 4.2`