diff --git a/docs/plugins/development/forms.md b/docs/plugins/development/forms.md index bd7640411..efe1ed5e9 100644 --- a/docs/plugins/development/forms.md +++ b/docs/plugins/development/forms.md @@ -35,6 +35,16 @@ In addition to the [form fields provided by Django](https://docs.djangoproject.c selection: members: false +## Choice Fields + +::: utilities.forms.ChoiceField + selection: + members: false + +::: utilities.forms.MultipleChoiceField + selection: + members: false + ## Dynamic Object Fields ::: utilities.forms.DynamicModelChoiceField diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 209f7ad7a..4f0d99895 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -6,7 +6,7 @@ from circuits.models import * from dcim.models import Region, Site, SiteGroup from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm -from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField +from utilities.forms import DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField __all__ = ( 'CircuitFilterForm', @@ -101,10 +101,9 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi }, label=_('Provider network') ) - status = forms.MultipleChoiceField( + status = MultipleChoiceField( choices=CircuitStatusChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 0e57c338e..d5335947a 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -10,8 +10,8 @@ from ipam.models import ASN, VRF from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms import ( - APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect, - StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget, + APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, MultipleChoiceField, + StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget, ) from wireless.choices import * @@ -140,10 +140,9 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte ('Tenant', ('tenant_group_id', 'tenant_id')), ('Contacts', ('contact', 'contact_role')), ) - status = forms.MultipleChoiceField( + status = MultipleChoiceField( choices=SiteStatusChoices, - required=False, - widget=StaticSelectMultiple(), + required=False ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -239,20 +238,17 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte }, label=_('Location') ) - status = forms.MultipleChoiceField( + status = MultipleChoiceField( choices=RackStatusChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) - type = forms.MultipleChoiceField( + type = MultipleChoiceField( choices=RackTypeChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) - width = forms.MultipleChoiceField( + width = MultipleChoiceField( choices=RackWidthChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) role_id = DynamicModelMultipleChoiceField( queryset=RackRole.objects.all(), @@ -346,15 +342,13 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm): part_number = forms.CharField( required=False ) - subdevice_role = forms.MultipleChoiceField( + subdevice_role = MultipleChoiceField( choices=add_blank_choice(SubdeviceRoleChoices), - required=False, - widget=StaticSelectMultiple() + required=False ) - airflow = forms.MultipleChoiceField( + airflow = MultipleChoiceField( choices=add_blank_choice(DeviceAirflowChoices), - required=False, - widget=StaticSelectMultiple() + required=False ) console_ports = forms.NullBooleanField( required=False, @@ -561,15 +555,13 @@ class DeviceFilterForm( null_option='None', label=_('Platform') ) - status = forms.MultipleChoiceField( + status = MultipleChoiceField( choices=DeviceStatusChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) - airflow = forms.MultipleChoiceField( + airflow = MultipleChoiceField( choices=add_blank_choice(DeviceAirflowChoices), - required=False, - widget=StaticSelectMultiple() + required=False ) serial = forms.CharField( required=False @@ -739,15 +731,13 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): }, label=_('Device') ) - type = forms.MultipleChoiceField( + type = MultipleChoiceField( choices=add_blank_choice(CableTypeChoices), - required=False, - widget=StaticSelect() + required=False ) - status = forms.ChoiceField( + status = MultipleChoiceField( required=False, - choices=add_blank_choice(LinkStatusChoices), - widget=StaticSelect() + choices=add_blank_choice(LinkStatusChoices) ) color = ColorField( required=False @@ -843,10 +833,9 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm): }, label=_('Rack') ) - status = forms.MultipleChoiceField( + status = MultipleChoiceField( choices=PowerFeedStatusChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) type = forms.ChoiceField( choices=add_blank_choice(PowerFeedTypeChoices), @@ -886,15 +875,13 @@ class ConsolePortFilterForm(DeviceComponentFilterForm): ('Attributes', ('name', 'label', 'type', 'speed')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), ) - type = forms.MultipleChoiceField( + type = MultipleChoiceField( choices=ConsolePortTypeChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) - speed = forms.MultipleChoiceField( + speed = MultipleChoiceField( choices=ConsolePortSpeedChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) tag = TagFilterField(model) @@ -906,15 +893,13 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm): ('Attributes', ('name', 'label', 'type', 'speed')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), ) - type = forms.MultipleChoiceField( + type = MultipleChoiceField( choices=ConsolePortTypeChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) - speed = forms.MultipleChoiceField( + speed = MultipleChoiceField( choices=ConsolePortSpeedChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) tag = TagFilterField(model) @@ -926,10 +911,9 @@ class PowerPortFilterForm(DeviceComponentFilterForm): ('Attributes', ('name', 'label', 'type')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), ) - type = forms.MultipleChoiceField( + type = MultipleChoiceField( choices=PowerPortTypeChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) tag = TagFilterField(model) @@ -941,10 +925,9 @@ class PowerOutletFilterForm(DeviceComponentFilterForm): ('Attributes', ('name', 'label', 'type')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), ) - type = forms.MultipleChoiceField( + type = MultipleChoiceField( choices=PowerOutletTypeChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) tag = TagFilterField(model) @@ -958,26 +941,22 @@ class InterfaceFilterForm(DeviceComponentFilterForm): ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), ) - kind = forms.MultipleChoiceField( + kind = MultipleChoiceField( choices=InterfaceKindChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) - type = forms.MultipleChoiceField( + type = MultipleChoiceField( choices=InterfaceTypeChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) speed = forms.IntegerField( required=False, label='Select Speed', widget=SelectSpeedWidget(attrs={'readonly': None}) ) - duplex = forms.MultipleChoiceField( + duplex = MultipleChoiceField( choices=InterfaceDuplexChoices, - required=False, - label='Select Duplex', - widget=StaticSelectMultiple() + required=False ) enabled = forms.NullBooleanField( required=False, @@ -999,16 +978,14 @@ class InterfaceFilterForm(DeviceComponentFilterForm): required=False, label='WWN' ) - rf_role = forms.MultipleChoiceField( + rf_role = MultipleChoiceField( choices=WirelessRoleChoices, required=False, - widget=StaticSelectMultiple(), label='Wireless role' ) - rf_channel = forms.MultipleChoiceField( + rf_channel = MultipleChoiceField( choices=WirelessChannelChoices, required=False, - widget=StaticSelectMultiple(), label='Wireless channel' ) rf_channel_frequency = forms.IntegerField( @@ -1040,10 +1017,9 @@ class FrontPortFilterForm(DeviceComponentFilterForm): ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), ) model = FrontPort - type = forms.MultipleChoiceField( + type = MultipleChoiceField( choices=PortTypeChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) color = ColorField( required=False @@ -1058,10 +1034,9 @@ class RearPortFilterForm(DeviceComponentFilterForm): ('Attributes', ('name', 'label', 'type', 'color')), ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), ) - type = forms.MultipleChoiceField( + type = MultipleChoiceField( choices=PortTypeChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) color = ColorField( required=False diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index b837f6914..e3ae9b1f3 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -10,7 +10,7 @@ from extras.utils import FeatureQuery from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DateTimePicker, - DynamicModelMultipleChoiceField, FilterForm, StaticSelect, StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, + DynamicModelMultipleChoiceField, FilterForm, MultipleChoiceField, StaticSelect, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup, ClusterType @@ -37,10 +37,9 @@ class CustomFieldFilterForm(FilterForm): limit_choices_to=FeatureQuery('custom_fields'), required=False ) - type = forms.MultipleChoiceField( + type = MultipleChoiceField( choices=CustomFieldTypeChoices, required=False, - widget=StaticSelectMultiple(), label=_('Field type') ) weight = forms.IntegerField( @@ -117,10 +116,9 @@ class WebhookFilterForm(FilterForm): limit_choices_to=FeatureQuery('webhooks'), required=False ) - http_method = forms.MultipleChoiceField( + http_method = MultipleChoiceField( choices=WebhookHttpMethodChoices, required=False, - widget=StaticSelectMultiple(), label=_('HTTP method') ) enabled = forms.NullBooleanField( diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index bf780a7d0..57f39f8c2 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -9,7 +9,7 @@ from ipam.models import ASN from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import ( - add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, + add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField, MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) @@ -164,11 +164,10 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): label=_('Address family'), widget=StaticSelect() ) - mask_length = forms.MultipleChoiceField( + mask_length = MultipleChoiceField( required=False, choices=PREFIX_MASK_LENGTH_CHOICES, - label=_('Mask length'), - widget=StaticSelectMultiple() + label=_('Mask length') ) vrf_id = DynamicModelMultipleChoiceField( queryset=VRF.objects.all(), @@ -181,10 +180,9 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): required=False, label=_('Present in VRF') ) - status = forms.MultipleChoiceField( + status = MultipleChoiceField( choices=PrefixStatusChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -247,10 +245,9 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): label=_('Assigned VRF'), null_option='Global' ) - status = forms.MultipleChoiceField( + status = MultipleChoiceField( choices=PrefixStatusChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) role_id = DynamicModelMultipleChoiceField( queryset=Role.objects.all(), @@ -301,15 +298,13 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): required=False, label=_('Present in VRF') ) - status = forms.MultipleChoiceField( + status = MultipleChoiceField( choices=IPAddressStatusChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) - role = forms.MultipleChoiceField( + role = MultipleChoiceField( choices=IPAddressRoleChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) assigned_to_interface = forms.NullBooleanField( required=False, @@ -328,20 +323,18 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm): ('Attributes', ('protocol', 'group_id')), ('Authentication', ('auth_type', 'auth_key')), ) - protocol = forms.MultipleChoiceField( + protocol = MultipleChoiceField( choices=FHRPGroupProtocolChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) group_id = forms.IntegerField( min_value=0, required=False, label='Group ID' ) - auth_type = forms.MultipleChoiceField( + auth_type = MultipleChoiceField( choices=FHRPGroupAuthTypeChoices, required=False, - widget=StaticSelectMultiple(), label='Authentication type' ) auth_key = forms.CharField( @@ -430,10 +423,9 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): }, label=_('VLAN group') ) - status = forms.MultipleChoiceField( + status = MultipleChoiceField( choices=VLANStatusChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) role_id = DynamicModelMultipleChoiceField( queryset=Role.objects.all(), @@ -457,7 +449,7 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): protocol = forms.ChoiceField( choices=add_blank_choice(ServiceProtocolChoices), required=False, - widget=StaticSelectMultiple() + widget=StaticSelect() ) port = forms.IntegerField( required=False, diff --git a/netbox/utilities/forms/fields/fields.py b/netbox/utilities/forms/fields/fields.py index ec0c4dc29..0d09d2ac7 100644 --- a/netbox/utilities/forms/fields/fields.py +++ b/netbox/utilities/forms/fields/fields.py @@ -9,11 +9,13 @@ from utilities.forms import widgets from utilities.validators import EnhancedURLValidator __all__ = ( + 'ChoiceField', 'ColorField', 'CommentField', 'JSONField', 'LaxURLField', 'MACAddressField', + 'MultipleChoiceField', 'SlugField', 'TagFilterField', ) @@ -125,3 +127,21 @@ class MACAddressField(forms.Field): raise forms.ValidationError(self.error_messages['invalid'], code='invalid') return value + + +# +# Choice fields +# + +class ChoiceField(forms.ChoiceField): + """ + Overrides Django's built-in `ChoiceField` to use NetBox's `StaticSelect` widget + """ + widget = widgets.StaticSelect + + +class MultipleChoiceField(forms.MultipleChoiceField): + """ + Overrides Django's built-in `MultipleChoiceField` to use NetBox's `StaticSelectMultiple` widget + """ + widget = widgets.StaticSelectMultiple diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index 66f95f01b..c27f3c646 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -88,9 +88,10 @@ class StaticSelect(forms.Select): class StaticSelectMultiple(StaticSelect, forms.SelectMultiple): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + """ + Extends `StaticSelect` to support multiple selections. + """ + pass class SelectWithPK(StaticSelect): diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index e8ba79cc8..2f386e889 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -7,7 +7,7 @@ from ipam.models import VRF from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms import ( - DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, + DynamicModelMultipleChoiceField, MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.choices import * from virtualization.models import * @@ -135,10 +135,9 @@ class VirtualMachineFilterForm( }, label=_('Role') ) - status = forms.MultipleChoiceField( + status = MultipleChoiceField( choices=VirtualMachineStatusChoices, - required=False, - widget=StaticSelectMultiple() + required=False ) platform_id = DynamicModelMultipleChoiceField( queryset=Platform.objects.all(),