From 325959fe36f4a1a98d99b772e2befa43c190dc4f Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 8 Sep 2026 14:21:15 -0700 Subject: [PATCH] delete handling --- netbox/circuits/apps.py | 16 ++++++++++++++++ netbox/circuits/signals.py | 5 +++-- netbox/circuits/tests/test_models.py | 8 +++----- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/netbox/circuits/apps.py b/netbox/circuits/apps.py index d1d4fe9e7..1cc517700 100644 --- a/netbox/circuits/apps.py +++ b/netbox/circuits/apps.py @@ -1,4 +1,20 @@ from django.apps import AppConfig +from django.db.models.signals import pre_delete + + +def _clear_circuit_termination_pointer(sender, **kwargs): + from .models import CircuitTermination + from .signals import clear_circuit_termination_pointer + + if sender is CircuitTermination: + clear_circuit_termination_pointer(**kwargs) + + +# This module is imported in populate() phase 1, ahead of the models phase which connects +# core.signals.handle_deleted_object. Connecting here records the Circuit pointer clear before the +# termination's own DELETE; branch revert replays newest-first and needs the termination restored +# before the pointer referencing it. (#23134) +pre_delete.connect(_clear_circuit_termination_pointer) class CircuitsConfig(AppConfig): diff --git a/netbox/circuits/signals.py b/netbox/circuits/signals.py index bf772bd8f..c9d9609fb 100644 --- a/netbox/circuits/signals.py +++ b/netbox/circuits/signals.py @@ -1,4 +1,4 @@ -from django.db.models.signals import post_delete, post_save, pre_delete +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from dcim.signals import rebuild_paths @@ -17,12 +17,13 @@ def rebuild_cablepaths(instance, raw=False, **kwargs): rebuild_paths([peer_termination]) -@receiver(pre_delete, sender=CircuitTermination) def clear_circuit_termination_pointer(instance, using=None, origin=None, **kwargs): """ Clear the parent Circuit's cached `termination_a`/`termination_z` pointer with a change-logged save. on_delete=SET_NULL clears it via a bulk UPDATE, and related_name='+' hides the relation from Circuit._meta.related_objects, so neither path records an ObjectChange. (#23134) + + Connected in CircuitsConfig, not here, so that it precedes handle_deleted_object. """ if not instance.term_side: return diff --git a/netbox/circuits/tests/test_models.py b/netbox/circuits/tests/test_models.py index 2fbbd7e57..162ddde3e 100644 --- a/netbox/circuits/tests/test_models.py +++ b/netbox/circuits/tests/test_models.py @@ -482,14 +482,12 @@ class CircuitTerminationChangeLoggingTestCase(TestCase): self.assertEqual(changes[0].prechange_data['termination_a'], termination_pk) self.assertIsNone(changes[0].postchange_data['termination_a']) - # core.signals.handle_deleted_object is connected before this app's receiver, so the - # DELETE precedes the pointer clear. Replaying in this order relies on the consumer - # applying the DELETE through the ORM, where on_delete=SET_NULL clears the pointer, or - # on the FK being DEFERRABLE INITIALLY DEFERRED within one transaction. + # The pointer clear must precede the DELETE, so that a consumer replaying in reverse + # restores the termination before the record which references it termination_delete = self._termination_change( termination_pk, ObjectChangeActionChoices.ACTION_DELETE ) - self.assertLess(termination_delete.pk, changes[0].pk) + self.assertLess(changes[0].pk, termination_delete.pk) @tag('regression') # Ref: #23134 def test_bulk_deletion_records_circuit_update(self):