From eb9eefd76fe8111558c3c19f3d80859e0529da9f Mon Sep 17 00:00:00 2001 From: Fabian Allendorf Date: Tue, 2 Dec 2025 16:49:16 +0100 Subject: [PATCH] Add new setting to control FK change representation (#779) * add new setting to activate string repr * skip using foreign key to construct and display diff of foreign key fields * concise name and documentation update # Conflicts: # docs/source/usage.rst * add test for enabled setting * fix code block in docs * fix version * add warning to documentation --- CHANGELOG.md | 1 + auditlog/conf.py | 5 +++++ auditlog/diff.py | 6 +++++- auditlog_tests/tests.py | 23 +++++++++++++++++++++++ docs/source/usage.rst | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8923781..a3eb8ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ #### Improvements +- feat: Add `AUDITLOG_USE_FK_STRING_REPRESENTATION` setting that controls how foreign key changes are represented ([#779)](https://github.com/jazzband/django-auditlog/pull/779)) - Add `AUDITLOG_USE_BASE_MANAGER` setting to override default manager use ([#766](https://github.com/jazzband/django-auditlog/pull/766)) - Drop 'Python 3.9' support ([#773](https://github.com/jazzband/django-auditlog/pull/773)) diff --git a/auditlog/conf.py b/auditlog/conf.py index 0839d32..754328b 100644 --- a/auditlog/conf.py +++ b/auditlog/conf.py @@ -71,3 +71,8 @@ settings.AUDITLOG_LOGENTRY_MODEL = getattr( settings.AUDITLOG_USE_BASE_MANAGER = getattr( settings, "AUDITLOG_USE_BASE_MANAGER", False ) + +# Use string representation of referenced object in foreign key changes instead of its primary key +settings.AUDITLOG_USE_FK_STRING_REPRESENTATION = getattr( + settings, "AUDITLOG_USE_FK_STRING_REPRESENTATION", False +) diff --git a/auditlog/diff.py b/auditlog/diff.py index dd64d20..8583f37 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -106,7 +106,11 @@ def get_field_value(obj, field, use_json_for_changes=False): value = json.dumps(value, sort_keys=True, cls=field.encoder) except TypeError: pass - elif (field.one_to_one or field.many_to_one) and hasattr(field, "rel_class"): + elif ( + not settings.AUDITLOG_USE_FK_STRING_REPRESENTATION + and (field.one_to_one or field.many_to_one) + and hasattr(field, "rel_class") + ): value = smart_str(getattr(obj, field.get_attname()), strings_only=True) else: value = getattr(obj, field.name) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index b446880..3183015 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -2407,6 +2407,29 @@ class TestRelatedDiffs(TestCase): self.assertEqual(int(log_create.changes_dict["related"][1]), one_simple.id) self.assertEqual(int(log_update.changes_dict["related"][1]), two_simple.id) + @override_settings(AUDITLOG_USE_FK_STRING_REPRESENTATION=True) + def test_string_representation_of_fk_changes(self): + """FK changes should be stored using string representation when setting is enabled""" + + t1 = self.test_date + with freezegun.freeze_time(t1): + simple = SimpleModel.objects.create(text="Test Foo") + two_simple = SimpleModel.objects.create(text="Test Bar") + instance = RelatedModel.objects.create(one_to_one=simple, related=simple) + + t2 = self.test_date + datetime.timedelta(days=20) + with freezegun.freeze_time(t2): + instance.one_to_one = two_simple + instance.related = two_simple + instance.save() + + self.assertEqual(instance.history.all().count(), 2) + log_update = instance.history.filter(timestamp=t2).first() + self.assertEqual(log_update.changes_dict["related"][0], "Test Foo") + self.assertEqual(log_update.changes_dict["related"][1], "Test Bar") + self.assertEqual(log_update.changes_dict["one_to_one"][0], "Test Foo") + self.assertEqual(log_update.changes_dict["one_to_one"][1], "Test Bar") + class TestModelSerialization(TestCase): def setUp(self): diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 9dc4a95..5824757 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -444,6 +444,41 @@ of the default one. .. versionadded:: 3.5.0 Custom LogEntry model configuration via ``AUDITLOG_LOGENTRY_MODEL`` +**AUDITLOG_USE_FK_STRING_REPRESENTATION** + +Determines how changes to foreign key fields are recorded in log entries. + +When `True`, changes to foreign key fields are stored using the string representation of related objects. +When `False` (default), the primary key of the related objects is stored instead. + +Before version 2.2.0, foreign key changes were stored using the string representation of the related objects. +Starting from version 2.2.0, the default behavior was updated to store the primary key of the related objects instead. + +Before: + +.. code-block:: json + + { "foreign_key_field": ["foo", "bar"] } + + +After: + +.. code-block:: json + + { "foreign_key_field": [1, 2] } + +You can use this option to enable the legacy behavior. + +.. warning:: + + This reintroduces a known issue https://github.com/jazzband/django-auditlog/issues/421 + Commission Error: Causes unnecessary LogEntries even though no update occurrs because the string representation in memory changed + Omission Error: More common problem, a related object is updated to another object with the same string representation, no update is logged + + Beware of these problem when enabling this setting. + +.. versionadded:: 3.4.0 + Actors ------