From 97b8e73716c8d6a76f9e11eb370d6273050a6889 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Apr 2020 11:15:39 -0400 Subject: [PATCH 1/5] Introduce model-specific bulk create forms for device components --- netbox/dcim/forms.py | 230 +++++++++++++++++++++++--------------- netbox/dcim/views.py | 12 +- netbox/utilities/forms.py | 10 ++ 3 files changed, 155 insertions(+), 97 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 48b0de903..c51332b76 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -23,8 +23,9 @@ from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, ExpandableNameField, FlexibleModelChoiceField, JSONField, SelectWithPK, - SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, + DynamicModelMultipleChoiceField, ExpandableNameField, FlexibleModelChoiceField, form_from_model, JSONField, + SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, + BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup, VirtualMachine from .choices import * @@ -2299,31 +2300,6 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form): ) -class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): - type = forms.ChoiceField( - choices=InterfaceTypeChoices, - widget=StaticSelect2() - ) - enabled = forms.BooleanField( - required=False, - initial=True - ) - mtu = forms.IntegerField( - required=False, - min_value=INTERFACE_MTU_MIN, - max_value=INTERFACE_MTU_MAX, - label='MTU' - ) - mgmt_only = forms.BooleanField( - required=False, - label='Management only' - ) - description = forms.CharField( - max_length=100, - required=False - ) - - # # Console ports # @@ -2375,6 +2351,15 @@ class ConsolePortCreateForm(BootstrapMixin, forms.Form): ) +class ConsolePortBulkCreateForm( + form_from_model(ConsolePort, ['type', 'description', 'tags']), + DeviceBulkAddComponentForm +): + tags = TagField( + required=False + ) + + class ConsolePortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ConsolePort.objects.all(), @@ -2462,6 +2447,15 @@ class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form): ) +class ConsoleServerPortBulkCreateForm( + form_from_model(ConsoleServerPort, ['type', 'description', 'tags']), + DeviceBulkAddComponentForm +): + tags = TagField( + required=False + ) + + class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ConsoleServerPort.objects.all(), @@ -2573,6 +2567,15 @@ class PowerPortCreateForm(BootstrapMixin, forms.Form): ) +class PowerPortBulkCreateForm( + form_from_model(PowerPort, ['type', 'maximum_draw', 'allocated_draw', 'description', 'tags']), + DeviceBulkAddComponentForm +): + tags = TagField( + required=False + ) + + class PowerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=PowerPort.objects.all(), @@ -2700,6 +2703,15 @@ class PowerOutletCreateForm(BootstrapMixin, forms.Form): self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) +class PowerOutletBulkCreateForm( + form_from_model(PowerOutlet, ['type', 'feed_leg', 'description', 'tags']), + DeviceBulkAddComponentForm +): + tags = TagField( + required=False + ) + + class PowerOutletCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), @@ -2985,71 +2997,13 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form): self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk) -class InterfaceCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( - queryset=Device.objects.all(), - required=False, - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } +class InterfaceBulkCreateForm( + form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'description', 'tags']), + DeviceBulkAddComponentForm +): + tags = TagField( + required=False ) - virtual_machine = FlexibleModelChoiceField( - queryset=VirtualMachine.objects.all(), - required=False, - to_field_name='name', - help_text='Name or ID of virtual machine', - error_messages={ - 'invalid_choice': 'Virtual machine not found.', - } - ) - lag = FlexibleModelChoiceField( - queryset=Interface.objects.all(), - required=False, - to_field_name='name', - help_text='Name or ID of LAG interface', - error_messages={ - 'invalid_choice': 'LAG interface not found.', - } - ) - type = CSVChoiceField( - choices=InterfaceTypeChoices, - ) - mode = CSVChoiceField( - choices=InterfaceModeChoices, - required=False, - ) - - class Meta: - model = Interface - fields = Interface.csv_headers - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Limit LAG choices to interfaces belonging to this device (or VC master) - if self.is_bound and 'device' in self.data: - try: - device = self.fields['device'].to_python(self.data['device']) - except forms.ValidationError: - device = None - else: - device = self.instance.device - - if device: - self.fields['lag'].queryset = Interface.objects.filter( - device__in=[device, device.get_vc_master()], type=InterfaceTypeChoices.TYPE_LAG - ) - else: - self.fields['lag'].queryset = Interface.objects.none() - - def clean_enabled(self): - # Make sure enabled is True when it's not included in the uploaded data - if 'enabled' not in self.data: - return True - else: - return self.cleaned_data['enabled'] class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): @@ -3175,6 +3129,73 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): ) +class InterfaceCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + virtual_machine = FlexibleModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of virtual machine', + error_messages={ + 'invalid_choice': 'Virtual machine not found.', + } + ) + lag = FlexibleModelChoiceField( + queryset=Interface.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of LAG interface', + error_messages={ + 'invalid_choice': 'LAG interface not found.', + } + ) + type = CSVChoiceField( + choices=InterfaceTypeChoices, + ) + mode = CSVChoiceField( + choices=InterfaceModeChoices, + required=False, + ) + + class Meta: + model = Interface + fields = Interface.csv_headers + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit LAG choices to interfaces belonging to this device (or VC master) + if self.is_bound and 'device' in self.data: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + device = self.instance.device + + if device: + self.fields['lag'].queryset = Interface.objects.filter( + device__in=[device, device.get_vc_master()], type=InterfaceTypeChoices.TYPE_LAG + ) + else: + self.fields['lag'].queryset = Interface.objects.none() + + def clean_enabled(self): + # Make sure enabled is True when it's not included in the uploaded data + if 'enabled' not in self.data: + return True + else: + return self.cleaned_data['enabled'] + + # # Front pass-through ports # @@ -3331,6 +3352,15 @@ class FrontPortCSVForm(forms.ModelForm): self.fields['rear_port'].queryset = RearPort.objects.none() +# class FrontPortBulkCreateForm( +# form_from_model(FrontPort, ['type', 'description', 'tags']), +# DeviceBulkAddComponentForm +# ): +# tags = TagField( +# required=False +# ) + + class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=FrontPort.objects.all(), @@ -3436,6 +3466,15 @@ class RearPortCSVForm(forms.ModelForm): fields = RearPort.csv_headers +# class RearPortBulkCreateForm( +# form_from_model(RearPort, ['type', 'positions', 'description', 'tags']), +# DeviceBulkAddComponentForm +# ): +# tags = TagField( +# required=False +# ) + + class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RearPort.objects.all(), @@ -4011,6 +4050,15 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form): ).exclude(pk=device_bay.device.pk) +class DeviceBayBulkCreateForm( + form_from_model(DeviceBay, ['description', 'tags']), + DeviceBulkAddComponentForm +): + tags = TagField( + required=False + ) + + class DeviceBayBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=DeviceBay.objects.all(), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c10a821dc..cc8f285c8 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1930,7 +1930,7 @@ class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateV permission_required = 'dcim.add_consoleport' parent_model = Device parent_field = 'device' - form = forms.DeviceBulkAddComponentForm + form = forms.ConsolePortBulkCreateForm model = ConsolePort model_form = forms.ConsolePortForm filterset = filters.DeviceFilterSet @@ -1942,7 +1942,7 @@ class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentC permission_required = 'dcim.add_consoleserverport' parent_model = Device parent_field = 'device' - form = forms.DeviceBulkAddComponentForm + form = forms.ConsoleServerPortBulkCreateForm model = ConsoleServerPort model_form = forms.ConsoleServerPortForm filterset = filters.DeviceFilterSet @@ -1954,7 +1954,7 @@ class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateVie permission_required = 'dcim.add_powerport' parent_model = Device parent_field = 'device' - form = forms.DeviceBulkAddComponentForm + form = forms.PowerPortBulkCreateForm model = PowerPort model_form = forms.PowerPortForm filterset = filters.DeviceFilterSet @@ -1966,7 +1966,7 @@ class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateV permission_required = 'dcim.add_poweroutlet' parent_model = Device parent_field = 'device' - form = forms.DeviceBulkAddComponentForm + form = forms.PowerOutletBulkCreateForm model = PowerOutlet model_form = forms.PowerOutletForm filterset = filters.DeviceFilterSet @@ -1978,7 +1978,7 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie permission_required = 'dcim.add_interface' parent_model = Device parent_field = 'device' - form = forms.DeviceBulkAddInterfaceForm + form = forms.InterfaceBulkCreateForm model = Interface model_form = forms.InterfaceForm filterset = filters.DeviceFilterSet @@ -1990,7 +1990,7 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie permission_required = 'dcim.add_devicebay' parent_model = Device parent_field = 'device' - form = forms.DeviceBulkAddComponentForm + form = forms.DeviceBayBulkCreateForm model = DeviceBay model_form = forms.DeviceBayForm filterset = filters.DeviceFilterSet diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index fd528f827..d787b2d67 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -10,6 +10,7 @@ from django.conf import settings from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput from django.db.models import Count from django.forms import BoundField +from django.forms.models import fields_for_model from django.urls import reverse from .choices import unpack_grouped_choices @@ -123,6 +124,15 @@ def add_blank_choice(choices): return ((None, '---------'),) + tuple(choices) +def form_from_model(model, fields): + """ + Return a Form class with the specified fields from a model. + """ + form_fields = fields_for_model(model, fields=fields) + + return type('FormFromModel', (forms.Form,), form_fields) + + # # Widgets # From 62cdf0d92864f6b7a8f570236008f495b6323c1e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Apr 2020 11:26:04 -0400 Subject: [PATCH 2/5] Add bulk creation view for rear ports --- netbox/dcim/forms.py | 136 ++++++++++++------------- netbox/dcim/urls.py | 2 +- netbox/dcim/views.py | 24 +++++ netbox/templates/dcim/device_list.html | 1 + 4 files changed, 94 insertions(+), 69 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c51332b76..765ad699b 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3304,6 +3304,50 @@ class FrontPortCreateForm(BootstrapMixin, forms.Form): } +# class FrontPortBulkCreateForm( +# form_from_model(FrontPort, ['type', 'description', 'tags']), +# DeviceBulkAddComponentForm +# ): +# tags = TagField( +# required=False +# ) + + +class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=FrontPort.objects.all(), + widget=forms.MultipleHiddenInput() + ) + type = forms.ChoiceField( + choices=add_blank_choice(PortTypeChoices), + required=False, + widget=StaticSelect2() + ) + description = forms.CharField( + max_length=100, + required=False + ) + + class Meta: + nullable_fields = [ + 'description', + ] + + +class FrontPortBulkRenameForm(BulkRenameForm): + pk = forms.ModelMultipleChoiceField( + queryset=FrontPort.objects.all(), + widget=forms.MultipleHiddenInput + ) + + +class FrontPortBulkDisconnectForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField( + queryset=FrontPort.objects.all(), + widget=forms.MultipleHiddenInput + ) + + class FrontPortCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), @@ -3352,50 +3396,6 @@ class FrontPortCSVForm(forms.ModelForm): self.fields['rear_port'].queryset = RearPort.objects.none() -# class FrontPortBulkCreateForm( -# form_from_model(FrontPort, ['type', 'description', 'tags']), -# DeviceBulkAddComponentForm -# ): -# tags = TagField( -# required=False -# ) - - -class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=FrontPort.objects.all(), - widget=forms.MultipleHiddenInput() - ) - type = forms.ChoiceField( - choices=add_blank_choice(PortTypeChoices), - required=False, - widget=StaticSelect2() - ) - description = forms.CharField( - max_length=100, - required=False - ) - - class Meta: - nullable_fields = [ - 'description', - ] - - -class FrontPortBulkRenameForm(BulkRenameForm): - pk = forms.ModelMultipleChoiceField( - queryset=FrontPort.objects.all(), - widget=forms.MultipleHiddenInput - ) - - -class FrontPortBulkDisconnectForm(ConfirmationForm): - pk = forms.ModelMultipleChoiceField( - queryset=FrontPort.objects.all(), - widget=forms.MultipleHiddenInput - ) - - # # Rear pass-through ports # @@ -3448,31 +3448,13 @@ class RearPortCreateForm(BootstrapMixin, forms.Form): ) -class RearPortCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( - queryset=Device.objects.all(), - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } +class RearPortBulkCreateForm( + form_from_model(RearPort, ['type', 'positions', 'description', 'tags']), + DeviceBulkAddComponentForm +): + tags = TagField( + required=False ) - type = CSVChoiceField( - choices=PortTypeChoices, - ) - - class Meta: - model = RearPort - fields = RearPort.csv_headers - - -# class RearPortBulkCreateForm( -# form_from_model(RearPort, ['type', 'positions', 'description', 'tags']), -# DeviceBulkAddComponentForm -# ): -# tags = TagField( -# required=False -# ) class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): @@ -3510,6 +3492,24 @@ class RearPortBulkDisconnectForm(ConfirmationForm): ) +class RearPortCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + type = CSVChoiceField( + choices=PortTypeChoices, + ) + + class Meta: + model = RearPort + fields = RearPort.csv_headers + + # # Cables # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index c62800386..36a272cf8 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -278,7 +278,7 @@ urlpatterns = [ path('rear-ports//edit/', views.RearPortEditView.as_view(), name='rearport_edit'), path('rear-ports//delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), path('rear-ports//trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), - # path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), + path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), # Device bays path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index cc8f285c8..9ca4c2edc 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1986,6 +1986,30 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie default_return_url = 'dcim:device_list' +# class DeviceBulkAddFrontPortView(PermissionRequiredMixin, BulkComponentCreateView): +# permission_required = 'dcim.add_frontport' +# parent_model = Device +# parent_field = 'device' +# form = forms.FrontPortBulkCreateForm +# model = FrontPort +# model_form = forms.FrontPortForm +# filterset = filters.DeviceFilterSet +# table = tables.DeviceTable +# default_return_url = 'dcim:device_list' + + +class DeviceBulkAddRearPortView(PermissionRequiredMixin, BulkComponentCreateView): + permission_required = 'dcim.add_rearport' + parent_model = Device + parent_field = 'device' + form = forms.RearPortBulkCreateForm + model = RearPort + model_form = forms.RearPortForm + filterset = filters.DeviceFilterSet + table = tables.DeviceTable + default_return_url = 'dcim:device_list' + + class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateView): permission_required = 'dcim.add_devicebay' parent_model = Device diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html index b12e4b5a8..ebee21d18 100644 --- a/netbox/templates/dcim/device_list.html +++ b/netbox/templates/dcim/device_list.html @@ -12,6 +12,7 @@ {% if perms.dcim.add_powerport %}
  • Power Ports
  • {% endif %} {% if perms.dcim.add_poweroutlet %}
  • Power Outlets
  • {% endif %} {% if perms.dcim.add_interface %}
  • Interfaces
  • {% endif %} + {% if perms.dcim.add_rearport %}
  • Rear Ports
  • {% endif %} {% if perms.dcim.add_devicebay %}
  • Device Bays
  • {% endif %} From e975f1b216b3307029572144226b6cd003869905 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Apr 2020 11:47:26 -0400 Subject: [PATCH 3/5] Update device component bulk edit forms to use form_from_model() --- netbox/dcim/forms.py | 522 ++++++++++++++++---------------------- netbox/utilities/forms.py | 6 +- 2 files changed, 229 insertions(+), 299 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 765ad699b..7e57bb723 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2360,20 +2360,16 @@ class ConsolePortBulkCreateForm( ) -class ConsolePortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class ConsolePortBulkEditForm( + form_from_model(ConsolePort, ['type', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + BulkEditForm +): pk = forms.ModelMultipleChoiceField( queryset=ConsolePort.objects.all(), widget=forms.MultipleHiddenInput() ) - type = forms.ChoiceField( - choices=add_blank_choice(ConsolePortTypeChoices), - required=False, - widget=StaticSelect2() - ) - description = forms.CharField( - max_length=100, - required=False - ) class Meta: nullable_fields = ( @@ -2456,20 +2452,16 @@ class ConsoleServerPortBulkCreateForm( ) -class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class ConsoleServerPortBulkEditForm( + form_from_model(ConsoleServerPort, ['type', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + BulkEditForm +): pk = forms.ModelMultipleChoiceField( queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput() ) - type = forms.ChoiceField( - choices=add_blank_choice(ConsolePortTypeChoices), - required=False, - widget=StaticSelect2() - ) - description = forms.CharField( - max_length=100, - required=False - ) class Meta: nullable_fields = [ @@ -2576,30 +2568,16 @@ class PowerPortBulkCreateForm( ) -class PowerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class PowerPortBulkEditForm( + form_from_model(PowerPort, ['type', 'maximum_draw', 'allocated_draw', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + BulkEditForm +): pk = forms.ModelMultipleChoiceField( queryset=PowerPort.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 draw in watts" - ) - allocated_draw = forms.IntegerField( - min_value=1, - required=False, - help_text="Allocated draw in watts" - ) - description = forms.CharField( - max_length=100, - required=False - ) class Meta: nullable_fields = ( @@ -2712,6 +2690,54 @@ class PowerOutletBulkCreateForm( ) +class PowerOutletBulkEditForm( + form_from_model(PowerOutlet, ['type', 'feed_leg', 'power_port', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + BulkEditForm +): + pk = forms.ModelMultipleChoiceField( + queryset=PowerOutlet.objects.all(), + widget=forms.MultipleHiddenInput() + ) + device = forms.ModelChoiceField( + queryset=Device.objects.all(), + required=False, + disabled=True, + widget=forms.HiddenInput() + ) + + class Meta: + nullable_fields = [ + 'type', 'feed_leg', 'power_port', 'description', + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit power_port queryset to PowerPorts which belong to the parent Device + if 'device' in self.initial: + device = Device.objects.filter(pk=self.initial['device']).first() + self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) + else: + self.fields['power_port'].choices = () + self.fields['power_port'].widget.attrs['disabled'] = True + + +class PowerOutletBulkRenameForm(BulkRenameForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerOutlet.objects.all(), + widget=forms.MultipleHiddenInput + ) + + +class PowerOutletBulkDisconnectForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField( + queryset=PowerOutlet.objects.all(), + widget=forms.MultipleHiddenInput + ) + + class PowerOutletCSVForm(forms.ModelForm): device = FlexibleModelChoiceField( queryset=Device.objects.all(), @@ -2762,65 +2788,6 @@ class PowerOutletCSVForm(forms.ModelForm): self.fields['power_port'].queryset = PowerPort.objects.none() -class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=PowerOutlet.objects.all(), - widget=forms.MultipleHiddenInput() - ) - device = forms.ModelChoiceField( - queryset=Device.objects.all(), - required=False, - disabled=True, - widget=forms.HiddenInput() - ) - type = forms.ChoiceField( - choices=add_blank_choice(PowerOutletTypeChoices), - required=False - ) - feed_leg = forms.ChoiceField( - choices=add_blank_choice(PowerOutletFeedLegChoices), - required=False, - ) - power_port = forms.ModelChoiceField( - queryset=PowerPort.objects.all(), - required=False - ) - description = forms.CharField( - max_length=100, - required=False - ) - - class Meta: - nullable_fields = [ - 'type', 'feed_leg', 'power_port', 'description', - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Limit power_port queryset to PowerPorts which belong to the parent Device - if 'device' in self.initial: - device = Device.objects.filter(pk=self.initial['device']).first() - self.fields['power_port'].queryset = PowerPort.objects.filter(device=device) - else: - self.fields['power_port'].choices = () - self.fields['power_port'].widget.attrs['disabled'] = True - - -class PowerOutletBulkRenameForm(BulkRenameForm): - pk = forms.ModelMultipleChoiceField( - queryset=PowerOutlet.objects.all(), - widget=forms.MultipleHiddenInput - ) - - -class PowerOutletBulkDisconnectForm(ConfirmationForm): - pk = forms.ModelMultipleChoiceField( - queryset=PowerOutlet.objects.all(), - widget=forms.MultipleHiddenInput - ) - - # # Interfaces # @@ -3006,7 +2973,12 @@ class InterfaceBulkCreateForm( ) -class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class InterfaceBulkEditForm( + form_from_model(Interface, ['type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode']), + BootstrapMixin, + AddRemoveTagsForm, + BulkEditForm +): pk = forms.ModelMultipleChoiceField( queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput() @@ -3017,45 +2989,6 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): disabled=True, widget=forms.HiddenInput() ) - type = forms.ChoiceField( - choices=add_blank_choice(InterfaceTypeChoices), - required=False, - widget=StaticSelect2() - ) - enabled = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - lag = forms.ModelChoiceField( - queryset=Interface.objects.all(), - required=False, - label='Parent LAG', - widget=StaticSelect2() - ) - mac_address = forms.CharField( - required=False, - label='MAC Address' - ) - mtu = forms.IntegerField( - required=False, - min_value=INTERFACE_MTU_MIN, - max_value=INTERFACE_MTU_MAX, - label='MTU' - ) - mgmt_only = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect(), - label='Management only' - ) - description = forms.CharField( - max_length=100, - required=False - ) - mode = forms.ChoiceField( - choices=add_blank_choice(InterfaceModeChoices), - required=False, - widget=StaticSelect2() - ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, @@ -3313,20 +3246,16 @@ class FrontPortCreateForm(BootstrapMixin, forms.Form): # ) -class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class FrontPortBulkEditForm( + form_from_model(FrontPort, ['type', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + BulkEditForm +): pk = forms.ModelMultipleChoiceField( queryset=FrontPort.objects.all(), widget=forms.MultipleHiddenInput() ) - type = forms.ChoiceField( - choices=add_blank_choice(PortTypeChoices), - required=False, - widget=StaticSelect2() - ) - description = forms.CharField( - max_length=100, - required=False - ) class Meta: nullable_fields = [ @@ -3457,20 +3386,16 @@ class RearPortBulkCreateForm( ) -class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class RearPortBulkEditForm( + form_from_model(RearPort, ['type', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + BulkEditForm +): pk = forms.ModelMultipleChoiceField( queryset=RearPort.objects.all(), widget=forms.MultipleHiddenInput() ) - type = forms.ChoiceField( - choices=add_blank_choice(PortTypeChoices), - required=False, - widget=StaticSelect2() - ) - description = forms.CharField( - max_length=100, - required=False - ) class Meta: nullable_fields = [ @@ -3510,6 +3435,146 @@ class RearPortCSVForm(forms.ModelForm): fields = RearPort.csv_headers +# +# Device bays +# + +class DeviceBayFilterForm(DeviceComponentFilterForm): + model = DeviceBay + tag = TagFilterField(model) + + +class DeviceBayForm(BootstrapMixin, forms.ModelForm): + tags = TagField( + required=False + ) + + class Meta: + model = DeviceBay + fields = [ + 'device', 'name', 'description', 'tags', + ] + widgets = { + 'device': forms.HiddenInput(), + } + + +class DeviceBayCreateForm(BootstrapMixin, forms.Form): + device = DynamicModelChoiceField( + queryset=Device.objects.prefetch_related('device_type__manufacturer') + ) + name_pattern = ExpandableNameField( + label='Name' + ) + tags = TagField( + required=False + ) + + +class PopulateDeviceBayForm(BootstrapMixin, forms.Form): + installed_device = forms.ModelChoiceField( + queryset=Device.objects.all(), + label='Child Device', + help_text="Child devices must first be created and assigned to the site/rack of the parent device.", + widget=StaticSelect2(), + ) + + def __init__(self, device_bay, *args, **kwargs): + + super().__init__(*args, **kwargs) + + self.fields['installed_device'].queryset = Device.objects.filter( + site=device_bay.device.site, + rack=device_bay.device.rack, + parent_bay__isnull=True, + device_type__u_height=0, + device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD + ).exclude(pk=device_bay.device.pk) + + +class DeviceBayBulkCreateForm( + form_from_model(DeviceBay, ['description', 'tags']), + DeviceBulkAddComponentForm +): + tags = TagField( + required=False + ) + + +class DeviceBayBulkEditForm( + form_from_model(DeviceBay, ['description']), + BootstrapMixin, + AddRemoveTagsForm, + BulkEditForm +): + pk = forms.ModelMultipleChoiceField( + queryset=DeviceBay.objects.all(), + widget=forms.MultipleHiddenInput() + ) + + class Meta: + nullable_fields = ( + 'description', + ) + + +class DeviceBayBulkRenameForm(BulkRenameForm): + pk = forms.ModelMultipleChoiceField( + queryset=DeviceBay.objects.all(), + widget=forms.MultipleHiddenInput() + ) + + +class DeviceBayCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + installed_device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Child device not found.', + } + ) + + class Meta: + model = DeviceBay + fields = DeviceBay.csv_headers + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit installed device choices to devices of the correct type and location + if self.is_bound: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + try: + device = self.instance.device + except Device.DoesNotExist: + device = None + + if device: + self.fields['installed_device'].queryset = Device.objects.filter( + site=device.site, + rack=device.rack, + parent_bay__isnull=True, + device_type__u_height=0, + device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD + ).exclude(pk=device.pk) + else: + self.fields['installed_device'].queryset = Interface.objects.none() + + # # Cables # @@ -3993,145 +4058,6 @@ class CableFilterForm(BootstrapMixin, forms.Form): ) -# -# Device bays -# - -class DeviceBayFilterForm(DeviceComponentFilterForm): - model = DeviceBay - tag = TagFilterField(model) - - -class DeviceBayForm(BootstrapMixin, forms.ModelForm): - tags = TagField( - required=False - ) - - class Meta: - model = DeviceBay - fields = [ - 'device', 'name', 'description', 'tags', - ] - widgets = { - 'device': forms.HiddenInput(), - } - - -class DeviceBayCreateForm(BootstrapMixin, forms.Form): - device = DynamicModelChoiceField( - queryset=Device.objects.prefetch_related('device_type__manufacturer') - ) - name_pattern = ExpandableNameField( - label='Name' - ) - tags = TagField( - required=False - ) - - -class PopulateDeviceBayForm(BootstrapMixin, forms.Form): - installed_device = forms.ModelChoiceField( - queryset=Device.objects.all(), - label='Child Device', - help_text="Child devices must first be created and assigned to the site/rack of the parent device.", - widget=StaticSelect2(), - ) - - def __init__(self, device_bay, *args, **kwargs): - - super().__init__(*args, **kwargs) - - self.fields['installed_device'].queryset = Device.objects.filter( - site=device_bay.device.site, - rack=device_bay.device.rack, - parent_bay__isnull=True, - device_type__u_height=0, - device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD - ).exclude(pk=device_bay.device.pk) - - -class DeviceBayBulkCreateForm( - form_from_model(DeviceBay, ['description', 'tags']), - DeviceBulkAddComponentForm -): - tags = TagField( - required=False - ) - - -class DeviceBayBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=DeviceBay.objects.all(), - widget=forms.MultipleHiddenInput() - ) - description = forms.CharField( - max_length=100, - required=False - ) - - class Meta: - nullable_fields = ( - 'description', - ) - - -class DeviceBayCSVForm(forms.ModelForm): - device = FlexibleModelChoiceField( - queryset=Device.objects.all(), - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Device not found.', - } - ) - installed_device = FlexibleModelChoiceField( - queryset=Device.objects.all(), - required=False, - to_field_name='name', - help_text='Name or ID of device', - error_messages={ - 'invalid_choice': 'Child device not found.', - } - ) - - class Meta: - model = DeviceBay - fields = DeviceBay.csv_headers - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Limit installed device choices to devices of the correct type and location - if self.is_bound: - try: - device = self.fields['device'].to_python(self.data['device']) - except forms.ValidationError: - device = None - else: - try: - device = self.instance.device - except Device.DoesNotExist: - device = None - - if device: - self.fields['installed_device'].queryset = Device.objects.filter( - site=device.site, - rack=device.rack, - parent_bay__isnull=True, - device_type__u_height=0, - device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD - ).exclude(pk=device.pk) - else: - self.fields['installed_device'].queryset = Interface.objects.none() - - -class DeviceBayBulkRenameForm(BulkRenameForm): - pk = forms.ModelMultipleChoiceField( - queryset=DeviceBay.objects.all(), - widget=forms.MultipleHiddenInput() - ) - - # # Connections # diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index d787b2d67..d95c86527 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -126,9 +126,13 @@ def add_blank_choice(choices): def form_from_model(model, fields): """ - Return a Form class with the specified fields from a model. + Return a Form class with the specified fields derived from a model. This is useful when we need a form to be used + for creating objects, but want to avoid the model's validation (e.g. for bulk create/edit functions). All fields + are marked as not required. """ form_fields = fields_for_model(model, fields=fields) + for field in form_fields.values(): + field.required = False return type('FormFromModel', (forms.Form,), form_fields) From 6a61f0911dfc8183444b428fc971acec4766eed6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Apr 2020 12:09:40 -0400 Subject: [PATCH 4/5] Update InterfaceBulkCreateForm for VMs --- netbox/virtualization/forms.py | 21 +++++++-------------- netbox/virtualization/views.py | 2 +- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index f4c2a36ec..a8232cbb5 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -15,7 +15,8 @@ from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - ExpandableNameField, JSONField, SlugField, SmallTextarea, StaticSelect2, StaticSelect2Multiple, TagFilterField, + ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea, StaticSelect2, StaticSelect2Multiple, + TagFilterField, ) from .choices import * from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -828,23 +829,15 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form): ) -class VirtualMachineBulkAddInterfaceForm(VirtualMachineBulkAddComponentForm): +class InterfaceBulkCreateForm( + form_from_model(Interface, ['enabled', 'mtu', 'description', 'tags']), + VirtualMachineBulkAddComponentForm +): type = forms.ChoiceField( choices=VMInterfaceTypeChoices, initial=VMInterfaceTypeChoices.TYPE_VIRTUAL, widget=forms.HiddenInput() ) - enabled = forms.BooleanField( - required=False, - initial=True - ) - mtu = forms.IntegerField( - required=False, - min_value=INTERFACE_MTU_MIN, - max_value=INTERFACE_MTU_MAX, - label='MTU' - ) - description = forms.CharField( - max_length=100, + tags = TagField( required=False ) diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 291392eb4..ff115d211 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -366,7 +366,7 @@ class VirtualMachineBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentC permission_required = 'dcim.add_interface' parent_model = VirtualMachine parent_field = 'virtual_machine' - form = forms.VirtualMachineBulkAddInterfaceForm + form = forms.InterfaceBulkCreateForm model = Interface model_form = forms.InterfaceForm filterset = filters.VirtualMachineFilterSet From 7b50f2b0eb7ac0071d21c7ab47ec37e23d353161 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Apr 2020 14:05:27 -0400 Subject: [PATCH 5/5] Fix tag assignment when bulk creating components --- netbox/dcim/forms.py | 33 +++++++++++----------------- netbox/utilities/views.py | 39 ++++++++++++++++++++-------------- netbox/virtualization/forms.py | 8 ++++--- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 7e57bb723..29710971e 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2299,6 +2299,11 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form): label='Name' ) + def clean_tags(self): + # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we + # must first convert the list of tags to a string. + return ','.join(self.cleaned_data.get('tags')) + # # Console ports @@ -2355,9 +2360,7 @@ class ConsolePortBulkCreateForm( form_from_model(ConsolePort, ['type', 'description', 'tags']), DeviceBulkAddComponentForm ): - tags = TagField( - required=False - ) + pass class ConsolePortBulkEditForm( @@ -2447,9 +2450,7 @@ class ConsoleServerPortBulkCreateForm( form_from_model(ConsoleServerPort, ['type', 'description', 'tags']), DeviceBulkAddComponentForm ): - tags = TagField( - required=False - ) + pass class ConsoleServerPortBulkEditForm( @@ -2563,9 +2564,7 @@ class PowerPortBulkCreateForm( form_from_model(PowerPort, ['type', 'maximum_draw', 'allocated_draw', 'description', 'tags']), DeviceBulkAddComponentForm ): - tags = TagField( - required=False - ) + pass class PowerPortBulkEditForm( @@ -2685,9 +2684,7 @@ class PowerOutletBulkCreateForm( form_from_model(PowerOutlet, ['type', 'feed_leg', 'description', 'tags']), DeviceBulkAddComponentForm ): - tags = TagField( - required=False - ) + pass class PowerOutletBulkEditForm( @@ -2968,9 +2965,7 @@ class InterfaceBulkCreateForm( form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'description', 'tags']), DeviceBulkAddComponentForm ): - tags = TagField( - required=False - ) + pass class InterfaceBulkEditForm( @@ -3241,9 +3236,7 @@ class FrontPortCreateForm(BootstrapMixin, forms.Form): # form_from_model(FrontPort, ['type', 'description', 'tags']), # DeviceBulkAddComponentForm # ): -# tags = TagField( -# required=False -# ) +# pass class FrontPortBulkEditForm( @@ -3381,9 +3374,7 @@ class RearPortBulkCreateForm( form_from_model(RearPort, ['type', 'positions', 'description', 'tags']), DeviceBulkAddComponentForm ): - tags = TagField( - required=False - ) + pass class RearPortBulkEditForm( diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 0d5153740..b671eec9c 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -972,25 +972,32 @@ class BulkComponentCreateView(GetReturnURLMixin, View): new_components = [] data = deepcopy(form.cleaned_data) - for obj in data['pk']: + try: + with transaction.atomic(): - names = data['name_pattern'] - for name in names: - component_data = { - self.parent_field: obj.pk, - 'name': name, - } - component_data.update(data) - component_form = self.model_form(component_data) - if component_form.is_valid(): - new_components.append(component_form.save(commit=False)) - else: - for field, errors in component_form.errors.as_data().items(): - for e in errors: - form.add_error(field, '{} {}: {}'.format(obj, name, ', '.join(e))) + for obj in data['pk']: + + names = data['name_pattern'] + for name in names: + component_data = { + self.parent_field: obj.pk, + 'name': name, + } + component_data.update(data) + component_form = self.model_form(component_data) + if component_form.is_valid(): + instance = component_form.save() + logger.debug(f"Created {instance} on {instance.parent}") + new_components.append(instance) + else: + for field, errors in component_form.errors.as_data().items(): + for e in errors: + form.add_error(field, '{} {}: {}'.format(obj, name, ', '.join(e))) + + except IntegrityError: + pass if not form.errors: - self.model.objects.bulk_create(new_components) msg = "Added {} {} to {} {}.".format( len(new_components), model_name, diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index a8232cbb5..9ba5ff032 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -828,6 +828,11 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form): label='Name' ) + def clean_tags(self): + # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we + # must first convert the list of tags to a string. + return ','.join(self.cleaned_data.get('tags')) + class InterfaceBulkCreateForm( form_from_model(Interface, ['enabled', 'mtu', 'description', 'tags']), @@ -838,6 +843,3 @@ class InterfaceBulkCreateForm( initial=VMInterfaceTypeChoices.TYPE_VIRTUAL, widget=forms.HiddenInput() ) - tags = TagField( - required=False - )