diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index 6589edbec..88b44b943 100644
--- a/netbox/dcim/forms.py
+++ b/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()
)
diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js
index 73d149408..adf56a0b2 100644
--- a/netbox/project-static/js/forms.js
+++ b/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 {
diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html
index 4bae11781..08d2a176c 100644
--- a/netbox/templates/dcim/device_list.html
+++ b/netbox/templates/dcim/device_list.html
@@ -20,88 +20,3 @@
{% endblock %}
-
-{% block javascript %}
-
-{% endblock %}
diff --git a/netbox/templates/dcim/inc/filter_rack_group.html b/netbox/templates/dcim/inc/filter_rack_group.html
deleted file mode 100644
index 9c5582f87..000000000
--- a/netbox/templates/dcim/inc/filter_rack_group.html
+++ /dev/null
@@ -1,29 +0,0 @@
-
diff --git a/netbox/templates/dcim/rack_list.html b/netbox/templates/dcim/rack_list.html
index e61f4eadf..049c50971 100644
--- a/netbox/templates/dcim/rack_list.html
+++ b/netbox/templates/dcim/rack_list.html
@@ -20,8 +20,3 @@
{% endblock %}
-
-{% block javascript %}
- {% include 'dcim/inc/filter_rack_group.html' %}
-{% endblock %}
-
diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py
index e53617321..05e23dd83 100644
--- a/netbox/utilities/forms.py
+++ b/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 `=`. 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):