Closes #1167: Introduced ChainedModelChoiceFields

This commit is contained in:
Jeremy Stretch 2017-05-11 16:24:57 -04:00
parent 0f97478b55
commit 58bb029666
5 changed files with 274 additions and 372 deletions

View File

@ -5,8 +5,8 @@ from dcim.models import Site, Device, Interface, Rack, VIRTUAL_IFACE_TYPES
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea, APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
SlugField, FilterChoiceField, Livesearch, SmallTextarea, SlugField,
) )
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@ -152,15 +152,16 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
# Circuit terminations # Circuit terminations
# #
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm): class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
widget=forms.Select( widget=forms.Select(
attrs={'filter-for': 'rack'} attrs={'filter-for': 'rack'}
) )
) )
rack = forms.ModelChoiceField( rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'site'},
required=False, required=False,
label='Rack', label='Rack',
widget=APISelect( widget=APISelect(
@ -168,8 +169,9 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
attrs={'filter-for': 'device', 'nullable': 'true'} attrs={'filter-for': 'device', 'nullable': 'true'}
) )
) )
device = forms.ModelChoiceField( device = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'},
required=False, required=False,
label='Device', label='Device',
widget=APISelect( widget=APISelect(
@ -187,8 +189,11 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
field_to_update='device' field_to_update='device'
) )
) )
interface = forms.ModelChoiceField( interface = ChainedModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
),
chains={'device': 'device'},
required=False, required=False,
label='Interface', label='Interface',
widget=APISelect( widget=APISelect(
@ -210,51 +215,16 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
'term_side': forms.HiddenInput(), 'term_side': forms.HiddenInput(),
} }
def __init__(self, *args, **kwargs): def __init__(self, instance=None, initial=None, *args, **kwargs):
super(CircuitTerminationForm, self).__init__(*args, **kwargs) # Initialize helper selectors
if instance and instance.interface is not None:
initial['rack'] = instance.interface.device.rack
initial['device'] = instance.interface.device
# If an interface has been assigned, initialize rack and device super(CircuitTerminationForm, self).__init__(instance=instance, initial=initial, *args, **kwargs)
if self.instance.interface:
self.initial['rack'] = self.instance.interface.device.rack
self.initial['device'] = self.instance.interface.device
# Limit rack choices # Mark connected interfaces as disabled
if self.is_bound:
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Limit device choices
if self.is_bound and self.data.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
elif self.initial.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
else:
self.fields['device'].choices = []
# Limit interface choices
if self.is_bound and self.data.get('device'):
interfaces = Interface.objects.filter(device=self.data['device']).exclude(
form_factor__in=VIRTUAL_IFACE_TYPES
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
)
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
elif self.initial.get('device'):
interfaces = Interface.objects.filter(device=self.initial['device']).exclude(
form_factor__in=VIRTUAL_IFACE_TYPES
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
)
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
else:
interfaces = []
self.fields['interface'].choices = [ self.fields['interface'].choices = [
(iface.id, { (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface'].queryset
'label': iface.name,
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
}) for iface in interfaces
] ]

View File

@ -11,8 +11,9 @@ from ipam.models import IPAddress
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
BulkImportForm, CommentField, CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField, ExpandableNameField,
Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
FilterTreeNodeMultipleChoiceField,
) )
from .formfields import MACAddressFormField from .formfields import MACAddressFormField
@ -184,16 +185,23 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm):
# Racks # Racks
# #
class RackForm(BootstrapMixin, CustomFieldForm): class RackForm(BootstrapMixin, ChainedFieldsMixin, CustomFieldForm):
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group', widget=APISelect( group = ChainedModelChoiceField(
api_url='/api/dcim/rack-groups/?site_id={{site}}', queryset=RackGroup.objects.all(),
)) chains={'site': 'site'},
required=False,
widget=APISelect(
api_url='/api/dcim/rack-groups/?site_id={{site}}',
)
)
comments = CommentField() comments = CommentField()
class Meta: class Meta:
model = Rack model = Rack
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units', fields = [
'comments'] 'site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
'comments',
]
help_texts = { help_texts = {
'site': "The site at which the rack exists", 'site': "The site at which the rack exists",
'name': "Organizational rack name", 'name': "Organizational rack name",
@ -204,18 +212,6 @@ class RackForm(BootstrapMixin, CustomFieldForm):
'site': forms.Select(attrs={'filter-for': 'group'}), 'site': forms.Select(attrs={'filter-for': 'group'}),
} }
def __init__(self, *args, **kwargs):
super(RackForm, self).__init__(*args, **kwargs)
# Limit rack group choices
if self.is_bound and self.data.get('site'):
self.fields['group'].queryset = RackGroup.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['group'].queryset = RackGroup.objects.filter(site=self.initial['site'])
else:
self.fields['group'].choices = []
class RackFromCSVForm(forms.ModelForm): class RackFromCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
@ -538,25 +534,46 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
# Devices # Devices
# #
class DeviceForm(BootstrapMixin, CustomFieldForm): class DeviceForm(BootstrapMixin, ChainedFieldsMixin, CustomFieldForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'})) site = forms.ModelChoiceField(
rack = forms.ModelChoiceField( queryset=Site.objects.all(),
queryset=Rack.objects.all(), required=False, widget=APISelect( widget=forms.Select(
attrs={'filter-for': 'rack'}
)
)
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains={'site': 'site'},
required=False,
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}', api_url='/api/dcim/racks/?site_id={{site}}',
display_field='display_name', display_field='display_name',
attrs={'filter-for': 'position'} attrs={'filter-for': 'position'}
) )
) )
position = forms.TypedChoiceField( position = forms.TypedChoiceField(
required=False, empty_value=None, help_text="The lowest-numbered unit occupied by the device", required=False,
widget=APISelect(api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}', disabled_indicator='device') empty_value=None,
help_text="The lowest-numbered unit occupied by the device",
widget=APISelect(
api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}',
disabled_indicator='device'
)
) )
manufacturer = forms.ModelChoiceField( manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(), widget=forms.Select(attrs={'filter-for': 'device_type'}) queryset=Manufacturer.objects.all(),
widget=forms.Select(
attrs={'filter-for': 'device_type'}
)
) )
device_type = forms.ModelChoiceField( device_type = ChainedModelChoiceField(
queryset=DeviceType.objects.all(), label='Device type', queryset=DeviceType.objects.all(),
widget=APISelect(api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}', display_field='model') chains={'manufacturer': 'manufacturer'},
label='Device type',
widget=APISelect(
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
display_field='model'
)
) )
comments = CommentField() comments = CommentField()
@ -572,19 +589,18 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
} }
widgets = { widgets = {
'face': forms.Select(attrs={'filter-for': 'position'}), 'face': forms.Select(attrs={'filter-for': 'position'}),
'manufacturer': forms.Select(attrs={'filter-for': 'device_type'}),
} }
def __init__(self, *args, **kwargs): def __init__(self, instance=None, initial=None, *args, **kwargs):
super(DeviceForm, self).__init__(*args, **kwargs) # Initialize helper selections
if instance and instance.device_type is not None:
initial['manufacturer'] = instance.device_type.manufacturer
super(DeviceForm, self).__init__(instance=instance, initial=initial, *args, **kwargs)
if self.instance.pk: if self.instance.pk:
# Initialize helper selections
self.initial['site'] = self.instance.site
self.initial['manufacturer'] = self.instance.device_type.manufacturer
# Compile list of choices for primary IPv4 and IPv6 addresses # Compile list of choices for primary IPv4 and IPv6 addresses
for family in [4, 6]: for family in [4, 6]:
ip_choices = [] ip_choices = []
@ -607,14 +623,6 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
self.fields['primary_ip6'].choices = [] self.fields['primary_ip6'].choices = []
self.fields['primary_ip6'].widget.attrs['readonly'] = True self.fields['primary_ip6'].widget.attrs['readonly'] = True
# Limit rack choices
if self.is_bound and self.data.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Rack position # Rack position
pk = self.instance.pk if self.instance.pk else None pk = self.instance.pk if self.instance.pk else None
try: try:
@ -635,16 +643,6 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
}) for p in position_choices }) for p in position_choices
] ]
# Limit device_type choices
if self.is_bound:
self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer__pk=self.data['manufacturer'])\
.select_related('manufacturer')
elif self.initial.get('manufacturer'):
self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer=self.initial['manufacturer'])\
.select_related('manufacturer')
else:
self.fields['device_type'].choices = []
# Disable rack assignment if this is a child device installed in a parent device # Disable rack assignment if this is a child device installed in a parent device
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'): if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
self.fields['site'].disabled = True self.fields['site'].disabled = True
@ -940,21 +938,23 @@ class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
self.cleaned_data['csv'] = connection_list self.cleaned_data['csv'] = connection_list
class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm): class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
widget=forms.HiddenInput(), widget=forms.HiddenInput(),
) )
rack = forms.ModelChoiceField( rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'site'},
label='Rack', label='Rack',
required=False, required=False,
widget=forms.Select( widget=forms.Select(
attrs={'filter-for': 'console_server', 'nullable': 'true'} attrs={'filter-for': 'console_server', 'nullable': 'true'}
) )
) )
console_server = forms.ModelChoiceField( console_server = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.filter(device_type__is_console_server=True),
chains={'site': 'site', 'rack': 'rack'},
label='Console Server', label='Console Server',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -972,8 +972,9 @@ class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
field_to_update='console_server', field_to_update='console_server',
) )
) )
cs_port = forms.ModelChoiceField( cs_port = ChainedModelChoiceField(
queryset=ConsoleServerPort.objects.all(), queryset=ConsoleServerPort.objects.all(),
chains={'device': 'console_server'},
label='Port', label='Port',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/console-server-ports/?device_id={{console_server}}', api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
@ -996,32 +997,6 @@ class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
if not self.instance.pk: if not self.instance.pk:
raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.") raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
# Initialize rack choices if site is set
if self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Initialize console_server choices if rack or site is set
if self.initial.get('rack'):
self.fields['console_server'].queryset = Device.objects.filter(
rack=self.initial['rack'], device_type__is_console_server=True
)
elif self.initial.get('site'):
self.fields['console_server'].queryset = Device.objects.filter(
site=self.initial['site'], rack__isnull=True, device_type__is_console_server=True
)
else:
self.fields['console_server'].choices = []
# Initialize CS port choices if console_server is set
if self.initial.get('console_server'):
self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(
device=self.initial['console_server']
)
else:
self.fields['cs_port'].choices = []
# #
# Console server ports # Console server ports
@ -1041,21 +1016,23 @@ class ConsoleServerPortCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form): class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
widget=forms.HiddenInput(), widget=forms.HiddenInput(),
) )
rack = forms.ModelChoiceField( rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'site'},
label='Rack', label='Rack',
required=False, required=False,
widget=forms.Select( widget=forms.Select(
attrs={'filter-for': 'device', 'nullable': 'true'} attrs={'filter-for': 'device', 'nullable': 'true'}
) )
) )
device = forms.ModelChoiceField( device = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'},
label='Device', label='Device',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1073,8 +1050,9 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
field_to_update='device' field_to_update='device'
) )
) )
port = forms.ModelChoiceField( port = ChainedModelChoiceField(
queryset=ConsolePort.objects.all(), queryset=ConsolePort.objects.all(),
chains={'device': 'device'},
label='Port', label='Port',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/console-ports/?device_id={{device}}', api_url='/api/dcim/console-ports/?device_id={{device}}',
@ -1096,30 +1074,6 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
'connection_status': 'Status', 'connection_status': 'Status',
} }
def __init__(self, *args, **kwargs):
super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
# Initialize rack choices if site is set
if self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Initialize device choices if rack or site is set
if self.initial.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
elif self.initial.get('site'):
self.fields['device'].queryset = Device.objects.filter(site=self.initial['site'], rack__isnull=True)
else:
self.fields['device'].choices = []
# Initialize port choices if device is set
if self.initial.get('device'):
self.fields['port'].queryset = ConsolePort.objects.filter(device=self.initial['device'])
else:
self.fields['port'].choices = []
# #
# Power ports # Power ports
@ -1211,18 +1165,20 @@ class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
self.cleaned_data['csv'] = connection_list self.cleaned_data['csv'] = connection_list
class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm): class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.HiddenInput()) site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.HiddenInput())
rack = forms.ModelChoiceField( rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'site'},
label='Rack', label='Rack',
required=False, required=False,
widget=forms.Select( widget=forms.Select(
attrs={'filter-for': 'pdu', 'nullable': 'true'} attrs={'filter-for': 'pdu', 'nullable': 'true'}
) )
) )
pdu = forms.ModelChoiceField( pdu = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'},
label='PDU', label='PDU',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1240,8 +1196,9 @@ class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
field_to_update='pdu' field_to_update='pdu'
) )
) )
power_outlet = forms.ModelChoiceField( power_outlet = ChainedModelChoiceField(
queryset=PowerOutlet.objects.all(), queryset=PowerOutlet.objects.all(),
chains={'device': 'device'},
label='Outlet', label='Outlet',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/power-outlets/?device_id={{pdu}}', api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
@ -1264,30 +1221,6 @@ class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
if not self.instance.pk: if not self.instance.pk:
raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.") raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
# Initialize rack choices if site is set
if self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Initialize pdu choices if rack or site is set
if self.initial.get('rack'):
self.fields['pdu'].queryset = Device.objects.filter(
rack=self.initial['rack'], device_type__is_pdu=True
)
elif self.initial.get('site'):
self.fields['pdu'].queryset = Device.objects.filter(
site=self.initial['site'], rack__isnull=True, device_type__is_pdu=True
)
else:
self.fields['pdu'].choices = []
# Initialize power outlet choices if pdu is set
if self.initial.get('pdu'):
self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device=self.initial['pdu'])
else:
self.fields['power_outlet'].choices = []
# #
# Power outlets # Power outlets
@ -1307,21 +1240,23 @@ class PowerOutletCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
class PowerOutletConnectionForm(BootstrapMixin, forms.Form): class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
widget=forms.HiddenInput() widget=forms.HiddenInput()
) )
rack = forms.ModelChoiceField( rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'site'},
label='Rack', label='Rack',
required=False, required=False,
widget=forms.Select( widget=forms.Select(
attrs={'filter-for': 'device', 'nullable': 'true'} attrs={'filter-for': 'device', 'nullable': 'true'}
) )
) )
device = forms.ModelChoiceField( device = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'},
label='Device', label='Device',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1339,8 +1274,9 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
field_to_update='device' field_to_update='device'
) )
) )
port = forms.ModelChoiceField( port = ChainedModelChoiceField(
queryset=PowerPort.objects.all(), queryset=PowerPort.objects.all(),
chains={'device': 'device'},
label='Port', label='Port',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/power-ports/?device_id={{device}}', api_url='/api/dcim/power-ports/?device_id={{device}}',
@ -1362,30 +1298,6 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
'connection_status': 'Status', 'connection_status': 'Status',
} }
def __init__(self, *args, **kwargs):
super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
# Initialize rack choices if site is set
if self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Initialize device choices if rack or site is set
if self.initial.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
elif self.initial.get('site'):
self.fields['device'].queryset = Device.objects.filter(site=self.initial['site'], rack__isnull=True)
else:
self.fields['device'].choices = []
# Initialize port choices if device is set
if self.initial.get('device'):
self.fields['port'].queryset = PowerPort.objects.filter(device=self.initial['device'])
else:
self.fields['port'].choices = []
# #
# Interfaces # Interfaces
@ -1468,7 +1380,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
# Interface connections # Interface connections
# #
class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm): class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
interface_a = forms.ChoiceField( interface_a = forms.ChoiceField(
choices=[], choices=[],
widget=SelectWithDisabled, widget=SelectWithDisabled,
@ -1482,8 +1394,9 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
attrs={'filter-for': 'rack_b'} attrs={'filter-for': 'rack_b'}
) )
) )
rack_b = forms.ModelChoiceField( rack_b = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains = {'site': 'site_b'},
label='Rack', label='Rack',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1491,8 +1404,9 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
attrs={'filter-for': 'device_b', 'nullable': 'true'} attrs={'filter-for': 'device_b', 'nullable': 'true'}
) )
) )
device_b = forms.ModelChoiceField( device_b = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains = {'site': 'site_b', 'rack': 'rack_b'},
label='Device', label='Device',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1510,8 +1424,11 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
field_to_update='device_b' field_to_update='device_b'
) )
) )
interface_b = forms.ModelChoiceField( interface_b = ChainedModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
),
chains = {'device': 'device_b'},
label='Interface', label='Interface',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical', api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
@ -1537,31 +1454,9 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
] ]
# Initialize rack_b choices if site_b is set # Mark connected interfaces as disabled
if self.initial.get('site_b'):
self.fields['rack_b'].queryset = Rack.objects.filter(site=self.initial['site_b'])
else:
self.fields['rack_b'].choices = []
# Initialize device_b choices if rack_b or site_b is set
if self.initial.get('rack_b'):
self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
elif self.initial.get('site_b'):
self.fields['device_b'].queryset = Device.objects.filter(site=self.initial['site_b'], rack__isnull=True)
else:
self.fields['device_b'].choices = []
# Initialize interface_b choices if device_b is set
if self.initial.get('device_b'):
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']).exclude(
form_factor__in=VIRTUAL_IFACE_TYPES
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
)
else:
device_b_interfaces = []
self.fields['interface_b'].choices = [ self.fields['interface_b'].choices = [
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_b_interfaces (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
] ]

View File

@ -5,8 +5,8 @@ from dcim.models import Site, Rack, Device, Interface
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, CSVDataField, ExpandableIPAddressField, APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField,
FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
) )
from .models import ( from .models import (
@ -163,12 +163,17 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
# Prefixes # Prefixes
# #
class PrefixForm(BootstrapMixin, CustomFieldForm): class PrefixForm(BootstrapMixin, ChainedFieldsMixin, CustomFieldForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site', site = forms.ModelChoiceField(
widget=forms.Select(attrs={'filter-for': 'vlan', 'nullable': 'true'})) queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN', attrs={'filter-for': 'vlan', 'nullable': 'true'}
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}', )
display_field='display_name')) )
vlan = ChainedModelChoiceField(
queryset=VLAN.objects.all(), chains={'site': 'site'}, required=False, label='VLAN', widget=APISelect(
api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name'
)
)
class Meta: class Meta:
model = Prefix model = Prefix
@ -179,14 +184,6 @@ class PrefixForm(BootstrapMixin, CustomFieldForm):
self.fields['vrf'].empty_label = 'Global' self.fields['vrf'].empty_label = 'Global'
# Initialize field without choices to avoid pulling all VLANs from the database
if self.is_bound and self.data.get('site'):
self.fields['vlan'].queryset = VLAN.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
else:
self.fields['vlan'].queryset = VLAN.objects.filter(site=None)
class PrefixFromCSVForm(forms.ModelForm): class PrefixFromCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd', vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
@ -214,7 +211,6 @@ class PrefixFromCSVForm(forms.ModelForm):
vlan_group_name = self.cleaned_data.get('vlan_group_name') vlan_group_name = self.cleaned_data.get('vlan_group_name')
vlan_vid = self.cleaned_data.get('vlan_vid') vlan_vid = self.cleaned_data.get('vlan_vid')
vlan_group = None vlan_group = None
vlan = None
# Validate VLAN group # Validate VLAN group
if vlan_group_name: if vlan_group_name:
@ -310,38 +306,93 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
# IP addresses # IP addresses
# #
class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm): class IPAddressForm(BootstrapMixin, ChainedFieldsMixin, ReturnURLForm, CustomFieldForm):
interface_site = forms.ModelChoiceField( interface_site = forms.ModelChoiceField(
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select( queryset=Site.objects.all(),
required=False,
label='Site',
widget=forms.Select(
attrs={'filter-for': 'interface_rack'} attrs={'filter-for': 'interface_rack'}
) )
) )
interface_rack = forms.ModelChoiceField( interface_rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), required=False, label='Rack', widget=APISelect( queryset=Rack.objects.all(),
api_url='/api/dcim/racks/?site_id={{interface_site}}', display_field='display_name', chains={'site': 'interface_site'},
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{interface_site}}',
display_field='display_name',
attrs={'filter-for': 'interface_device', 'nullable': 'true'} attrs={'filter-for': 'interface_device', 'nullable': 'true'}
) )
) )
interface_device = forms.ModelChoiceField( interface_device = ChainedModelChoiceField(
queryset=Device.objects.all(), required=False, label='Device', widget=APISelect( queryset=Device.objects.all(),
chains={'site': 'interface_site', 'rack': 'interface_rack'},
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{interface_site}}&rack_id={{interface_rack}}', api_url='/api/dcim/devices/?site_id={{interface_site}}&rack_id={{interface_rack}}',
display_field='display_name', attrs={'filter-for': 'interface'} display_field='display_name',
attrs={'filter-for': 'interface'}
)
)
interface = ChainedModelChoiceField(
queryset=Interface.objects.all(),
chains={'device': 'interface_device'},
required=False,
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{interface_device}}'
) )
) )
nat_site = forms.ModelChoiceField( nat_site = forms.ModelChoiceField(
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select( queryset=Site.objects.all(),
required=False,
label='Site',
widget=forms.Select(
attrs={'filter-for': 'nat_device'} attrs={'filter-for': 'nat_device'}
) )
) )
nat_device = forms.ModelChoiceField( nat_rack = ChainedModelChoiceField(
queryset=Device.objects.all(), required=False, label='Device', widget=APISelect( queryset=Rack.objects.all(),
api_url='/api/dcim/devices/?site_id={{nat_site}}', display_field='display_name', chains={'site': 'nat_site'},
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{interface_site}}',
display_field='display_name',
attrs={'filter-for': 'nat_device', 'nullable': 'true'}
)
)
nat_device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains={'site': 'nat_site'},
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{nat_site}}',
display_field='display_name',
attrs={'filter-for': 'nat_inside'} attrs={'filter-for': 'nat_inside'}
) )
) )
nat_inside = ChainedModelChoiceField(
queryset=IPAddress.objects.all(),
chains={'interface__device': 'nat_device'},
required=False,
label='IP Address',
widget=APISelect(
api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
display_field='address'
)
)
livesearch = forms.CharField( livesearch = forms.CharField(
required=False, label='IP Address', widget=Livesearch( required=False,
query_key='q', query_url='ipam-api:ipaddress-list', field_to_update='nat_inside', obj_label='address' label='IP Address',
widget=Livesearch(
query_key='q',
query_url='ipam-api:ipaddress-list',
field_to_update='nat_inside',
obj_label='address'
) )
) )
primary_for_device = forms.BooleanField(required=False, label='Make this the primary IP for the device') primary_for_device = forms.BooleanField(required=False, label='Make this the primary IP for the device')
@ -349,46 +400,25 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = ['address', 'vrf', 'tenant', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside'] fields = ['address', 'vrf', 'tenant', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside']
widgets = {
'interface': APISelect(api_url='/api/dcim/interfaces/?device_id={{interface_device}}'),
'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address')
}
def __init__(self, *args, **kwargs): def __init__(self, instance=None, initial=None, *args, **kwargs):
super(IPAddressForm, self).__init__(*args, **kwargs)
# Initialize interface selectors
if instance and instance.interface is not None:
initial['interface_site'] = instance.interface.device.site
initial['interface_rack'] = instance.interface.device.rack
initial['interface_device'] = instance.interface.device
# Initialize NAT selectors
if instance and instance.nat_inside is not None:
initial['nat_site'] = instance.nat_inside.device.site
initial['nat_rack'] = instance.nat_inside.device.rack
initial['nat_device'] = instance.nat_inside.device
super(IPAddressForm, self).__init__(instance=instance, initial=initial, *args, **kwargs)
self.fields['vrf'].empty_label = 'Global' self.fields['vrf'].empty_label = 'Global'
# If an interface has been assigned, initialize site, rack, and device
if self.instance.interface:
self.initial['interface_site'] = self.instance.interface.device.site
self.initial['interface_rack'] = self.instance.interface.device.rack
self.initial['interface_device'] = self.instance.interface.device
# Limit rack choices
if self.is_bound and self.data.get('interface_site'):
self.fields['interface_rack'].queryset = Rack.objects.filter(site__pk=self.data['interface_site'])
elif self.initial.get('interface_site'):
self.fields['interface_rack'].queryset = Rack.objects.filter(site=self.initial['interface_site'])
else:
self.fields['interface_rack'].choices = []
# Limit device choices
if self.is_bound and self.data.get('interface_rack'):
self.fields['interface_device'].queryset = Device.objects.filter(rack=self.data['interface_rack'])
elif self.initial.get('interface_rack'):
self.fields['interface_device'].queryset = Device.objects.filter(rack=self.initial['interface_rack'])
else:
self.fields['interface_device'].choices = []
# Limit interface choices
if self.is_bound and self.data.get('interface_device'):
self.fields['interface'].queryset = Interface.objects.filter(device=self.data['interface_device'])
elif self.initial.get('interface_device'):
self.fields['interface'].queryset = Interface.objects.filter(device=self.initial['interface_device'])
else:
self.fields['interface'].choices = []
# Initialize primary_for_device if IP address is already assigned # Initialize primary_for_device if IP address is already assigned
if self.instance.interface is not None: if self.instance.interface is not None:
device = self.instance.interface.device device = self.instance.interface.device
@ -398,38 +428,6 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
): ):
self.initial['primary_for_device'] = True self.initial['primary_for_device'] = True
if self.instance.nat_inside:
nat_inside = self.instance.nat_inside
# If the IP is assigned to an interface, populate site/device fields accordingly
if self.instance.nat_inside.interface:
self.initial['nat_site'] = self.instance.nat_inside.interface.device.site.pk
self.initial['nat_device'] = self.instance.nat_inside.interface.device.pk
self.fields['nat_device'].queryset = Device.objects.filter(
site=nat_inside.interface.device.site
)
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
interface__device=nat_inside.interface.device
)
else:
self.fields['nat_inside'].queryset = IPAddress.objects.filter(pk=nat_inside.pk)
else:
# Initialize nat_device choices if nat_site is set
if self.is_bound and self.data.get('nat_site'):
self.fields['nat_device'].queryset = Device.objects.filter(site__pk=self.data['nat_site'])
elif self.initial.get('nat_site'):
self.fields['nat_device'].queryset = Device.objects.filter(site=self.initial['nat_site'])
else:
self.fields['nat_device'].choices = []
# Initialize nat_inside choices if nat_device is set
if self.is_bound and self.data.get('nat_device'):
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
interface__device__pk=self.data['nat_device'])
elif self.initial.get('nat_device'):
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
interface__device__pk=self.initial['nat_device'])
else:
self.fields['nat_inside'].choices = []
def clean(self): def clean(self):
super(IPAddressForm, self).clean() super(IPAddressForm, self).clean()
@ -602,10 +600,22 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
# VLANs # VLANs
# #
class VLANForm(BootstrapMixin, CustomFieldForm): class VLANForm(BootstrapMixin, ChainedFieldsMixin, CustomFieldForm):
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect( site = forms.ModelChoiceField(
api_url='/api/ipam/vlan-groups/?site_id={{site}}', queryset=Site.objects.all(),
)) widget=forms.Select(
attrs={'filter-for': 'group', 'nullable': 'true'}
)
)
group = ChainedModelChoiceField(
queryset=VLANGroup.objects.all(),
chains={'site': 'site'},
required=False,
label='Group',
widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
)
)
class Meta: class Meta:
model = VLAN model = VLAN
@ -618,21 +628,6 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
'status': "Operational status of this VLAN", 'status': "Operational status of this VLAN",
'role': "The primary function of this VLAN", 'role': "The primary function of this VLAN",
} }
widgets = {
'site': forms.Select(attrs={'filter-for': 'group', 'nullable': 'true'}),
}
def __init__(self, *args, **kwargs):
super(VLANForm, self).__init__(*args, **kwargs)
# Limit VLAN group choices
if self.is_bound and self.data.get('site'):
self.fields['group'].queryset = VLANGroup.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
else:
self.fields['group'].queryset = VLANGroup.objects.filter(site=None)
class VLANFromCSVForm(forms.ModelForm): class VLANFromCSVForm(forms.ModelForm):
@ -663,7 +658,7 @@ class VLANFromCSVForm(forms.ModelForm):
group_name = self.cleaned_data.get('group_name') group_name = self.cleaned_data.get('group_name')
if group_name: if group_name:
try: try:
vlan_group = VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name) VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
except VLANGroup.DoesNotExist: except VLANGroup.DoesNotExist:
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name)) self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))

