From bf8d57c7d1b7c6b0e0ef62d6f932810ee78d67d0 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 8 Jan 2019 15:35:34 -0800 Subject: [PATCH] DCIM filter forms select2 --- netbox/dcim/forms.py | 310 +++++++++++++----- netbox/project-static/js/forms.js | 47 ++- netbox/templates/dcim/device_list.html | 85 ----- .../templates/dcim/inc/filter_rack_group.html | 29 -- netbox/templates/dcim/rack_list.html | 5 - netbox/utilities/forms.py | 29 +- 6 files changed, 296 insertions(+), 209 deletions(-) delete mode 100644 netbox/templates/dcim/inc/filter_rack_group.html 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):