diff --git a/netbox/dcim/cable_profiles.py b/netbox/dcim/cable_profiles.py index d2c5c6e09..49a736a8b 100644 --- a/netbox/dcim/cable_profiles.py +++ b/netbox/dcim/cable_profiles.py @@ -75,6 +75,11 @@ class BaseCableProfile: except CableTermination.DoesNotExist: return None, None + @staticmethod + def get_position_list(n): + """Return a list of integers from 1 to n, inclusive.""" + return list(range(1, n + 1)) + # Profile naming: # - Single: One connector per side, with one or more positions diff --git a/netbox/dcim/migrations/0220_cable_profile.py b/netbox/dcim/migrations/0220_cable_profile.py index f196707c5..5160506ed 100644 --- a/netbox/dcim/migrations/0220_cable_profile.py +++ b/netbox/dcim/migrations/0220_cable_profile.py @@ -44,7 +44,7 @@ class Migration(migrations.Migration): ), migrations.AlterModelOptions( name='cabletermination', - options={'ordering': ('cable', 'cable_end', 'connector', 'positions', 'pk')}, + options={'ordering': ('cable', 'cable_end', 'connector', 'pk')}, # connector may be null ), migrations.AddConstraint( model_name='cabletermination', diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index b811061dc..3b3dbb658 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -359,15 +359,16 @@ class Cable(PrimaryModel): ct.delete() # Save any new CableTerminations + profile = self.profile_class() if self.profile else None for i, termination in enumerate(self.a_terminations, start=1): if not termination.pk or termination not in a_terminations: connector = positions = None - if self.profile: + if profile: connector = i - positions = list(range(1, self.profile_class().a_connectors[i] + 1)) + positions = profile.get_position_list(profile.a_connectors[i]) CableTermination( cable=self, - cable_end='A', + cable_end=CableEndChoices.SIDE_A, connector=connector, positions=positions, termination=termination @@ -375,12 +376,12 @@ class Cable(PrimaryModel): for i, termination in enumerate(self.b_terminations, start=1): if not termination.pk or termination not in b_terminations: connector = positions = None - if self.profile: + if profile: connector = i - positions = list(range(1, self.profile_class().b_connectors[i] + 1)) + positions = profile.get_position_list(profile.b_connectors[i]) CableTermination( cable=self, - cable_end='B', + cable_end=CableEndChoices.SIDE_B, connector=connector, positions=positions, termination=termination @@ -459,7 +460,7 @@ class CableTermination(ChangeLoggedModel): objects = RestrictedQuerySet.as_manager() class Meta: - ordering = ('cable', 'cable_end', 'connector', 'positions', 'pk') + ordering = ('cable', 'cable_end', 'connector', 'pk') constraints = ( models.UniqueConstraint( fields=('termination_type', 'termination_id'), @@ -530,10 +531,7 @@ class CableTermination(ChangeLoggedModel): # Set the cable on the terminating object termination = self.termination._meta.model.objects.get(pk=self.termination_id) termination.snapshot() - termination.cable = self.cable - termination.cable_end = self.cable_end - termination.cable_connector = self.connector - termination.cable_positions = self.positions + termination.set_cable_termination(self) termination.save() def delete(self, *args, **kwargs): @@ -541,10 +539,7 @@ class CableTermination(ChangeLoggedModel): # Delete the cable association on the terminating object termination = self.termination._meta.model.objects.get(pk=self.termination_id) termination.snapshot() - termination.cable = None - termination.cable_end = None - termination.cable_connector = None - termination.cable_positions = None + termination.clear_cable_termination(self) termination.save() super().delete(*args, **kwargs) @@ -753,7 +748,6 @@ class CablePath(models.Model): object_to_path_node(t) for t in terminations ]) # If not null, push cable position onto the stack - # TODO: Handle multiple positions? if isinstance(terminations[0], PathEndpoint) and terminations[0].cable_positions: position_stack.append([terminations[0].cable_positions[0]]) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index f29289647..0b96cc0f7 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -279,6 +279,22 @@ class CabledObjectModel(models.Model): return None return CableEndChoices.SIDE_A if self.cable_end == CableEndChoices.SIDE_B else CableEndChoices.SIDE_B + def set_cable_termination(self, termination): + """Save attributes from the given CableTermination on the terminating object.""" + self.cable = termination.cable + self.cable_end = termination.cable_end + self.cable_connector = termination.connector + self.cable_positions = termination.positions + set_cable_termination.alters_data = True + + def clear_cable_termination(self, termination): + """Clear all cable termination attributes from the terminating object.""" + self.cable = None + self.cable_end = None + self.cable_connector = None + self.cable_positions = None + clear_cable_termination.alters_data = True + class PathEndpoint(models.Model): """ diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index c8476b0ca..3bda18755 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -2620,7 +2620,9 @@ class CableTerminationTest( APIViewTestCases.ListObjectsViewTestCase, ): model = CableTermination - brief_fields = ['cable', 'cable_end', 'display', 'id', 'positions', 'termination_id', 'termination_type', 'url'] + brief_fields = [ + 'cable', 'cable_end', 'connector', 'display', 'id', 'positions', 'termination_id', 'termination_type', 'url', + ] @classmethod def setUpTestData(cls):