From 2c64d45c697455760b9b9c136e7a8b1791ecc140 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 11 Aug 2020 16:23:36 -0400 Subject: [PATCH 01/20] Drop support for conditional_query_params on APISelect --- netbox/project-static/js/forms.js | 14 -------------- netbox/utilities/forms/widgets.py | 19 ------------------- 2 files changed, 33 deletions(-) diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 26c8338c2..9fed76f90 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -178,20 +178,6 @@ $(document).ready(function() { } }); - // Conditional query params - $.each(element.attributes, function(index, attr){ - if (attr.name.includes("data-conditional-query-param-")){ - var conditional = attr.name.split("data-conditional-query-param-")[1].split("__"); - var field = $("#id_" + conditional[0]); - var field_value = conditional[1]; - - if ($('option:selected', field).attr('api-value') === field_value){ - var _val = attr.value.split("="); - parameters[_val[0]] = _val[1]; - } - } - }); - // Additional query params $.each(element.attributes, function(index, attr){ if (attr.name.includes("data-additional-query-param-")){ diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index 1c9d654f3..80ff12ac3 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -145,11 +145,6 @@ class APISelect(SelectWithDisabled): :param disabled_indicator: (Optional) Mark option as disabled if this field equates true. :param filter_for: (Optional) A dict of chained form fields for which this field is a filter. The key is the name of the filter-for field (child field) and the value is the name of the query param filter. - :param conditional_query_params: (Optional) A dict of URL query params to append to the URL if the - condition is met. The condition is the dict key and is specified in the form `__`. - If the provided field value is selected for the given field, the URL query param will be appended to - the rendered URL. The value is the in the from `=`. This is useful in cases where - a particular field value dictates an additional API filter. :param additional_query_params: Optional) A dict of query params to append to the API request. The key is the name of the query param and the value if the query param's value. :param null_option: If true, include the static null option in the selection list. @@ -161,7 +156,6 @@ class APISelect(SelectWithDisabled): value_field=None, disabled_indicator=None, filter_for=None, - conditional_query_params=None, additional_query_params=None, null_option=False, full=False, @@ -185,9 +179,6 @@ class APISelect(SelectWithDisabled): if filter_for: for key, value in filter_for.items(): self.add_filter_for(key, value) - if conditional_query_params: - for key, value in conditional_query_params.items(): - self.add_conditional_query_param(key, value) if additional_query_params: for key, value in additional_query_params.items(): self.add_additional_query_param(key, value) @@ -217,16 +208,6 @@ class APISelect(SelectWithDisabled): self.attrs[key] = json.dumps(values) - def add_conditional_query_param(self, condition, value): - """ - Add details for a URL query strings to append to the URL if the condition is met. - The condition is specified in the form `__`. - - :param condition: The condition for the query param - :param value: The value of the query param - """ - self.attrs['data-conditional-query-param-{}'.format(condition)] = value - class APISelectMultiple(APISelect, forms.SelectMultiple): From fdc43f8279c720ce6118402dea66ae9e7b8b9740 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 11 Aug 2020 17:00:28 -0400 Subject: [PATCH 02/20] Add display_field kwarg to DynamicModelChoiceMixin --- netbox/dcim/forms.py | 46 ++++++++++++-------------------- netbox/extras/forms.py | 2 +- netbox/ipam/forms.py | 12 +++------ netbox/utilities/forms/fields.py | 11 ++++++++ netbox/virtualization/forms.py | 14 +++++----- 5 files changed, 40 insertions(+), 45 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index febeeaa12..49fa1173d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -716,9 +716,7 @@ class RackElevationFilterForm(RackFilterForm): queryset=Rack.objects.all(), label='Rack', required=False, - widget=APISelectMultiple( - display_field='display_name', - ) + display_field='display_name' ) def __init__(self, *args, **kwargs): @@ -1048,9 +1046,7 @@ class ComponentTemplateCreateForm(ComponentForm): ) device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all(), - widget=APISelect( - display_field='model' - ) + display_field='model' ) description = forms.CharField( required=False @@ -1739,9 +1735,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, - widget=APISelect( - display_field='display_name' - ) + display_field='display_name' ) position = forms.TypedChoiceField( required=False, @@ -1764,9 +1758,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): ) device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all(), - widget=APISelect( - display_field='model' - ) + display_field='model' ) device_role = DynamicModelChoiceField( queryset=DeviceRole.objects.all() @@ -2078,9 +2070,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all(), required=False, - widget=APISelect( - display_field="model", - ) + display_field='model' ) device_role = DynamicModelChoiceField( queryset=DeviceRole.objects.all(), @@ -2184,9 +2174,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt queryset=DeviceType.objects.all(), required=False, label='Model', - widget=APISelectMultiple( - display_field="model", - ) + display_field='model' ) platform = DynamicModelMultipleChoiceField( queryset=Platform.objects.all(), @@ -2715,8 +2703,8 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): queryset=VLAN.objects.all(), required=False, label='Untagged VLAN', + display_field='display_name', widget=APISelect( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', @@ -2727,8 +2715,8 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): queryset=VLAN.objects.all(), required=False, label='Tagged VLANs', + display_field='display_name', widget=APISelectMultiple( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', @@ -2816,8 +2804,8 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, + display_field='display_name', widget=APISelect( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', @@ -2827,8 +2815,8 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, + display_field='display_name', widget=APISelectMultiple( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', @@ -2885,8 +2873,8 @@ class InterfaceBulkEditForm( untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, + display_field='display_name', widget=APISelect( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', @@ -2896,8 +2884,8 @@ class InterfaceBulkEditForm( tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, + display_field='display_name', widget=APISelectMultiple( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', @@ -3514,8 +3502,8 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm): queryset=Device.objects.all(), label='Device', required=False, + display_field='display_name', widget=APISelect( - display_field='display_name', filter_for={ 'termination_b_id': 'device_id', } @@ -3632,8 +3620,8 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm): termination_b_circuit = DynamicModelChoiceField( queryset=Circuit.objects.all(), label='Circuit', + display_field='cid', widget=APISelect( - display_field='cid', filter_for={ 'termination_b_id': 'circuit_id', } @@ -3662,8 +3650,8 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm): queryset=Site.objects.all(), label='Site', required=False, + display_field='cid', widget=APISelect( - display_field='cid', filter_for={ 'termination_b_rackgroup': 'site_id', 'termination_b_powerpanel': 'site_id', @@ -3674,8 +3662,8 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm): queryset=RackGroup.objects.all(), label='Rack Group', required=False, + display_field='cid', widget=APISelect( - display_field='cid', filter_for={ 'termination_b_powerpanel': 'rackgroup_id', } @@ -4199,8 +4187,8 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form): queryset=Device.objects.filter( virtual_chassis__isnull=True ), + display_field='display_name', widget=APISelect( - display_field='display_name', disabled_indicator='virtual_chassis' ) ) diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index a565375c5..30279e012 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -413,9 +413,9 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form): user = DynamicModelMultipleChoiceField( queryset=User.objects.all(), required=False, + display_field='username', widget=APISelectMultiple( api_url='/api/users/users/', - display_field='username' ) ) changed_object_type = forms.ModelChoiceField( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 15c69c504..aad3eaade 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -287,9 +287,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): queryset=VLAN.objects.all(), required=False, label='VLAN', - widget=APISelect( - display_field='display_name' - ) + display_field='display_name' ) role = DynamicModelChoiceField( queryset=Role.objects.all(), @@ -569,8 +567,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel queryset=Rack.objects.all(), required=False, label='Rack', + display_field='display_name', widget=APISelect( - display_field='display_name', filter_for={ 'nat_device': 'rack_id' }, @@ -583,8 +581,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel queryset=Device.objects.all(), required=False, label='Device', + display_field='display_name', widget=APISelect( - display_field='display_name', filter_for={ 'nat_inside': 'device_id' } @@ -604,9 +602,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel queryset=IPAddress.objects.all(), required=False, label='IP Address', - widget=APISelect( - display_field='address' - ) + display_field='address' ) primary_for_parent = forms.BooleanField( required=False, diff --git a/netbox/utilities/forms/fields.py b/netbox/utilities/forms/fields.py index b4948a419..fbcc285a9 100644 --- a/netbox/utilities/forms/fields.py +++ b/netbox/utilities/forms/fields.py @@ -11,6 +11,7 @@ from django.db.models import Count from django.forms import BoundField from django.urls import reverse +from utilities.api import get_serializer_for_model from utilities.choices import unpack_grouped_choices from utilities.validators import EnhancedURLValidator from . import widgets @@ -247,6 +248,16 @@ class DynamicModelChoiceMixin: filter = django_filters.ModelChoiceFilter widget = widgets.APISelect + def __init__(self, *args, display_field='name', **kwargs): + self.display_field = display_field + + super().__init__(*args, **kwargs) + + def widget_attrs(self, widget): + return { + 'display-field': self.display_field + } + def get_bound_field(self, form, field_name): bound_field = BoundField(form, self, field_name) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index d32b08ffa..b1a2de6aa 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -240,8 +240,8 @@ class ClusterAddDevicesForm(BootstrapMixin, forms.Form): ) devices = DynamicModelMultipleChoiceField( queryset=Device.objects.filter(cluster__isnull=True), + display_field='display_name', widget=APISelectMultiple( - display_field='display_name', disabled_indicator='cluster' ) ) @@ -575,8 +575,8 @@ class VMInterfaceForm(BootstrapMixin, forms.ModelForm): untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, + display_field='display_name', widget=APISelect( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', @@ -586,8 +586,8 @@ class VMInterfaceForm(BootstrapMixin, forms.ModelForm): tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, + display_field='display_name', widget=APISelectMultiple( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', @@ -679,8 +679,8 @@ class VMInterfaceCreateForm(BootstrapMixin, forms.Form): untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, + display_field='display_name', widget=APISelect( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', @@ -690,8 +690,8 @@ class VMInterfaceCreateForm(BootstrapMixin, forms.Form): tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, + display_field='display_name', widget=APISelectMultiple( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', @@ -773,8 +773,8 @@ class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm): untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, + display_field='display_name', widget=APISelect( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', @@ -784,8 +784,8 @@ class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm): tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, + display_field='display_name', widget=APISelectMultiple( - display_field='display_name', full=True, additional_query_params={ 'site_id': 'null', From a831e525da52922f26ab311d8ac10c67a8a56b55 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 11 Aug 2020 17:19:40 -0400 Subject: [PATCH 03/20] Remove value_field from APISelect; reference to_field_name on field --- netbox/circuits/forms.py | 22 +++---------- netbox/dcim/forms.py | 54 ++++--------------------------- netbox/extras/forms.py | 40 +++++------------------ netbox/ipam/forms.py | 13 +------- netbox/secrets/forms.py | 5 +-- netbox/tenancy/forms.py | 3 -- netbox/utilities/forms/fields.py | 11 +++++-- netbox/utilities/forms/widgets.py | 4 --- netbox/virtualization/forms.py | 14 +------- 9 files changed, 30 insertions(+), 136 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 341a7a9b7..70a220b58 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -108,7 +108,6 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region' } @@ -117,10 +116,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) asn = forms.IntegerField( required=False, @@ -271,18 +267,12 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm type = DynamicModelMultipleChoiceField( queryset=CircuitType.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) provider = DynamicModelMultipleChoiceField( queryset=Provider.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) status = forms.MultipleChoiceField( choices=CircuitStatusChoices, @@ -294,7 +284,6 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region' } @@ -303,10 +292,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) commit_rate = forms.IntegerField( required=False, diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 49fa1173d..7700545df 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -70,7 +70,6 @@ class DeviceComponentFilterForm(BootstrapMixin, forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field='slug', filter_for={ 'site': 'region' } @@ -81,7 +80,6 @@ class DeviceComponentFilterForm(BootstrapMixin, forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'device_id': 'site', } @@ -348,10 +346,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) tag = TagFilterField(model) @@ -409,7 +404,6 @@ class RackGroupFilterForm(BootstrapMixin, forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region', 'parent': 'region', @@ -421,7 +415,6 @@ class RackGroupFilterForm(BootstrapMixin, forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'parent': 'site', } @@ -430,11 +423,7 @@ class RackGroupFilterForm(BootstrapMixin, forms.Form): parent = DynamicModelMultipleChoiceField( queryset=RackGroup.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - api_url="/api/dcim/rack-groups/", - value_field="slug", - ) + required=False ) @@ -662,7 +651,6 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region' } @@ -673,7 +661,6 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'group_id': 'site' } @@ -699,7 +686,6 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) @@ -861,10 +847,7 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm): site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) group_id = DynamicModelMultipleChoiceField( queryset=RackGroup.objects.prefetch_related('site'), @@ -972,10 +955,7 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): manufacturer = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) subdevice_role = forms.MultipleChoiceField( choices=add_blank_choice(SubdeviceRoleChoices), @@ -2116,7 +2096,6 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region' } @@ -2127,7 +2106,6 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'rack_group_id': 'site', 'rack_id': 'site', @@ -2157,7 +2135,6 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", ) ) manufacturer_id = DynamicModelMultipleChoiceField( @@ -2181,7 +2158,6 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) @@ -3446,10 +3422,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): manufacturer = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), to_field_name='slug', - required=False, - widget=APISelect( - value_field="slug", - ) + required=False ) serial = forms.CharField( required=False @@ -3900,7 +3873,6 @@ class CableFilterForm(BootstrapMixin, forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'rack_id': 'site', 'device_id': 'site', @@ -3912,7 +3884,6 @@ class CableFilterForm(BootstrapMixin, forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field='slug', filter_for={ 'device_id': 'tenant', } @@ -3962,7 +3933,6 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'device_id': 'site', } @@ -3981,7 +3951,6 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'device_id': 'site', } @@ -4000,7 +3969,6 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'device_id': 'site', } @@ -4240,7 +4208,6 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region' } @@ -4249,17 +4216,13 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) tenant_group = DynamicModelMultipleChoiceField( queryset=TenantGroup.objects.all(), to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, filter_for={ 'tenant': 'group' @@ -4271,7 +4234,6 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) @@ -4369,7 +4331,6 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region' } @@ -4380,7 +4341,6 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'rack_group_id': 'site', } @@ -4589,7 +4549,6 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region' } @@ -4600,7 +4559,6 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'power_panel_id': 'site', 'rack_id': 'site', diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 30279e012..cecbfae72 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -290,42 +290,27 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) role = DynamicModelMultipleChoiceField( queryset=DeviceRole.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) platform = DynamicModelMultipleChoiceField( queryset=Platform.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) cluster_group = DynamicModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) cluster_id = DynamicModelMultipleChoiceField( queryset=Cluster.objects.all(), @@ -335,26 +320,17 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form): tenant_group = DynamicModelMultipleChoiceField( queryset=TenantGroup.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) tenant = DynamicModelMultipleChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) tag = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index aad3eaade..393903d37 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -217,10 +217,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=RIR.objects.all(), to_field_name='slug', required=False, - label='RIR', - widget=APISelectMultiple( - value_field="slug", - ) + label='RIR' ) tag = TagFilterField(model) @@ -481,7 +478,6 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region' } @@ -492,7 +488,6 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) @@ -501,7 +496,6 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) @@ -978,7 +972,6 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region', } @@ -989,7 +982,6 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) @@ -1145,7 +1137,6 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region', 'group_id': 'region' @@ -1157,7 +1148,6 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) @@ -1179,7 +1169,6 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 54178f46b..d0ef5ba3a 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -178,10 +178,7 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm): role = DynamicModelMultipleChoiceField( queryset=SecretRole.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field="slug", - ) + required=False ) tag = TagFilterField(model) diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 04a3980a1..436f37d3f 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -107,7 +107,6 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) @@ -154,7 +153,6 @@ class TenancyFilterForm(forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, filter_for={ 'tenant': 'group' @@ -166,7 +164,6 @@ class TenancyFilterForm(forms.Form): to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) diff --git a/netbox/utilities/forms/fields.py b/netbox/utilities/forms/fields.py index fbcc285a9..cfd025a4e 100644 --- a/netbox/utilities/forms/fields.py +++ b/netbox/utilities/forms/fields.py @@ -251,12 +251,19 @@ class DynamicModelChoiceMixin: def __init__(self, *args, display_field='name', **kwargs): self.display_field = display_field + # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference + # by widget_attrs() + self.to_field_name = kwargs.get('to_field_name') + super().__init__(*args, **kwargs) def widget_attrs(self, widget): - return { - 'display-field': self.display_field + attrs = { + 'display-field': self.display_field, } + if self.to_field_name: + attrs['value-field'] = self.to_field_name + return attrs def get_bound_field(self, form, field_name): bound_field = BoundField(form, self, field_name) diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index 80ff12ac3..0e03931f2 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -141,7 +141,6 @@ class APISelect(SelectWithDisabled): :param api_url: API endpoint URL. Required if not set automatically by the parent field. :param display_field: (Optional) Field to display for child in selection list. Defaults to `name`. - :param value_field: (Optional) Field to use for the option value in selection list. Defaults to `id`. :param disabled_indicator: (Optional) Mark option as disabled if this field equates true. :param filter_for: (Optional) A dict of chained form fields for which this field is a filter. The key is the name of the filter-for field (child field) and the value is the name of the query param filter. @@ -153,7 +152,6 @@ class APISelect(SelectWithDisabled): self, api_url=None, display_field=None, - value_field=None, disabled_indicator=None, filter_for=None, additional_query_params=None, @@ -172,8 +170,6 @@ class APISelect(SelectWithDisabled): self.attrs['data-full'] = full if display_field: self.attrs['display-field'] = display_field - if value_field: - self.attrs['value-field'] = value_field if disabled_indicator: self.attrs['disabled-indicator'] = disabled_indicator if filter_for: diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index b1a2de6aa..b53c2b151 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -166,17 +166,13 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm type = DynamicModelMultipleChoiceField( queryset=ClusterType.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - value_field='slug', - ) + required=False ) region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region' } @@ -187,7 +183,6 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm to_field_name='slug', required=False, widget=APISelectMultiple( - value_field='slug', null_option=True, ) ) @@ -196,7 +191,6 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm to_field_name='slug', required=False, widget=APISelectMultiple( - value_field='slug', null_option=True, ) ) @@ -496,7 +490,6 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) @@ -505,7 +498,6 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) @@ -519,7 +511,6 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", filter_for={ 'site': 'region' } @@ -530,7 +521,6 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) @@ -539,7 +529,6 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, additional_query_params={ 'vm_role': "True" @@ -556,7 +545,6 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil to_field_name='slug', required=False, widget=APISelectMultiple( - value_field="slug", null_option=True, ) ) From f3fb85933f40fa65f3fafdb5d64310482e5d76ab Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 12 Aug 2020 09:28:24 -0400 Subject: [PATCH 04/20] Add query_params attribute to DynamicModelChoiceMixin --- netbox/dcim/forms.py | 68 ++++++++++--------------- netbox/utilities/forms/fields.py | 10 +++- netbox/virtualization/forms.py | 86 +++++++++++++------------------- 3 files changed, 70 insertions(+), 94 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 7700545df..cf79458a6 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1746,11 +1746,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): platform = DynamicModelChoiceField( queryset=Platform.objects.all(), required=False, - widget=APISelect( - additional_query_params={ - "manufacturer_id": "null" - } - ) + query_params={ + "manufacturer_id": "null" + } ) cluster_group = DynamicModelChoiceField( queryset=ClusterGroup.objects.all(), @@ -2680,24 +2678,20 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): required=False, label='Untagged VLAN', display_field='display_name', - widget=APISelect( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelect(full=True) ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, label='Tagged VLANs', display_field='display_name', - widget=APISelectMultiple( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelectMultiple(full=True) ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -2781,23 +2775,19 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): queryset=VLAN.objects.all(), required=False, display_field='display_name', - widget=APISelect( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelect(full=True) ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, display_field='display_name', - widget=APISelectMultiple( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelectMultiple(full=True) ) field_order = ( 'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'description', @@ -2850,23 +2840,19 @@ class InterfaceBulkEditForm( queryset=VLAN.objects.all(), required=False, display_field='display_name', - widget=APISelect( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelect(full=True) ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, display_field='display_name', - widget=APISelectMultiple( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelectMultiple(full=True) ) class Meta: diff --git a/netbox/utilities/forms/fields.py b/netbox/utilities/forms/fields.py index cfd025a4e..065086c4e 100644 --- a/netbox/utilities/forms/fields.py +++ b/netbox/utilities/forms/fields.py @@ -248,8 +248,9 @@ class DynamicModelChoiceMixin: filter = django_filters.ModelChoiceFilter widget = widgets.APISelect - def __init__(self, *args, display_field='name', **kwargs): + def __init__(self, *args, display_field='name', query_params=None, **kwargs): self.display_field = display_field + self.query_params = query_params or {} # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference # by widget_attrs() @@ -261,8 +262,15 @@ class DynamicModelChoiceMixin: attrs = { 'display-field': self.display_field, } + + # Set value-field attribute if the field specifies to_field_name if self.to_field_name: attrs['value-field'] = self.to_field_name + + # Attach any static query parameters + for key, value in self.query_params.items(): + widget.add_additional_query_param(key, value) + return attrs def get_bound_field(self, form, field_name): diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index b53c2b151..f33d4645a 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -297,11 +297,9 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): role = DynamicModelChoiceField( queryset=DeviceRole.objects.all(), required=False, - widget=APISelect( - additional_query_params={ - "vm_role": "True" - } - ) + query_params={ + "vm_role": "True" + } ) platform = DynamicModelChoiceField( queryset=Platform.objects.all(), @@ -438,11 +436,9 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB vm_role=True ), required=False, - widget=APISelect( - additional_query_params={ - "vm_role": "True" - } - ) + query_params={ + "vm_role": "True" + } ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), @@ -528,12 +524,10 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil queryset=DeviceRole.objects.filter(vm_role=True), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - additional_query_params={ - 'vm_role': "True" - } - ) + query_params={ + 'vm_role': "True" + }, + widget=APISelectMultiple(null_option=True) ) status = forms.MultipleChoiceField( choices=VirtualMachineStatusChoices, @@ -564,23 +558,19 @@ class VMInterfaceForm(BootstrapMixin, forms.ModelForm): queryset=VLAN.objects.all(), required=False, display_field='display_name', - widget=APISelect( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelect(full=True) ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, display_field='display_name', - widget=APISelectMultiple( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelectMultiple(full=True) ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -668,23 +658,19 @@ class VMInterfaceCreateForm(BootstrapMixin, forms.Form): queryset=VLAN.objects.all(), required=False, display_field='display_name', - widget=APISelect( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelect(full=True) ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, display_field='display_name', - widget=APISelectMultiple( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelectMultiple(full=True) ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -762,23 +748,19 @@ class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm): queryset=VLAN.objects.all(), required=False, display_field='display_name', - widget=APISelect( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelect(full=True) ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, display_field='display_name', - widget=APISelectMultiple( - full=True, - additional_query_params={ - 'site_id': 'null', - }, - ) + query_params={ + 'site_id': 'null', + }, + widget=APISelectMultiple(full=True) ) class Meta: From 8a8b4e728a2cf1b074b600590d15100e2fa6c6cf Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 12 Aug 2020 09:47:13 -0400 Subject: [PATCH 05/20] Move null_option to DynamicModelChoiceMixin --- netbox/dcim/forms.py | 40 +++++++++---------------------- netbox/ipam/forms.py | 32 +++++++------------------ netbox/project-static/js/forms.js | 2 +- netbox/tenancy/forms.py | 10 +++----- netbox/utilities/forms/fields.py | 12 +++++++++- netbox/utilities/forms/widgets.py | 4 ---- netbox/virtualization/forms.py | 28 +++++++--------------- 7 files changed, 42 insertions(+), 86 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index cf79458a6..9c590c996 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -672,9 +672,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): ), required=False, label='Rack group', - widget=APISelectMultiple( - null_option=True - ) + null_option='None' ) status = forms.MultipleChoiceField( choices=RackStatusChoices, @@ -685,9 +683,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): queryset=RackRole.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) tag = TagFilterField(model) @@ -853,9 +849,7 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm): queryset=RackGroup.objects.prefetch_related('site'), required=False, label='Rack group', - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) tag = TagFilterField(model) @@ -2124,9 +2118,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt queryset=Rack.objects.all(), required=False, label='Rack', - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) role = DynamicModelMultipleChoiceField( queryset=DeviceRole.objects.all(), @@ -2155,9 +2147,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt queryset=Platform.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) status = forms.MultipleChoiceField( choices=DeviceStatusChoices, @@ -3879,8 +3869,8 @@ class CableFilterForm(BootstrapMixin, forms.Form): queryset=Rack.objects.all(), required=False, label='Rack', + null_option='None', widget=APISelectMultiple( - null_option=True, filter_for={ 'device_id': 'rack_id', } @@ -4208,8 +4198,8 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=TenantGroup.objects.all(), to_field_name='slug', required=False, + null_option='None', widget=APISelectMultiple( - null_option=True, filter_for={ 'tenant': 'group' } @@ -4219,9 +4209,7 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=Tenant.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) tag = TagFilterField(model) @@ -4336,9 +4324,7 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=RackGroup.objects.all(), required=False, label='Rack group (ID)', - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) tag = TagFilterField(model) @@ -4555,17 +4541,13 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=PowerPanel.objects.all(), required=False, label='Power panel', - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), required=False, label='Rack', - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) status = forms.MultipleChoiceField( choices=PowerFeedStatusChoices, diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 393903d37..59ccf7807 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -464,9 +464,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) queryset=VRF.objects.all(), required=False, label='VRF', - widget=APISelectMultiple( - null_option=True, - ) + null_option='Global' ) status = forms.MultipleChoiceField( choices=PrefixStatusChoices, @@ -487,17 +485,13 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) queryset=Site.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) role = DynamicModelMultipleChoiceField( queryset=Role.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) is_pool = forms.NullBooleanField( required=False, @@ -910,9 +904,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo queryset=VRF.objects.all(), required=False, label='VRF', - widget=APISelectMultiple( - null_option=True, - ) + null_option='Global' ) status = forms.MultipleChoiceField( choices=IPAddressStatusChoices, @@ -981,9 +973,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form): queryset=Site.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) @@ -1147,17 +1137,13 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): queryset=Site.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) group_id = DynamicModelMultipleChoiceField( queryset=VLANGroup.objects.all(), required=False, label='VLAN group', - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) status = forms.MultipleChoiceField( choices=VLANStatusChoices, @@ -1168,9 +1154,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): queryset=Role.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) tag = TagFilterField(model) diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 9fed76f90..c496a0377 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -247,7 +247,7 @@ $(document).ready(function() { if (element.getAttribute('data-null-option') && data.previous === null) { results.unshift({ id: 'null', - text: 'None' + text: element.getAttribute('data-null-option') }); } diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 436f37d3f..13ff4da65 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -106,9 +106,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=TenantGroup.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) tag = TagFilterField(model) @@ -152,8 +150,8 @@ class TenancyFilterForm(forms.Form): queryset=TenantGroup.objects.all(), to_field_name='slug', required=False, + null_option='None', widget=APISelectMultiple( - null_option=True, filter_for={ 'tenant': 'group' } @@ -163,7 +161,5 @@ class TenancyFilterForm(forms.Form): queryset=Tenant.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) diff --git a/netbox/utilities/forms/fields.py b/netbox/utilities/forms/fields.py index 065086c4e..398a8a0e8 100644 --- a/netbox/utilities/forms/fields.py +++ b/netbox/utilities/forms/fields.py @@ -245,12 +245,18 @@ class TagFilterField(forms.MultipleChoiceField): class DynamicModelChoiceMixin: + """ + :param display_field: The name of the attribute of an API response object to display in the selection list + :param query_params: A dictionary of additional key/value pairs to attach to the API request + :param null_option: The string used to represent a null selection (if any) + """ filter = django_filters.ModelChoiceFilter widget = widgets.APISelect - def __init__(self, *args, display_field='name', query_params=None, **kwargs): + def __init__(self, *args, display_field='name', query_params=None, null_option=None, **kwargs): self.display_field = display_field self.query_params = query_params or {} + self.null_option = null_option # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference # by widget_attrs() @@ -267,6 +273,10 @@ class DynamicModelChoiceMixin: if self.to_field_name: attrs['value-field'] = self.to_field_name + # Set the string used to represent a null option + if self.null_option is not None: + attrs['data-null-option'] = self.null_option + # Attach any static query parameters for key, value in self.query_params.items(): widget.add_additional_query_param(key, value) diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index 0e03931f2..91c9656cb 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -146,7 +146,6 @@ class APISelect(SelectWithDisabled): name of the filter-for field (child field) and the value is the name of the query param filter. :param additional_query_params: Optional) A dict of query params to append to the API request. The key is the name of the query param and the value if the query param's value. - :param null_option: If true, include the static null option in the selection list. """ def __init__( self, @@ -155,7 +154,6 @@ class APISelect(SelectWithDisabled): disabled_indicator=None, filter_for=None, additional_query_params=None, - null_option=False, full=False, *args, **kwargs @@ -178,8 +176,6 @@ class APISelect(SelectWithDisabled): if additional_query_params: for key, value in additional_query_params.items(): self.add_additional_query_param(key, value) - if null_option: - self.attrs['data-null-option'] = 1 def add_filter_for(self, name, value): """ diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index f33d4645a..e42a6597e 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -182,17 +182,13 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm queryset=Site.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) group = DynamicModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) tag = TagFilterField(model) @@ -485,17 +481,13 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil queryset=ClusterGroup.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) cluster_type = DynamicModelMultipleChoiceField( queryset=ClusterType.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) cluster_id = DynamicModelMultipleChoiceField( queryset=Cluster.objects.all(), @@ -516,18 +508,16 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil queryset=Site.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) role = DynamicModelMultipleChoiceField( queryset=DeviceRole.objects.filter(vm_role=True), to_field_name='slug', required=False, + null_option='None', query_params={ 'vm_role': "True" - }, - widget=APISelectMultiple(null_option=True) + } ) status = forms.MultipleChoiceField( choices=VirtualMachineStatusChoices, @@ -538,9 +528,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil queryset=Platform.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - null_option=True, - ) + null_option='None' ) mac_address = forms.CharField( required=False, From b917403c934060635141b8e770f166946ace4e8f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 12 Aug 2020 10:05:12 -0400 Subject: [PATCH 06/20] Replace 'nullable' attribute with null_option --- netbox/dcim/forms.py | 16 ++++------------ netbox/ipam/forms.py | 16 ++++------------ netbox/project-static/js/forms.js | 8 ++++---- netbox/tenancy/forms.py | 4 +--- netbox/virtualization/forms.py | 12 +++--------- 5 files changed, 16 insertions(+), 40 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 9c590c996..f6d8fccab 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1747,12 +1747,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): cluster_group = DynamicModelChoiceField( queryset=ClusterGroup.objects.all(), required=False, + null_option='None', widget=APISelect( filter_for={ 'cluster': 'group_id' - }, - attrs={ - 'nullable': 'true' } ) ) @@ -3438,12 +3436,10 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm): queryset=Rack.objects.all(), label='Rack', required=False, + null_option='None', widget=APISelect( filter_for={ 'termination_b_device': 'rack_id', - }, - attrs={ - 'nullable': 'true', } ) ) @@ -3982,12 +3978,10 @@ class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm): rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, + null_option='None', widget=APISelect( filter_for={ 'members': 'rack_id' - }, - attrs={ - 'nullable': 'true', } ) ) @@ -4118,12 +4112,10 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form): rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, + null_option='None', widget=APISelect( filter_for={ 'device': 'rack_id' - }, - attrs={ - 'nullable': 'true', } ) ) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 59ccf7807..d86895b2c 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -257,13 +257,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, + null_option='None', widget=APISelect( filter_for={ 'vlan_group': 'site_id', 'vlan': 'site_id', - }, - attrs={ - 'nullable': 'true', } ) ) @@ -271,12 +269,10 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): queryset=VLANGroup.objects.all(), required=False, label='VLAN group', + null_option='None', widget=APISelect( filter_for={ 'vlan': 'group_id' - }, - attrs={ - 'nullable': 'true', } ) ) @@ -556,12 +552,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel required=False, label='Rack', display_field='display_name', + null_option='None', widget=APISelect( filter_for={ 'nat_device': 'rack_id' - }, - attrs={ - 'nullable': 'true' } ) ) @@ -985,12 +979,10 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, + null_option='None', widget=APISelect( filter_for={ 'group': 'site_id' - }, - attrs={ - 'nullable': 'true', } ) ) diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index c496a0377..c06eb3045 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -74,7 +74,7 @@ $(document).ready(function() { form.submit(); }); - // Parse URLs which may contain variable refrences to other field values + // Parse URLs which may contain variable references to other field values function parseURL(url) { var filter_regex = /\{\{([a-z_]+)\}\}/g; var match; @@ -87,7 +87,7 @@ $(document).ready(function() { rendered_url = rendered_url.replace(match[0], custom_attr); } else if (filter_field.val()) { rendered_url = rendered_url.replace(match[0], filter_field.val()); - } else if (filter_field.attr('nullable') == 'true') { + } else if (filter_field.attr('data-null-option')) { rendered_url = rendered_url.replace(match[0], 'null'); } } @@ -123,7 +123,7 @@ $(document).ready(function() { // API backed selection // Includes live search and chained fields - // The `multiple` setting may be controled via a data-* attribute + // The `multiple` setting may be controlled via a data-* attribute $('.netbox-select2-api').select2({ allowClear: true, placeholder: "---------", @@ -165,7 +165,7 @@ $(document).ready(function() { filter_for_elements.each(function(index, filter_for_element) { var param_name = $(filter_for_element).attr(attr_name); var is_required = $(filter_for_element).attr("required"); - var is_nullable = $(filter_for_element).attr("nullable"); + var is_nullable = $(filter_for_element).attr("data-null-option"); var is_visible = $(filter_for_element).is(":visible"); var value = $(filter_for_element).val(); diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 13ff4da65..94df2fc95 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -119,12 +119,10 @@ class TenancyForm(forms.Form): tenant_group = DynamicModelChoiceField( queryset=TenantGroup.objects.all(), required=False, + null_option='None', widget=APISelect( filter_for={ 'tenant': 'group_id', - }, - attrs={ - 'nullable': 'true', } ) ) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index e42a6597e..b05dbb476 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -197,12 +197,10 @@ class ClusterAddDevicesForm(BootstrapMixin, forms.Form): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, + null_option='None', widget=APISelect( filter_for={ "site": "region_id", - }, - attrs={ - 'nullable': 'true', } ) ) @@ -219,12 +217,10 @@ class ClusterAddDevicesForm(BootstrapMixin, forms.Form): rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, + null_option='None', widget=APISelect( filter_for={ "devices": "rack_id" - }, - attrs={ - 'nullable': 'true', } ) ) @@ -278,12 +274,10 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): cluster_group = DynamicModelChoiceField( queryset=ClusterGroup.objects.all(), required=False, + null_option='None', widget=APISelect( filter_for={ "cluster": "group_id", - }, - attrs={ - 'nullable': 'true', } ) ) From d8d752b62313ffafef1f4e69f6c4c2d7fc1f071f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 12 Aug 2020 10:14:21 -0400 Subject: [PATCH 07/20] Fix form for adding devices to a cluster --- .../virtualization/cluster_add_devices.html | 30 ------------------- netbox/virtualization/forms.py | 8 ++--- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/netbox/templates/virtualization/cluster_add_devices.html b/netbox/templates/virtualization/cluster_add_devices.html index 397f53d01..d53fc3a03 100644 --- a/netbox/templates/virtualization/cluster_add_devices.html +++ b/netbox/templates/virtualization/cluster_add_devices.html @@ -35,33 +35,3 @@ {% endblock %} - -{% block javascript %} - -{% endblock %} diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index b05dbb476..9bf55e497 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -225,11 +225,11 @@ class ClusterAddDevicesForm(BootstrapMixin, forms.Form): ) ) devices = DynamicModelMultipleChoiceField( - queryset=Device.objects.filter(cluster__isnull=True), + queryset=Device.objects.all(), display_field='display_name', - widget=APISelectMultiple( - disabled_indicator='cluster' - ) + query_params={ + 'cluster_id': 'null' + } ) class Meta: From d752c36ea8cf07f0aa0fbe83280b30269715b2c7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 12 Aug 2020 10:17:21 -0400 Subject: [PATCH 08/20] Fix virtual chassis member add form --- netbox/dcim/forms.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index f6d8fccab..7e7a75125 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -4120,13 +4120,11 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form): ) ) device = DynamicModelChoiceField( - queryset=Device.objects.filter( - virtual_chassis__isnull=True - ), + queryset=Device.objects.all(), display_field='display_name', - widget=APISelect( - disabled_indicator='virtual_chassis' - ) + query_params={ + 'virtual_chassis_id': 'null' + } ) def clean_device(self): From 100c8fef206642b1b7a60176a80249fad72430e8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 12 Aug 2020 12:36:53 -0400 Subject: [PATCH 09/20] Add support for referencing peer field values in query_params --- netbox/circuits/forms.py | 24 +- netbox/dcim/forms.py | 526 ++++++++++++------------------ netbox/ipam/forms.py | 150 ++++----- netbox/project-static/js/forms.js | 11 + netbox/tenancy/forms.py | 24 +- netbox/utilities/forms/widgets.py | 7 +- netbox/virtualization/forms.py | 76 ++--- 7 files changed, 340 insertions(+), 478 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 70a220b58..23540a507 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -106,17 +106,15 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False + required=False, + query_params={ + 'region': '$region' + } ) asn = forms.IntegerField( required=False, @@ -282,17 +280,15 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False + required=False, + query_params={ + 'region': '$region' + } ) commit_rate = forms.IntegerField( required=False, diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 7e7a75125..dc79e40f0 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -68,27 +68,23 @@ class DeviceComponentFilterForm(BootstrapMixin, forms.Form): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - filter_for={ - 'device_id': 'site', - } - ) + query_params={ + 'region': '$region' + } ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, - label='Device' + label='Device', + query_params={ + 'site': '$site' + } ) @@ -357,16 +353,14 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): class RackGroupForm(BootstrapMixin, forms.ModelForm): site = DynamicModelChoiceField( - queryset=Site.objects.all(), - widget=APISelect( - filter_for={ - 'parent': 'site_id', - } - ) + queryset=Site.objects.all() ) parent = DynamicModelChoiceField( queryset=RackGroup.objects.all(), - required=False + required=False, + query_params={ + 'site_id': '$site' + } ) slug = SlugField() @@ -402,28 +396,24 @@ class RackGroupFilterForm(BootstrapMixin, forms.Form): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region', - 'parent': 'region', - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - filter_for={ - 'parent': 'site', - } - ) + query_params={ + 'region': '$region' + } ) parent = DynamicModelMultipleChoiceField( queryset=RackGroup.objects.all(), to_field_name='slug', - required=False + required=False, + query_params={ + 'region': '$region', + 'site': '$site', + } ) @@ -458,16 +448,14 @@ class RackRoleCSVForm(CSVModelForm): class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = DynamicModelChoiceField( - queryset=Site.objects.all(), - widget=APISelect( - filter_for={ - 'group': 'site_id', - } - ) + queryset=Site.objects.all() ) group = DynamicModelChoiceField( queryset=RackGroup.objects.all(), - required=False + required=False, + query_params={ + 'site_id': '$site' + } ) role = DynamicModelChoiceField( queryset=RackRole.objects.all(), @@ -562,16 +550,14 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ) site = DynamicModelChoiceField( queryset=Site.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'group': 'site_id', - } - ) + required=False ) group = DynamicModelChoiceField( queryset=RackGroup.objects.all(), - required=False + required=False, + query_params={ + 'site_id': '$site' + } ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), @@ -649,30 +635,24 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - filter_for={ - 'group_id': 'site' - } - ) + query_params={ + 'region': '$region' + } ) group_id = DynamicModelMultipleChoiceField( - queryset=RackGroup.objects.prefetch_related( - 'site' - ), + queryset=RackGroup.objects.all(), required=False, label='Rack group', - null_option='None' + null_option='None', + query_params={ + 'site': '$site' + } ) status = forms.MultipleChoiceField( choices=RackStatusChoices, @@ -698,16 +678,13 @@ class RackElevationFilterForm(RackFilterForm): queryset=Rack.objects.all(), label='Rack', required=False, - display_field='display_name' + display_field='display_name', + query_params={ + 'site': '$site', + 'group_id': '$group_id', + } ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Filter the rack field based on the site and group - self.fields['site'].widget.add_filter_for('id', 'site') - self.fields['group_id'].widget.add_filter_for('id', 'group_id') - # # Rack reservations @@ -716,25 +693,21 @@ class RackElevationFilterForm(RackFilterForm): class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): site = DynamicModelChoiceField( queryset=Site.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'rack_group': 'site_id', - 'rack': 'site_id', - } - ) + required=False ) rack_group = DynamicModelChoiceField( queryset=RackGroup.objects.all(), required=False, - widget=APISelect( - filter_for={ - 'rack': 'group_id' - } - ) + query_params={ + 'site_id': '$site' + } ) rack = DynamicModelChoiceField( - queryset=Rack.objects.all() + queryset=Rack.objects.all(), + query_params={ + 'site_id': '$site', + 'group_id': 'rack', + } ) units = NumericArrayField( base_field=forms.IntegerField(), @@ -1011,16 +984,14 @@ class ComponentTemplateCreateForm(ComponentForm): """ manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'device_type': 'manufacturer_id' - } - ) + required=False ) device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all(), - display_field='model' + display_field='model', + query_params={ + 'manufacturer_id': '$manufacturer' + } ) description = forms.CharField( required=False @@ -1699,17 +1670,15 @@ class PlatformCSVForm(CSVModelForm): class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = DynamicModelChoiceField( - queryset=Site.objects.all(), - widget=APISelect( - filter_for={ - 'rack': 'site_id' - } - ) + queryset=Site.objects.all() ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, - display_field='display_name' + display_field='display_name', + query_params={ + 'site_id': '$site' + } ) position = forms.TypedChoiceField( required=False, @@ -1717,22 +1686,22 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): help_text="The lowest-numbered unit occupied by the device", widget=APISelect( api_url='/api/dcim/racks/{{rack}}/elevation/', - disabled_indicator='device' + disabled_indicator='device', + additional_query_params={ + 'face': '$face' + } ) ) manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'device_type': 'manufacturer_id', - 'platform': 'manufacturer_id' - } - ) + required=False ) device_type = DynamicModelChoiceField( queryset=DeviceType.objects.all(), - display_field='model' + display_field='model', + query_params={ + 'manufacturer_id': '$manufacturer' + } ) device_role = DynamicModelChoiceField( queryset=DeviceRole.objects.all() @@ -1741,22 +1710,20 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): queryset=Platform.objects.all(), required=False, query_params={ - "manufacturer_id": "null" + 'manufacturer_id': ['$manufacturer', 'null'] } ) cluster_group = DynamicModelChoiceField( queryset=ClusterGroup.objects.all(), required=False, - null_option='None', - widget=APISelect( - filter_for={ - 'cluster': 'group_id' - } - ) + null_option='None' ) cluster = DynamicModelChoiceField( queryset=Cluster.objects.all(), - required=False + required=False, + query_params={ + 'group_id': '$cluster_group' + } ) comments = CommentField() local_context_data = JSONField( @@ -1782,11 +1749,6 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): "config context", } widgets = { - 'face': StaticSelect2( - filter_for={ - 'position': 'face' - } - ), 'status': StaticSelect2(), 'primary_ip4': StaticSelect2(), 'primary_ip6': StaticSelect2(), @@ -2084,39 +2046,33 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - filter_for={ - 'rack_group_id': 'site', - 'rack_id': 'site', - } - ) + query_params={ + 'region': '$region' + } ) rack_group_id = DynamicModelMultipleChoiceField( queryset=RackGroup.objects.all(), required=False, label='Rack group', - widget=APISelectMultiple( - filter_for={ - 'rack_id': 'group_id', - } - ) + query_params={ + 'site': '$site' + } ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), required=False, label='Rack', - null_option='None' + null_option='None', + query_params={ + 'site': '$site', + 'group_id': '$rack_group_id', + } ) role = DynamicModelMultipleChoiceField( queryset=DeviceRole.objects.all(), @@ -2125,21 +2081,20 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt widget=APISelectMultiple( ) ) - manufacturer_id = DynamicModelMultipleChoiceField( + manufacturer = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), + to_field_name='slug', required=False, - label='Manufacturer', - widget=APISelectMultiple( - filter_for={ - 'device_type_id': 'manufacturer_id', - } - ) + label='Manufacturer' ) device_type_id = DynamicModelMultipleChoiceField( queryset=DeviceType.objects.all(), required=False, label='Model', - display_field='model' + display_field='model', + query_params={ + 'manufacturer': '$manufacturer' + } ) platform = DynamicModelMultipleChoiceField( queryset=Platform.objects.all(), @@ -3424,30 +3379,26 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm): termination_b_site = DynamicModelChoiceField( queryset=Site.objects.all(), label='Site', - required=False, - widget=APISelect( - filter_for={ - 'termination_b_rack': 'site_id', - 'termination_b_device': 'site_id', - } - ) + required=False ) termination_b_rack = DynamicModelChoiceField( queryset=Rack.objects.all(), label='Rack', required=False, null_option='None', - widget=APISelect( - filter_for={ - 'termination_b_device': 'rack_id', - } - ) + query_params={ + 'site_id': '$termination_b_site' + } ) termination_b_device = DynamicModelChoiceField( queryset=Device.objects.all(), label='Device', required=False, display_field='display_name', + query_params={ + 'site_id': '$termination_b_site', + 'rack_id': '$termination_b_rack', + }, widget=APISelect( filter_for={ 'termination_b_id': 'device_id', @@ -3545,27 +3496,21 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm): termination_b_provider = DynamicModelChoiceField( queryset=Provider.objects.all(), label='Provider', - required=False, - widget=APISelect( - filter_for={ - 'termination_b_circuit': 'provider_id', - } - ) + required=False ) termination_b_site = DynamicModelChoiceField( queryset=Site.objects.all(), label='Site', - required=False, - widget=APISelect( - filter_for={ - 'termination_b_circuit': 'site_id', - } - ) + required=False ) termination_b_circuit = DynamicModelChoiceField( queryset=Circuit.objects.all(), label='Circuit', display_field='cid', + query_params={ + 'provider_id': '$termination_b_provider', + 'site_id': '$termination_b_site', + }, widget=APISelect( filter_for={ 'termination_b_id': 'circuit_id', @@ -3595,29 +3540,25 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm): queryset=Site.objects.all(), label='Site', required=False, - display_field='cid', - widget=APISelect( - filter_for={ - 'termination_b_rackgroup': 'site_id', - 'termination_b_powerpanel': 'site_id', - } - ) + display_field='cid' ) termination_b_rackgroup = DynamicModelChoiceField( queryset=RackGroup.objects.all(), label='Rack Group', required=False, display_field='cid', - widget=APISelect( - filter_for={ - 'termination_b_powerpanel': 'rackgroup_id', - } - ) + query_params={ + 'site_id': '$termination_b_site' + } ) termination_b_powerpanel = DynamicModelChoiceField( queryset=PowerPanel.objects.all(), label='Power Panel', required=False, + query_params={ + 'site_id': '$termination_b_site', + 'rack_group_id': '$termination_b_rackgroup', + }, widget=APISelect( filter_for={ 'termination_b_id': 'power_panel_id', @@ -3843,34 +3784,21 @@ class CableFilterForm(BootstrapMixin, forms.Form): site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'rack_id': 'site', - 'device_id': 'site', - } - ) + required=False ) tenant = DynamicModelMultipleChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'device_id': 'tenant', - } - ) + required=False ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), required=False, label='Rack', null_option='None', - widget=APISelectMultiple( - filter_for={ - 'device_id': 'rack_id', - } - ) + query_params={ + 'site': '$site' + } ) type = forms.MultipleChoiceField( choices=add_blank_choice(CableTypeChoices), @@ -3890,7 +3818,12 @@ class CableFilterForm(BootstrapMixin, forms.Form): device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, - label='Device' + label='Device', + query_params={ + 'site': '$site', + 'tenant': '$tenant', + 'rack_id': '$rack_id', + } ) tag = TagFilterField(model) @@ -3903,17 +3836,15 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'device_id': 'site', - } - ) + required=False ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, - label='Device' + label='Device', + query_params={ + 'site': '$site' + } ) @@ -3921,17 +3852,15 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form): site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'device_id': 'site', - } - ) + required=False ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, - label='Device' + label='Device', + query_params={ + 'site': '$site' + } ) @@ -3939,17 +3868,15 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'device_id': 'site', - } - ) + required=False ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, - label='Device' + label='Device', + query_params={ + 'site': '$site' + } ) @@ -3967,27 +3894,23 @@ class DeviceSelectionForm(forms.Form): class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm): site = DynamicModelChoiceField( queryset=Site.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'rack': 'site_id', - 'members': 'site_id', - } - ) + required=False ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, null_option='None', - widget=APISelect( - filter_for={ - 'members': 'rack_id' - } - ) + query_params={ + 'site_id': '$site' + } ) members = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, + query_params={ + 'site_id': '$site', + 'rack_id': '$rack', + } ) initial_position = forms.IntegerField( initial=1, @@ -4101,29 +4024,23 @@ class DeviceVCMembershipForm(forms.ModelForm): class VCMemberSelectForm(BootstrapMixin, forms.Form): site = DynamicModelChoiceField( queryset=Site.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'rack': 'site_id', - 'device': 'site_id', - } - ) + required=False ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, null_option='None', - widget=APISelect( - filter_for={ - 'device': 'rack_id' - } - ) + query_params={ + 'site_id': '$site' + } ) device = DynamicModelChoiceField( queryset=Device.objects.all(), display_field='display_name', query_params={ - 'virtual_chassis_id': 'null' + 'site_id': '$site', + 'rack_id': '$rack', + 'virtual_chassis_id': 'null', } ) @@ -4172,34 +4089,30 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', - required=False + required=False, + query_params={ + 'region': '$region' + } ) tenant_group = DynamicModelMultipleChoiceField( queryset=TenantGroup.objects.all(), to_field_name='slug', required=False, - null_option='None', - widget=APISelectMultiple( - filter_for={ - 'tenant': 'group' - } - ) + null_option='None' ) tenant = DynamicModelMultipleChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', required=False, - null_option='None' + null_option='None', + query_params={ + 'group': '$tenant_group' + } ) tag = TagFilterField(model) @@ -4210,16 +4123,14 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): class PowerPanelForm(BootstrapMixin, forms.ModelForm): site = DynamicModelChoiceField( - queryset=Site.objects.all(), - widget=APISelect( - filter_for={ - 'rack_group': 'site_id', - } - ) + queryset=Site.objects.all() ) rack_group = DynamicModelChoiceField( queryset=RackGroup.objects.all(), - required=False + required=False, + query_params={ + 'site_id': '$site' + } ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -4266,16 +4177,14 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): ) site = DynamicModelChoiceField( queryset=Site.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'rack_group': 'site_id', - } - ) + required=False ) rack_group = DynamicModelChoiceField( queryset=RackGroup.objects.all(), - required=False + required=False, + query_params={ + 'site_id': '$site' + } ) class Meta: @@ -4293,28 +4202,24 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - filter_for={ - 'rack_group_id': 'site', - } - ) + query_params={ + 'region': '$region' + } ) rack_group_id = DynamicModelMultipleChoiceField( queryset=RackGroup.objects.all(), required=False, label='Rack group (ID)', - null_option='None' + null_option='None', + query_params={ + 'site': '$site' + } ) tag = TagFilterField(model) @@ -4326,20 +4231,20 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): class PowerFeedForm(BootstrapMixin, CustomFieldModelForm): site = DynamicModelChoiceField( queryset=Site.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'power_panel': 'site_id', - 'rack': 'site_id', - } - ) + required=False ) power_panel = DynamicModelChoiceField( - queryset=PowerPanel.objects.all() + queryset=PowerPanel.objects.all(), + query_params={ + 'site_id': '$site' + } ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), - required=False + required=False, + query_params={ + 'site_id': '$site' + } ) comments = CommentField() tags = DynamicModelMultipleChoiceField( @@ -4445,12 +4350,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd ) power_panel = DynamicModelChoiceField( queryset=PowerPanel.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'rackgroup': 'site_id', - } - ) + required=False ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), @@ -4509,35 +4409,33 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', required=False, - widget=APISelectMultiple( - filter_for={ - 'power_panel_id': 'site', - 'rack_id': 'site', - } - ) + query_params={ + 'region': '$region' + } ) power_panel_id = DynamicModelMultipleChoiceField( queryset=PowerPanel.objects.all(), required=False, label='Power panel', - null_option='None' + null_option='None', + query_params={ + 'site': '$site' + } ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), required=False, label='Rack', - null_option='None' + null_option='None', + query_params={ + 'site': '$site' + } ) status = forms.MultipleChoiceField( choices=PowerFeedStatusChoices, diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index d86895b2c..cc83568b1 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -257,30 +257,26 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, - null_option='None', - widget=APISelect( - filter_for={ - 'vlan_group': 'site_id', - 'vlan': 'site_id', - } - ) + null_option='None' ) vlan_group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), required=False, label='VLAN group', null_option='None', - widget=APISelect( - filter_for={ - 'vlan': 'group_id' - } - ) + query_params={ + 'site_id': '$site' + } ) vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, label='VLAN', - display_field='display_name' + display_field='display_name', + query_params={ + 'site_id': '$site', + 'group_id': '$vlan_group', + } ) role = DynamicModelChoiceField( queryset=Role.objects.all(), @@ -470,18 +466,16 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', required=False, - null_option='None' + null_option='None', + query_params={ + 'region': '$region' + } ) role = DynamicModelMultipleChoiceField( queryset=Role.objects.all(), @@ -506,30 +500,26 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm): device = DynamicModelChoiceField( queryset=Device.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'interface': 'device_id' - } - ) + required=False ) interface = DynamicModelChoiceField( queryset=Interface.objects.all(), - required=False + required=False, + query_params={ + 'device_id': '$device' + } ) virtual_machine = DynamicModelChoiceField( queryset=VirtualMachine.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'vminterface': 'virtual_machine_id' - } - ) + required=False ) vminterface = DynamicModelChoiceField( queryset=VMInterface.objects.all(), required=False, - label='Interface' + label='Interface', + query_params={ + 'virtual_machine_id': '$virtual_machine' + } ) vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), @@ -539,13 +529,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel nat_site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, - label='Site', - widget=APISelect( - filter_for={ - 'nat_rack': 'site_id', - 'nat_device': 'site_id' - } - ) + label='Site' ) nat_rack = DynamicModelChoiceField( queryset=Rack.objects.all(), @@ -553,38 +537,34 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel label='Rack', display_field='display_name', null_option='None', - widget=APISelect( - filter_for={ - 'nat_device': 'rack_id' - } - ) + query_params={ + 'site_id': '$site' + } ) nat_device = DynamicModelChoiceField( queryset=Device.objects.all(), required=False, label='Device', display_field='display_name', - widget=APISelect( - filter_for={ - 'nat_inside': 'device_id' - } - ) + query_params={ + 'site_id': '$site', + 'rack_id': '$nat_rack', + } ) nat_vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, - label='VRF', - widget=APISelect( - filter_for={ - 'nat_inside': 'vrf_id' - } - ) + label='VRF' ) nat_inside = DynamicModelChoiceField( queryset=IPAddress.objects.all(), required=False, label='IP Address', - display_field='address' + display_field='address', + query_params={ + 'device_id': '$nat_device', + 'vrf_if': '$nat_vrf', + } ) primary_for_parent = forms.BooleanField( required=False, @@ -956,18 +936,16 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region', - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', required=False, - null_option='None' + null_option='None', + query_params={ + 'region': '$region' + } ) @@ -979,16 +957,14 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, - null_option='None', - widget=APISelect( - filter_for={ - 'group': 'site_id' - } - ) + null_option='None' ) group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), - required=False + required=False, + query_params={ + 'site_id': '$site' + } ) role = DynamicModelChoiceField( queryset=Role.objects.all(), @@ -1072,16 +1048,14 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ) site = DynamicModelChoiceField( queryset=Site.objects.all(), - required=False, - widget=APISelect( - filter_for={ - 'group': 'site_id' - } - ) + required=False ) group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), - required=False + required=False, + query_params={ + 'site_id': '$site' + } ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), @@ -1117,25 +1091,25 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region', - 'group_id': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', required=False, - null_option='None' + null_option='None', + query_params={ + 'region': '$region' + } ) group_id = DynamicModelMultipleChoiceField( queryset=VLANGroup.objects.all(), required=False, label='VLAN group', - null_option='None' + null_option='None', + query_params={ + 'region': '$region' + } ) status = forms.MultipleChoiceField( choices=VLANStatusChoices, diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index c06eb3045..051b506ed 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -184,6 +184,17 @@ $(document).ready(function() { var param_name = attr.name.split("data-additional-query-param-")[1]; $.each($.parseJSON(attr.value), function(index, value) { + // Referencing the value of another form field + if (value.startsWith('$')) { + let ref_field = $('#id_' + value.slice(1)); + if (ref_field.val() && ref_field.is(":visible")) { + value = ref_field.val(); + } else if (ref_field.attr("required") && ref_field.attr("data-null-option")) { + value = "null"; + } else { + return true; // Skip if ref_field has no value + } + } if (param_name in parameters) { if (Array.isArray(parameters[param_name])) { parameters[param_name].push(value); diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 94df2fc95..5a420f016 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -119,16 +119,14 @@ class TenancyForm(forms.Form): tenant_group = DynamicModelChoiceField( queryset=TenantGroup.objects.all(), required=False, - null_option='None', - widget=APISelect( - filter_for={ - 'tenant': 'group_id', - } - ) + null_option='None' ) tenant = DynamicModelChoiceField( queryset=Tenant.objects.all(), - required=False + required=False, + query_params={ + 'group_id': '$tenant_group' + } ) def __init__(self, *args, **kwargs): @@ -148,16 +146,14 @@ class TenancyFilterForm(forms.Form): queryset=TenantGroup.objects.all(), to_field_name='slug', required=False, - null_option='None', - widget=APISelectMultiple( - filter_for={ - 'tenant': 'group' - } - ) + null_option='None' ) tenant = DynamicModelMultipleChoiceField( queryset=Tenant.objects.all(), to_field_name='slug', required=False, - null_option='None' + null_option='None', + query_params={ + 'group': '$tenant_group' + } ) diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index 91c9656cb..f9eb42663 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -193,10 +193,13 @@ class APISelect(SelectWithDisabled): :param name: The name of the query param :param value: The value of the query param """ - key = 'data-additional-query-param-{}'.format(name) + key = f'data-additional-query-param-{name}' values = json.loads(self.attrs.get(key, '[]')) - values.append(value) + if type(value) is list: + values.extend(value) + else: + values.append(value) self.attrs[key] = json.dumps(values) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 9bf55e497..c3b243978 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -171,18 +171,16 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', required=False, - null_option='None' + null_option='None', + query_params={ + 'region': '$region' + } ) group = DynamicModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), @@ -197,38 +195,30 @@ class ClusterAddDevicesForm(BootstrapMixin, forms.Form): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, - null_option='None', - widget=APISelect( - filter_for={ - "site": "region_id", - } - ) + null_option='None' ) site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, - widget=APISelect( - filter_for={ - "rack": "site_id", - "devices": "site_id", - } - ) + query_params={ + 'region_id': '$region' + } ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, null_option='None', - widget=APISelect( - filter_for={ - "devices": "rack_id" - } - ) + query_params={ + 'site_id': '$site' + } ) devices = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), display_field='display_name', query_params={ - 'cluster_id': 'null' + 'site_id': '$site', + 'rack_id': '$rack', + 'cluster_id': 'null', } ) @@ -274,15 +264,13 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): cluster_group = DynamicModelChoiceField( queryset=ClusterGroup.objects.all(), required=False, - null_option='None', - widget=APISelect( - filter_for={ - "cluster": "group_id", - } - ) + null_option='None' ) cluster = DynamicModelChoiceField( - queryset=Cluster.objects.all() + queryset=Cluster.objects.all(), + query_params={ + 'group_id': '$cluster_group' + } ) role = DynamicModelChoiceField( queryset=DeviceRole.objects.all(), @@ -491,18 +479,16 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), to_field_name='slug', - required=False, - widget=APISelectMultiple( - filter_for={ - 'site': 'region' - } - ) + required=False ) site = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), to_field_name='slug', required=False, - null_option='None' + null_option='None', + query_params={ + 'region': '$region' + } ) role = DynamicModelMultipleChoiceField( queryset=DeviceRole.objects.filter(vm_role=True), @@ -776,17 +762,15 @@ class VMInterfaceFilterForm(forms.Form): cluster_id = DynamicModelMultipleChoiceField( queryset=Cluster.objects.all(), required=False, - label='Cluster', - widget=APISelectMultiple( - filter_for={ - 'virtual_machine_id': 'cluster_id' - } - ) + label='Cluster' ) virtual_machine_id = DynamicModelMultipleChoiceField( queryset=VirtualMachine.objects.all(), required=False, - label='Virtual machine' + label='Virtual machine', + query_params={ + 'cluster_id': '$cluster_id' + } ) enabled = forms.NullBooleanField( required=False, From c45a57ff4399989dcd8d2153f06210736275f6e7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 12 Aug 2020 12:58:32 -0400 Subject: [PATCH 10/20] Update cable termination forms to use DynamicModelChoiceField --- netbox/dcim/forms.py | 117 +++++++++++++++++++++++++--------------- netbox/ipam/forms.py | 8 ++- netbox/tenancy/forms.py | 4 +- 3 files changed, 79 insertions(+), 50 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index dc79e40f0..fe08822a5 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -10,7 +10,7 @@ from netaddr import EUI from netaddr.core import AddrFormatError from timezone_field import TimeZoneFormField -from circuits.models import Circuit, Provider +from circuits.models import Circuit, CircuitTermination, Provider from extras.forms import ( AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, CustomFieldModelForm, LocalConfigContextFilterForm, @@ -3398,12 +3398,7 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm): query_params={ 'site_id': '$termination_b_site', 'rack_id': '$termination_b_rack', - }, - widget=APISelect( - filter_for={ - 'termination_b_id': 'device_id', - } - ) + } ) class Meta: @@ -3418,76 +3413,106 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm): 'length_unit': StaticSelect2, } + def clean_termination_b_id(self): + # Return the PK rather than the object + return getattr(self.cleaned_data['termination_b_id'], 'pk', None) + class ConnectCableToConsolePortForm(ConnectCableToDeviceForm): - termination_b_id = forms.IntegerField( + termination_b_id = DynamicModelChoiceField( + queryset=ConsolePort.objects.all(), label='Name', + query_params={ + 'device_id': '$termination_b_device' + }, widget=APISelect( - api_url='/api/dcim/console-ports/', disabled_indicator='cable', + full=True ) ) class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm): - termination_b_id = forms.IntegerField( + termination_b_id = DynamicModelChoiceField( + queryset=ConsoleServerPort.objects.all(), label='Name', + query_params={ + 'device_id': '$termination_b_device' + }, widget=APISelect( - api_url='/api/dcim/console-server-ports/', disabled_indicator='cable', + full=True ) ) class ConnectCableToPowerPortForm(ConnectCableToDeviceForm): - termination_b_id = forms.IntegerField( + termination_b_id = DynamicModelChoiceField( + queryset=PowerPort.objects.all(), label='Name', + query_params={ + 'device_id': '$termination_b_device' + }, widget=APISelect( - api_url='/api/dcim/power-ports/', disabled_indicator='cable', + full=True ) ) class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm): - termination_b_id = forms.IntegerField( + termination_b_id = DynamicModelChoiceField( + queryset=PowerOutlet.objects.all(), label='Name', + query_params={ + 'device_id': '$termination_b_device' + }, widget=APISelect( - api_url='/api/dcim/power-outlets/', disabled_indicator='cable', + full=True ) ) class ConnectCableToInterfaceForm(ConnectCableToDeviceForm): - termination_b_id = forms.IntegerField( + termination_b_id = DynamicModelChoiceField( + queryset=Interface.objects.all(), label='Name', + query_params={ + 'device_id': '$termination_b_device', + 'kind': 'physical', + }, widget=APISelect( - api_url='/api/dcim/interfaces/', disabled_indicator='cable', - additional_query_params={ - 'kind': 'physical', - } + full=True ) ) class ConnectCableToFrontPortForm(ConnectCableToDeviceForm): - termination_b_id = forms.IntegerField( + termination_b_id = DynamicModelChoiceField( + queryset=FrontPort.objects.all(), label='Name', + query_params={ + 'device_id': '$termination_b_device' + }, widget=APISelect( - api_url='/api/dcim/front-ports/', disabled_indicator='cable', + full=True ) ) class ConnectCableToRearPortForm(ConnectCableToDeviceForm): - termination_b_id = forms.IntegerField( + termination_b_id = DynamicModelChoiceField( + queryset=RearPort.objects.all(), label='Name', + query_params={ + 'device_id': '$termination_b_device' + }, widget=APISelect( - api_url='/api/dcim/rear-ports/', disabled_indicator='cable', + full=True ) ) @@ -3510,19 +3535,17 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm): query_params={ 'provider_id': '$termination_b_provider', 'site_id': '$termination_b_site', + } + ) + termination_b_id = DynamicModelChoiceField( + queryset=CircuitTermination.objects.all(), + label='Side', + display_field='term_side', + query_params={ + 'circuit_id': '$termination_b_circuit' }, widget=APISelect( - filter_for={ - 'termination_b_id': 'circuit_id', - } - ) - ) - termination_b_id = forms.IntegerField( - label='Side', - widget=APISelect( - api_url='/api/circuits/circuit-terminations/', disabled_indicator='cable', - display_field='term_side', full=True ) ) @@ -3534,6 +3557,10 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm): 'status', 'label', 'color', 'length', 'length_unit', ] + def clean_termination_b_id(self): + # Return the PK rather than the object + return getattr(self.cleaned_data['termination_b_id'], 'pk', None) + class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm): termination_b_site = DynamicModelChoiceField( @@ -3558,17 +3585,17 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm): query_params={ 'site_id': '$termination_b_site', 'rack_group_id': '$termination_b_rackgroup', + } + ) + termination_b_id = DynamicModelChoiceField( + queryset=PowerFeed.objects.all(), + label='Name', + query_params={ + 'power_panel_id': '$termination_b_powerpanel' }, widget=APISelect( - filter_for={ - 'termination_b_id': 'power_panel_id', - } - ) - ) - termination_b_id = forms.IntegerField( - label='Name', - widget=APISelect( - api_url='/api/dcim/power-feeds/', + disabled_indicator='cable', + full=True ) ) @@ -3579,6 +3606,10 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm): 'color', 'length', 'length_unit', ] + def clean_termination_b_id(self): + # Return the PK rather than the object + return getattr(self.cleaned_data['termination_b_id'], 'pk', None) + class CableForm(BootstrapMixin, forms.ModelForm): tags = DynamicModelMultipleChoiceField( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index cc83568b1..2e1917b2e 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -1,5 +1,4 @@ from django import forms -from django.contrib.contenttypes.models import ContentType from django.core.validators import MaxValueValidator, MinValueValidator from dcim.models import Device, Interface, Rack, Region, Site @@ -10,10 +9,9 @@ from extras.models import Tag from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant from utilities.forms import ( - add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, - CSVModelChoiceField, CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - ExpandableIPAddressField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, - BOOLEAN_WITH_BLANK_CHOICES, + add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField, CSVModelChoiceField, CSVModelForm, + DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, ReturnURLForm, + SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import VirtualMachine, VMInterface from .choices import * diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 5a420f016..142333bff 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -5,8 +5,8 @@ from extras.forms import ( ) from extras.models import Tag from utilities.forms import ( - APISelect, APISelectMultiple, BootstrapMixin, CommentField, CSVModelChoiceField, CSVModelForm, - DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, TagFilterField, + BootstrapMixin, CommentField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, + DynamicModelMultipleChoiceField, SlugField, TagFilterField, ) from .models import Tenant, TenantGroup From 09849391e393d853c86ec077139dd578e524fd98 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 12 Aug 2020 13:03:59 -0400 Subject: [PATCH 11/20] Remove filter_for argument from Select2 widgets --- netbox/utilities/forms/widgets.py | 36 ++----------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index f9eb42663..58aca97c7 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -79,29 +79,12 @@ class SelectWithDisabled(forms.Select): class StaticSelect2(SelectWithDisabled): """ - A static content using the Select2 widget - - :param filter_for: (Optional) A dict of chained form fields for which this field is a filter. The key is the - name of the filter-for field (child field) and the value is the name of the query param filter. + A static