mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
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:
commit
ca08125d55
@ -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
|
||||
#
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user