diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index b7d7351ab..052048de7 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1119,7 +1119,7 @@ class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm): FieldSet('device_type', name=_('Device Type')), FieldSet('module_type', name=_('Module Type')), ), - 'name', 'label', 'positions', 'rear_ports', 'description', + 'name', 'label', 'type', 'positions', 'rear_ports', 'description', ), ) diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index 6b34ecc24..69678e185 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -133,9 +133,10 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp # Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate # positions + positions = self.cleaned_data['positions'] frontport_count = len(self.cleaned_data['name']) rearport_count = len(self.cleaned_data['rear_ports']) - if frontport_count != rearport_count: + if frontport_count * positions != rearport_count: raise forms.ValidationError({ 'rear_ports': _( "The number of front port templates to be created ({frontport_count}) must match the selected " @@ -251,10 +252,11 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm): def clean(self): super(NetBoxModelForm, self).clean() - # Check that the number of FrontPorts to be created matches the selected number of RearPort positions + # Check that the number of FrontPorts to be created matches the selected number of RearPorts + positions = self.cleaned_data['positions'] frontport_count = len(self.cleaned_data['name']) rearport_count = len(self.cleaned_data['rear_ports']) - if frontport_count != rearport_count: + if frontport_count * positions != rearport_count: raise forms.ValidationError({ 'rear_ports': _( "The number of front ports to be created ({frontport_count}) must match the selected number of " diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index b30815889..d58e4ee44 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -635,6 +635,20 @@ class RearPortTemplate(ModularComponentTemplateModel): verbose_name = _('rear port template') verbose_name_plural = _('rear port templates') + def clean(self): + super().clean() + + # Check that positions count is greater than or equal to the number of associated FrontPortTemplates + if not self._state.adding: + assignment_count = self.assignments.count() + if self.positions < assignment_count: + raise ValidationError({ + "positions": _( + "The number of positions cannot be less than the number of mapped front port templates " + "({count})" + ).format(count=assignment_count) + }) + def instantiate(self, **kwargs): return self.component_model( name=self.resolve_name(kwargs.get('module')), diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index cfdeb14ce..d24987265 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -749,12 +749,9 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable): color = columns.ColorColumn( verbose_name=_('Color'), ) - rear_port_position = tables.Column( - verbose_name=_('Position') - ) - rear_port = tables.Column( - verbose_name=_('Rear Port'), - linkify=True + assignments = columns.ManyToManyColumn( + verbose_name=_('Assignments'), + transform=lambda obj: f'{obj.rear_port}:{obj.rear_port_position}' ) tags = columns.TagColumn( url_name='dcim:frontport_list' @@ -763,12 +760,12 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable): class Meta(DeviceComponentTable.Meta): model = models.FrontPort fields = ( - 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description', - 'mark_connected', 'cable', 'cable_color', 'link_peer', - 'inventory_items', 'tags', 'created', 'last_updated', + 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'assignments', + 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created', + 'last_updated', ) default_columns = ( - 'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', + 'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'assignments', 'description', ) @@ -786,11 +783,11 @@ class DeviceFrontPortTable(FrontPortTable): class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): model = models.FrontPort fields = ( - 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'rear_port', 'rear_port_position', + 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'assignments', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions', ) default_columns = ( - 'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer', + 'pk', 'name', 'label', 'type', 'color', 'positions', 'assignments', 'description', 'cable', 'link_peer', ) @@ -805,6 +802,10 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable): color = columns.ColorColumn( verbose_name=_('Color'), ) + assignments = columns.ManyToManyColumn( + verbose_name=_('Assignments'), + transform=lambda obj: f'{obj.front_port}:{obj.front_port_position}' + ) tags = columns.TagColumn( url_name='dcim:rearport_list' ) @@ -812,10 +813,13 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable): class Meta(DeviceComponentTable.Meta): model = models.RearPort fields = ( - 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description', - 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created', 'last_updated', + 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'assignments', + 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'inventory_items', 'tags', 'created', + 'last_updated', + ) + default_columns = ( + 'pk', 'name', 'device', 'label', 'type', 'color', 'positions', 'assignments', 'description', ) - default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description') class DeviceRearPortTable(RearPortTable): @@ -832,11 +836,11 @@ class DeviceRearPortTable(RearPortTable): class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): model = models.RearPort fields = ( - 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'positions', 'description', 'mark_connected', - 'cable', 'cable_color', 'link_peer', 'tags', 'actions', + 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'assignments', + 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions', ) default_columns = ( - 'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer', + 'pk', 'name', 'label', 'type', 'positions', 'assignments', 'description', 'cable', 'link_peer', ) diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 979689b75..0872832fa 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -249,12 +249,13 @@ class InterfaceTemplateTable(ComponentTemplateTable): class FrontPortTemplateTable(ComponentTemplateTable): - rear_port_position = tables.Column( - verbose_name=_('Position') - ) color = columns.ColorColumn( verbose_name=_('Color'), ) + assignments = columns.ManyToManyColumn( + verbose_name=_('Assignments'), + transform=lambda obj: f'{obj.rear_port}:{obj.rear_port_position}' + ) actions = columns.ActionsColumn( actions=('edit', 'delete'), extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS @@ -262,7 +263,7 @@ class FrontPortTemplateTable(ComponentTemplateTable): class Meta(ComponentTemplateTable.Meta): model = models.FrontPortTemplate - fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions') + fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'assignments', 'description', 'actions') empty_text = "None" @@ -270,6 +271,10 @@ class RearPortTemplateTable(ComponentTemplateTable): color = columns.ColorColumn( verbose_name=_('Color'), ) + assignments = columns.ManyToManyColumn( + verbose_name=_('Assignments'), + transform=lambda obj: f'{obj.front_port}:{obj.front_port_position}' + ) actions = columns.ActionsColumn( actions=('edit', 'delete'), extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS @@ -277,7 +282,7 @@ class RearPortTemplateTable(ComponentTemplateTable): class Meta(ComponentTemplateTable.Meta): model = models.RearPortTemplate - fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions') + fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'assignments', 'description', 'actions') empty_text = "None"