View File

@ -2,15 +2,12 @@ from django_tables2 import RequestConfig
import netaddr import netaddr
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib import messages
from django.db.models import Count, Q from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from dcim.models import Device from dcim.models import Device
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator from utilities.paginator import EnhancedPaginator
from utilities.views import ( from utilities.views import (
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,

View File

@ -331,6 +331,25 @@ class FlexibleModelChoiceField(forms.ModelChoiceField):
return value return value
class ChainedModelChoiceField(forms.ModelChoiceField):
"""
A ModelChoiceField which is initialized based on the values of other fields within a form. `chains` is a dictionary
mapping of model fields to peer fields within the form. For example:
country1 = forms.ModelChoiceField(queryset=Country.objects.all())
city1 = ChainedModelChoiceField(queryset=City.objects.all(), chains={'country': 'country1'}
The queryset of the `city1` field will be modified as
.filter(country=<value>)
where <value> is the value of the `country1` field. (Note: The form must inherit from ChainedFieldsMixin.)
"""
def __init__(self, chains=None, *args, **kwargs):
self.chains = chains
super(ChainedModelChoiceField, self).__init__(*args, **kwargs)
class SlugField(forms.SlugField): class SlugField(forms.SlugField):
def __init__(self, slug_source='name', *args, **kwargs): def __init__(self, slug_source='name', *args, **kwargs):
@ -411,6 +430,32 @@ class BootstrapMixin(forms.BaseForm):
field.widget.attrs['placeholder'] = field.label field.widget.attrs['placeholder'] = field.label
class ChainedFieldsMixin(forms.BaseForm):
"""
Iterate through all ChainedModelChoiceFields in the form and modify their querysets based on chained fields.
"""
def __init__(self, *args, **kwargs):
super(ChainedFieldsMixin, self).__init__(*args, **kwargs)
for field_name, field in self.fields.items():
if isinstance(field, ChainedModelChoiceField):
filters_dict = {}
for db_field, parent_field in field.chains.items():
if self.is_bound and self.data.get(parent_field):
filters_dict[db_field] = self.data.get(parent_field)
elif self.initial.get(parent_field):
filters_dict[db_field] = self.initial[parent_field]
else:
filters_dict[db_field] = None
if filters_dict:
field.queryset = field.queryset.filter(**filters_dict)
else:
field.queryset = field.queryset.none()
class ReturnURLForm(forms.Form): class ReturnURLForm(forms.Form):
""" """
Provides a hidden return URL field to control where the user is directed after the form is submitted. Provides a hidden return URL field to control where the user is directed after the form is submitted.