diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 585b18687..68286a91f 100644 --- a/netbox/dcim/models/cables.py +++ b/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): diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index c3c3acf99..45e07f257 100644 --- a/netbox/dcim/tests/test_models.py +++ b/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):