From 32078bea377808ee22ffddaba97f2dae5048efd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleh=20Ryma=C5=A1e=C5=ADski?= Date: Sat, 2 Dec 2023 22:08:20 -0100 Subject: [PATCH] Handle ObjectDoesNotExist in evaluation of repr --- CHANGELOG.md | 4 ++++ auditlog/models.py | 14 ++++++++++++-- auditlog_tests/models.py | 3 +++ auditlog_tests/tests.py | 21 ++++++++++++++++++++- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a501a09..fc9acbca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ - Python: Confirm Python 3.12 support ([#572](https://github.com/jazzband/django-auditlog/pull/572)) - feat: `thread.local` replaced with `ContextVar` to improve context managers in Django 4.2+ +#### Fixes + +- fix: Handle `ObjectDoesNotExist` in evaluation of `object_repr` ([#592](https://github.com/jazzband/django-auditlog/pull/592)) + ## 3.0.0-beta.2 (2023-10-05) #### Breaking Changes diff --git a/auditlog/models.py b/auditlog/models.py index 81198379..e0c637db 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -25,6 +25,8 @@ from auditlog.diff import mask_str +DEFAULT_OBJECT_REPR = "" + class LogEntryManager(models.Manager): """ @@ -54,7 +56,11 @@ def log_create(self, instance, force_log: bool = False, **kwargs): "content_type", ContentType.objects.get_for_model(instance) ) kwargs.setdefault("object_pk", pk) - kwargs.setdefault("object_repr", smart_str(instance)) + try: + object_repr = smart_str(instance) + except ObjectDoesNotExist: + object_repr = DEFAULT_OBJECT_REPR + kwargs.setdefault("object_repr", object_repr) kwargs.setdefault( "serialized_data", self._get_serialized_data_or_none(instance) ) @@ -96,7 +102,11 @@ def log_m2m_changes( "content_type", ContentType.objects.get_for_model(instance) ) kwargs.setdefault("object_pk", pk) - kwargs.setdefault("object_repr", smart_str(instance)) + try: + object_repr = smart_str(instance) + except ObjectDoesNotExist: + object_repr = DEFAULT_OBJECT_REPR + kwargs.setdefault("object_repr", object_repr) kwargs.setdefault("action", LogEntry.Action.UPDATE) if isinstance(pk, int): diff --git a/auditlog_tests/models.py b/auditlog_tests/models.py index 04736a05..1e16f933 100644 --- a/auditlog_tests/models.py +++ b/auditlog_tests/models.py @@ -86,6 +86,9 @@ class RelatedModel(RelatedModelParent): history = AuditlogHistoryField(delete_related=True) + def __str__(self): + return f"RelatedModel #{self.pk} -> {self.related.id}" + class ManyRelatedModel(models.Model): """ diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 8a84385c..79a65869 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -29,7 +29,7 @@ from auditlog.context import disable_auditlog, set_actor from auditlog.diff import model_instance_diff from auditlog.middleware import AuditlogMiddleware -from auditlog.models import LogEntry +from auditlog.models import DEFAULT_OBJECT_REPR, LogEntry from auditlog.registry import AuditlogModelRegistry, AuditLogRegistrationError, auditlog from auditlog.signals import post_log, pre_log from auditlog_tests.fixtures.custom_get_cid import get_cid as custom_get_cid @@ -1860,6 +1860,25 @@ def test_diff_models_with_related_fields(self): model_instance_diff(simple2, simple1) model_instance_diff(simple1, simple2) + def test_object_repr_related_deleted(self): + """No error is raised when __str__() loads a related object that has been deleted.""" + simple = SimpleModel() + simple.save() + related = RelatedModel(related=simple, one_to_one=simple) + related.save() + related_id = related.id + + related.refresh_from_db() + simple.delete() + related.delete() + + log_entry = ( + LogEntry.objects.get_for_model(RelatedModel) + .filter(object_id=related_id) + .get(action=LogEntry.Action.DELETE) + ) + self.assertEqual(log_entry.object_repr, DEFAULT_OBJECT_REPR) + def test_when_field_doesnt_exist(self): """No error is raised and the default is returned.""" first = SimpleModel(boolean=True)