From 3051d230b9158308cf231cc844a96d5f2645ba82 Mon Sep 17 00:00:00 2001 From: Youngkwang Yang Date: Thu, 2 Oct 2025 03:50:54 +0900 Subject: [PATCH] Fix None type mismatch in change detection (#763) * Add test model and test cases for None value type mismatch - issue #750 @The-Alchemist * Fix None type mismatch in change detection --- auditlog/diff.py | 16 +++---- auditlog_tests/test_app/models.py | 8 ++++ auditlog_tests/test_use_json_for_changes.py | 49 ++++++++++++++++++++- auditlog_tests/tests.py | 2 +- 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/auditlog/diff.py b/auditlog/diff.py index 502f0a8..fc98987 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -73,15 +73,15 @@ def get_field_value(obj, field, use_json_for_changes=False): try: model_field = obj._meta.get_field(field.name) default = model_field.default - if default is NOT_PROVIDED: - return None - - if callable(default): - return default() - - return default except AttributeError: - return None + default = NOT_PROVIDED + + if default is NOT_PROVIDED: + default = None + elif callable(default): + default = default() + + return smart_str(default) if not use_json_for_changes else default try: if isinstance(field, DateTimeField): diff --git a/auditlog_tests/test_app/models.py b/auditlog_tests/test_app/models.py index 996407e..dd240fc 100644 --- a/auditlog_tests/test_app/models.py +++ b/auditlog_tests/test_app/models.py @@ -442,6 +442,13 @@ class CustomMaskModel(models.Model): history = AuditlogHistoryField(delete_related=True) +class NullableFieldModel(models.Model): + time = models.TimeField(null=True, blank=True) + optional_text = models.CharField(max_length=100, null=True, blank=True) + + history = AuditlogHistoryField(delete_related=True) + + auditlog.register(AltPrimaryKeyModel) auditlog.register(UUIDPrimaryKeyModel) auditlog.register(ModelPrimaryKeyModel) @@ -485,3 +492,4 @@ auditlog.register( mask_fields=["credit_card"], mask_callable="auditlog_tests.test_app.mask.custom_mask_str", ) +auditlog.register(NullableFieldModel) diff --git a/auditlog_tests/test_use_json_for_changes.py b/auditlog_tests/test_use_json_for_changes.py index 844f700..e3d6087 100644 --- a/auditlog_tests/test_use_json_for_changes.py +++ b/auditlog_tests/test_use_json_for_changes.py @@ -1,5 +1,5 @@ from django.test import TestCase, override_settings -from test_app.models import JSONModel, RelatedModel, SimpleModel +from test_app.models import JSONModel, NullableFieldModel, RelatedModel, SimpleModel from auditlog.models import LogEntry from auditlog.registry import AuditlogModelRegistry @@ -147,3 +147,50 @@ class JSONForChangesTest(TestCase): all(v[1] is None for k, v in changes_dict.items()), 'all values in the changes dict should None, not "None"', ) + + @override_settings(AUDITLOG_STORE_JSON_CHANGES=False) + def test_nullable_field_with_none_not_logged(self): + self.test_auditlog.register_from_settings() + + obj = NullableFieldModel.objects.create(time=None, optional_text=None) + changes_dict = obj.history.latest().changes_dict + + # None → None should NOT be logged as a change + self.assertNotIn("time", changes_dict) + self.assertNotIn("optional_text", changes_dict) + + @override_settings(AUDITLOG_STORE_JSON_CHANGES=False) + def test_nullable_field_with_value_logged(self): + self.test_auditlog.register_from_settings() + + obj = NullableFieldModel.objects.create(optional_text="something") + changes_dict = obj.history.latest().changes_dict + + # None → "something" should be logged + self.assertIn("optional_text", changes_dict) + self.assertEqual(changes_dict["optional_text"], ["None", "something"]) + + @override_settings(AUDITLOG_STORE_JSON_CHANGES=True) + def test_nullable_field_with_none_not_logged_json_mode(self): + self.test_auditlog.register_from_settings() + + obj = NullableFieldModel.objects.create(time=None, optional_text=None) + changes_dict = obj.history.latest().changes_dict + + # None → None should NOT be logged + self.assertNotIn("time", changes_dict) + self.assertNotIn("optional_text", changes_dict) + + @override_settings(AUDITLOG_STORE_JSON_CHANGES=False) + def test_nullable_field_update_none_to_value(self): + self.test_auditlog.register_from_settings() + + obj = NullableFieldModel.objects.create(optional_text=None) + obj.optional_text = "updated" + obj.save() + + changes_dict = obj.history.latest().changes_dict + + # None → "updated" should be logged + self.assertIn("optional_text", changes_dict) + self.assertEqual(changes_dict["optional_text"], ["None", "updated"]) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index b8f59ae..7c2e1cc 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1358,7 +1358,7 @@ class RegisterModelSettingsTest(TestCase): self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel)) self.assertTrue(self.test_auditlog.contains(ChoicesFieldModel)) - self.assertEqual(len(self.test_auditlog.get_models()), 33) + self.assertEqual(len(self.test_auditlog.get_models()), 34) def test_register_models_register_model_with_attrs(self): self.test_auditlog._register_models(