diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 48b0de903..29710971e 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 * @@ -2298,30 +2299,10 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form): label='Name' ) - -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 - ) + 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')) # @@ -2375,20 +2356,23 @@ class ConsolePortCreateForm(BootstrapMixin, forms.Form): ) -class ConsolePortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class ConsolePortBulkCreateForm( + form_from_model(ConsolePort, ['type', 'description', 'tags']), + DeviceBulkAddComponentForm +): + pass + + +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 = ( @@ -2462,20 +2446,23 @@ class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form): ) -class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class ConsoleServerPortBulkCreateForm( + form_from_model(ConsoleServerPort, ['type', 'description', 'tags']), + DeviceBulkAddComponentForm +): + pass + + +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 = [ @@ -2573,30 +2560,23 @@ class PowerPortCreateForm(BootstrapMixin, forms.Form): ) -class PowerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class PowerPortBulkCreateForm( + form_from_model(PowerPort, ['type', 'maximum_draw', 'allocated_draw', 'description', 'tags']), + DeviceBulkAddComponentForm +): + pass + + +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 = ( @@ -2700,6 +2680,61 @@ 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 +): + pass + + +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(), @@ -2750,65 +2785,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 # @@ -2985,74 +2961,19 @@ 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.', - } - ) - 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 InterfaceBulkCreateForm( + form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'description', 'tags']), + DeviceBulkAddComponentForm +): + pass -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() @@ -3063,45 +2984,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, @@ -3175,6 +3057,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 # @@ -3283,6 +3232,44 @@ class FrontPortCreateForm(BootstrapMixin, forms.Form): } +# class FrontPortBulkCreateForm( +# form_from_model(FrontPort, ['type', 'description', 'tags']), +# DeviceBulkAddComponentForm +# ): +# pass + + +class FrontPortBulkEditForm( + form_from_model(FrontPort, ['type', 'description']), + BootstrapMixin, + AddRemoveTagsForm, + BulkEditForm +): + pk = forms.ModelMultipleChoiceField( + queryset=FrontPort.objects.all(), + widget=forms.MultipleHiddenInput() + ) + + 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(), @@ -3331,41 +3318,6 @@ class FrontPortCSVForm(forms.ModelForm): self.fields['rear_port'].queryset = RearPort.objects.none() -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 # @@ -3418,38 +3370,23 @@ 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.', - } - ) - type = CSVChoiceField( - choices=PortTypeChoices, - ) - - class Meta: - model = RearPort - fields = RearPort.csv_headers +class RearPortBulkCreateForm( + form_from_model(RearPort, ['type', 'positions', 'description', 'tags']), + DeviceBulkAddComponentForm +): + pass -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 = [ @@ -3471,6 +3408,164 @@ 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 + + +# +# 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 # @@ -3954,136 +4049,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 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/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 c10a821dc..9ca4c2edc 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 @@ -1986,11 +1986,35 @@ 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 parent_field = 'device' - form = forms.DeviceBulkAddComponentForm + form = forms.DeviceBayBulkCreateForm model = DeviceBay model_form = forms.DeviceBayForm filterset = filters.DeviceFilterSet 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 %} diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index fd528f827..d95c86527 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,19 @@ def add_blank_choice(choices): return ((None, '---------'),) + tuple(choices) +def form_from_model(model, fields): + """ + 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) + + # # Widgets # 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 f4c2a36ec..9ba5ff032 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 @@ -827,24 +828,18 @@ 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 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, - 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