delete handling

This commit is contained in:
Arthur 2026-09-08 14:21:15 -07:00
parent 00ed38771a
commit 325959fe36
3 changed files with 22 additions and 7 deletions

View File

@ -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):

View File

@ -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

View File

@ -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):