Quellcode durchsuchen

DCIM filter forms select2

John Anderson vor 7 Jahren
Ursprung
Commit
bf8d57c7d1

+ 233 - 77
netbox/dcim/forms.py

@@ -15,11 +15,12 @@ from ipam.models import IPAddress, VLAN, VLANGroup
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
-    AnnotatedMultipleChoiceField, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
-    BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
-    ConfirmationForm, ContentTypeSelect, CSVChoiceField, ExpandableNameField, FilterChoiceField,
-    FilterTreeNodeMultipleChoiceField, FlexibleModelChoiceField, JSONField, Livesearch, SelectWithPK, SmallTextarea,
-    SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, COLOR_CHOICES,
+    AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple,
+    BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField,
+    ColorSelect, CommentField, ComponentForm, ConfirmationForm, ContentTypeSelect, CSVChoiceField, 
+    ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField, FlexibleModelChoiceField,
+    JSONField, Livesearch, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple,
+    BOOLEAN_WITH_BLANK_CHOICES, COLOR_CHOICES,
 
 
 )
 )
 from virtualization.models import Cluster, ClusterGroup
 from virtualization.models import Cluster, ClusterGroup
@@ -218,15 +219,22 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
     status = forms.ChoiceField(
     status = forms.ChoiceField(
         choices=add_blank_choice(SITE_STATUS_CHOICES),
         choices=add_blank_choice(SITE_STATUS_CHOICES),
         required=False,
         required=False,
-        initial=''
+        initial='',
+        widget=StaticSelect2()
     )
     )
     region = TreeNodeChoiceField(
     region = TreeNodeChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/regions/"
+        )
     )
     )
     tenant = forms.ModelChoiceField(
     tenant = forms.ModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenants",
+        )
     )
     )
     asn = forms.IntegerField(
     asn = forms.IntegerField(
         min_value=1,
         min_value=1,
@@ -240,7 +248,8 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
     )
     )
     time_zone = TimeZoneFormField(
     time_zone = TimeZoneFormField(
         choices=add_blank_choice(TimeZoneFormField().choices),
         choices=add_blank_choice(TimeZoneFormField().choices),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     )
 
 
     class Meta:
     class Meta:
@@ -259,18 +268,27 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
         choices=SITE_STATUS_CHOICES,
         choices=SITE_STATUS_CHOICES,
         annotate=Site.objects.all(),
         annotate=Site.objects.all(),
         annotate_field='status',
         annotate_field='status',
-        required=False
+        required=False,
+        widget=StaticSelect2Multiple()
     )
     )
-    region = FilterTreeNodeMultipleChoiceField(
+    region = forms.ModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
-        count_attr='site_count'
+        widget=APISelectMultiple(
+            api_url="/api/dcim/regions/",
+            value_field="slug",
+        )
     )
     )
     tenant = FilterChoiceField(
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(filter_count=Count('sites')),
         queryset=Tenant.objects.annotate(filter_count=Count('sites')),
         to_field_name='slug',
         to_field_name='slug',
-        null_label='-- None --'
+        null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/tenancy/tenants/",
+            value_field="slug",
+            null_option=True,
+        )
     )
     )
 
 
 
 
@@ -317,7 +335,11 @@ class RackGroupFilterForm(BootstrapMixin, forms.Form):
         queryset=Site.objects.annotate(
         queryset=Site.objects.annotate(
             filter_count=Count('rack_groups')
             filter_count=Count('rack_groups')
         ),
         ),
-        to_field_name='slug'
+        to_field_name='slug',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/sites/",
+            value_field="slug",
+        )
     )
     )
 
 
 
 
@@ -494,24 +516,40 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
     )
     )
     site = forms.ModelChoiceField(
     site = forms.ModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/sites",
+            filter_for={
+                'group': 'site_id',
+            }
+        )
     )
     )
     group = forms.ModelChoiceField(
     group = forms.ModelChoiceField(
         queryset=RackGroup.objects.all(),
         queryset=RackGroup.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/rack-groups",
+        )
     )
     )
     tenant = forms.ModelChoiceField(
     tenant = forms.ModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenants",
+        )
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
         choices=add_blank_choice(RACK_STATUS_CHOICES),
         choices=add_blank_choice(RACK_STATUS_CHOICES),
         required=False,
         required=False,
