Arthur 1 день назад
Родитель
Сommit
848b613020

+ 1 - 0
docs/plugins/development/models.md

@@ -196,6 +196,7 @@ class MyModel(NetBoxModel):
     def update_dependent_objects(self):
         # Recreate any objects derived from this one
         ...
+    update_dependent_objects.alters_data = True
 ```
 
 The method is optional; callers should check for its presence before calling it. NetBox never calls it during a normal save.

+ 1 - 0
netbox/dcim/models/cables.py

@@ -520,6 +520,7 @@ class Cable(PrimaryModel):
                     ct.termination.propagate_channel_cables()
 
             rebuild_cable_paths(self)
+    update_dependent_objects.alters_data = True
 
     def get_terminations(self):
         """

+ 31 - 0
netbox/dcim/tests/test_cablepaths.py

@@ -3115,6 +3115,37 @@ class LegacyCablePathTestCase(BaseCablePathTestCase):
         self.assertPathIsSet(interface1, path1)
         self.assertPathIsSet(interface2, path2)
 
+    def test_311_retrace_cable_preserves_circuittermination_origin(self):
+        """
+        [CT1] --C1-- [RP1] [FP1]
+
+        A CircuitTermination origin is not a PathEndpoint, so the retrace cannot reproduce its path by
+        tracing the Cable's terminations; it must be restored from the recorded origin instead.
+        """
+        rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1')
+        frontport1 = FrontPort.objects.create(device=self.device, name='Front Port 1')
+        PortMapping.objects.create(
+            device=self.device, front_port=frontport1, front_port_position=1,
+            rear_port=rearport1, rear_port_position=1,
+        )
+        circuittermination1 = CircuitTermination.objects.create(
+            circuit=self.circuit,
+            termination=self.site,
+            term_side='A'
+        )
+        cable1 = Cable(a_terminations=[circuittermination1], b_terminations=[rearport1])
+        cable1.save()
+
+        circuittermination1.refresh_from_db()
+        CablePath.from_origin([circuittermination1]).save()
+        self.assertEqual(CablePath.objects.count(), 1)
+
+        for _ in range(2):
+            Cable.objects.get(pk=cable1.pk).update_dependent_objects()
+
+            self.assertPathExists((circuittermination1, cable1, rearport1, frontport1), is_complete=False)
+            self.assertEqual(CablePath.objects.count(), 1)
+
     def test_401_exclude_midspan_devices(self):
         """
         [IF1] --C1-- [FP1][Test Device][RP1] --C2-- [RP2][Test Device][FP2] --C3-- [IF2]

+ 3 - 3
netbox/dcim/utils.py

@@ -223,9 +223,9 @@ def rebuild_cable_paths(cable):
             if not nodes:
                 continue
 
-            # An origin which terminates this Cable belongs to the tracing above: that it produced no path means
-            # the origin no longer has one (e.g. a channel subinterface moved to another parent).
-            if any(getattr(obj, 'cable_id', None) == cable.pk for obj in nodes):
+            # A path endpoint terminating this Cable belongs to the tracing above: that it produced no path
+            # means the origin no longer has one (e.g. a channel subinterface moved to another parent).
+            if any(isinstance(obj, PathEndpoint) and obj.cable_id == cable.pk for obj in nodes):
                 continue
 
             # Nor restore an origin whose path has already been traced through another Cable