diff --git a/netbox/core/signals.py b/netbox/core/signals.py index 6316cfd01..edb95ae06 100644 --- a/netbox/core/signals.py +++ b/netbox/core/signals.py @@ -209,22 +209,28 @@ def handle_deleted_object(sender, instance, **kwargs): # for the forward direction of the relationship, ensuring that the change is recorded. # Similarly, for many-to-one relationships, we set the value on the related object to None # and save it to trigger a change record on that object. - for relation in instance._meta.related_objects: - if type(relation) not in [ManyToManyRel, ManyToOneRel]: - continue - related_model = relation.related_model - related_field_name = relation.remote_field.name - if not issubclass(related_model, ChangeLoggingMixin): - # We only care about triggering the m2m_changed signal for models which support - # change logging - continue - for obj in related_model.objects.filter(**{related_field_name: instance.pk}): - obj.snapshot() # Ensure the change record includes the "before" state - if type(relation) is ManyToManyRel: - getattr(obj, related_field_name).remove(instance) - elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT): - setattr(obj, related_field_name, None) - obj.save() + # + # Skip this for private models (e.g. CablePath) whose lifecycle is an internal + # implementation detail. Django's on_delete handlers (e.g. SET_NULL) already take + # care of the database integrity; recording changelog entries for the related + # objects would be spurious. (Ref: #21390) + if not getattr(instance, '_netbox_private', False): + for relation in instance._meta.related_objects: + if type(relation) not in [ManyToManyRel, ManyToOneRel]: + continue + related_model = relation.related_model + related_field_name = relation.remote_field.name + if not issubclass(related_model, ChangeLoggingMixin): + # We only care about triggering the m2m_changed signal for models which support + # change logging + continue + for obj in related_model.objects.filter(**{related_field_name: instance.pk}): + obj.snapshot() # Ensure the change record includes the "before" state + if type(relation) is ManyToManyRel: + getattr(obj, related_field_name).remove(instance) + elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT): + setattr(obj, related_field_name, None) + obj.save() # Enqueue the object for event processing queue = events_queue.get()