diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b12d273a9..29d5ba551 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -22,9 +22,9 @@ from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, - BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ConfirmationForm, - CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField, SelectWithPK, - SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, + BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, + ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField, SelectWithPK, SmallTextarea, SlugField, + StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup, VirtualMachine from .choices import * @@ -472,11 +472,8 @@ class RackRoleCSVForm(forms.ModelForm): # class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): - group = ChainedModelChoiceField( + group = DynamicModelChoiceField( queryset=RackGroup.objects.all(), - chains=( - ('site', 'site'), - ), required=False, widget=APISelect( api_url='/api/dcim/rack-groups/', @@ -761,13 +758,9 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): class RackElevationFilterForm(RackFilterForm): field_order = ['q', 'region', 'site', 'group_id', 'id', 'status', 'role', 'tenant_group', 'tenant'] - id = ChainedModelChoiceField( + id = FilterChoiceField( queryset=Rack.objects.all(), label='Rack', - chains=( - ('site', 'site'), - ('group_id', 'group_id'), - ), required=False, widget=APISelectMultiple( api_url='/api/dcim/racks/', @@ -1706,11 +1699,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } ) ) - rack = ChainedModelChoiceField( + rack = DynamicModelChoiceField( queryset=Rack.objects.all(), - chains=( - ('site', 'site'), - ), required=False, widget=APISelect( api_url='/api/dcim/racks/', @@ -1737,11 +1727,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } ) ) - device_type = ChainedModelChoiceField( + device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all(), - chains=( - ('manufacturer', 'manufacturer'), - ), label='Device type', widget=APISelect( api_url='/api/dcim/device-types/', @@ -1761,11 +1748,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } ) ) - cluster = ChainedModelChoiceField( + cluster = DynamicModelChoiceField( queryset=Cluster.objects.all(), - chains=( - ('group', 'cluster_group'), - ), required=False, widget=APISelect( api_url='/api/virtualization/clusters/', @@ -3433,7 +3417,7 @@ class RearPortBulkDisconnectForm(ConfirmationForm): # Cables # -class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): +class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm): """ Base form for connecting a Cable to a Device component """ @@ -3449,11 +3433,8 @@ class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFo } ) ) - termination_b_rack = ChainedModelChoiceField( + termination_b_rack = DynamicModelChoiceField( queryset=Rack.objects.all(), - chains=( - ('site', 'termination_b_site'), - ), label='Rack', required=False, widget=APISelect( @@ -3466,12 +3447,8 @@ class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFo } ) ) - termination_b_device = ChainedModelChoiceField( + termination_b_device = DynamicModelChoiceField( queryset=Device.objects.all(), - chains=( - ('site', 'termination_b_site'), - ('rack', 'termination_b_rack'), - ), label='Device', required=False, widget=APISelect( @@ -3569,7 +3546,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm): ) -class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): +class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm): termination_b_provider = forms.ModelChoiceField( queryset=Provider.objects.all(), label='Provider', @@ -3581,7 +3558,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, f } ) ) - termination_b_site = forms.ModelChoiceField( + termination_b_site = DynamicModelChoiceField( queryset=Site.objects.all(), label='Site', required=False, @@ -3592,11 +3569,8 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, f } ) ) - termination_b_circuit = ChainedModelChoiceField( + termination_b_circuit = DynamicModelChoiceField( queryset=Circuit.objects.all(), - chains=( - ('provider', 'termination_b_provider'), - ), label='Circuit', widget=APISelect( api_url='/api/circuits/circuits/', @@ -3623,7 +3597,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, f ] -class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): +class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm): termination_b_site = forms.ModelChoiceField( queryset=Site.objects.all(), label='Site', @@ -3637,12 +3611,9 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.Mode } ) ) - termination_b_rackgroup = ChainedModelChoiceField( + termination_b_rackgroup = DynamicModelChoiceField( queryset=RackGroup.objects.all(), label='Rack Group', - chains=( - ('site', 'termination_b_site'), - ), required=False, widget=APISelect( api_url='/api/dcim/rack-groups/', @@ -3652,12 +3623,8 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.Mode } ) ) - termination_b_powerpanel = ChainedModelChoiceField( + termination_b_powerpanel = DynamicModelChoiceField( queryset=PowerPanel.objects.all(), - chains=( - ('site', 'termination_b_site'), - ('rack_group', 'termination_b_rackgroup'), - ), label='Power Panel', required=False, widget=APISelect( @@ -4380,10 +4347,9 @@ class DeviceVCMembershipForm(forms.ModelForm): return vc_position -class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): +class VCMemberSelectForm(BootstrapMixin, forms.Form): site = forms.ModelChoiceField( queryset=Site.objects.all(), - label='Site', required=False, widget=APISelect( api_url="/api/dcim/sites/", @@ -4393,12 +4359,8 @@ class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): } ) ) - rack = ChainedModelChoiceField( + rack = DynamicModelChoiceField( queryset=Rack.objects.all(), - chains=( - ('site', 'site'), - ), - label='Rack', required=False, widget=APISelect( api_url='/api/dcim/racks/', @@ -4410,15 +4372,10 @@ class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): } ) ) - device = ChainedModelChoiceField( + device = DynamicModelChoiceField( queryset=Device.objects.filter( virtual_chassis__isnull=True ), - chains=( - ('site', 'site'), - ('rack', 'rack'), - ), - label='Device', widget=APISelect( api_url='/api/dcim/devices/', display_field='display_name', @@ -4490,11 +4447,8 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): # class PowerPanelForm(BootstrapMixin, forms.ModelForm): - rack_group = ChainedModelChoiceField( + rack_group = DynamicModelChoiceField( queryset=RackGroup.objects.all(), - chains=( - ('site', 'site'), - ), required=False, widget=APISelect( api_url='/api/dcim/rack-groups/', @@ -4595,7 +4549,7 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): # class PowerFeedForm(BootstrapMixin, CustomFieldModelForm): - site = ChainedModelChoiceField( + site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, widget=APISelect( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 71aa73d18..059587082 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -10,9 +10,9 @@ from extras.forms import ( from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( - add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, - CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, - SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES + add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, + DatePicker, DynamicModelChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, + ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import VirtualMachine from .constants import * @@ -271,7 +271,6 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, - label='Site', widget=APISelect( api_url="/api/dcim/sites/", filter_for={ @@ -283,11 +282,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } ) ) - vlan_group = ChainedModelChoiceField( + vlan_group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), - chains=( - ('site', 'site'), - ), required=False, label='VLAN group', widget=APISelect( @@ -300,12 +296,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } ) ) - vlan = ChainedModelChoiceField( + vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), - chains=( - ('site', 'site'), - ('group', 'vlan_group'), - ), required=False, label='VLAN', widget=APISelect( @@ -603,11 +595,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel } ) ) - nat_rack = ChainedModelChoiceField( + nat_rack = DynamicModelChoiceField( queryset=Rack.objects.all(), - chains=( - ('site', 'nat_site'), - ), required=False, label='Rack', widget=APISelect( @@ -621,12 +610,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel } ) ) - nat_device = ChainedModelChoiceField( + nat_device = DynamicModelChoiceField( queryset=Device.objects.all(), - chains=( - ('site', 'nat_site'), - ('rack', 'nat_rack'), - ), required=False, label='Device', widget=APISelect( @@ -648,11 +633,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel } ) ) - nat_inside = ChainedModelChoiceField( + nat_inside = DynamicModelChoiceField( queryset=IPAddress.objects.all(), - chains=( - ('interface__device', 'nat_device'), - ), required=False, label='IP Address', widget=APISelect( @@ -1102,13 +1084,9 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } ) ) - group = ChainedModelChoiceField( + group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), - chains=( - ('site', 'site'), - ), required=False, - label='Group', widget=APISelect( api_url='/api/ipam/vlan-groups/', ) diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 4babd753f..553e79f1b 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -2,11 +2,11 @@ from django import forms from taggit.forms import TagField from extras.forms import ( - AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, + AddRemoveTagsForm, CustomFieldModelForm, CustomFieldBulkEditForm, CustomFieldFilterForm, ) from utilities.forms import ( - APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, - FilterChoiceField, SlugField, TagFilterField + APISelect, APISelectMultiple, BootstrapMixin, CommentField, DynamicModelChoiceField, FilterChoiceField, SlugField, + TagFilterField, ) from .models import Tenant, TenantGroup @@ -121,8 +121,8 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): # Form extensions # -class TenancyForm(ChainedFieldsMixin, forms.Form): - tenant_group = forms.ModelChoiceField( +class TenancyForm(forms.Form): + tenant_group = DynamicModelChoiceField( queryset=TenantGroup.objects.all(), required=False, widget=APISelect( @@ -135,11 +135,8 @@ class TenancyForm(ChainedFieldsMixin, forms.Form): } ) ) - tenant = ChainedModelChoiceField( + tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), - chains=( - ('group', 'tenant_group'), - ), required=False, widget=APISelect( api_url='/api/tenancy/tenants/' diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 8c0f0d8d1..49d7e882d 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -522,34 +522,6 @@ class FlexibleModelChoiceField(forms.ModelChoiceField): 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=) - - where 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().__init__(*args, **kwargs) - - -class ChainedModelMultipleChoiceField(forms.ModelMultipleChoiceField): - """ - See ChainedModelChoiceField - """ - def __init__(self, chains=None, *args, **kwargs): - self.chains = chains - super().__init__(*args, **kwargs) - - class SlugField(forms.SlugField): """ Extend the built-in SlugField to automatically populate from a field called `name` unless otherwise specified. @@ -578,16 +550,12 @@ class TagFilterField(forms.MultipleChoiceField): super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs) -class FilterChoiceField(forms.ModelMultipleChoiceField): +class DynamicModelChoiceField(forms.ModelChoiceField): """ Override get_bound_field() to avoid pre-populating field choices with a SQL query. The field will be rendered only with choices set via bound data. Choices are populated on-demand via the APISelect widget. """ - def __init__(self, *args, **kwargs): - # Filter fields are not required by default - if 'required' not in kwargs: - kwargs['required'] = False - super().__init__(*args, **kwargs) + field_modifier = '' def get_bound_field(self, form, field_name): bound_field = BoundField(form, self, field_name) @@ -595,7 +563,8 @@ class FilterChoiceField(forms.ModelMultipleChoiceField): # Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options # will be populated on-demand via the APISelect widget. if bound_field.data: - kwargs = {'{}__in'.format(self.to_field_name or 'pk'): bound_field.data} + field_name = '{}{}'.format(self.to_field_name or 'pk', self.field_modifier) + kwargs = {field_name: bound_field.data} self.queryset = self.queryset.filter(**kwargs) else: self.queryset = self.queryset.none() @@ -603,6 +572,24 @@ class FilterChoiceField(forms.ModelMultipleChoiceField): return bound_field +class DynamicModelMultipleChoiceField(DynamicModelChoiceField): + """ + A multiple-choice version of DynamicModelChoiceField. + """ + field_modifier = '__in' + + +class FilterChoiceField(DynamicModelMultipleChoiceField): + """ + A version of DynamicModelMultipleChoiceField which defaults to required=False. + """ + def __init__(self, *args, **kwargs): + # Filter fields are not required by default + if 'required' not in kwargs: + kwargs['required'] = False + super().__init__(*args, **kwargs) + + class LaxURLField(forms.URLField): """ Modifies Django's built-in URLField in two ways: @@ -655,46 +642,6 @@ class BootstrapMixin(forms.BaseForm): 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().__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: - if self.is_bound and parent_field in self.data and self.data[parent_field]: - filters_dict[db_field] = self.data[parent_field] or None - elif self.initial.get(parent_field): - filters_dict[db_field] = self.initial[parent_field] - elif self.fields[parent_field].widget.attrs.get('nullable'): - filters_dict[db_field] = None - else: - break - - # Limit field queryset by chained field values - if filters_dict: - field.queryset = field.queryset.filter(**filters_dict) - # Editing an existing instance; limit field to its current value - elif not self.is_bound and getattr(self, 'instance', None) and hasattr(self.instance, field_name): - obj = getattr(self.instance, field_name) - if obj is not None: - field.queryset = field.queryset.filter(pk=obj.pk) - else: - field.queryset = field.queryset.none() - # Creating a new instance with no bound data; nullify queryset - elif not self.data.get(field_name): - field.queryset = field.queryset.none() - # Creating a new instance with bound data; limit queryset to the specified value - else: - field.queryset = field.queryset.filter(pk=self.data.get(field_name)) - - class ReturnURLForm(forms.Form): """ Provides a hidden return URL field to control where the user is directed after the form is submitted. diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 96136b4de..8c66f1c23 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -14,9 +14,9 @@ from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, - ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ConfirmationForm, - CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, SmallTextarea, StaticSelect2, - StaticSelect2Multiple, TagFilterField, + CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + ExpandableNameField, FilterChoiceField, JSONField, SlugField, SmallTextarea, StaticSelect2, StaticSelect2Multiple, + TagFilterField, ) from .choices import * from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -233,7 +233,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm tag = TagFilterField(model) -class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): +class ClusterAddDevicesForm(BootstrapMixin, forms.Form): region = forms.ModelChoiceField( queryset=Region.objects.all(), required=False, @@ -247,11 +247,8 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): } ) ) - site = ChainedModelChoiceField( + site = DynamicModelChoiceField( queryset=Site.objects.all(), - chains=( - ('region', 'region'), - ), required=False, widget=APISelect( api_url='/api/dcim/sites/', @@ -261,11 +258,8 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): } ) ) - rack = ChainedModelChoiceField( + rack = DynamicModelChoiceField( queryset=Rack.objects.all(), - chains=( - ('site', 'site'), - ), required=False, widget=APISelect( api_url='/api/dcim/racks/', @@ -277,12 +271,8 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): } ) ) - devices = ChainedModelMultipleChoiceField( + devices = DynamicModelMultipleChoiceField( queryset=Device.objects.filter(cluster__isnull=True), - chains=( - ('site', 'site'), - ('rack', 'rack'), - ), widget=APISelectMultiple( api_url='/api/dcim/devices/', display_field='display_name', @@ -342,11 +332,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } ) ) - cluster = ChainedModelChoiceField( + cluster = DynamicModelChoiceField( queryset=Cluster.objects.all(), - chains=( - ('group', 'cluster_group'), - ), widget=APISelect( api_url='/api/virtualization/clusters/' )