Просмотр исходного кода

Fixes #21618: Preserve cable terminations when bulk-editing cable profile

When `update_terminations(force=True)` is called (e.g. after a profile
change), cache the termination objects from the database before deleting
CableTermination records. Without this, the `a_terminations`/`b_terminations`
properties fall back to querying the (now-empty) DB and return empty lists,
resulting in all terminations being lost.

Also removes a leftover debug print statement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GeertJohan 4 дней назад
Родитель
Сommit
dad96c525f
2 измененных файлов с 38 добавлено и 1 удалено
  1. 9 1
      netbox/dcim/models/cables.py
  2. 29 0
      netbox/dcim/tests/test_models.py

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

@@ -293,7 +293,6 @@ class Cable(PrimaryModel):
             self._pk = self.pk
 
         if self._orig_profile != self.profile:
-            print(f'profile changed from {self._orig_profile} to {self.profile}')
             self.update_terminations(force=True)
         elif self._terminations_modified:
             self.update_terminations()
@@ -403,6 +402,15 @@ class Cable(PrimaryModel):
         """
         a_terminations, b_terminations = self.get_terminations()
 
+        # When force-recreating terminations (e.g. after a profile change), cache the termination objects
+        # from the database before deleting, so they are available for recreation. Without this, the
+        # a_terminations/b_terminations properties would query the DB after deletion and return empty lists.
+        if force:
+            if not hasattr(self, '_a_terminations'):
+                self._a_terminations = list(a_terminations.keys())
+            if not hasattr(self, '_b_terminations'):
+                self._b_terminations = list(b_terminations.keys())
+
         # Delete any stale CableTerminations
         for termination, ct in a_terminations.items():
             if force or (termination.pk and termination not in self.a_terminations):

+ 29 - 0
netbox/dcim/tests/test_models.py

@@ -1201,6 +1201,35 @@ class CableTestCase(TestCase):
         with self.assertRaises(ValidationError):
             cable.clean()
 
+    def test_cable_profile_change_preserves_terminations(self):
+        """
+        When a Cable's profile is changed via save() without explicitly setting terminations (as happens during
+        bulk edit), the existing termination points must be preserved.
+        """
+        cable = Cable.objects.first()
+        interface1 = Interface.objects.get(device__name='TestDevice1', name='eth0')
+        interface2 = Interface.objects.get(device__name='TestDevice2', name='eth0')
+
+        # Verify initial state: cable has terminations and no profile
+        self.assertEqual(cable.profile, '')
+        self.assertEqual(CableTermination.objects.filter(cable=cable).count(), 2)
+
+        # Simulate what bulk edit does: load the cable from DB, set profile via setattr, and save.
+        # Crucially, do NOT set a_terminations or b_terminations on the instance.
+        cable_from_db = Cable.objects.get(pk=cable.pk)
+        cable_from_db.profile = CableProfileChoices.SINGLE_1C1P
+        cable_from_db.save()
+
+        # Verify terminations are preserved
+        self.assertEqual(CableTermination.objects.filter(cable=cable).count(), 2)
+
+        # Verify the correct interfaces are still terminated
+        cable_from_db.refresh_from_db()
+        a_terms = [ct.termination for ct in CableTermination.objects.filter(cable=cable, cable_end='A')]
+        b_terms = [ct.termination for ct in CableTermination.objects.filter(cable=cable, cable_end='B')]
+        self.assertEqual(a_terms, [interface1])
+        self.assertEqual(b_terms, [interface2])
+
 
 class VirtualDeviceContextTestCase(TestCase):