-        initial=''
+        initial='',
+        widget=StaticSelect2()
     )
     )
     role = forms.ModelChoiceField(
     role = forms.ModelChoiceField(
         queryset=RackRole.objects.all(),
         queryset=RackRole.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/rack-roles",
+        )
     )
     )
     serial = forms.CharField(
     serial = forms.CharField(
         max_length=50,
         max_length=50,
@@ -524,11 +562,13 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
         choices=add_blank_choice(RACK_TYPE_CHOICES),
         choices=add_blank_choice(RACK_TYPE_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     )
     width = forms.ChoiceField(
     width = forms.ChoiceField(
         choices=add_blank_choice(RACK_WIDTH_CHOICES),
         choices=add_blank_choice(RACK_WIDTH_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     )
     u_height = forms.IntegerField(
     u_height = forms.IntegerField(
         required=False,
         required=False,
@@ -549,7 +589,8 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
     )
     )
     outer_unit = forms.ChoiceField(
     outer_unit = forms.ChoiceField(
         choices=add_blank_choice(RACK_DIMENSION_UNIT_CHOICES),
         choices=add_blank_choice(RACK_DIMENSION_UNIT_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     )
     comments = CommentField(
     comments = CommentField(
         widget=SmallTextarea
         widget=SmallTextarea
@@ -571,7 +612,11 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
         queryset=Site.objects.annotate(
         queryset=Site.objects.annotate(
             filter_count=Count('racks')
             filter_count=Count('racks')
         ),
         ),
-        to_field_name='slug'
+        to_field_name='slug',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/sites/",
+            value_field="slug",
+        )
     )
     )
     group_id = FilterChoiceField(
     group_id = FilterChoiceField(
         queryset=RackGroup.objects.select_related(
         queryset=RackGroup.objects.select_related(
@@ -580,27 +625,42 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
             filter_count=Count('racks')
             filter_count=Count('racks')
         ),
         ),
         label='Rack group',
         label='Rack group',
-        null_label='-- None --'
+        null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/rack-groups/",
+            null_option=True,
+        )
     )
     )
     tenant = FilterChoiceField(
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(
         queryset=Tenant.objects.annotate(
             filter_count=Count('racks')
             filter_count=Count('racks')
         ),
         ),
         to_field_name='slug',
         to_field_name='slug',
-        null_label='-- None --'
+        null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/tenancy/tenants/",
+            value_field="slug",
+            null_option=True,
+        )
     )
     )
     status = AnnotatedMultipleChoiceField(
     status = AnnotatedMultipleChoiceField(
         choices=RACK_STATUS_CHOICES,
         choices=RACK_STATUS_CHOICES,
         annotate=Rack.objects.all(),
         annotate=Rack.objects.all(),
         annotate_field='status',
         annotate_field='status',
-        required=False
+        required=False,
+        widget=StaticSelect2Multiple()
     )
     )
     role = FilterChoiceField(
     role = FilterChoiceField(
         queryset=RackRole.objects.annotate(
         queryset=RackRole.objects.annotate(
             filter_count=Count('racks')
             filter_count=Count('racks')
         ),
         ),
         to_field_name='slug',
         to_field_name='slug',
-        null_label='-- None --'
+        null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/rack-roles/",
+            value_field="slug",
+            null_option=True,
+        )
     )
     )
 
 
 
 
@@ -620,7 +680,8 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
     user = forms.ModelChoiceField(
     user = forms.ModelChoiceField(
         queryset=User.objects.order_by(
         queryset=User.objects.order_by(
             'username'
             'username'
-        )
+        ),
+        widget=StaticSelect2()
     )
     )
 
 
     class Meta:
     class Meta:
@@ -655,7 +716,11 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
         queryset=Site.objects.annotate(
         queryset=Site.objects.annotate(
             filter_count=Count('racks__reservations')
             filter_count=Count('racks__reservations')
         ),
         ),
-        to_field_name='slug'
+        to_field_name='slug',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/sites/",
+            value_field="slug",
+        )
     )
     )
     group_id = FilterChoiceField(
     group_id = FilterChoiceField(
         queryset=RackGroup.objects.select_related(
         queryset=RackGroup.objects.select_related(
@@ -664,14 +729,23 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
             filter_count=Count('racks__reservations')
             filter_count=Count('racks__reservations')
         ),
         ),
         label='Rack group',
         label='Rack group',
-        null_label='-- None --'
+        null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/rack-groups/",
+            null_option=True,
+        )
     )
     )
     tenant = FilterChoiceField(
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(
         queryset=Tenant.objects.annotate(
             filter_count=Count('rackreservations')
             filter_count=Count('rackreservations')
         ),
         ),
         to_field_name='slug',
         to_field_name='slug',
-        null_label='-- None --'
+        null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/tenancy/tenants/",
+            value_field="slug",
+            null_option=True,
+        )
     )
     )
 
 
 
 
