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,