diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index dfcb9e977..9041468d8 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1058,6 +1058,21 @@ class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form): ) +class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ConsolePortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = ('type',) + + class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1086,6 +1101,21 @@ class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form): ) +class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ConsoleServerPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = ('type',) + + class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1124,6 +1154,31 @@ class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form): ) +class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerPortTypeChoices), + required=False, + widget=StaticSelect2() + ) + maximum_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Maximum power draw (watts)" + ) + allocated_draw = forms.IntegerField( + min_value=1, + required=False, + help_text="Allocated power draw (watts)" + ) + + class Meta: + nullable_fields = ('type', 'maximum_draw', 'allocated_draw') + + class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1182,6 +1237,26 @@ class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form): ) +class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerOutletTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerOutletTypeChoices), + required=False, + widget=StaticSelect2() + ) + feed_leg = forms.ChoiceField( + choices=add_blank_choice(PowerOutletFeedLegChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = ('type', 'feed_leg') + + class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1324,6 +1399,21 @@ class FrontPortTemplateCreateForm(BootstrapMixin, forms.Form): } +class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=FrontPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = () + + class RearPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1359,6 +1449,21 @@ class RearPortTemplateCreateForm(BootstrapMixin, forms.Form): ) +class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=RearPortTemplate.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PortTypeChoices), + required=False, + widget=StaticSelect2() + ) + + class Meta: + nullable_fields = () + + class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: @@ -1383,6 +1488,17 @@ class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form): ) +# TODO: DeviceBayTemplate has no fields suitable for bulk-editing yet +# class DeviceBayTemplateBulkEditForm(BootstrapMixin, BulkEditForm): +# pk = forms.ModelMultipleChoiceField( +# queryset=FrontPortTemplate.objects.all(), +# widget=forms.MultipleHiddenInput() +# ) +# +# class Meta: +# nullable_fields = () + + # # Component template import forms # diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index f653c9590..1b3076c6c 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -440,7 +440,7 @@ class ConsoleServerPortTemplateTable(BaseTable): class Meta(BaseTable.Meta): model = ConsoleServerPortTemplate - fields = ('pk', 'name', 'actions') + fields = ('pk', 'name', 'type', 'actions') empty_text = "None" diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 48f7a4f24..2f157d96d 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -537,7 +537,6 @@ class ConsolePortTemplateTestCase(StandardTestCases.Views): test_create_object = None test_delete_object = None test_import_objects = None - test_bulk_edit_objects = None def test_bulk_create_objects(self): return self._test_bulk_create_objects(expected_count=3) @@ -569,6 +568,10 @@ class ConsolePortTemplateTestCase(StandardTestCases.Views): 'type': ConsolePortTypeChoices.TYPE_RJ45, } + cls.bulk_edit_data = { + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + class ConsoleServerPortTemplateTestCase(StandardTestCases.Views): model = ConsoleServerPortTemplate @@ -579,7 +582,6 @@ class ConsoleServerPortTemplateTestCase(StandardTestCases.Views): test_create_object = None test_delete_object = None test_import_objects = None - test_bulk_edit_objects = None def test_bulk_create_objects(self): return self._test_bulk_create_objects(expected_count=3) @@ -611,6 +613,10 @@ class ConsoleServerPortTemplateTestCase(StandardTestCases.Views): 'type': ConsolePortTypeChoices.TYPE_RJ45, } + cls.bulk_edit_data = { + 'type': ConsolePortTypeChoices.TYPE_RJ45, + } + class PowerPortTemplateTestCase(StandardTestCases.Views): model = PowerPortTemplate @@ -621,7 +627,6 @@ class PowerPortTemplateTestCase(StandardTestCases.Views): test_create_object = None test_delete_object = None test_import_objects = None - test_bulk_edit_objects = None def test_bulk_create_objects(self): return self._test_bulk_create_objects(expected_count=3) @@ -645,7 +650,7 @@ class PowerPortTemplateTestCase(StandardTestCases.Views): 'device_type': devicetypes[1].pk, 'name': 'Power Port Template X', 'type': PowerPortTypeChoices.TYPE_IEC_C14, - 'maxiumum_draw': 100, + 'maximum_draw': 100, 'allocated_draw': 50, } @@ -653,7 +658,13 @@ class PowerPortTemplateTestCase(StandardTestCases.Views): 'device_type': devicetypes[1].pk, 'name_pattern': 'Power Port Template [4-6]', 'type': PowerPortTypeChoices.TYPE_IEC_C14, - 'maxiumum_draw': 100, + 'maximum_draw': 100, + 'allocated_draw': 50, + } + + cls.bulk_edit_data = { + 'type': PowerPortTypeChoices.TYPE_IEC_C14, + 'maximum_draw': 100, 'allocated_draw': 50, } @@ -667,7 +678,6 @@ class PowerOutletTemplateTestCase(StandardTestCases.Views): test_create_object = None test_delete_object = None test_import_objects = None - test_bulk_edit_objects = None def test_bulk_create_objects(self): return self._test_bulk_create_objects(expected_count=3) @@ -704,6 +714,11 @@ class PowerOutletTemplateTestCase(StandardTestCases.Views): 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, } + cls.bulk_edit_data = { + 'type': PowerOutletTypeChoices.TYPE_IEC_C13, + 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, + } + class InterfaceTemplateTestCase(StandardTestCases.Views): model = InterfaceTemplate @@ -762,7 +777,6 @@ class FrontPortTemplateTestCase(StandardTestCases.Views): test_create_object = None test_delete_object = None test_import_objects = None - test_bulk_edit_objects = None def test_bulk_create_objects(self): return self._test_bulk_create_objects(expected_count=3) @@ -805,6 +819,10 @@ class FrontPortTemplateTestCase(StandardTestCases.Views): ], } + cls.bulk_edit_data = { + 'type': PortTypeChoices.TYPE_8P8C, + } + class RearPortTemplateTestCase(StandardTestCases.Views): model = RearPortTemplate @@ -815,7 +833,6 @@ class RearPortTemplateTestCase(StandardTestCases.Views): test_create_object = None test_delete_object = None test_import_objects = None - test_bulk_edit_objects = None def test_bulk_create_objects(self): return self._test_bulk_create_objects(expected_count=3) @@ -849,6 +866,10 @@ class RearPortTemplateTestCase(StandardTestCases.Views): 'positions': 2, } + cls.bulk_edit_data = { + 'type': PortTypeChoices.TYPE_8P8C, + } + class DeviceBayTemplateTestCase(StandardTestCases.Views): model = DeviceBayTemplate diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index f73ccef09..94d1d82ed 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -92,21 +92,25 @@ urlpatterns = [ # Console port templates path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'), + path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'), path('console-port-templates/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='consoleporttemplate_bulk_delete'), path('console-port-templates//edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'), # Console server port templates path('console-server-port-templates/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='consoleserverporttemplate_add'), + path('console-server-port-templates/edit/', views.ConsoleServerPortTemplateBulkEditView.as_view(), name='consoleserverporttemplate_bulk_edit'), path('console-server-port-templates/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='consoleserverporttemplate_bulk_delete'), path('console-server-port-templates//edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'), # Power port templates path('power-port-templates/add/', views.PowerPortTemplateCreateView.as_view(), name='powerporttemplate_add'), + path('power-port-templates/edit/', views.PowerPortTemplateBulkEditView.as_view(), name='powerporttemplate_bulk_edit'), path('power-port-templates/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='powerporttemplate_bulk_delete'), path('power-port-templates//edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'), # Power outlet templates path('power-outlet-templates/add/', views.PowerOutletTemplateCreateView.as_view(), name='poweroutlettemplate_add'), + path('power-outlet-templates/edit/', views.PowerOutletTemplateBulkEditView.as_view(), name='poweroutlettemplate_bulk_edit'), path('power-outlet-templates/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='poweroutlettemplate_bulk_delete'), path('power-outlet-templates//edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'), @@ -118,16 +122,19 @@ urlpatterns = [ # Front port templates path('front-port-templates/add/', views.FrontPortTemplateCreateView.as_view(), name='frontporttemplate_add'), + path('front-port-templates/edit/', views.FrontPortTemplateBulkEditView.as_view(), name='frontporttemplate_bulk_edit'), path('front-port-templates/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='frontporttemplate_bulk_delete'), path('front-port-templates//edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'), # Rear port templates path('rear-port-templates/add/', views.RearPortTemplateCreateView.as_view(), name='rearporttemplate_add'), + path('rear-port-templates/edit/', views.RearPortTemplateBulkEditView.as_view(), name='rearporttemplate_bulk_edit'), path('rear-port-templates/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='rearporttemplate_bulk_delete'), path('rear-port-templates//edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'), # Device bay templates path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'), + # path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'), path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'), path('device-bay-templates//edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 22ba08f85..f8d6e2211 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -717,6 +717,13 @@ class ConsolePortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.ConsolePortTemplateForm +class ConsolePortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_consoleporttemplate' + queryset = ConsolePortTemplate.objects.all() + table = tables.ConsolePortTemplateTable + form = forms.ConsolePortTemplateBulkEditForm + + class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleporttemplate' queryset = ConsolePortTemplate.objects.all() @@ -737,6 +744,13 @@ class ConsoleServerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView) model_form = forms.ConsoleServerPortTemplateForm +class ConsoleServerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_consoleserverporttemplate' + queryset = ConsoleServerPortTemplate.objects.all() + table = tables.ConsoleServerPortTemplateTable + form = forms.ConsoleServerPortTemplateBulkEditForm + + class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleserverporttemplate' queryset = ConsoleServerPortTemplate.objects.all() @@ -757,6 +771,13 @@ class PowerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.PowerPortTemplateForm +class PowerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_powerporttemplate' + queryset = PowerPortTemplate.objects.all() + table = tables.PowerPortTemplateTable + form = forms.PowerPortTemplateBulkEditForm + + class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_powerporttemplate' queryset = PowerPortTemplate.objects.all() @@ -777,6 +798,13 @@ class PowerOutletTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.PowerOutletTemplateForm +class PowerOutletTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_poweroutlettemplate' + queryset = PowerOutletTemplate.objects.all() + table = tables.PowerOutletTemplateTable + form = forms.PowerOutletTemplateBulkEditForm + + class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_poweroutlettemplate' queryset = PowerOutletTemplate.objects.all() @@ -824,6 +852,13 @@ class FrontPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.FrontPortTemplateForm +class FrontPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_frontporttemplate' + queryset = FrontPortTemplate.objects.all() + table = tables.FrontPortTemplateTable + form = forms.FrontPortTemplateBulkEditForm + + class FrontPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_frontporttemplate' queryset = FrontPortTemplate.objects.all() @@ -844,6 +879,13 @@ class RearPortTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.RearPortTemplateForm +class RearPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'dcim.change_rearporttemplate' + queryset = RearPortTemplate.objects.all() + table = tables.RearPortTemplateTable + form = forms.RearPortTemplateBulkEditForm + + class RearPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rearporttemplate' queryset = RearPortTemplate.objects.all() @@ -864,6 +906,13 @@ class DeviceBayTemplateEditView(PermissionRequiredMixin, ObjectEditView): model_form = forms.DeviceBayTemplateForm +# class DeviceBayTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): +# permission_required = 'dcim.change_devicebaytemplate' +# queryset = DeviceBayTemplate.objects.all() +# table = tables.DeviceBayTemplateTable +# form = forms.DeviceBayTemplateBulkEditForm + + class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicebaytemplate' queryset = DeviceBayTemplate.objects.all() diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index bba259ee4..68384f12b 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -136,10 +136,10 @@ {% if devicetype.consoleport_templates.exists or devicetype.powerport_templates.exists %}
- {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:consoleporttemplate_add' edit_url=None delete_url='dcim:consoleporttemplate_bulk_delete' %} + {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:consoleporttemplate_add' edit_url='dcim:consoleporttemplate_bulk_edit' delete_url='dcim:consoleporttemplate_bulk_delete' %}
- {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:powerporttemplate_add' edit_url=None delete_url='dcim:powerporttemplate_bulk_delete' %} + {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:powerporttemplate_add' edit_url='dcim:powerporttemplate_bulk_edit' delete_url='dcim:powerporttemplate_bulk_delete' %}
{% endif %} @@ -153,14 +153,14 @@ {% if devicetype.consoleserverport_templates.exists %}
- {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:consoleserverporttemplate_add' edit_url=None delete_url='dcim:consoleserverporttemplate_bulk_delete' %} + {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:consoleserverporttemplate_add' edit_url='dcim:consoleserverporttemplate_bulk_edit' delete_url='dcim:consoleserverporttemplate_bulk_delete' %}
{% endif %} {% if devicetype.poweroutlet_templates.exists %}
- {% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:poweroutlettemplate_add' edit_url=None delete_url='dcim:poweroutlettemplate_bulk_delete' %} + {% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:poweroutlettemplate_add' edit_url='dcim:poweroutlettemplate_bulk_edit' delete_url='dcim:poweroutlettemplate_bulk_delete' %}
{% endif %} @@ -174,10 +174,10 @@ {% if devicetype.frontport_templates.exists or devicetype.rearport_templates.exists %}
- {% include 'dcim/inc/devicetype_component_table.html' with table=front_port_table title='Front Ports' add_url='dcim:frontporttemplate_add' edit_url=None delete_url='dcim:frontporttemplate_bulk_delete' %} + {% include 'dcim/inc/devicetype_component_table.html' with table=front_port_table title='Front Ports' add_url='dcim:frontporttemplate_add' edit_url='dcim:frontporttemplate_bulk_edit' delete_url='dcim:frontporttemplate_bulk_delete' %}
- {% include 'dcim/inc/devicetype_component_table.html' with table=rear_port_table title='Rear Ports' add_url='dcim:rearporttemplate_add' edit_url=None delete_url='dcim:rearporttemplate_bulk_delete' %} + {% include 'dcim/inc/devicetype_component_table.html' with table=rear_port_table title='Rear Ports' add_url='dcim:rearporttemplate_add' edit_url='dcim:rearporttemplate_bulk_edit' delete_url='dcim:rearporttemplate_bulk_delete' %}
{% endif %} diff --git a/netbox/templates/dcim/inc/devicetype_component_table.html b/netbox/templates/dcim/inc/devicetype_component_table.html index 6e81e65b8..a83059980 100644 --- a/netbox/templates/dcim/inc/devicetype_component_table.html +++ b/netbox/templates/dcim/inc/devicetype_component_table.html @@ -20,7 +20,7 @@ {% endif %} {% endif %}
- + Add {{ title }} diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 6db0d1bdf..979f95af9 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -219,6 +219,6 @@ def querydict_to_dict(querydict): """ assert isinstance(querydict, QueryDict) return { - key: querydict.get(key) if len(value) == 1 else querydict.getlist(key) + key: querydict.get(key) if len(value) == 1 and key != 'pk' else querydict.getlist(key) for key, value in querydict.lists() }