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()