Merge pull request #4524 from netbox-community/4139-component-bulk-create

Fixes #4139: Extend forms for bulk device component creation
This commit is contained in:
Jeremy Stretch 2020-04-22 14:11:07 -04:00 committed by GitHub
commit ca08125d55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 467 additions and 461 deletions

View File

@ -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
#

View File

@ -278,7 +278,7 @@ urlpatterns = [
path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
path('rear-ports/<int:pk>/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'),

View File

@ -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

View File

@ -12,6 +12,7 @@
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Ports</a></li>{% endif %}
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Outlets</a></li>{% endif %}
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_rearport %}<li><a href="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Rear Ports</a></li>{% endif %}
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
</ul>
</div>

View File

@ -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
#

View File

@ -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,

View File

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

View File

@ -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