diff --git a/CHANGELOG.md b/CHANGELOG.md index b14ea8b5f..fbe981db3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ v2.6.6 (FUTURE) * [#1941](https://github.com/netbox-community/netbox/issues/1941) - Add InfiniBand interface types * [#3259](https://github.com/netbox-community/netbox/issues/3259) - Add `rack` and `site` filters for cables +* [#3563](https://github.com/netbox-community/netbox/issues/3563) - Enable editing of individual DeviceType components --- diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 887e3f908..3fa704d8d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1023,6 +1023,16 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): 'device_type': forms.HiddenInput(), } + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + # Limit power_port choices to current DeviceType + if hasattr(self.instance, 'device_type'): + self.fields['power_port'].queryset = PowerPortTemplate.objects.filter( + device_type=self.instance.device_type + ) + class PowerOutletTemplateCreateForm(ComponentForm): name_pattern = ExpandableNameField( @@ -1107,6 +1117,16 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm): 'rear_port': StaticSelect2(), } + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + # Limit rear_port choices to current DeviceType + if hasattr(self.instance, 'device_type'): + self.fields['rear_port'].queryset = RearPortTemplate.objects.filter( + device_type=self.instance.device_type + ) + class FrontPortTemplateCreateForm(ComponentForm): name_pattern = ExpandableNameField( diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index ae42c507e..70a9aa5c8 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -195,6 +195,16 @@ POWERPANEL_POWERFEED_COUNT = """ """ +def get_component_template_actions(model_name): + return """ + {{% if perms.dcim.change_{model_name} %}} + + + + {{% endif %}} + """.format(model_name=model_name).strip() + + # # Regions # @@ -404,74 +414,117 @@ class DeviceTypeTable(BaseTable): class ConsolePortTemplateTable(BaseTable): pk = ToggleColumn() + actions = tables.TemplateColumn( + template_code=get_component_template_actions('consoleporttemplate'), + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = ConsolePortTemplate - fields = ('pk', 'name') + fields = ('pk', 'name', 'actions') empty_text = "None" class ConsoleServerPortTemplateTable(BaseTable): pk = ToggleColumn() + actions = tables.TemplateColumn( + template_code=get_component_template_actions('consoleserverporttemplate'), + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = ConsoleServerPortTemplate - fields = ('pk', 'name') + fields = ('pk', 'name', 'actions') empty_text = "None" class PowerPortTemplateTable(BaseTable): pk = ToggleColumn() + actions = tables.TemplateColumn( + template_code=get_component_template_actions('powerporttemplate'), + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = PowerPortTemplate - fields = ('pk', 'name', 'maximum_draw', 'allocated_draw') + fields = ('pk', 'name', 'maximum_draw', 'allocated_draw', 'actions') empty_text = "None" class PowerOutletTemplateTable(BaseTable): pk = ToggleColumn() + actions = tables.TemplateColumn( + template_code=get_component_template_actions('poweroutlettemplate'), + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = PowerOutletTemplate - fields = ('pk', 'name', 'power_port', 'feed_leg') + fields = ('pk', 'name', 'power_port', 'feed_leg', 'actions') empty_text = "None" class InterfaceTemplateTable(BaseTable): pk = ToggleColumn() mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}") + actions = tables.TemplateColumn( + template_code=get_component_template_actions('interfacetemplate'), + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = InterfaceTemplate - fields = ('pk', 'name', 'mgmt_only', 'type') + fields = ('pk', 'name', 'mgmt_only', 'type', 'actions') empty_text = "None" class FrontPortTemplateTable(BaseTable): pk = ToggleColumn() + rear_port_position = tables.Column( + verbose_name='Position' + ) + actions = tables.TemplateColumn( + template_code=get_component_template_actions('frontporttemplate'), + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = FrontPortTemplate - fields = ('pk', 'name', 'type', 'rear_port', 'rear_port_position') + fields = ('pk', 'name', 'type', 'rear_port', 'rear_port_position', 'actions') empty_text = "None" class RearPortTemplateTable(BaseTable): pk = ToggleColumn() + actions = tables.TemplateColumn( + template_code=get_component_template_actions('rearporttemplate'), + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = RearPortTemplate - fields = ('pk', 'name', 'type', 'positions') + fields = ('pk', 'name', 'type', 'positions', 'actions') empty_text = "None" class DeviceBayTemplateTable(BaseTable): pk = ToggleColumn() + actions = tables.TemplateColumn( + template_code=get_component_template_actions('devicebaytemplate'), + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = DeviceBayTemplate - fields = ('pk', 'name') + fields = ('pk', 'name', 'actions') empty_text = "None" diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 43316baf4..c3e852d1e 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -93,35 +93,43 @@ urlpatterns = [ # Console port templates path(r'device-types//console-ports/add/', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'), path(r'device-types//console-ports/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'), + path(r'console-port-templates//edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'), # Console server port templates path(r'device-types//console-server-ports/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'), path(r'device-types//console-server-ports/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'), + path(r'console-server-port-templates//edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'), # Power port templates path(r'device-types//power-ports/add/', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'), path(r'device-types//power-ports/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'), + path(r'power-port-templates//edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'), # Power outlet templates path(r'device-types//power-outlets/add/', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'), path(r'device-types//power-outlets/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'), + path(r'power-outlet-templates//edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'), # Interface templates path(r'device-types//interfaces/add/', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'), path(r'device-types//interfaces/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'), path(r'device-types//interfaces/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'), + path(r'interface-templates//edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'), # Front port templates path(r'device-types//front-ports/add/', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'), path(r'device-types//front-ports/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'), + path(r'front-port-templates//edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'), # Rear port templates path(r'device-types//rear-ports/add/', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'), path(r'device-types//rear-ports/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'), + path(r'rear-port-templates//edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'), # Device bay templates path(r'device-types//device-bays/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'), path(r'device-types//device-bays/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'), + path(r'device-bay-templates//edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'), # Device roles path(r'device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 301d05e84..58c759822 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -693,6 +693,12 @@ class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView template_name = 'dcim/device_component_add.html' +class ConsolePortTemplateEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.change_consoleporttemplate' + model = ConsolePortTemplate + model_form = forms.ConsolePortTemplateForm + + class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleporttemplate' queryset = ConsolePortTemplate.objects.all() @@ -710,6 +716,12 @@ class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCrea template_name = 'dcim/device_component_add.html' +class ConsoleServerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.change_consoleserverporttemplate' + model = ConsoleServerPortTemplate + model_form = forms.ConsoleServerPortTemplateForm + + class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleserverporttemplate' queryset = ConsoleServerPortTemplate.objects.all() @@ -727,6 +739,12 @@ class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): template_name = 'dcim/device_component_add.html' +class PowerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.change_powerporttemplate' + model = PowerPortTemplate + model_form = forms.PowerPortTemplateForm + + class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_powerporttemplate' queryset = PowerPortTemplate.objects.all() @@ -744,6 +762,12 @@ class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView template_name = 'dcim/device_component_add.html' +class PowerOutletTemplateEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.change_poweroutlettemplate' + model = PowerOutletTemplate + model_form = forms.PowerOutletTemplateForm + + class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_poweroutlettemplate' queryset = PowerOutletTemplate.objects.all() @@ -761,6 +785,12 @@ class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): template_name = 'dcim/device_component_add.html' +class InterfaceTemplateEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.change_interfacetemplate' + model = InterfaceTemplate + model_form = forms.InterfaceTemplateForm + + class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_interfacetemplate' queryset = InterfaceTemplate.objects.all() @@ -786,6 +816,12 @@ class FrontPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): template_name = 'dcim/device_component_add.html' +class FrontPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.change_frontporttemplate' + model = FrontPortTemplate + model_form = forms.FrontPortTemplateForm + + class FrontPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_frontporttemplate' queryset = FrontPortTemplate.objects.all() @@ -803,6 +839,12 @@ class RearPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): template_name = 'dcim/device_component_add.html' +class RearPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.change_rearporttemplate' + model = RearPortTemplate + model_form = forms.RearPortTemplateForm + + class RearPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rearporttemplate' queryset = RearPortTemplate.objects.all() @@ -820,6 +862,12 @@ class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): template_name = 'dcim/device_component_add.html' +class DeviceBayTemplateEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.change_devicebaytemplate' + model = DeviceBayTemplate + model_form = forms.DeviceBayTemplateForm + + class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicebaytemplate' queryset = DeviceBayTemplate.objects.all()