@@ -684,11 +758,15 @@ class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
         queryset=User.objects.order_by(
         queryset=User.objects.order_by(
             'username'
             'username'
         ),
         ),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     )
     tenant = forms.ModelChoiceField(
     tenant = forms.ModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenant",
+        )
     )
     )
     description = forms.CharField(
     description = forms.CharField(
         max_length=100,
         max_length=100,
@@ -782,7 +860,10 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkE
     )
     )
     manufacturer = forms.ModelChoiceField(
     manufacturer = forms.ModelChoiceField(
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/manufactureres"
+        )
     )
     )
     u_height = forms.IntegerField(
     u_height = forms.IntegerField(
         min_value=1,
         min_value=1,
@@ -808,54 +889,58 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
         queryset=Manufacturer.objects.annotate(
         queryset=Manufacturer.objects.annotate(
             filter_count=Count('device_types')
             filter_count=Count('device_types')
         ),
         ),
-        to_field_name='slug'
+        to_field_name='slug',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/manufacturers/",
+            value_field="slug",
+        )
     )
     )
     subdevice_role = forms.NullBooleanField(
     subdevice_role = forms.NullBooleanField(
         required=False,
         required=False,
         label='Subdevice role',
         label='Subdevice role',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=add_blank_choice(SUBDEVICE_ROLE_CHOICES)
             choices=add_blank_choice(SUBDEVICE_ROLE_CHOICES)
         )
         )
     )
     )
     console_ports = forms.NullBooleanField(
     console_ports = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has console ports',
         label='Has console ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
     console_server_ports = forms.NullBooleanField(
     console_server_ports = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has console server ports',
         label='Has console server ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
     power_ports = forms.NullBooleanField(
     power_ports = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has power ports',
         label='Has power ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
     power_outlets = forms.NullBooleanField(
     power_outlets = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has power outlets',
         label='Has power outlets',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
     interfaces = forms.NullBooleanField(
     interfaces = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has interfaces',
         label='Has interfaces',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
     pass_through_ports = forms.NullBooleanField(
     pass_through_ports = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has pass-through ports',
         label='Has pass-through ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
@@ -971,7 +1056,8 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
     )
     )
     form_factor = forms.ChoiceField(
     form_factor = forms.ChoiceField(
         choices=add_blank_choice(IFACE_FF_CHOICES),
         choices=add_blank_choice(IFACE_FF_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     )
     mgmt_only = forms.NullBooleanField(
     mgmt_only = forms.NullBooleanField(
         required=False,
         required=False,
@@ -1001,7 +1087,8 @@ class FrontPortTemplateCreateForm(ComponentForm):
         label='Name'
         label='Name'
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
-        choices=PORT_TYPE_CHOICES
+        choices=PORT_TYPE_CHOICES,
+        widget=StaticSelect2()
     )
     )
     rear_port_set = forms.MultipleChoiceField(
     rear_port_set = forms.MultipleChoiceField(
         choices=[],
         choices=[],
@@ -1539,25 +1626,38 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
     device_type = forms.ModelChoiceField(
     device_type = forms.ModelChoiceField(
         queryset=DeviceType.objects.all(),
         queryset=DeviceType.objects.all(),
         required=False,
         required=False,
-        label='Type'
+        label='Type',
+        widget=APISelect(
+            api_url="/api/dcim/device-types"
+        )
     )
     )
     device_role = forms.ModelChoiceField(
     device_role = forms.ModelChoiceField(
         queryset=DeviceRole.objects.all(),
         queryset=DeviceRole.objects.all(),
         required=False,
         required=False,
-        label='Role'
+        label='Role',
+        widget=APISelect(
+            api_url="/api/dcim/device-roles"
+        )
     )
     )
     tenant = forms.ModelChoiceField(
     tenant = forms.ModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenants"
+        )
     )
     )
     platform = forms.ModelChoiceField(
     platform = forms.ModelChoiceField(
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/platforms"
+        )
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
         choices=add_blank_choice(DEVICE_STATUS_CHOICES),
         choices=add_blank_choice(DEVICE_STATUS_CHOICES),
         required=False,
         required=False,
-        initial=''
+        initial='',
+        widget=StaticSelect2()
     )
     )
     serial = forms.CharField(
     serial = forms.CharField(
         max_length=50,
         max_length=50,
@@ -1577,16 +1677,31 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         required=False,
         required=False,
         label='Search'
         label='Search'
     )
     )
-    region = FilterTreeNodeMultipleChoiceField(
+    region = FilterChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         required=False,
         required=False,
+        widget=APISelectMultiple(
+            api_url="/api/dcim/regions/",
+            value_field="slug",
+            filter_for={
+                'site': 'region'
+            }
+        )
     )
     )
     site = FilterChoiceField(
     site = FilterChoiceField(
         queryset=Site.objects.annotate(
         queryset=Site.objects.annotate(
             filter_count=Count('devices')
             filter_count=Count('devices')
         ),
         ),
         to_field_name='slug',
         to_field_name='slug',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/sites/",
+            value_field="slug",
+            filter_for={
+                'rack_group_id': 'site',
+                'rack_id': 'site',
+            }
+        )
     )
     )
     rack_group_id = FilterChoiceField(
     rack_group_id = FilterChoiceField(
         queryset=RackGroup.objects.select_related(
         queryset=RackGroup.objects.select_related(
@@ -1595,6 +1710,12 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
             filter_count=Count('racks__devices')
             filter_count=Count('racks__devices')
         ),
         ),
         label='Rack group',
         label='Rack group',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/rack-groups/",
+            filter_for={
+                'rack_id': 'rack_group_id',
+            }
+        )
     )
     )
     rack_id = FilterChoiceField(
     rack_id = FilterChoiceField(
         queryset=Rack.objects.annotate(
         queryset=Rack.objects.annotate(
@@ -1602,12 +1723,21 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         ),
         ),
         label='Rack',
         label='Rack',
         null_label='-- None --',
         null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/racks/",
+            null_option=True,
+        )
     )
     )
     role = FilterChoiceField(
     role = FilterChoiceField(
         queryset=DeviceRole.objects.annotate(
         queryset=DeviceRole.objects.annotate(
             filter_count=Count('devices')
             filter_count=Count('devices')
         ),
         ),
         to_field_name='slug',
         to_field_name='slug',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/device-roles/",
+            value_field="slug",
+            null_option=True,
+        )
     )
     )
     tenant = FilterChoiceField(
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(
         queryset=Tenant.objects.annotate(
@@ -1615,10 +1745,21 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         ),
         ),
         to_field_name='slug',
         to_field_name='slug',
         null_label='-- None --',
         null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/tenancy/tenants/",
+            value_field="slug",
+            null_option=True,
+        )
     )
     )
     manufacturer_id = FilterChoiceField(
     manufacturer_id = FilterChoiceField(
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
-        label='Manufacturer'
+        label='Manufacturer',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/manufacturers/",
+            filter_for={
+                'device_type_id': 'manufacturer_id',
+            }
+        )
     )
     )
     device_type_id = FilterChoiceField(
     device_type_id = FilterChoiceField(
         queryset=DeviceType.objects.select_related(
         queryset=DeviceType.objects.select_related(
@@ -1629,6 +1770,10 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
             filter_count=Count('instances'),
             filter_count=Count('instances'),
         ),
         ),
         label='Model',
         label='Model',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/device-types/",
+            display_field="model",
+        )
     )
     )
     platform = FilterChoiceField(
     platform = FilterChoiceField(
         queryset=Platform.objects.annotate(
         queryset=Platform.objects.annotate(
@@ -1636,12 +1781,18 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         ),
         ),
         to_field_name='slug',
         to_field_name='slug',
         null_label='-- None --',
         null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/platforms/",
+            value_field="slug",
+            null_option=True,
+        )
     )
     )
     status = AnnotatedMultipleChoiceField(
     status = AnnotatedMultipleChoiceField(
         choices=DEVICE_STATUS_CHOICES,
         choices=DEVICE_STATUS_CHOICES,
         annotate=Device.objects.all(),
         annotate=Device.objects.all(),
         annotate_field='status',
         annotate_field='status',
-        required=False
+        required=False,
+        widget=StaticSelect2Multiple()
     )
     )
     mac_address = forms.CharField(
     mac_address = forms.CharField(
         required=False,
         required=False,
@@ -1650,49 +1801,49 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     has_primary_ip = forms.NullBooleanField(
     has_primary_ip = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has a primary IP',
         label='Has a primary IP',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
     console_ports = forms.NullBooleanField(
     console_ports = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has console ports',
         label='Has console ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
     console_server_ports = forms.NullBooleanField(
     console_server_ports = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has console server ports',
         label='Has console server ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
     power_ports = forms.NullBooleanField(
     power_ports = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has power ports',
         label='Has power ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
     power_outlets = forms.NullBooleanField(
     power_outlets = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has power outlets',
         label='Has power outlets',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
     interfaces = forms.NullBooleanField(
     interfaces = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has interfaces',
         label='Has interfaces',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
     pass_through_ports = forms.NullBooleanField(
     pass_through_ports = forms.NullBooleanField(
         required=False,
         required=False,
         label='Has pass-through ports',
         label='Has pass-through ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
@@ -1714,7 +1865,8 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
 
 
 class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
 class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
     form_factor = forms.ChoiceField(
     form_factor = forms.ChoiceField(
-        choices=IFACE_FF_CHOICES
+        choices=IFACE_FF_CHOICES,
+        widget=StaticSelect2()
     )
     )
     enabled = forms.BooleanField(
     enabled = forms.BooleanField(
         required=False,
         required=False,
@@ -1941,7 +2093,7 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
     vlans = forms.MultipleChoiceField(
     vlans = forms.MultipleChoiceField(
         choices=[],
         choices=[],
         label='VLANs',
         label='VLANs',
-        widget=forms.SelectMultiple(
+        widget=StaticSelect2Multiple(
             attrs={
             attrs={
                 'size': 20,
                 'size': 20,
             }
             }
@@ -2093,7 +2245,8 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
     )
     )
     form_factor = forms.ChoiceField(
     form_factor = forms.ChoiceField(
         choices=add_blank_choice(IFACE_FF_CHOICES),
         choices=add_blank_choice(IFACE_FF_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     )
     enabled = forms.NullBooleanField(
     enabled = forms.NullBooleanField(
         required=False,
         required=False,
@@ -2102,7 +2255,8 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
     lag = forms.ModelChoiceField(
     lag = forms.ModelChoiceField(
         queryset=Interface.objects.all(),
         queryset=Interface.objects.all(),
         required=False,
         required=False,
-        label='Parent LAG'
+        label='Parent LAG',
+        widget=StaticSelect2()
     )
     )
     mtu = forms.IntegerField(
     mtu = forms.IntegerField(
         required=False,
         required=False,
@@ -2121,7 +2275,8 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
     )
     )
     mode = forms.ChoiceField(
     mode = forms.ChoiceField(
         choices=add_blank_choice(IFACE_MODE_CHOICES),
         choices=add_blank_choice(IFACE_MODE_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     )
 
 
     class Meta:
     class Meta:
@@ -2199,7 +2354,7 @@ class FrontPortCreateForm(ComponentForm):
     rear_port_set = forms.MultipleChoiceField(
     rear_port_set = forms.MultipleChoiceField(
         choices=[],
         choices=[],
         label='Rear ports',
         label='Rear ports',
-        help_text='Select one rear port assignment for each front port being created.'
+        help_text='Select one rear port assignment for each front port being created.',
     )
     )
     description = forms.CharField(
     description = forms.CharField(
         required=False
         required=False
@@ -2546,7 +2701,8 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
     type = forms.ChoiceField(
     type = forms.ChoiceField(
         choices=add_blank_choice(CABLE_TYPE_CHOICES),
         choices=add_blank_choice(CABLE_TYPE_CHOICES),
         required=False,
         required=False,
-        initial=''
+        initial='',
+        widget=StaticSelect2()
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
         choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
         choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
@@ -2555,7 +2711,8 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
     )
     )
     label = forms.CharField(
     label = forms.CharField(
         max_length=100,
         max_length=100,
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     )
     color = forms.CharField(
     color = forms.CharField(
         max_length=6,
         max_length=6,
@@ -2569,7 +2726,8 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
     length_unit = forms.ChoiceField(
     length_unit = forms.ChoiceField(
         choices=add_blank_choice(CABLE_LENGTH_UNIT_CHOICES),
         choices=add_blank_choice(CABLE_LENGTH_UNIT_CHOICES),
         required=False,
         required=False,
-        initial=''
+        initial='',
+        widget=StaticSelect2()
     )
     )
 
 
     class Meta:
     class Meta:
@@ -2594,17 +2752,15 @@ class CableFilterForm(BootstrapMixin, forms.Form):
         required=False,
         required=False,
         label='Search'
         label='Search'
     )
     )
-    type = AnnotatedMultipleChoiceField(
+    type = forms.MultipleChoiceField(
         choices=CABLE_TYPE_CHOICES,
         choices=CABLE_TYPE_CHOICES,
-        annotate=Cable.objects.all(),
-        annotate_field='type',
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     )
-    color = AnnotatedMultipleChoiceField(
-        choices=COLOR_CHOICES,
-        annotate=Cable.objects.all(),
-        annotate_field='color',
-        required=False
+    color = forms.CharField(
+        max_length=6,
+        required=False,
+        widget=ColorSelect()
     )
     )
 
 
 
 

+ 38 - 9
netbox/project-static/js/forms.js

@@ -67,6 +67,7 @@ $(document).ready(function() {
         form.submit();
         form.submit();
     });
     });
 
 
+    // Parse URLs which may contain variable refrences to other field values
     function parseURL(url) {
     function parseURL(url) {
         var filter_regex = /\{\{([a-z_]+)\}\}/g;
         var filter_regex = /\{\{([a-z_]+)\}\}/g;
         var match;
         var match;
@@ -86,8 +87,8 @@ $(document).ready(function() {
         return rendered_url
         return rendered_url
     }
     }
 
 
+    // Assign color picker selection classes
     function colorPickerClassCopy(data, container) {
     function colorPickerClassCopy(data, container) {
-        console.log("hello");
         if (data.element) {
         if (data.element) {
             $(container).addClass($(data.element).attr("class"));
             $(container).addClass($(data.element).attr("class"));
         }
         }
@@ -108,23 +109,27 @@ $(document).ready(function() {
         placeholder: "---------",
         placeholder: "---------",
     })
     })
 
 
-    // API backed single selection
+    // API backed selection
     // Includes live search and chained fields
     // Includes live search and chained fields
+    // The `multiple` setting may be controled via a data-* attribute
     $('.netbox-select2-api').select2({
     $('.netbox-select2-api').select2({
         allowClear: true,
         allowClear: true,
         placeholder: "---------",
         placeholder: "---------",
+
         ajax: {
         ajax: {
             delay: 500,
             delay: 500,
+
             url: function(params) {
             url: function(params) {
                 var element = this[0];
                 var element = this[0];
-                var url = element.getAttribute("data-url");
-                url = parseURL(url);
+                var url = parseURL(element.getAttribute("data-url"));
+
                 if (url.includes("{{")) {
                 if (url.includes("{{")) {
-                    // URL is not furry rendered yet, abort the request
-                    return null;
+                    // URL is not fully rendered yet, abort the request
+                    return false;
                 }
                 }
                 return url;
                 return url;
             },
             },
+
             data: function(params) {
             data: function(params) {
                 var element = this[0];
                 var element = this[0];
                 // Paging
                 // Paging
@@ -136,29 +141,35 @@ $(document).ready(function() {
                     limit: 50,
                     limit: 50,
                     offset: offset,
                     offset: offset,
                 };
                 };
+
                 // filter-for fields from a chain
                 // filter-for fields from a chain
                 var attr_name = "data-filter-for-" + $(element).attr("name");
                 var attr_name = "data-filter-for-" + $(element).attr("name");
                 var form = $(element).closest('form');
                 var form = $(element).closest('form');
                 var filter_for_elements = form.find("select[" + attr_name + "]");
                 var filter_for_elements = form.find("select[" + attr_name + "]");
+
                 filter_for_elements.each(function(index, filter_for_element) {
                 filter_for_elements.each(function(index, filter_for_element) {
                     var param_name = $(filter_for_element).attr(attr_name);
                     var param_name = $(filter_for_element).attr(attr_name);
                     var value = $(filter_for_element).val();
                     var value = $(filter_for_element).val();
+
                     if (param_name && value) {
                     if (param_name && value) {
-                        parameters[param_name] = $(filter_for_element).val();
+                        parameters[param_name] = value;
                     }
                     }
                 });
                 });
+
                 // Conditional query params
                 // Conditional query params
                 $.each(element.attributes, function(index, attr){
                 $.each(element.attributes, function(index, attr){
                     if (attr.name.includes("data-conditional-query-param-")){
                     if (attr.name.includes("data-conditional-query-param-")){
                         var conditional = attr.name.split("data-conditional-query-param-")[1].split("__");
                         var conditional = attr.name.split("data-conditional-query-param-")[1].split("__");
                         var field = $("#id_" + conditional[0]);
                         var field = $("#id_" + conditional[0]);
                         var field_value = conditional[1];
                         var field_value = conditional[1];
+                        
                         if ($('option:selected', field).attr('api-value') === field_value){
                         if ($('option:selected', field).attr('api-value') === field_value){
                             var _val = attr.value.split("=");
                             var _val = attr.value.split("=");
                             parameters[_val[0]] = _val[1];
                             parameters[_val[0]] = _val[1];
                         }
                         }
                     }
                     }
                 })
                 })
+
                 // Additional query params
                 // Additional query params
                 $.each(element.attributes, function(index, attr){
                 $.each(element.attributes, function(index, attr){
                     if (attr.name.includes("data-additional-query-param-")){
                     if (attr.name.includes("data-additional-query-param-")){
@@ -166,14 +177,28 @@ $(document).ready(function() {
                         parameters[param_name] = attr.value;
                         parameters[param_name] = attr.value;
                     }
                     }
                 })
                 })
-                return parameters;
+
+                // This will handle params with multiple values (i.e. for list filter forms)
+                return $.param(parameters, true);
             },
             },
+
             processResults: function (data) {
             processResults: function (data) {
                 var element = this.$element[0];
                 var element = this.$element[0];
                 var results = $.map(data.results, function (obj) {
                 var results = $.map(data.results, function (obj) {
-                    obj.text = obj.name || obj[element.getAttribute('display-field')];
+                    obj.text = obj[element.getAttribute('display-field')] || obj.name;
+                    obj.id = obj[element.getAttribute('value-field')] || obj.id;
                     return obj;
                     return obj;
                 });
                 });
+
+                // Handle the null option
+                if (element.getAttribute('data-null-option')) {
+                    var null_option = $(element).children()[0]
+                    results.unshift({
+                        id: null_option.value,
+                        text: null_option.text
+                    });
+                }
+
                 // Check if there are more results to page
                 // Check if there are more results to page
                 var page = data.next !== null;
                 var page = data.next !== null;
                 return {
                 return {
@@ -208,9 +233,11 @@ $(document).ready(function() {
         multiple: true,
         multiple: true,
         allowClear: true,
         allowClear: true,
         placeholder: "Tags",
         placeholder: "Tags",
+
         ajax: {
         ajax: {
             delay: 250,
             delay: 250,
             url: "/api/extras/tags/",
             url: "/api/extras/tags/",
+
             data: function(params) {
             data: function(params) {
                 // paging
                 // paging
                 var offset = params.page * 50 || 0;
                 var offset = params.page * 50 || 0;
@@ -222,6 +249,7 @@ $(document).ready(function() {
                 };
                 };
                 return parameters;
                 return parameters;
             },
             },
+
             processResults: function (data) {
             processResults: function (data) {
                 var results = $.map(data.results, function (obj) {
                 var results = $.map(data.results, function (obj) {
                     return {
                     return {
@@ -229,6 +257,7 @@ $(document).ready(function() {
                         text: obj.name
                         text: obj.name
                     }
                     }
                 });
                 });
+
                 // Check if there are more results to page
                 // Check if there are more results to page
                 var page = data.next !== null;
                 var page = data.next !== null;
                 return {
                 return {

+ 0 - 85
netbox/templates/dcim/device_list.html

@@ -20,88 +20,3 @@
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
-
-{% block javascript %}
-<script type="text/javascript">
-$(document).ready(function() {
-
-    var site_list = $('#id_site');
-    var rack_group_list = $('#id_rack_group_id');
-    var rack_list = $('#id_rack_id');
-    var manufacturer_list = $('#id_manufacturer_id');
-    var model_list = $('#id_device_type_id');
-
-    // Update device type options based on selected manufacturer
-    manufacturer_list.change(function() {
-        var selected_manufacturers = $(this).val();
-        if (selected_manufacturers) {
-            model_list.empty();
-            $.ajax({
-                url: netbox_api_path + 'dcim/device-types/?limit=500&manufacturer_id=' + selected_manufacturers.join('&manufacturer_id='),
-                dataType: 'json',
-                success: function (response, status) {
-                    $.each(response["results"], function (index, device_type) {
-                        var option = $("<option></option>").attr("value", device_type.id).text(device_type.model + " (" + device_type.instance_count + ")");
-                        model_list.append(option);
-                    });
-                }
-            });
-        }
-    });
-
-    // Update rack group and rack options based on selected site
-    site_list.change(function() {
-        var selected_sites = $(this).val();
-        if (selected_sites) {
-
-            // Update rack group options
-            rack_group_list.empty();
-            $.ajax({
-                url: netbox_api_path + 'dcim/rack-groups/?limit=500&site=' + selected_sites.join('&site='),
-                dataType: 'json',
-                success: function (response, status) {
-                    $.each(response["results"], function (index, group) {
-                        var option = $("<option></option>").attr("value", group.id).text(group.name);
-                        rack_group_list.append(option);
-                    });
-                }
-            });
-
-            // Update rack options
-            rack_list.empty();
-            rack_list.append($("<option></option>").attr("value", "0").text("None"));
-            $.ajax({
-                url: netbox_api_path + 'dcim/racks/?limit=500&site=' + selected_sites.join('&site='),
-                dataType: 'json',
-                success: function (response, status) {
-                    $.each(response["results"], function (index, rack) {
-                        var option = $("<option></option>").attr("value", rack.id).text(rack.display_name);
-                        rack_list.append(option);
-                    });
-                }
-            });
-
-        }
-    });
-
-    // Update rack options based on selected rack group
-    rack_group_list.change(function() {
-        var selected_rack_groups = $(this).val();
-        if (selected_rack_groups) {
-            rack_list.empty();
-            $.ajax({
-                url: netbox_api_path + 'dcim/racks/?limit=500&group_id=' + selected_rack_groups.join('&group_id='),
-                dataType: 'json',
-                success: function (response, status) {
-                    $.each(response["results"], function (index, rack) {
-                        var option = $("<option></option>").attr("value", rack.id).text(rack.display_name);
-                        rack_list.append(option);
-                    });
-                }
-            });
-        }
-    });
-
-});
-</script>
-{% endblock %}

+ 0 - 29
netbox/templates/dcim/inc/filter_rack_group.html

@@ -1,29 +0,0 @@
-<script type="text/javascript">
-$(document).ready(function() {
-
-    var site_list = $('#id_site');
-    var rack_group_list = $('#id_group_id');
-
-    // Update rack group and rack options based on selected site
-    site_list.change(function() {
-        var selected_sites = $(this).val();
-        if (selected_sites) {
-
-            // Update rack group options
-            rack_group_list.empty();
-            $.ajax({
-                url: netbox_api_path + 'dcim/rack-groups/?limit=500&site=' + selected_sites.join('&site='),
-                dataType: 'json',
-                success: function (response, status) {
-                    $.each(response["results"], function (index, group) {
-                        var option = $("<option></option>").attr("value", group.id).text(group.name);
-                        rack_group_list.append(option);
-                    });
-                }
-            });
-
-        }
-    });
-
-});
-</script>

+ 0 - 5
netbox/templates/dcim/rack_list.html

@@ -20,8 +20,3 @@
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
-
-{% block javascript %}
-    {% include 'dcim/inc/filter_rack_group.html' %}
-{% endblock %}
-

+ 25 - 4
netbox/utilities/forms.py

@@ -186,6 +186,7 @@ class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
             ('2', 'Yes'),
             ('2', 'Yes'),
             ('3', 'No'),
             ('3', 'No'),
         )
         )
+        self.attrs['class'] = 'netbox-select2-static'
 
 
 
 
 class SelectWithDisabled(forms.Select):
 class SelectWithDisabled(forms.Select):
@@ -223,6 +224,14 @@ class StaticSelect2(SelectWithDisabled):
         self.attrs['data-filter-for-{}'.format(name)] = value
         self.attrs['data-filter-for-{}'.format(name)] = value
 
 
 
 
+class StaticSelect2Multiple(StaticSelect2, forms.SelectMultiple):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        self.attrs['data-multiple'] = 1
+
+
 class SelectWithPK(StaticSelect2):
 class SelectWithPK(StaticSelect2):
     """
     """
     Include the primary key of each option in the option label (e.g. "Router7 (4721)").
     Include the primary key of each option in the option label (e.g. "Router7 (4721)").
@@ -265,6 +274,7 @@ class APISelect(SelectWithDisabled):
 
 
     :param api_url: API URL
     :param api_url: API URL
     :param display_field: (Optional) Field to display for child in selection list. Defaults to `name`.
     :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 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
     :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.
         name of the filter-for field (child field) and the value is the name of the query param filter.
@@ -273,18 +283,21 @@ class APISelect(SelectWithDisabled):
         If the provided field value is selected for the given field, the URL query param will be appended to
         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 `<param_name>=<param_value>`. This is useful in cases where
         the rendered URL. The value is the in the from `<param_name>=<param_value>`. This is useful in cases where
         a particular field value dictates an additional API filter.
         a particular field value dictates an additional API filter.
-    :param additional_query_params: 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 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__(
     def __init__(
         self,
         self,
         api_url,
         api_url,
         display_field=None,
         display_field=None,
+        value_field=None,
         disabled_indicator=None,
         disabled_indicator=None,
         filter_for=None,
         filter_for=None,
         conditional_query_params=None,
         conditional_query_params=None,
         additional_query_params=None,
         additional_query_params=None,
+        null_option=False,
         *args,
         *args,
         **kwargs
         **kwargs
     ):
     ):
@@ -295,6 +308,8 @@ class APISelect(SelectWithDisabled):
         self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/'))  # Inject BASE_PATH
         self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/'))  # Inject BASE_PATH
         if display_field:
         if display_field:
             self.attrs['display-field'] = display_field
             self.attrs['display-field'] = display_field
+        if value_field:
+            self.attrs['value-field'] = value_field
         if disabled_indicator:
         if disabled_indicator:
             self.attrs['disabled-indicator'] = disabled_indicator
             self.attrs['disabled-indicator'] = disabled_indicator
         if filter_for:
         if filter_for:
@@ -306,6 +321,8 @@ class APISelect(SelectWithDisabled):
         if additional_query_params:
         if additional_query_params:
             for key, value in additional_query_params.items():
             for key, value in additional_query_params.items():
                 self.add_additional_query_param(key, value)
                 self.add_additional_query_param(key, value)
+        if null_option:
+            self.attrs['data-null-option'] = 1
 
 
     def add_filter_for(self, name, value):
     def add_filter_for(self, name, value):
         """
         """
@@ -336,8 +353,12 @@ class APISelect(SelectWithDisabled):
         self.attrs['data-conditional-query-param-{}'.format(condition)] = value
         self.attrs['data-conditional-query-param-{}'.format(condition)] = value
 
 
 
 
-class APISelectMultiple(APISelect):
-    allow_multiple_selected = True
+class APISelectMultiple(APISelect, forms.SelectMultiple):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        self.attrs['data-multiple'] = 1
 
 
 
 
 class Livesearch(forms.TextInput):
 class Livesearch(forms.TextInput):