Make diffing more robust for polymorphic models (#784)

* Add failing test for diffing polymorphic model instances.

* Make diffing more robust for polymorphic models:

When working with polymorphic models, where a child model inherits from a
parent model, Django's pre_save signal may send model instances in a way
where the log_update() handler receives an instance of the child as the
`old` model, but an instance of the parent as the `new` model.

This leads to a `FieldDoesNotExist` error when a field that only exists on the
child was modified, and `get_field_value()` attempts look up that field on the
parent.

This change makes diffing polymorphic models more robust by considering this
case in `get_default_value()`. Changes to those child fields won't be tracked
in these cases, but at least `django-auditlog` won't prevent the model from
being saved.
This commit is contained in:
Lukas Graf 2025-11-25 09:12:41 +01:00 committed by GitHub
parent 074e6aa145
commit d02ed6b9e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 29 additions and 2 deletions

View file

@ -7,6 +7,10 @@
- Add `AUDITLOG_USE_BASE_MANAGER` setting to override default manager use ([#766](https://github.com/jazzband/django-auditlog/pull/766)) - 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)) - Drop 'Python 3.9' support ([#773](https://github.com/jazzband/django-auditlog/pull/773))
#### Fixes
- Make diffing more robust for polymorphic models ([#784](https://github.com/jazzband/django-auditlog/pull/784))
## 3.3.0 (2025-09-18) ## 3.3.0 (2025-09-18)
#### Improvements #### Improvements

View file

@ -3,7 +3,7 @@ from collections.abc import Callable
from datetime import timezone from datetime import timezone
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
from django.db.models import NOT_PROVIDED, DateTimeField, ForeignKey, JSONField, Model from django.db.models import NOT_PROVIDED, DateTimeField, ForeignKey, JSONField, Model
from django.utils import timezone as django_timezone from django.utils import timezone as django_timezone
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
@ -74,7 +74,7 @@ def get_field_value(obj, field, use_json_for_changes=False):
try: try:
model_field = obj._meta.get_field(field.name) model_field = obj._meta.get_field(field.name)
default = model_field.default default = model_field.default
except AttributeError: except (AttributeError, FieldDoesNotExist):
default = NOT_PROVIDED default = NOT_PROVIDED
if default is NOT_PROVIDED: if default is NOT_PROVIDED:

View file

@ -487,6 +487,7 @@ auditlog.register(AltPrimaryKeyModel)
auditlog.register(UUIDPrimaryKeyModel) auditlog.register(UUIDPrimaryKeyModel)
auditlog.register(ModelPrimaryKeyModel) auditlog.register(ModelPrimaryKeyModel)
auditlog.register(ProxyModel) auditlog.register(ProxyModel)
auditlog.register(RelatedModelParent)
auditlog.register(RelatedModel) auditlog.register(RelatedModel)
auditlog.register(ManyRelatedModel) auditlog.register(ManyRelatedModel)
auditlog.register(ManyRelatedModel.recursive.through) auditlog.register(ManyRelatedModel.recursive.through)

View file

@ -47,6 +47,7 @@ from test_app.models import (
NullableJSONModel, NullableJSONModel,
ProxyModel, ProxyModel,
RelatedModel, RelatedModel,
RelatedModelParent,
ReusableThroughRelatedModel, ReusableThroughRelatedModel,
SecretM2MModel, SecretM2MModel,
SecretRelatedModel, SecretRelatedModel,
@ -2131,6 +2132,27 @@ class ModelInstanceDiffTest(TestCase):
model_instance_diff(simple2, simple1) model_instance_diff(simple2, simple1)
model_instance_diff(simple1, simple2) model_instance_diff(simple1, simple2)
def test_diff_polymorphic_models(self):
"""No error is raised when comparing parent/child for polymorphic models."""
# This tests that when a polymorphic model is compared to its parent,
# no FieldDoesNotExist errors are raised because those fields don't exist
# on the parent model.
# relation target
simple = SimpleModel()
simple.save()
# the parent model
related_parent = RelatedModelParent()
related_parent.save()
# the child model, with some fields that don't exist on the parent
related = RelatedModel(related=simple, one_to_one=simple)
related.save()
model_instance_diff(related, related_parent)
def test_object_repr_related_deleted(self): def test_object_repr_related_deleted(self):
"""No error is raised when __str__() loads a related object that has been deleted.""" """No error is raised when __str__() loads a related object that has been deleted."""
simple = SimpleModel() simple = SimpleModel()