mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -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 (
|
from utilities.forms import (
|
||||||
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||||
BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField,
|
BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField,
|
||||||
DynamicModelMultipleChoiceField, ExpandableNameField, FlexibleModelChoiceField, JSONField, SelectWithPK,
|
DynamicModelMultipleChoiceField, ExpandableNameField, FlexibleModelChoiceField, form_from_model, JSONField,
|
||||||
SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||||
|
BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
||||||
from .choices import *
|
from .choices import *
|
||||||
@ -2298,30 +2299,10 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
|
|||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clean_tags(self):
|
||||||
class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
|
# Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we
|
||||||
type = forms.ChoiceField(
|
# must first convert the list of tags to a string.
|
||||||
choices=InterfaceTypeChoices,
|
return ','.join(self.cleaned_data.get('tags'))
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -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(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=ConsolePort.objects.all(),
|
queryset=ConsolePort.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
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:
|
class Meta:
|
||||||
nullable_fields = (
|
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(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=ConsoleServerPort.objects.all(),
|
queryset=ConsoleServerPort.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
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:
|
class Meta:
|
||||||
nullable_fields = [
|
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(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=PowerPort.objects.all(),
|
queryset=PowerPort.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
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:
|
class Meta:
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
@ -2700,6 +2680,61 @@ class PowerOutletCreateForm(BootstrapMixin, forms.Form):
|
|||||||
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
|
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):
|
class PowerOutletCSVForm(forms.ModelForm):
|
||||||
device = FlexibleModelChoiceField(
|
device = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@ -2750,65 +2785,6 @@ class PowerOutletCSVForm(forms.ModelForm):
|
|||||||
self.fields['power_port'].queryset = PowerPort.objects.none()
|
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
|
# 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)
|
self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceCSVForm(forms.ModelForm):
|
class InterfaceBulkCreateForm(
|
||||||
device = FlexibleModelChoiceField(
|
form_from_model(Interface, ['type', 'enabled', 'mtu', 'mgmt_only', 'description', 'tags']),
|
||||||
queryset=Device.objects.all(),
|
DeviceBulkAddComponentForm
|
||||||
required=False,
|
):
|
||||||
to_field_name='name',
|
pass
|
||||||
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 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(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
@ -3063,45 +2984,6 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
|||||||
disabled=True,
|
disabled=True,
|
||||||
widget=forms.HiddenInput()
|
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(
|
untagged_vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
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
|
# 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):
|
class FrontPortCSVForm(forms.ModelForm):
|
||||||
device = FlexibleModelChoiceField(
|
device = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@ -3331,41 +3318,6 @@ class FrontPortCSVForm(forms.ModelForm):
|
|||||||
self.fields['rear_port'].queryset = RearPort.objects.none()
|
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
|
# Rear pass-through ports
|
||||||
#
|
#
|
||||||
@ -3418,38 +3370,23 @@ class RearPortCreateForm(BootstrapMixin, forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RearPortCSVForm(forms.ModelForm):
|
class RearPortBulkCreateForm(
|
||||||
device = FlexibleModelChoiceField(
|
form_from_model(RearPort, ['type', 'positions', 'description', 'tags']),
|
||||||
queryset=Device.objects.all(),
|
DeviceBulkAddComponentForm
|
||||||
to_field_name='name',
|
):
|
||||||
help_text='Name or ID of device',
|
pass
|
||||||
error_messages={
|
|
||||||
'invalid_choice': 'Device not found.',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
type = CSVChoiceField(
|
|
||||||
choices=PortTypeChoices,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = RearPort
|
|
||||||
fields = RearPort.csv_headers
|
|
||||||
|
|
||||||
|
|
||||||
class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
class RearPortBulkEditForm(
|
||||||
|
form_from_model(RearPort, ['type', 'description']),
|
||||||
|
BootstrapMixin,
|
||||||
|
AddRemoveTagsForm,
|
||||||
|
BulkEditForm
|
||||||
|
):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=RearPort.objects.all(),
|
queryset=RearPort.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
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:
|
class Meta:
|
||||||
nullable_fields = [
|
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
|
# 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
|
# Connections
|
||||||
#
|
#
|
||||||
|
@ -278,7 +278,7 @@ urlpatterns = [
|
|||||||
path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
|
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>/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('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
|
# Device bays
|
||||||
path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
|
path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
|
||||||
|
@ -1930,7 +1930,7 @@ class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateV
|
|||||||
permission_required = 'dcim.add_consoleport'
|
permission_required = 'dcim.add_consoleport'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
form = forms.DeviceBulkAddComponentForm
|
form = forms.ConsolePortBulkCreateForm
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
model_form = forms.ConsolePortForm
|
model_form = forms.ConsolePortForm
|
||||||
filterset = filters.DeviceFilterSet
|
filterset = filters.DeviceFilterSet
|
||||||
@ -1942,7 +1942,7 @@ class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentC
|
|||||||
permission_required = 'dcim.add_consoleserverport'
|
permission_required = 'dcim.add_consoleserverport'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
form = forms.DeviceBulkAddComponentForm
|
form = forms.ConsoleServerPortBulkCreateForm
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
model_form = forms.ConsoleServerPortForm
|
model_form = forms.ConsoleServerPortForm
|
||||||
filterset = filters.DeviceFilterSet
|
filterset = filters.DeviceFilterSet
|
||||||
@ -1954,7 +1954,7 @@ class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateVie
|
|||||||
permission_required = 'dcim.add_powerport'
|
permission_required = 'dcim.add_powerport'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
form = forms.DeviceBulkAddComponentForm
|
form = forms.PowerPortBulkCreateForm
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
model_form = forms.PowerPortForm
|
model_form = forms.PowerPortForm
|
||||||
filterset = filters.DeviceFilterSet
|
filterset = filters.DeviceFilterSet
|
||||||
@ -1966,7 +1966,7 @@ class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateV
|
|||||||
permission_required = 'dcim.add_poweroutlet'
|
permission_required = 'dcim.add_poweroutlet'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
form = forms.DeviceBulkAddComponentForm
|
form = forms.PowerOutletBulkCreateForm
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
model_form = forms.PowerOutletForm
|
model_form = forms.PowerOutletForm
|
||||||
filterset = filters.DeviceFilterSet
|
filterset = filters.DeviceFilterSet
|
||||||
@ -1978,7 +1978,7 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie
|
|||||||
permission_required = 'dcim.add_interface'
|
permission_required = 'dcim.add_interface'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
form = forms.DeviceBulkAddInterfaceForm
|
form = forms.InterfaceBulkCreateForm
|
||||||
model = Interface
|
model = Interface
|
||||||
model_form = forms.InterfaceForm
|
model_form = forms.InterfaceForm
|
||||||
filterset = filters.DeviceFilterSet
|
filterset = filters.DeviceFilterSet
|
||||||
@ -1986,11 +1986,35 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie
|
|||||||
default_return_url = 'dcim:device_list'
|
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):
|
class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateView):
|
||||||
permission_required = 'dcim.add_devicebay'
|
permission_required = 'dcim.add_devicebay'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
form = forms.DeviceBulkAddComponentForm
|
form = forms.DeviceBayBulkCreateForm
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
model_form = forms.DeviceBayForm
|
model_form = forms.DeviceBayForm
|
||||||
filterset = filters.DeviceFilterSet
|
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_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_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_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 %}
|
{% 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,7 @@ from django.conf import settings
|
|||||||
from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
|
from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.forms import BoundField
|
from django.forms import BoundField
|
||||||
|
from django.forms.models import fields_for_model
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .choices import unpack_grouped_choices
|
from .choices import unpack_grouped_choices
|
||||||
@ -123,6 +124,19 @@ def add_blank_choice(choices):
|
|||||||
return ((None, '---------'),) + tuple(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
|
# Widgets
|
||||||
#
|
#
|
||||||
|
@ -972,25 +972,32 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
|
|||||||
new_components = []
|
new_components = []
|
||||||
data = deepcopy(form.cleaned_data)
|
data = deepcopy(form.cleaned_data)
|
||||||
|
|
||||||
for obj in data['pk']:
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
|
||||||
names = data['name_pattern']
|
for obj in data['pk']:
|
||||||
for name in names:
|
|
||||||
component_data = {
|
names = data['name_pattern']
|
||||||
self.parent_field: obj.pk,
|
for name in names:
|
||||||
'name': name,
|
component_data = {
|
||||||
}
|
self.parent_field: obj.pk,
|
||||||
component_data.update(data)
|
'name': name,
|
||||||
component_form = self.model_form(component_data)
|
}
|
||||||
if component_form.is_valid():
|
component_data.update(data)
|
||||||
new_components.append(component_form.save(commit=False))
|
component_form = self.model_form(component_data)
|
||||||
else:
|
if component_form.is_valid():
|
||||||
for field, errors in component_form.errors.as_data().items():
|
instance = component_form.save()
|
||||||
for e in errors:
|
logger.debug(f"Created {instance} on {instance.parent}")
|
||||||
form.add_error(field, '{} {}: {}'.format(obj, name, ', '.join(e)))
|
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:
|
if not form.errors:
|
||||||
self.model.objects.bulk_create(new_components)
|
|
||||||
msg = "Added {} {} to {} {}.".format(
|
msg = "Added {} {} to {} {}.".format(
|
||||||
len(new_components),
|
len(new_components),
|
||||||
model_name,
|
model_name,
|
||||||
|
@ -15,7 +15,8 @@ from tenancy.models import Tenant
|
|||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||||
CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
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 .choices import *
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||||
@ -827,24 +828,18 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
|
|||||||
label='Name'
|
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(
|
type = forms.ChoiceField(
|
||||||
choices=VMInterfaceTypeChoices,
|
choices=VMInterfaceTypeChoices,
|
||||||
initial=VMInterfaceTypeChoices.TYPE_VIRTUAL,
|
initial=VMInterfaceTypeChoices.TYPE_VIRTUAL,
|
||||||
widget=forms.HiddenInput()
|
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'
|
permission_required = 'dcim.add_interface'
|
||||||
parent_model = VirtualMachine
|
parent_model = VirtualMachine
|
||||||
parent_field = 'virtual_machine'
|
parent_field = 'virtual_machine'
|
||||||
form = forms.VirtualMachineBulkAddInterfaceForm
|
form = forms.InterfaceBulkCreateForm
|
||||||
model = Interface
|
model = Interface
|
||||||
model_form = forms.InterfaceForm
|
model_form = forms.InterfaceForm
|
||||||
filterset = filters.VirtualMachineFilterSet
|
filterset = filters.VirtualMachineFilterSet
|
||||||
|
Loading…
Reference in New Issue
Block a user