Przeglądaj źródła

DCIM filter forms select2

John Anderson 7 lat temu
rodzic
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.models import Tenant
 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
@@ -218,15 +219,22 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
     status = forms.ChoiceField(
         choices=add_blank_choice(SITE_STATUS_CHOICES),
         required=False,
-        initial=''
+        initial='',
+        widget=StaticSelect2()
     )
     region = TreeNodeChoiceField(
         queryset=Region.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/regions/"
+        )
     )
     tenant = forms.ModelChoiceField(
         queryset=Tenant.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenants",
+        )
     )
     asn = forms.IntegerField(
         min_value=1,
@@ -240,7 +248,8 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
     )
     time_zone = TimeZoneFormField(
         choices=add_blank_choice(TimeZoneFormField().choices),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
 
     class Meta:
@@ -259,18 +268,27 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
         choices=SITE_STATUS_CHOICES,
         annotate=Site.objects.all(),
         annotate_field='status',
-        required=False
+        required=False,
+        widget=StaticSelect2Multiple()
     )
-    region = FilterTreeNodeMultipleChoiceField(
+    region = forms.ModelMultipleChoiceField(
         queryset=Region.objects.all(),
         to_field_name='slug',
         required=False,
-        count_attr='site_count'
+        widget=APISelectMultiple(
+            api_url="/api/dcim/regions/",
+            value_field="slug",
+        )
     )
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(filter_count=Count('sites')),
         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(
             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(
         queryset=Site.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/sites",
+            filter_for={
+                'group': 'site_id',
+            }
+        )
     )
     group = forms.ModelChoiceField(
         queryset=RackGroup.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/rack-groups",
+        )
     )
     tenant = forms.ModelChoiceField(
         queryset=Tenant.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenants",
+        )
     )
     status = forms.ChoiceField(
         choices=add_blank_choice(RACK_STATUS_CHOICES),
         required=False,
-        initial=''
+        initial='',
+        widget=StaticSelect2()
     )
     role = forms.ModelChoiceField(
         queryset=RackRole.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/rack-roles",
+        )
     )
     serial = forms.CharField(
         max_length=50,
@@ -524,11 +562,13 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
     )
     type = forms.ChoiceField(
         choices=add_blank_choice(RACK_TYPE_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     width = forms.ChoiceField(
         choices=add_blank_choice(RACK_WIDTH_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     u_height = forms.IntegerField(
         required=False,
@@ -549,7 +589,8 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
     )
     outer_unit = forms.ChoiceField(
         choices=add_blank_choice(RACK_DIMENSION_UNIT_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     comments = CommentField(
         widget=SmallTextarea
@@ -571,7 +612,11 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
         queryset=Site.objects.annotate(
             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(
         queryset=RackGroup.objects.select_related(
@@ -580,27 +625,42 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
             filter_count=Count('racks')
         ),
         label='Rack group',
-        null_label='-- None --'
+        null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/rack-groups/",
+            null_option=True,
+        )
     )
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(
             filter_count=Count('racks')
         ),
         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(
         choices=RACK_STATUS_CHOICES,
         annotate=Rack.objects.all(),
         annotate_field='status',
-        required=False
+        required=False,
+        widget=StaticSelect2Multiple()
     )
     role = FilterChoiceField(
         queryset=RackRole.objects.annotate(
             filter_count=Count('racks')
         ),
         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(
         queryset=User.objects.order_by(
             'username'
-        )
+        ),
+        widget=StaticSelect2()
     )
 
     class Meta:
@@ -655,7 +716,11 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
         queryset=Site.objects.annotate(
             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(
         queryset=RackGroup.objects.select_related(
@@ -664,14 +729,23 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
             filter_count=Count('racks__reservations')
         ),
         label='Rack group',
-        null_label='-- None --'
+        null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/rack-groups/",
+            null_option=True,
+        )
     )
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(
             filter_count=Count('rackreservations')
         ),
         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(
             'username'
         ),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     tenant = forms.ModelChoiceField(
         queryset=Tenant.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenant",
+        )
     )
     description = forms.CharField(
         max_length=100,
@@ -782,7 +860,10 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkE
     )
     manufacturer = forms.ModelChoiceField(
         queryset=Manufacturer.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/manufactureres"
+        )
     )
     u_height = forms.IntegerField(
         min_value=1,
@@ -808,54 +889,58 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
         queryset=Manufacturer.objects.annotate(
             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(
         required=False,
         label='Subdevice role',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=add_blank_choice(SUBDEVICE_ROLE_CHOICES)
         )
     )
     console_ports = forms.NullBooleanField(
         required=False,
         label='Has console ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     console_server_ports = forms.NullBooleanField(
         required=False,
         label='Has console server ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     power_ports = forms.NullBooleanField(
         required=False,
         label='Has power ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     power_outlets = forms.NullBooleanField(
         required=False,
         label='Has power outlets',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     interfaces = forms.NullBooleanField(
         required=False,
         label='Has interfaces',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     pass_through_ports = forms.NullBooleanField(
         required=False,
         label='Has pass-through ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
@@ -971,7 +1056,8 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
     )
     form_factor = forms.ChoiceField(
         choices=add_blank_choice(IFACE_FF_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     mgmt_only = forms.NullBooleanField(
         required=False,
@@ -1001,7 +1087,8 @@ class FrontPortTemplateCreateForm(ComponentForm):
         label='Name'
     )
     type = forms.ChoiceField(
-        choices=PORT_TYPE_CHOICES
+        choices=PORT_TYPE_CHOICES,
+        widget=StaticSelect2()
     )
     rear_port_set = forms.MultipleChoiceField(
         choices=[],
@@ -1539,25 +1626,38 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
     device_type = forms.ModelChoiceField(
         queryset=DeviceType.objects.all(),
         required=False,
-        label='Type'
+        label='Type',
+        widget=APISelect(
+            api_url="/api/dcim/device-types"
+        )
     )
     device_role = forms.ModelChoiceField(
         queryset=DeviceRole.objects.all(),
         required=False,
-        label='Role'
+        label='Role',
+        widget=APISelect(
+            api_url="/api/dcim/device-roles"
+        )
     )
     tenant = forms.ModelChoiceField(
         queryset=Tenant.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/tenancy/tenants"
+        )
     )
     platform = forms.ModelChoiceField(
         queryset=Platform.objects.all(),
-        required=False
+        required=False,
+        widget=APISelect(
+            api_url="/api/dcim/platforms"
+        )
     )
     status = forms.ChoiceField(
         choices=add_blank_choice(DEVICE_STATUS_CHOICES),
         required=False,
-        initial=''
+        initial='',
+        widget=StaticSelect2()
     )
     serial = forms.CharField(
         max_length=50,
@@ -1577,16 +1677,31 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         required=False,
         label='Search'
     )
-    region = FilterTreeNodeMultipleChoiceField(
+    region = FilterChoiceField(
         queryset=Region.objects.all(),
         to_field_name='slug',
         required=False,
+        widget=APISelectMultiple(
+            api_url="/api/dcim/regions/",
+            value_field="slug",
+            filter_for={
+                'site': 'region'
+            }
+        )
     )
     site = FilterChoiceField(
         queryset=Site.objects.annotate(
             filter_count=Count('devices')
         ),
         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(
         queryset=RackGroup.objects.select_related(
@@ -1595,6 +1710,12 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
             filter_count=Count('racks__devices')
         ),
         label='Rack group',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/rack-groups/",
+            filter_for={
+                'rack_id': 'rack_group_id',
+            }
+        )
     )
     rack_id = FilterChoiceField(
         queryset=Rack.objects.annotate(
@@ -1602,12 +1723,21 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         ),
         label='Rack',
         null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/racks/",
+            null_option=True,
+        )
     )
     role = FilterChoiceField(
         queryset=DeviceRole.objects.annotate(
             filter_count=Count('devices')
         ),
         to_field_name='slug',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/device-roles/",
+            value_field="slug",
+            null_option=True,
+        )
     )
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(
@@ -1615,10 +1745,21 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         ),
         to_field_name='slug',
         null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/tenancy/tenants/",
+            value_field="slug",
+            null_option=True,
+        )
     )
     manufacturer_id = FilterChoiceField(
         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(
         queryset=DeviceType.objects.select_related(
@@ -1629,6 +1770,10 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
             filter_count=Count('instances'),
         ),
         label='Model',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/device-types/",
+            display_field="model",
+        )
     )
     platform = FilterChoiceField(
         queryset=Platform.objects.annotate(
@@ -1636,12 +1781,18 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
         ),
         to_field_name='slug',
         null_label='-- None --',
+        widget=APISelectMultiple(
+            api_url="/api/dcim/platforms/",
+            value_field="slug",
+            null_option=True,
+        )
     )
     status = AnnotatedMultipleChoiceField(
         choices=DEVICE_STATUS_CHOICES,
         annotate=Device.objects.all(),
         annotate_field='status',
-        required=False
+        required=False,
+        widget=StaticSelect2Multiple()
     )
     mac_address = forms.CharField(
         required=False,
@@ -1650,49 +1801,49 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     has_primary_ip = forms.NullBooleanField(
         required=False,
         label='Has a primary IP',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     console_ports = forms.NullBooleanField(
         required=False,
         label='Has console ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     console_server_ports = forms.NullBooleanField(
         required=False,
         label='Has console server ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     power_ports = forms.NullBooleanField(
         required=False,
         label='Has power ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     power_outlets = forms.NullBooleanField(
         required=False,
         label='Has power outlets',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     interfaces = forms.NullBooleanField(
         required=False,
         label='Has interfaces',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     pass_through_ports = forms.NullBooleanField(
         required=False,
         label='Has pass-through ports',
-        widget=forms.Select(
+        widget=StaticSelect2(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
@@ -1714,7 +1865,8 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
 
 class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
     form_factor = forms.ChoiceField(
-        choices=IFACE_FF_CHOICES
+        choices=IFACE_FF_CHOICES,
+        widget=StaticSelect2()
     )
     enabled = forms.BooleanField(
         required=False,
@@ -1941,7 +2093,7 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
     vlans = forms.MultipleChoiceField(
         choices=[],
         label='VLANs',
-        widget=forms.SelectMultiple(
+        widget=StaticSelect2Multiple(
             attrs={
                 'size': 20,
             }
@@ -2093,7 +2245,8 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
     )
     form_factor = forms.ChoiceField(
         choices=add_blank_choice(IFACE_FF_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     enabled = forms.NullBooleanField(
         required=False,
@@ -2102,7 +2255,8 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
     lag = forms.ModelChoiceField(
         queryset=Interface.objects.all(),
         required=False,
-        label='Parent LAG'
+        label='Parent LAG',
+        widget=StaticSelect2()
     )
     mtu = forms.IntegerField(
         required=False,
@@ -2121,7 +2275,8 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
     )
     mode = forms.ChoiceField(
         choices=add_blank_choice(IFACE_MODE_CHOICES),
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
 
     class Meta:
@@ -2199,7 +2354,7 @@ class FrontPortCreateForm(ComponentForm):
     rear_port_set = forms.MultipleChoiceField(
         choices=[],
         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(
         required=False
@@ -2546,7 +2701,8 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
     type = forms.ChoiceField(
         choices=add_blank_choice(CABLE_TYPE_CHOICES),
         required=False,
-        initial=''
+        initial='',
+        widget=StaticSelect2()
     )
     status = forms.ChoiceField(
         choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
@@ -2555,7 +2711,8 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
     )
     label = forms.CharField(
         max_length=100,
-        required=False
+        required=False,
+        widget=StaticSelect2()
     )
     color = forms.CharField(
         max_length=6,
@@ -2569,7 +2726,8 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
     length_unit = forms.ChoiceField(
         choices=add_blank_choice(CABLE_LENGTH_UNIT_CHOICES),
         required=False,
-        initial=''
+        initial='',
+        widget=StaticSelect2()
     )
 
     class Meta:
@@ -2594,17 +2752,15 @@ class CableFilterForm(BootstrapMixin, forms.Form):
         required=False,
         label='Search'
     )
-    type = AnnotatedMultipleChoiceField(
+    type = forms.MultipleChoiceField(
         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();
     });
 
+    // Parse URLs which may contain variable refrences to other field values
     function parseURL(url) {
         var filter_regex = /\{\{([a-z_]+)\}\}/g;
         var match;
@@ -86,8 +87,8 @@ $(document).ready(function() {
         return rendered_url
     }
 
+    // Assign color picker selection classes
     function colorPickerClassCopy(data, container) {
-        console.log("hello");
         if (data.element) {
             $(container).addClass($(data.element).attr("class"));
         }
@@ -108,23 +109,27 @@ $(document).ready(function() {
         placeholder: "---------",
     })
 
-    // API backed single selection
+    // API backed selection
     // Includes live search and chained fields
+    // The `multiple` setting may be controled via a data-* attribute
     $('.netbox-select2-api').select2({
         allowClear: true,
         placeholder: "---------",
+
         ajax: {
             delay: 500,
+
             url: function(params) {
                 var element = this[0];
-                var url = element.getAttribute("data-url");
-                url = parseURL(url);
+                var url = parseURL(element.getAttribute("data-url"));
+
                 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;
             },
+
             data: function(params) {
                 var element = this[0];
                 // Paging
@@ -136,29 +141,35 @@ $(document).ready(function() {
                     limit: 50,
                     offset: offset,
                 };
+
                 // filter-for fields from a chain
                 var attr_name = "data-filter-for-" + $(element).attr("name");
                 var form = $(element).closest('form');
                 var filter_for_elements = form.find("select[" + attr_name + "]");
+
                 filter_for_elements.each(function(index, filter_for_element) {
                     var param_name = $(filter_for_element).attr(attr_name);
                     var value = $(filter_for_element).val();
+
                     if (param_name && value) {
-                        parameters[param_name] = $(filter_for_element).val();
+                        parameters[param_name] = value;
                     }
                 });
+
                 // 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-")){
@@ -166,14 +177,28 @@ $(document).ready(function() {
                         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) {
                 var element = this.$element[0];
                 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;
                 });
+
+                // 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
                 var page = data.next !== null;
                 return {
@@ -208,9 +233,11 @@ $(document).ready(function() {
         multiple: true,
         allowClear: true,
         placeholder: "Tags",
+
         ajax: {
             delay: 250,
             url: "/api/extras/tags/",
+
             data: function(params) {
                 // paging
                 var offset = params.page * 50 || 0;
@@ -222,6 +249,7 @@ $(document).ready(function() {
                 };
                 return parameters;
             },
+
             processResults: function (data) {
                 var results = $.map(data.results, function (obj) {
                     return {
@@ -229,6 +257,7 @@ $(document).ready(function() {
                         text: obj.name
                     }
                 });
+
                 // Check if there are more results to page
                 var page = data.next !== null;
                 return {

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

@@ -20,88 +20,3 @@
     </div>
 </div>
 {% 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>
 {% 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'),
             ('3', 'No'),
         )
+        self.attrs['class'] = 'netbox-select2-static'
 
 
 class SelectWithDisabled(forms.Select):
@@ -223,6 +224,14 @@ class StaticSelect2(SelectWithDisabled):
         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):
     """
     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 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.
@@ -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
         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.
-    :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__(
         self,
         api_url,
         display_field=None,
+        value_field=None,
         disabled_indicator=None,
         filter_for=None,
         conditional_query_params=None,
         additional_query_params=None,
+        null_option=False,
         *args,
         **kwargs
     ):
@@ -295,6 +308,8 @@ class APISelect(SelectWithDisabled):
         self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/'))  # Inject BASE_PATH
         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:
@@ -306,6 +321,8 @@ 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):
         """
@@ -336,8 +353,12 @@ class APISelect(SelectWithDisabled):
         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):