mirror of
https://github.com/Hopiu/django-model-utils.git
synced 2026-03-16 20:00:23 +00:00
Merge branch 'master' into ptbr-translations
This commit is contained in:
commit
f39c975e8b
12 changed files with 68 additions and 139 deletions
|
|
@ -46,6 +46,7 @@
|
|||
| Jannis Leidel <jannis@leidel.info>
|
||||
| Javier Garcia Sogo <jgsogo@gmail.com>
|
||||
| Jeff Elmore <jelmore@lexile.com>
|
||||
| Joe Riddle <joeriddles10@gmail.com>
|
||||
| John Vandenberg <jayvdb@gmail.com>
|
||||
| Jonathan Sundqvist <sundqvist.jonathan@gmail.com>
|
||||
| João Amaro <joaoamaro70@gmail.com>
|
||||
|
|
@ -104,3 +105,4 @@
|
|||
| Éric Araujo <merwok@netwok.org>
|
||||
| Őry Máté <ory.mate@cloud.bme.hu>
|
||||
| Nafees Anwar <h.nafees.anwar@gmail.com>
|
||||
| meanmail <github@meanmail.dev>
|
||||
|
|
|
|||
16
CHANGES.rst
16
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)
|
||||
------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1
setup.py
1
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={
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
7
tox.ini
7
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue