From e2894b54009298c924c5e70656492a085c404529 Mon Sep 17 00:00:00 2001 From: Anders <6058745+ddabble@users.noreply.github.com> Date: Fri, 6 Sep 2024 08:56:52 +0200 Subject: [PATCH] Added delete_without_historical_record() --- CHANGES.rst | 2 + docs/disabling_history.rst | 37 +++++++++---------- simple_history/models.py | 27 ++++++++++++-- simple_history/tests/tests/test_models.py | 45 +++++++++++++++-------- 4 files changed, 71 insertions(+), 40 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2c7d87bd..c6b8f94d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,8 @@ Unreleased - Made ``skip_history_when_saving`` work when creating an object - not just when updating an object (gh-1262) +- Added ``delete_without_historical_record()`` to all history-tracked model objects, + which complements ``save_without_historical_record()`` (gh-1387) **Deprecations:** diff --git a/docs/disabling_history.rst b/docs/disabling_history.rst index 9fc288a1..1e3c16f2 100644 --- a/docs/disabling_history.rst +++ b/docs/disabling_history.rst @@ -1,16 +1,26 @@ Disable Creating Historical Records =================================== -Save without creating historical records ----------------------------------------- +``save_without_historical_record()`` and ``delete_without_historical_record()`` +------------------------------------------------------------------------------- -If you want to save model objects without triggering the creation of any historical -records, you can do the following: +These methods are automatically added to a model when registering it for history-tracking +(i.e. defining a ``HistoricalRecords`` manager on the model), +and can be called instead of ``save()`` and ``delete()``, respectively. + +Setting the ``skip_history_when_saving`` attribute +-------------------------------------------------- + +If you want to save or delete model objects without triggering the creation of any +historical records, you can do the following: .. code-block:: python poll.skip_history_when_saving = True + # It applies both when saving... poll.save() + # ...and when deleting + poll.delete() # We recommend deleting the attribute afterward del poll.skip_history_when_saving @@ -27,23 +37,10 @@ This also works when creating an object, but only when calling ``save()``: .. note:: Historical records will always be created when calling the ``create()`` manager method. -Alternatively, call the ``save_without_historical_record()`` method on each object -instead of ``save()``. -This method is automatically added to a model when registering it for history-tracking -(i.e. defining a ``HistoricalRecords`` manager field on the model), -and it looks like this: - -.. code-block:: python - - def save_without_historical_record(self, *args, **kwargs): - self.skip_history_when_saving = True - try: - ret = self.save(*args, **kwargs) - finally: - del self.skip_history_when_saving - return ret +The ``SIMPLE_HISTORY_ENABLED`` setting +-------------------------------------- -Or disable the creation of historical records for *all* models +Disable the creation of historical records for *all* models by adding the following line to your settings: .. code-block:: python diff --git a/simple_history/models.py b/simple_history/models.py index 887b42b3..ac053598 100644 --- a/simple_history/models.py +++ b/simple_history/models.py @@ -195,7 +195,7 @@ def contribute_to_class(self, cls, name): warnings.warn(msg, UserWarning) def add_extra_methods(self, cls): - def save_without_historical_record(self, *args, **kwargs): + def save_without_historical_record(self: models.Model, *args, **kwargs): """ Save the model instance without creating a historical record. @@ -208,7 +208,21 @@ def save_without_historical_record(self, *args, **kwargs): del self.skip_history_when_saving return ret - setattr(cls, "save_without_historical_record", save_without_historical_record) + def delete_without_historical_record(self: models.Model, *args, **kwargs): + """ + Delete the model instance without creating a historical record. + + Make sure you know what you're doing before using this method. + """ + self.skip_history_when_saving = True + try: + ret = self.delete(*args, **kwargs) + finally: + del self.skip_history_when_saving + return ret + + cls.save_without_historical_record = save_without_historical_record + cls.delete_without_historical_record = delete_without_historical_record def finalize(self, sender, **kwargs): inherited = False @@ -665,7 +679,9 @@ def get_meta_options(self, model): ) return meta_fields - def post_save(self, instance, created, using=None, **kwargs): + def post_save( + self, instance: models.Model, created: bool, using: str = None, **kwargs + ): if not getattr(settings, "SIMPLE_HISTORY_ENABLED", True): return if hasattr(instance, "skip_history_when_saving"): @@ -674,9 +690,12 @@ def post_save(self, instance, created, using=None, **kwargs): if not kwargs.get("raw", False): self.create_historical_record(instance, created and "+" or "~", using=using) - def post_delete(self, instance, using=None, **kwargs): + def post_delete(self, instance: models.Model, using: str = None, **kwargs): if not getattr(settings, "SIMPLE_HISTORY_ENABLED", True): return + if hasattr(instance, "skip_history_when_saving"): + return + if self.cascade_delete_history: manager = getattr(instance, self.manager_name) manager.using(using).all().delete() diff --git a/simple_history/tests/tests/test_models.py b/simple_history/tests/tests/test_models.py index d70ca3ce..c9daea9e 100644 --- a/simple_history/tests/tests/test_models.py +++ b/simple_history/tests/tests/test_models.py @@ -262,7 +262,14 @@ def test_cascade_delete_history(self): self.assertEqual(len(thames.history.all()), 1) self.assertEqual(len(nile.history.all()), 0) - def test_save_without_historical_record(self): + def test_registered_model_has_extra_methods(self): + model = ExternalModelSpecifiedWithAppParam.objects.create( + name="registered model" + ) + self.assertTrue(hasattr(model, "save_without_historical_record")) + self.assertTrue(hasattr(model, "delete_without_historical_record")) + + def test__save_without_historical_record__creates_no_records(self): pizza_place = Restaurant.objects.create(name="Pizza Place", rating=3) pizza_place.rating = 4 pizza_place.save_without_historical_record() @@ -290,6 +297,26 @@ def test_save_without_historical_record(self): }, ) + def test__delete_without_historical_record__creates_no_records(self): + self.assertEqual(Restaurant.objects.count(), 0) + pizza_place = Restaurant.objects.create(name="Pizza Place", rating=3) + self.assertEqual(Restaurant.objects.count(), 1) + pizza_place.rating = 4 + pizza_place.delete_without_historical_record() + self.assertEqual(Restaurant.objects.count(), 0) + + (create_record,) = Restaurant.updates.all() + self.assertRecordValues( + create_record, + Restaurant, + { + "name": "Pizza Place", + "rating": 3, + "id": pizza_place.id, + "history_type": "+", + }, + ) + @override_settings(SIMPLE_HISTORY_ENABLED=False) def test_save_with_disabled_history(self): anthony = Person.objects.create(name="Anthony Gillard") @@ -299,12 +326,6 @@ def test_save_with_disabled_history(self): anthony.delete() self.assertEqual(Person.history.count(), 0) - def test_save_without_historical_record_for_registered_model(self): - model = ExternalModelSpecifiedWithAppParam.objects.create( - name="registered model" - ) - self.assertTrue(hasattr(model, "save_without_historical_record")) - def test_save_raises_exception(self): anthony = Person(name="Anthony Gillard") with self.assertRaises(RuntimeError): @@ -2142,7 +2163,7 @@ def setUp(self): self.poll = self.model.objects.create(question="what's up?", pub_date=today) -class ManyToManyTest(TestCase): +class ManyToManyTest(HistoricalTestCase): def setUp(self): self.model = PollWithManyToMany self.history_model = self.model.history.model @@ -2154,14 +2175,6 @@ def setUp(self): def assertDatetimesEqual(self, time1, time2): self.assertAlmostEqual(time1, time2, delta=timedelta(seconds=2)) - def assertRecordValues(self, record, klass, values_dict): - for key, value in values_dict.items(): - self.assertEqual(getattr(record, key), value) - self.assertEqual(record.history_object.__class__, klass) - for key, value in values_dict.items(): - if key not in ["history_type", "history_change_reason"]: - self.assertEqual(getattr(record.history_object, key), value) - def test_create(self): # There should be 1 history record for our poll, the create from setUp self.assertEqual(self.poll.history.all().count(), 1)