From 3a58e0a9997eee0a9a5a696659835a14031db8e7 Mon Sep 17 00:00:00 2001 From: Youngkwang Yang Date: Mon, 9 Jun 2025 22:00:28 +0900 Subject: [PATCH] Fix `get_field_value` field default value handling for Django 6.0 compatibility (#726) * 'get_field_value' - improve default value handling * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add Test cases --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- auditlog/diff.py | 40 ++++++++++++++++++++----------- auditlog_tests/test_app/models.py | 1 + auditlog_tests/tests.py | 27 +++++++++++++++++++++ 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/auditlog/diff.py b/auditlog/diff.py index 5a46731..b8455f1 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -62,11 +62,31 @@ def get_field_value(obj, field, use_json_for_changes=False): :return: The value of the field as a string. :rtype: str """ + + def get_default_value(): + """ + Attempts to get the default value for a field from the model's field definition. + + :return: The default value of the field or None + """ + 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 + try: if isinstance(field, DateTimeField): # DateTimeFields are timezone-aware, so we need to convert the field # to its naive form before we can accurately compare them for changes. - value = getattr(obj, field.name, None) + value = getattr(obj, field.name) try: value = field.to_python(value) except TypeError: @@ -78,30 +98,22 @@ def get_field_value(obj, field, use_json_for_changes=False): ): value = django_timezone.make_naive(value, timezone=timezone.utc) elif isinstance(field, JSONField): - value = field.to_python(getattr(obj, field.name, None)) + value = field.to_python(getattr(obj, field.name)) if not use_json_for_changes: try: 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"): - value = smart_str( - getattr(obj, field.get_attname(), None), strings_only=True - ) + value = smart_str(getattr(obj, field.get_attname()), strings_only=True) else: - value = getattr(obj, field.name, None) - + value = getattr(obj, field.name) if not use_json_for_changes: value = smart_str(value) if type(value).__name__ == "__proxy__": value = str(value) - - except ObjectDoesNotExist: - value = ( - field.default - if getattr(field, "default", NOT_PROVIDED) is not NOT_PROVIDED - else None - ) + except (ObjectDoesNotExist, AttributeError): + return get_default_value() return value diff --git a/auditlog_tests/test_app/models.py b/auditlog_tests/test_app/models.py index 38d6966..cf35cc4 100644 --- a/auditlog_tests/test_app/models.py +++ b/auditlog_tests/test_app/models.py @@ -20,6 +20,7 @@ class SimpleModel(models.Model): boolean = models.BooleanField(default=False) integer = models.IntegerField(blank=True, null=True) datetime = models.DateTimeField(auto_now=True) + char = models.CharField(null=True, max_length=100, default=lambda: "default value") history = AuditlogHistoryField(delete_related=True) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index c5c9fe8..4be1251 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -2111,6 +2111,33 @@ class ModelInstanceDiffTest(TestCase): msg="ObjectDoesNotExist should be handled", ) + def test_field_with_no_default_provided(self): + """Field with no default (NOT_PROVIDED) should return None.""" + first = SimpleModel(integer=1) + second = SimpleModel() + + delattr(second, "integer") + + changes = model_instance_diff(first, second) + self.assertEqual( + changes, + {"integer": ("1", "None")}, + msg="field with no default should return None", + ) + + def test_field_with_callable_default(self): + first = SimpleModel(char="value") + second = SimpleModel() + + delattr(second, "char") + + changes = model_instance_diff(first, second) + self.assertEqual( + changes, + {"char": ("value", "default value")}, + msg="callable default should be handled", + ) + def test_diff_models_with_json_fields(self): first = JSONModel.objects.create( json={