2024-03-25 16:30:48 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
|
|
2025-04-03 17:58:57 +00:00
|
|
|
import pytest
|
2020-11-29 20:58:00 +00:00
|
|
|
from django.core.cache import cache
|
2016-11-23 23:49:53 +00:00
|
|
|
from django.core.exceptions import FieldError
|
2024-03-25 16:30:48 +00:00
|
|
|
from django.db import models
|
2025-04-03 17:58:57 +00:00
|
|
|
from django.db.models.deletion import ProtectedError
|
2021-10-09 15:30:12 +00:00
|
|
|
from django.db.models.fields.files import FieldFile
|
2016-11-23 23:49:53 +00:00
|
|
|
from django.test import TestCase
|
2020-11-29 20:58:00 +00:00
|
|
|
|
2016-11-23 23:49:53 +00:00
|
|
|
from model_utils import FieldTracker
|
2024-03-29 16:12:32 +00:00
|
|
|
from model_utils.tracker import DescriptorWrapper, FieldInstanceTracker
|
2017-02-15 23:00:10 +00:00
|
|
|
from tests.models import (
|
2020-11-29 20:58:00 +00:00
|
|
|
InheritedModelTracked,
|
|
|
|
|
InheritedTracked,
|
|
|
|
|
InheritedTrackedFK,
|
|
|
|
|
ModelTracked,
|
|
|
|
|
ModelTrackedFK,
|
|
|
|
|
ModelTrackedMultiple,
|
|
|
|
|
ModelTrackedNotDefault,
|
|
|
|
|
Tracked,
|
|
|
|
|
TrackedAbstract,
|
|
|
|
|
TrackedFileField,
|
|
|
|
|
TrackedFK,
|
|
|
|
|
TrackedMultiple,
|
|
|
|
|
TrackedNonFieldAttr,
|
|
|
|
|
TrackedNotDefault,
|
2025-04-03 17:58:57 +00:00
|
|
|
TrackedProtectedSelfRefFK,
|
2020-11-29 20:58:00 +00:00
|
|
|
TrackerTimeStamped,
|
2016-11-24 21:31:23 +00:00
|
|
|
)
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
MixinBase = TestCase
|
|
|
|
|
else:
|
|
|
|
|
MixinBase = object
|
|
|
|
|
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class FieldTrackerMixin(MixinBase):
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
tracker: FieldInstanceTracker
|
|
|
|
|
instance: models.Model
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
def assertHasChanged(self, *, tracker: FieldInstanceTracker | None = None, **kwargs: Any) -> None:
|
2024-03-26 12:56:39 +00:00
|
|
|
if tracker is None:
|
|
|
|
|
tracker = self.tracker
|
2016-11-23 23:49:53 +00:00
|
|
|
for field, value in kwargs.items():
|
|
|
|
|
if value is None:
|
|
|
|
|
with self.assertRaises(FieldError):
|
|
|
|
|
tracker.has_changed(field)
|
|
|
|
|
else:
|
|
|
|
|
self.assertEqual(tracker.has_changed(field), value)
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
def assertPrevious(self, *, tracker: FieldInstanceTracker | None = None, **kwargs: Any) -> None:
|
2024-03-26 12:56:39 +00:00
|
|
|
if tracker is None:
|
|
|
|
|
tracker = self.tracker
|
2016-11-23 23:49:53 +00:00
|
|
|
for field, value in kwargs.items():
|
|
|
|
|
self.assertEqual(tracker.previous(field), value)
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
def assertChanged(self, *, tracker: FieldInstanceTracker | None = None, **kwargs: Any) -> None:
|
2024-03-26 12:56:39 +00:00
|
|
|
if tracker is None:
|
|
|
|
|
tracker = self.tracker
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertEqual(tracker.changed(), kwargs)
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
def assertCurrent(self, *, tracker: FieldInstanceTracker | None = None, **kwargs: Any) -> None:
|
2024-03-26 12:56:39 +00:00
|
|
|
if tracker is None:
|
|
|
|
|
tracker = self.tracker
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertEqual(tracker.current(), kwargs)
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
def update_instance(self, **kwargs: Any) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
for field, value in kwargs.items():
|
|
|
|
|
setattr(self.instance, field, value)
|
|
|
|
|
self.instance.save()
|
|
|
|
|
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class FieldTrackerCommonMixin(FieldTrackerMixin):
|
|
|
|
|
|
|
|
|
|
instance: (
|
|
|
|
|
Tracked | TrackedNotDefault | TrackedMultiple
|
|
|
|
|
| ModelTracked | ModelTrackedNotDefault | ModelTrackedMultiple
|
|
|
|
|
| TrackedAbstract
|
|
|
|
|
)
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_previous(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertPrevious(name=None, number=None)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertPrevious(name=None, number=None)
|
|
|
|
|
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class FieldTrackerTests(FieldTrackerCommonMixin, TestCase):
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
tracked_class: type[Tracked | ModelTracked | TrackedAbstract] = Tracked
|
|
|
|
|
instance: Tracked | ModelTracked | TrackedAbstract
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def setUp(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance = self.tracked_class()
|
|
|
|
|
self.tracker = self.instance.tracker
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_descriptor(self) -> None:
|
2024-04-16 04:36:09 +00:00
|
|
|
tracker = self.tracked_class.tracker
|
|
|
|
|
self.assertTrue(isinstance(tracker, FieldTracker))
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertChanged(name=None, number=None)
|
|
|
|
|
self.instance.name = ''
|
|
|
|
|
self.assertChanged(name=None, number=None)
|
2018-07-02 18:49:16 +00:00
|
|
|
self.instance.mutable = [1, 2, 3]
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged(name=None, number=None, mutable=None)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_has_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertHasChanged(name=True, number=False, mutable=False)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertHasChanged(name=True, number=False, mutable=False)
|
|
|
|
|
self.instance.number = 7
|
|
|
|
|
self.assertHasChanged(name=True, number=True)
|
2018-07-02 18:49:16 +00:00
|
|
|
self.instance.mutable = [1, 2, 3]
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertHasChanged(name=True, number=True, mutable=True)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_save_with_args(self) -> None:
|
2018-12-10 15:35:26 +00:00
|
|
|
self.instance.number = 1
|
|
|
|
|
self.instance.save(False, False, None, None)
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_first_save(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertHasChanged(name=True, number=False, mutable=False)
|
|
|
|
|
self.assertPrevious(name=None, number=None, mutable=None)
|
|
|
|
|
self.assertCurrent(name='', number=None, id=None, mutable=None)
|
|
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
self.instance.name = 'retro'
|
|
|
|
|
self.instance.number = 4
|
2018-07-02 18:49:16 +00:00
|
|
|
self.instance.mutable = [1, 2, 3]
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertHasChanged(name=True, number=True, mutable=True)
|
|
|
|
|
self.assertPrevious(name=None, number=None, mutable=None)
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertCurrent(name='retro', number=4, id=None, mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged(name=None, number=None, mutable=None)
|
2016-03-27 00:02:49 +00:00
|
|
|
|
|
|
|
|
self.instance.save(update_fields=[])
|
|
|
|
|
self.assertHasChanged(name=True, number=True, mutable=True)
|
|
|
|
|
self.assertPrevious(name=None, number=None, mutable=None)
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertCurrent(name='retro', number=4, id=None, mutable=[1, 2, 3])
|
2016-03-27 00:02:49 +00:00
|
|
|
self.assertChanged(name=None, number=None, mutable=None)
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
self.instance.save(update_fields=['number'])
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_has_changed(self) -> None:
|
2018-07-02 18:49:16 +00:00
|
|
|
self.update_instance(name='retro', number=4, mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertHasChanged(name=False, number=False, mutable=False)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertHasChanged(name=True, number=False)
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
self.instance.mutable[1] = 4
|
|
|
|
|
self.assertHasChanged(name=True, number=True, mutable=True)
|
|
|
|
|
self.instance.name = 'retro'
|
|
|
|
|
self.assertHasChanged(name=False, number=True, mutable=True)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_previous(self) -> None:
|
2018-07-02 18:49:16 +00:00
|
|
|
self.update_instance(name='retro', number=4, mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance.name = 'new age'
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertPrevious(name='retro', number=4, mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance.mutable[1] = 4
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertPrevious(name='retro', number=4, mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_changed(self) -> None:
|
2018-07-02 18:49:16 +00:00
|
|
|
self.update_instance(name='retro', number=4, mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertChanged(name='retro')
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertChanged(name='retro', number=4)
|
|
|
|
|
self.instance.name = 'retro'
|
|
|
|
|
self.assertChanged(number=4)
|
|
|
|
|
self.instance.mutable[1] = 4
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertChanged(number=4, mutable=[1, 2, 3])
|
|
|
|
|
self.instance.mutable = [1, 2, 3]
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged(number=4)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_current(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertCurrent(id=None, name='', number=None, mutable=None)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertCurrent(id=None, name='new age', number=None, mutable=None)
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertCurrent(id=None, name='new age', number=8, mutable=None)
|
2018-07-02 18:49:16 +00:00
|
|
|
self.instance.mutable = [1, 2, 3]
|
|
|
|
|
self.assertCurrent(id=None, name='new age', number=8, mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance.mutable[1] = 4
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertCurrent(id=None, name='new age', number=8, mutable=[1, 4, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance.save()
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertCurrent(id=self.instance.id, name='new age', number=8, mutable=[1, 4, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_update_fields(self) -> None:
|
2018-07-02 18:49:16 +00:00
|
|
|
self.update_instance(name='retro', number=4, mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.instance.number = 8
|
2018-07-02 18:49:16 +00:00
|
|
|
self.instance.mutable = [4, 5, 6]
|
|
|
|
|
self.assertChanged(name='retro', number=4, mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance.save(update_fields=[])
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertChanged(name='retro', number=4, mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance.save(update_fields=['name'])
|
|
|
|
|
in_db = self.tracked_class.objects.get(id=self.instance.id)
|
|
|
|
|
self.assertEqual(in_db.name, self.instance.name)
|
|
|
|
|
self.assertNotEqual(in_db.number, self.instance.number)
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertChanged(number=4, mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance.save(update_fields=['number'])
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertChanged(mutable=[1, 2, 3])
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance.save(update_fields=['mutable'])
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
in_db = self.tracked_class.objects.get(id=self.instance.id)
|
|
|
|
|
self.assertEqual(in_db.name, self.instance.name)
|
|
|
|
|
self.assertEqual(in_db.number, self.instance.number)
|
|
|
|
|
self.assertEqual(in_db.mutable, self.instance.mutable)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_refresh_from_db(self) -> None:
|
2019-12-15 14:28:15 +00:00
|
|
|
self.update_instance(name='retro', number=4, mutable=[1, 2, 3])
|
|
|
|
|
self.tracked_class.objects.filter(pk=self.instance.pk).update(
|
|
|
|
|
name='new age', number=8, mutable=[3, 2, 1])
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = 'like in db'
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.instance.mutable = [3, 2, 1]
|
|
|
|
|
self.assertChanged(name='retro', number=4, mutable=[1, 2, 3])
|
|
|
|
|
self.instance.refresh_from_db(fields=('name',))
|
|
|
|
|
self.assertChanged(number=4, mutable=[1, 2, 3])
|
|
|
|
|
self.instance.refresh_from_db(fields={'mutable'})
|
|
|
|
|
self.assertChanged(number=4)
|
|
|
|
|
self.instance.refresh_from_db()
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_with_deferred(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.instance.number = 1
|
|
|
|
|
self.instance.save()
|
2018-02-09 19:39:14 +00:00
|
|
|
item = self.tracked_class.objects.only('name').first()
|
2024-03-29 16:12:32 +00:00
|
|
|
assert item is not None
|
2019-08-21 09:02:23 +00:00
|
|
|
self.assertTrue(item.get_deferred_fields())
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2018-02-09 19:39:14 +00:00
|
|
|
# has_changed() returns False for deferred fields, without un-deferring them.
|
|
|
|
|
# Use an if because ModelTracked doesn't support has_changed() in this case.
|
|
|
|
|
if self.tracked_class == Tracked:
|
2018-06-28 21:08:03 +00:00
|
|
|
self.assertFalse(item.tracker.has_changed('number'))
|
2019-08-21 09:02:23 +00:00
|
|
|
self.assertIsInstance(item.__class__.number, DescriptorWrapper)
|
|
|
|
|
self.assertTrue('number' in item.get_deferred_fields())
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2018-02-09 19:39:14 +00:00
|
|
|
# previous() un-defers field and returns value
|
|
|
|
|
self.assertEqual(item.tracker.previous('number'), 1)
|
2019-08-21 09:02:23 +00:00
|
|
|
self.assertNotIn('number', item.get_deferred_fields())
|
2018-02-09 19:39:14 +00:00
|
|
|
|
|
|
|
|
# examining a deferred field un-defers it
|
|
|
|
|
item = self.tracked_class.objects.only('name').first()
|
2024-03-29 16:12:32 +00:00
|
|
|
assert item is not None
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertEqual(item.number, 1)
|
2019-08-21 09:02:23 +00:00
|
|
|
self.assertTrue('number' not in item.get_deferred_fields())
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertEqual(item.tracker.previous('number'), 1)
|
|
|
|
|
self.assertFalse(item.tracker.has_changed('number'))
|
|
|
|
|
|
2018-02-09 19:39:14 +00:00
|
|
|
# has_changed() returns correct values after deferred field is examined
|
|
|
|
|
self.assertFalse(item.tracker.has_changed('number'))
|
2016-11-23 23:49:53 +00:00
|
|
|
item.number = 2
|
|
|
|
|
self.assertTrue(item.tracker.has_changed('number'))
|
|
|
|
|
|
2018-02-09 19:39:14 +00:00
|
|
|
# previous() returns correct value after deferred field is examined
|
|
|
|
|
self.assertEqual(item.tracker.previous('number'), 1)
|
|
|
|
|
|
|
|
|
|
# assigning to a deferred field un-defers it
|
|
|
|
|
# Use an if because ModelTracked doesn't handle this case.
|
|
|
|
|
if self.tracked_class == Tracked:
|
|
|
|
|
|
|
|
|
|
item = self.tracked_class.objects.only('name').first()
|
2024-03-29 16:12:32 +00:00
|
|
|
assert item is not None
|
2018-02-09 19:39:14 +00:00
|
|
|
item.number = 2
|
|
|
|
|
|
|
|
|
|
# previous() fetches correct value from database after deferred field is assigned
|
|
|
|
|
self.assertEqual(item.tracker.previous('number'), 1)
|
|
|
|
|
|
|
|
|
|
# database fetch of previous() value doesn't affect current value
|
|
|
|
|
self.assertEqual(item.number, 2)
|
|
|
|
|
|
|
|
|
|
# has_changed() returns correct values after deferred field is assigned
|
|
|
|
|
self.assertTrue(item.tracker.has_changed('number'))
|
|
|
|
|
item.number = 1
|
|
|
|
|
self.assertFalse(item.tracker.has_changed('number'))
|
|
|
|
|
|
|
|
|
|
|
2016-11-23 23:49:53 +00:00
|
|
|
class FieldTrackerMultipleInstancesTests(TestCase):
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_with_deferred_fields_access_multiple(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
Tracked.objects.create(pk=1, name='foo', number=1)
|
|
|
|
|
Tracked.objects.create(pk=2, name='bar', number=2)
|
|
|
|
|
|
|
|
|
|
queryset = Tracked.objects.only('id')
|
|
|
|
|
|
|
|
|
|
for instance in queryset:
|
|
|
|
|
instance.name
|
|
|
|
|
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class FieldTrackedModelCustomTests(FieldTrackerCommonMixin, TestCase):
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
tracked_class: type[TrackedNotDefault | ModelTrackedNotDefault] = TrackedNotDefault
|
|
|
|
|
instance: TrackedNotDefault | ModelTrackedNotDefault
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def setUp(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance = self.tracked_class()
|
|
|
|
|
self.tracker = self.instance.name_tracker
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
self.instance.name = ''
|
|
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_first_save(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertHasChanged(name=True, number=None)
|
|
|
|
|
self.assertPrevious(name=None, number=None)
|
|
|
|
|
self.assertCurrent(name='')
|
|
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
self.instance.name = 'retro'
|
|
|
|
|
self.instance.number = 4
|
|
|
|
|
self.assertHasChanged(name=True, number=None)
|
|
|
|
|
self.assertPrevious(name=None, number=None)
|
|
|
|
|
self.assertCurrent(name='retro')
|
|
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_has_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertHasChanged(name=True, number=None)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertHasChanged(name=True, number=None)
|
|
|
|
|
self.instance.number = 7
|
|
|
|
|
self.assertHasChanged(name=True, number=None)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_has_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.update_instance(name='retro', number=4)
|
|
|
|
|
self.assertHasChanged(name=False, number=None)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertHasChanged(name=True, number=None)
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertHasChanged(name=True, number=None)
|
|
|
|
|
self.instance.name = 'retro'
|
|
|
|
|
self.assertHasChanged(name=False, number=None)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_previous(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.update_instance(name='retro', number=4)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertPrevious(name='retro', number=None)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.update_instance(name='retro', number=4)
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertChanged(name='retro')
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertChanged(name='retro')
|
|
|
|
|
self.instance.name = 'retro'
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_current(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertCurrent(name='')
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertCurrent(name='new age')
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertCurrent(name='new age')
|
|
|
|
|
self.instance.save()
|
|
|
|
|
self.assertCurrent(name='new age')
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_update_fields(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.update_instance(name='retro', number=4)
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.instance.save(update_fields=['name', 'number'])
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class FieldTrackedModelAttributeTests(FieldTrackerMixin, TestCase):
|
2016-11-23 23:49:53 +00:00
|
|
|
|
|
|
|
|
tracked_class = TrackedNonFieldAttr
|
2024-03-29 16:12:32 +00:00
|
|
|
instance: TrackedNonFieldAttr
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def setUp(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance = self.tracked_class()
|
|
|
|
|
self.tracker = self.instance.tracker
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_previous(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertPrevious(rounded=None)
|
|
|
|
|
self.instance.number = 7.5
|
|
|
|
|
self.assertPrevious(rounded=None)
|
|
|
|
|
self.instance.save()
|
|
|
|
|
self.assertPrevious(rounded=8)
|
|
|
|
|
self.instance.number = 7.2
|
|
|
|
|
self.assertPrevious(rounded=8)
|
|
|
|
|
self.instance.save()
|
|
|
|
|
self.assertPrevious(rounded=7)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_has_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertHasChanged(rounded=False)
|
|
|
|
|
self.instance.number = 7.5
|
|
|
|
|
self.assertHasChanged(rounded=True)
|
|
|
|
|
self.instance.save()
|
|
|
|
|
self.assertHasChanged(rounded=False)
|
|
|
|
|
self.instance.number = 7.2
|
|
|
|
|
self.assertHasChanged(rounded=True)
|
|
|
|
|
self.instance.number = 7.8
|
|
|
|
|
self.assertHasChanged(rounded=False)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.number = 7.5
|
|
|
|
|
self.assertPrevious(rounded=None)
|
|
|
|
|
self.instance.save()
|
|
|
|
|
self.assertPrevious()
|
|
|
|
|
self.instance.number = 7.8
|
|
|
|
|
self.assertPrevious()
|
|
|
|
|
self.instance.number = 7.2
|
|
|
|
|
self.assertPrevious(rounded=8)
|
|
|
|
|
self.instance.save()
|
|
|
|
|
self.assertPrevious()
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_current(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertCurrent(rounded=None)
|
|
|
|
|
self.instance.number = 7.5
|
|
|
|
|
self.assertCurrent(rounded=8)
|
|
|
|
|
self.instance.save()
|
|
|
|
|
self.assertCurrent(rounded=8)
|
|
|
|
|
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class FieldTrackedModelMultiTests(FieldTrackerCommonMixin, TestCase):
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
tracked_class: type[TrackedMultiple | ModelTrackedMultiple] = TrackedMultiple
|
|
|
|
|
instance: TrackedMultiple | ModelTrackedMultiple
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def setUp(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance = self.tracked_class()
|
|
|
|
|
self.trackers = [self.instance.name_tracker,
|
|
|
|
|
self.instance.number_tracker]
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.tracker = self.instance.name_tracker
|
|
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
self.instance.name = ''
|
|
|
|
|
self.assertChanged(name=None)
|
|
|
|
|
self.tracker = self.instance.number_tracker
|
|
|
|
|
self.assertChanged(number=None)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertChanged(number=None)
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertChanged(number=None)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_has_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.tracker = self.instance.name_tracker
|
|
|
|
|
self.assertHasChanged(name=True, number=None)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertHasChanged(name=True, number=None)
|
|
|
|
|
self.tracker = self.instance.number_tracker
|
|
|
|
|
self.assertHasChanged(name=None, number=False)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertHasChanged(name=None, number=False)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_previous(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
for tracker in self.trackers:
|
|
|
|
|
self.tracker = tracker
|
2019-11-14 16:50:04 +00:00
|
|
|
super().test_pre_save_previous()
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_has_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.update_instance(name='retro', number=4)
|
|
|
|
|
self.assertHasChanged(tracker=self.trackers[0], name=False, number=None)
|
|
|
|
|
self.assertHasChanged(tracker=self.trackers[1], name=None, number=False)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertHasChanged(tracker=self.trackers[0], name=True, number=None)
|
|
|
|
|
self.assertHasChanged(tracker=self.trackers[1], name=None, number=False)
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertHasChanged(tracker=self.trackers[0], name=True, number=None)
|
|
|
|
|
self.assertHasChanged(tracker=self.trackers[1], name=None, number=True)
|
|
|
|
|
self.instance.name = 'retro'
|
|
|
|
|
self.instance.number = 4
|
|
|
|
|
self.assertHasChanged(tracker=self.trackers[0], name=False, number=None)
|
|
|
|
|
self.assertHasChanged(tracker=self.trackers[1], name=None, number=False)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_previous(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.update_instance(name='retro', number=4)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertPrevious(tracker=self.trackers[0], name='retro', number=None)
|
|
|
|
|
self.assertPrevious(tracker=self.trackers[1], name=None, number=4)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.update_instance(name='retro', number=4)
|
|
|
|
|
self.assertChanged(tracker=self.trackers[0])
|
|
|
|
|
self.assertChanged(tracker=self.trackers[1])
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertChanged(tracker=self.trackers[0], name='retro')
|
|
|
|
|
self.assertChanged(tracker=self.trackers[1])
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertChanged(tracker=self.trackers[0], name='retro')
|
|
|
|
|
self.assertChanged(tracker=self.trackers[1], number=4)
|
|
|
|
|
self.instance.name = 'retro'
|
|
|
|
|
self.instance.number = 4
|
|
|
|
|
self.assertChanged(tracker=self.trackers[0])
|
|
|
|
|
self.assertChanged(tracker=self.trackers[1])
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_current(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertCurrent(tracker=self.trackers[0], name='')
|
|
|
|
|
self.assertCurrent(tracker=self.trackers[1], number=None)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertCurrent(tracker=self.trackers[0], name='new age')
|
|
|
|
|
self.assertCurrent(tracker=self.trackers[1], number=None)
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertCurrent(tracker=self.trackers[0], name='new age')
|
|
|
|
|
self.assertCurrent(tracker=self.trackers[1], number=8)
|
|
|
|
|
self.instance.save()
|
|
|
|
|
self.assertCurrent(tracker=self.trackers[0], name='new age')
|
|
|
|
|
self.assertCurrent(tracker=self.trackers[1], number=8)
|
|
|
|
|
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class FieldTrackerForeignKeyMixin(FieldTrackerMixin):
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
fk_class: type[Tracked | ModelTracked]
|
|
|
|
|
tracked_class: type[TrackedFK | ModelTrackedFK]
|
|
|
|
|
instance: TrackedFK | ModelTrackedFK
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def setUp(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.old_fk = self.fk_class.objects.create(number=8)
|
2024-04-16 04:36:09 +00:00
|
|
|
self.instance = self.tracked_class.objects.create(fk=self.old_fk) # type: ignore[misc]
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_default(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.tracker = self.instance.tracker
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.assertPrevious()
|
|
|
|
|
self.assertCurrent(id=self.instance.id, fk_id=self.old_fk.id)
|
2024-04-16 04:36:09 +00:00
|
|
|
self.instance.fk = self.fk_class.objects.create(number=8) # type: ignore[assignment]
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged(fk_id=self.old_fk.id)
|
|
|
|
|
self.assertPrevious(fk_id=self.old_fk.id)
|
|
|
|
|
self.assertCurrent(id=self.instance.id, fk_id=self.instance.fk_id)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_custom(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.tracker = self.instance.custom_tracker
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.assertPrevious()
|
|
|
|
|
self.assertCurrent(fk_id=self.old_fk.id)
|
2024-04-16 04:36:09 +00:00
|
|
|
self.instance.fk = self.fk_class.objects.create(number=8) # type: ignore[assignment]
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged(fk_id=self.old_fk.id)
|
|
|
|
|
self.assertPrevious(fk_id=self.old_fk.id)
|
|
|
|
|
self.assertCurrent(fk_id=self.instance.fk_id)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_custom_without_id(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
with self.assertNumQueries(1):
|
|
|
|
|
self.tracked_class.objects.get()
|
|
|
|
|
self.tracker = self.instance.custom_tracker_without_id
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.assertPrevious()
|
|
|
|
|
self.assertCurrent(fk=self.old_fk.id)
|
2024-04-16 04:36:09 +00:00
|
|
|
self.instance.fk = self.fk_class.objects.create(number=8) # type: ignore[assignment]
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged(fk=self.old_fk.id)
|
|
|
|
|
self.assertPrevious(fk=self.old_fk.id)
|
|
|
|
|
self.assertCurrent(fk=self.instance.fk_id)
|
|
|
|
|
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class FieldTrackerForeignKeyTests(FieldTrackerForeignKeyMixin, TestCase):
|
|
|
|
|
|
|
|
|
|
fk_class = Tracked
|
|
|
|
|
tracked_class = TrackedFK
|
|
|
|
|
|
|
|
|
|
|
2025-04-03 17:58:57 +00:00
|
|
|
class FieldTrackerProtectedForeignKeyTests(FieldTrackerMixin, TestCase):
|
|
|
|
|
"""test case for issue #533 FieldTracker infinite recursion on a deleting object"""
|
|
|
|
|
|
|
|
|
|
fk_class = Tracked
|
|
|
|
|
tracked_class = TrackedProtectedSelfRefFK
|
|
|
|
|
|
|
|
|
|
def setUp(self) -> None:
|
|
|
|
|
self.old_fk = self.fk_class.objects.create(number=8)
|
|
|
|
|
self.instance = self.tracked_class.objects.create(fk=self.old_fk)
|
|
|
|
|
self.instance_2 = self.tracked_class.objects.create(
|
|
|
|
|
fk=self.old_fk, self_ref=self.instance
|
|
|
|
|
)
|
|
|
|
|
self.instance.self_ref = self.instance_2
|
|
|
|
|
self.instance.save()
|
|
|
|
|
|
|
|
|
|
def test_fk_delete(self) -> None:
|
|
|
|
|
with pytest.raises(ProtectedError):
|
|
|
|
|
self.old_fk.delete()
|
|
|
|
|
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class FieldTrackerForeignKeyPrefetchRelatedTests(FieldTrackerMixin, TestCase):
|
2023-01-26 22:25:43 +00:00
|
|
|
"""Test that using `prefetch_related` on a tracked field does not raise a ValueError."""
|
2023-01-26 21:17:49 +00:00
|
|
|
|
2023-01-26 22:25:09 +00:00
|
|
|
fk_class = Tracked
|
|
|
|
|
tracked_class = TrackedFK
|
2024-03-29 16:12:32 +00:00
|
|
|
instance: TrackedFK
|
2023-01-26 21:17:49 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def setUp(self) -> None:
|
2023-01-26 22:25:09 +00:00
|
|
|
model_tracked = self.fk_class.objects.create(name="", number=0)
|
|
|
|
|
self.instance = self.tracked_class.objects.create(fk=model_tracked)
|
2023-01-26 21:17:49 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_default(self) -> None:
|
2023-01-26 21:17:49 +00:00
|
|
|
self.tracker = self.instance.tracker
|
2023-01-26 22:25:09 +00:00
|
|
|
self.assertIsNotNone(list(self.tracked_class.objects.prefetch_related("fk")))
|
2023-01-26 21:17:49 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_custom(self) -> None:
|
2023-01-26 21:17:49 +00:00
|
|
|
self.tracker = self.instance.custom_tracker
|
2023-01-26 22:25:09 +00:00
|
|
|
self.assertIsNotNone(list(self.tracked_class.objects.prefetch_related("fk")))
|
2023-01-26 21:17:49 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_custom_without_id(self) -> None:
|
2023-01-26 21:17:49 +00:00
|
|
|
self.tracker = self.instance.custom_tracker_without_id
|
2023-01-26 22:25:09 +00:00
|
|
|
self.assertIsNotNone(list(self.tracked_class.objects.prefetch_related("fk")))
|
2023-01-26 21:17:49 +00:00
|
|
|
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class FieldTrackerTimeStampedTests(FieldTrackerMixin, TestCase):
|
2019-12-15 16:43:58 +00:00
|
|
|
|
|
|
|
|
fk_class = Tracked
|
|
|
|
|
tracked_class = TrackerTimeStamped
|
2024-03-29 16:12:32 +00:00
|
|
|
instance: TrackerTimeStamped
|
2019-12-15 16:43:58 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def setUp(self) -> None:
|
2019-12-15 16:43:58 +00:00
|
|
|
self.instance = self.tracked_class.objects.create(name='old', number=1)
|
|
|
|
|
self.tracker = self.instance.tracker
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_set_modified_on_save(self) -> None:
|
2019-12-15 16:43:58 +00:00
|
|
|
old_modified = self.instance.modified
|
|
|
|
|
self.instance.name = 'new'
|
|
|
|
|
self.instance.save()
|
|
|
|
|
self.assertGreater(self.instance.modified, old_modified)
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_set_modified_on_save_update_fields(self) -> None:
|
2019-12-15 16:43:58 +00:00
|
|
|
old_modified = self.instance.modified
|
|
|
|
|
self.instance.name = 'new'
|
|
|
|
|
self.instance.save(update_fields=('name',))
|
|
|
|
|
self.assertGreater(self.instance.modified, old_modified)
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
|
|
|
|
|
|
2016-11-23 23:49:53 +00:00
|
|
|
class InheritedFieldTrackerTests(FieldTrackerTests):
|
|
|
|
|
|
|
|
|
|
tracked_class = InheritedTracked
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_child_fields_not_tracked(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.name2 = 'test'
|
|
|
|
|
self.assertEqual(self.tracker.previous('name2'), None)
|
|
|
|
|
self.assertRaises(FieldError, self.tracker.has_changed, 'name2')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FieldTrackerInheritedForeignKeyTests(FieldTrackerForeignKeyTests):
|
|
|
|
|
|
|
|
|
|
tracked_class = InheritedTrackedFK
|
|
|
|
|
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class FieldTrackerFileFieldTests(FieldTrackerMixin, TestCase):
|
2016-11-23 23:49:53 +00:00
|
|
|
|
|
|
|
|
tracked_class = TrackedFileField
|
2024-03-29 16:12:32 +00:00
|
|
|
instance: TrackedFileField
|
2016-11-23 23:49:53 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def setUp(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.instance = self.tracked_class()
|
|
|
|
|
self.tracker = self.instance.tracker
|
|
|
|
|
self.some_file = 'something.txt'
|
|
|
|
|
self.another_file = 'another.txt'
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_saved_data_without_instance(self) -> None:
|
2021-10-09 15:30:12 +00:00
|
|
|
"""
|
|
|
|
|
Tests that instance won't get copied by the Field Tracker.
|
|
|
|
|
|
|
|
|
|
This change was introduced in Django 3.1 with
|
|
|
|
|
https://github.com/django/django/pull/12055
|
|
|
|
|
It results in a dramatic CPU and memory usage of FieldTracker on FileField and
|
|
|
|
|
its subclasses.
|
|
|
|
|
The pickling/deepcopying the instance is useless in the context of FieldTracker
|
|
|
|
|
thus we are skipping it.
|
|
|
|
|
"""
|
|
|
|
|
self.assertEqual(self.tracker.saved_data, {})
|
|
|
|
|
self.update_instance(some_file=self.some_file)
|
|
|
|
|
field_file_copy = self.tracker.saved_data.get('some_file')
|
2024-03-29 16:12:32 +00:00
|
|
|
assert field_file_copy is not None
|
2021-10-09 15:30:12 +00:00
|
|
|
self.assertEqual(field_file_copy.__getstate__().get('instance'), None)
|
|
|
|
|
self.assertEqual(self.instance.some_file.instance, self.instance)
|
|
|
|
|
self.assertIsInstance(self.instance.some_file, FieldFile)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertChanged(some_file=None)
|
|
|
|
|
self.instance.some_file = self.some_file
|
|
|
|
|
self.assertChanged(some_file=None)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_has_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertHasChanged(some_file=True)
|
|
|
|
|
self.instance.some_file = self.some_file
|
|
|
|
|
self.assertHasChanged(some_file=True)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_previous(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertPrevious(some_file=None)
|
|
|
|
|
self.instance.some_file = self.some_file
|
|
|
|
|
self.assertPrevious(some_file=None)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.update_instance(some_file=self.some_file)
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
previous_file = self.instance.some_file
|
|
|
|
|
self.instance.some_file = self.another_file
|
|
|
|
|
self.assertChanged(some_file=previous_file)
|
|
|
|
|
# test deferred file field
|
|
|
|
|
deferred_instance = self.tracked_class.objects.defer('some_file')[0]
|
|
|
|
|
deferred_instance.some_file # access field to fetch from database
|
|
|
|
|
self.assertChanged(tracker=deferred_instance.tracker)
|
|
|
|
|
|
|
|
|
|
previous_file = deferred_instance.some_file
|
|
|
|
|
deferred_instance.some_file = self.another_file
|
|
|
|
|
self.assertChanged(
|
|
|
|
|
tracker=deferred_instance.tracker,
|
|
|
|
|
some_file=previous_file,
|
|
|
|
|
)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_has_changed(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.update_instance(some_file=self.some_file)
|
|
|
|
|
self.assertHasChanged(some_file=False)
|
|
|
|
|
self.instance.some_file = self.another_file
|
|
|
|
|
self.assertHasChanged(some_file=True)
|
|
|
|
|
|
|
|
|
|
# test deferred file field
|
|
|
|
|
deferred_instance = self.tracked_class.objects.defer('some_file')[0]
|
|
|
|
|
deferred_instance.some_file # access field to fetch from database
|
|
|
|
|
self.assertHasChanged(
|
|
|
|
|
tracker=deferred_instance.tracker,
|
|
|
|
|
some_file=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
deferred_instance.some_file = self.another_file
|
|
|
|
|
self.assertHasChanged(
|
|
|
|
|
tracker=deferred_instance.tracker,
|
|
|
|
|
some_file=True,
|
|
|
|
|
)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_post_save_previous(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.update_instance(some_file=self.some_file)
|
|
|
|
|
previous_file = self.instance.some_file
|
|
|
|
|
self.instance.some_file = self.another_file
|
|
|
|
|
self.assertPrevious(some_file=previous_file)
|
|
|
|
|
|
|
|
|
|
# test deferred file field
|
|
|
|
|
deferred_instance = self.tracked_class.objects.defer('some_file')[0]
|
|
|
|
|
deferred_instance.some_file # access field to fetch from database
|
|
|
|
|
self.assertPrevious(
|
|
|
|
|
tracker=deferred_instance.tracker,
|
|
|
|
|
some_file=previous_file,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
deferred_instance.some_file = self.another_file
|
|
|
|
|
self.assertPrevious(
|
|
|
|
|
tracker=deferred_instance.tracker,
|
|
|
|
|
some_file=previous_file,
|
|
|
|
|
)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_current(self) -> None:
|
2016-11-23 23:49:53 +00:00
|
|
|
self.assertCurrent(some_file=self.instance.some_file, id=None)
|
|
|
|
|
self.instance.some_file = self.some_file
|
|
|
|
|
self.assertCurrent(some_file=self.instance.some_file, id=None)
|
|
|
|
|
|
|
|
|
|
# test deferred file field
|
|
|
|
|
self.instance.save()
|
|
|
|
|
deferred_instance = self.tracked_class.objects.defer('some_file')[0]
|
|
|
|
|
deferred_instance.some_file # access field to fetch from database
|
|
|
|
|
self.assertCurrent(
|
|
|
|
|
some_file=self.instance.some_file,
|
|
|
|
|
id=self.instance.id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.instance.some_file = self.another_file
|
|
|
|
|
self.assertCurrent(
|
|
|
|
|
some_file=self.instance.some_file,
|
|
|
|
|
id=self.instance.id,
|
|
|
|
|
)
|
2016-11-24 21:31:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ModelTrackerTests(FieldTrackerTests):
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
tracked_class: type[ModelTracked | TrackedAbstract] = ModelTracked
|
|
|
|
|
instance: ModelTracked
|
2016-11-24 21:31:23 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_cache_compatible(self) -> None:
|
2018-11-20 14:49:52 +00:00
|
|
|
cache.set('key', self.instance)
|
|
|
|
|
instance = cache.get('key')
|
|
|
|
|
instance.number = 1
|
|
|
|
|
instance.name = 'cached'
|
|
|
|
|
instance.save()
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
instance.number = 2
|
|
|
|
|
self.assertHasChanged(number=True)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_changed(self) -> None:
|
2016-11-24 21:31:23 +00:00
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = ''
|
|
|
|
|
self.assertChanged()
|
2018-07-02 18:49:16 +00:00
|
|
|
self.instance.mutable = [1, 2, 3]
|
2016-11-24 21:31:23 +00:00
|
|
|
self.assertChanged()
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_first_save(self) -> None:
|
2016-11-24 21:31:23 +00:00
|
|
|
self.assertHasChanged(name=True, number=True, mutable=True)
|
|
|
|
|
self.assertPrevious(name=None, number=None, mutable=None)
|
|
|
|
|
self.assertCurrent(name='', number=None, id=None, mutable=None)
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = 'retro'
|
|
|
|
|
self.instance.number = 4
|
2018-07-02 18:49:16 +00:00
|
|
|
self.instance.mutable = [1, 2, 3]
|
2016-11-24 21:31:23 +00:00
|
|
|
self.assertHasChanged(name=True, number=True, mutable=True)
|
|
|
|
|
self.assertPrevious(name=None, number=None, mutable=None)
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertCurrent(name='retro', number=4, id=None, mutable=[1, 2, 3])
|
2016-11-24 21:31:23 +00:00
|
|
|
self.assertChanged()
|
2016-03-27 00:02:49 +00:00
|
|
|
|
|
|
|
|
self.instance.save(update_fields=[])
|
|
|
|
|
self.assertHasChanged(name=True, number=True, mutable=True)
|
|
|
|
|
self.assertPrevious(name=None, number=None, mutable=None)
|
2018-07-02 18:49:16 +00:00
|
|
|
self.assertCurrent(name='retro', number=4, id=None, mutable=[1, 2, 3])
|
2016-03-27 00:02:49 +00:00
|
|
|
self.assertChanged()
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
self.instance.save(update_fields=['number'])
|
2016-11-24 21:31:23 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_has_changed(self) -> None:
|
2016-11-24 21:31:23 +00:00
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
self.instance.number = 7
|
|
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ModelTrackedModelCustomTests(FieldTrackedModelCustomTests):
|
|
|
|
|
|
|
|
|
|
tracked_class = ModelTrackedNotDefault
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_first_save(self) -> None:
|
2016-11-24 21:31:23 +00:00
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
self.assertPrevious(name=None, number=None)
|
|
|
|
|
self.assertCurrent(name='')
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = 'retro'
|
|
|
|
|
self.instance.number = 4
|
|
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
self.assertPrevious(name=None, number=None)
|
|
|
|
|
self.assertCurrent(name='retro')
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_has_changed(self) -> None:
|
2016-11-24 21:31:23 +00:00
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
self.instance.number = 7
|
|
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_changed(self) -> None:
|
2016-11-24 21:31:23 +00:00
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = ''
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ModelTrackedModelMultiTests(FieldTrackedModelMultiTests):
|
|
|
|
|
|
|
|
|
|
tracked_class = ModelTrackedMultiple
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_has_changed(self) -> None:
|
2016-11-24 21:31:23 +00:00
|
|
|
self.tracker = self.instance.name_tracker
|
|
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
self.tracker = self.instance.number_tracker
|
|
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertHasChanged(name=True, number=True)
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_pre_save_changed(self) -> None:
|
2016-11-24 21:31:23 +00:00
|
|
|
self.tracker = self.instance.name_tracker
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = ''
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.tracker = self.instance.number_tracker
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.name = 'new age'
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.instance.number = 8
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
class ModelTrackerForeignKeyTests(FieldTrackerForeignKeyMixin, TestCase):
|
2016-11-24 21:31:23 +00:00
|
|
|
|
|
|
|
|
fk_class = ModelTracked
|
|
|
|
|
tracked_class = ModelTrackedFK
|
2024-03-29 16:12:32 +00:00
|
|
|
instance: ModelTrackedFK
|
2016-11-24 21:31:23 +00:00
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_custom_without_id(self) -> None:
|
2016-11-24 21:31:23 +00:00
|
|
|
with self.assertNumQueries(2):
|
|
|
|
|
self.tracked_class.objects.get()
|
|
|
|
|
self.tracker = self.instance.custom_tracker_without_id
|
|
|
|
|
self.assertChanged()
|
|
|
|
|
self.assertPrevious()
|
|
|
|
|
self.assertCurrent(fk=self.old_fk)
|
|
|
|
|
self.instance.fk = self.fk_class.objects.create(number=8)
|
|
|
|
|
self.assertNotEqual(self.instance.fk, self.old_fk)
|
|
|
|
|
self.assertChanged(fk=self.old_fk)
|
|
|
|
|
self.assertPrevious(fk=self.old_fk)
|
|
|
|
|
self.assertCurrent(fk=self.instance.fk)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InheritedModelTrackerTests(ModelTrackerTests):
|
|
|
|
|
|
|
|
|
|
tracked_class = InheritedModelTracked
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_child_fields_not_tracked(self) -> None:
|
2016-11-24 21:31:23 +00:00
|
|
|
self.name2 = 'test'
|
|
|
|
|
self.assertEqual(self.tracker.previous('name2'), None)
|
|
|
|
|
self.assertTrue(self.tracker.has_changed('name2'))
|
2019-03-29 06:46:40 +00:00
|
|
|
|
|
|
|
|
|
2019-08-05 11:45:48 +00:00
|
|
|
class AbstractModelTrackerTests(ModelTrackerTests):
|
2019-03-29 06:46:40 +00:00
|
|
|
|
|
|
|
|
tracked_class = TrackedAbstract
|
2021-10-08 09:09:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TrackerContextDecoratorTests(TestCase):
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def setUp(self) -> None:
|
2021-10-08 09:09:20 +00:00
|
|
|
self.instance = Tracked.objects.create(number=1)
|
|
|
|
|
self.tracker = self.instance.tracker
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
def assertChanged(self, *fields: str) -> None:
|
2021-10-08 09:09:20 +00:00
|
|
|
for f in fields:
|
|
|
|
|
self.assertTrue(self.tracker.has_changed(f))
|
|
|
|
|
|
2024-03-29 16:12:32 +00:00
|
|
|
def assertNotChanged(self, *fields: str) -> None:
|
2021-10-08 09:09:20 +00:00
|
|
|
for f in fields:
|
|
|
|
|
self.assertFalse(self.tracker.has_changed(f))
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_context_manager(self) -> None:
|
2021-10-08 09:09:20 +00:00
|
|
|
with self.tracker:
|
|
|
|
|
with self.tracker:
|
|
|
|
|
self.instance.name = 'new'
|
|
|
|
|
|
|
|
|
|
self.assertChanged('name')
|
|
|
|
|
|
|
|
|
|
self.assertChanged('name')
|
|
|
|
|
|
|
|
|
|
self.assertNotChanged('name')
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_context_manager_fields(self) -> None:
|
2021-10-08 09:09:20 +00:00
|
|
|
with self.tracker('number'):
|
|
|
|
|
with self.tracker('number', 'name'):
|
|
|
|
|
self.instance.name = 'new'
|
|
|
|
|
self.instance.number += 1
|
|
|
|
|
|
|
|
|
|
self.assertChanged('name', 'number')
|
|
|
|
|
|
|
|
|
|
self.assertChanged('number')
|
|
|
|
|
self.assertNotChanged('name')
|
|
|
|
|
|
|
|
|
|
self.assertNotChanged('number', 'name')
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_tracker_decorator(self) -> None:
|
2021-10-08 09:09:20 +00:00
|
|
|
|
|
|
|
|
@Tracked.tracker
|
2024-03-29 16:12:32 +00:00
|
|
|
def tracked_method(obj: Tracked) -> None:
|
2021-10-08 09:09:20 +00:00
|
|
|
obj.name = 'new'
|
|
|
|
|
self.assertChanged('name')
|
|
|
|
|
|
|
|
|
|
tracked_method(self.instance)
|
|
|
|
|
|
|
|
|
|
self.assertNotChanged('name')
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_tracker_decorator_fields(self) -> None:
|
2021-10-08 09:09:20 +00:00
|
|
|
|
|
|
|
|
@Tracked.tracker(fields=['name'])
|
2024-03-29 16:12:32 +00:00
|
|
|
def tracked_method(obj: Tracked) -> None:
|
2021-10-08 09:09:20 +00:00
|
|
|
obj.name = 'new'
|
|
|
|
|
obj.number += 1
|
|
|
|
|
self.assertChanged('name', 'number')
|
|
|
|
|
|
|
|
|
|
tracked_method(self.instance)
|
|
|
|
|
|
|
|
|
|
self.assertChanged('number')
|
|
|
|
|
self.assertNotChanged('name')
|
|
|
|
|
|
2023-03-22 17:50:18 +00:00
|
|
|
def test_tracker_context_with_save(self) -> None:
|
2021-10-08 09:09:20 +00:00
|
|
|
|
|
|
|
|
with self.tracker:
|
|
|
|
|
self.instance.name = 'new'
|
|
|
|
|
self.instance.save()
|
|
|
|
|
|
|
|
|
|
self.assertChanged('name')
|
|
|
|
|
|
|
|
|
|
self.assertNotChanged('name')
|