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={