diff --git a/.gitignore b/.gitignore
index b33d46a40..e6e625454 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@
fabfile.py
*.swp
gunicorn_config.py
+.DS_Store
diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py
index 0d9deadc2..41848d640 100644
--- a/netbox/dcim/filters.py
+++ b/netbox/dcim/filters.py
@@ -750,6 +750,10 @@ class InterfaceFilter(django_filters.FilterSet):
"""
Not using DeviceComponentFilterSet for Interfaces because we need to check for VirtualChassis membership.
"""
+ q = django_filters.CharFilter(
+ method='search',
+ label='Search',
+ )
device = django_filters.CharFilter(
method='filter_device',
field_name='name',
@@ -796,6 +800,13 @@ class InterfaceFilter(django_filters.FilterSet):
model = Interface
fields = ['name', 'connection_status', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(name__icontains=value)
+ ).distinct()
+
def filter_device(self, queryset, name, value):
try:
device = Device.objects.get(**{name: value})
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index d02235277..3ecc56533 100644
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -1247,7 +1247,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
# If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
# can be flipped from one face to another.
- self.fields['position'].widget.attrs['api-url'] += '&exclude={}'.format(self.instance.pk)
+ self.fields['position'].widget.add_additional_query_param('exclude', self.instance.pk)
# Limit platform by manufacturer
self.fields['platform'].queryset = Platform.objects.filter(
@@ -2243,7 +2243,8 @@ class CableCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
required=False,
widget=forms.Select(
attrs={
- 'filter-for': 'termination_b_rack',
+ 'data-filter-for-termination_b_rack': 'site_id',
+ 'data-filter-for-termination_b_device': 'site_id',
}
)
)
@@ -2255,9 +2256,9 @@ class CableCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
label='Rack',
required=False,
widget=APISelect(
- api_url='/api/dcim/racks/?site_id={{termination_b_site}}',
+ api_url='/api/dcim/racks/',
attrs={
- 'filter-for': 'termination_b_device',
+ 'data-filter-for-termination_b_device': 'rack_id',
'nullable': 'true',
}
)
@@ -2269,12 +2270,11 @@ class CableCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
('rack', 'termination_b_rack'),
),
label='Device',
- required=False,
widget=APISelect(
- api_url='/api/dcim/devices/?site_id={{termination_b_site}}&rack_id={{termination_b_rack}}',
+ api_url='/api/dcim/devices/',
display_field='display_name',
attrs={
- 'filter-for': 'termination_b_id',
+ 'data-filter-for-termination_b_id': 'device_id',
}
)
)
@@ -2290,19 +2290,15 @@ class CableCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
termination_b_type = forms.ModelChoiceField(
queryset=ContentType.objects.all(),
label='Type',
- widget=ContentTypeSelect(
- attrs={
- 'filter-for': 'termination_b_id',
- }
- )
+ widget=ContentTypeSelect()
)
termination_b_id = forms.IntegerField(
label='Name',
widget=APISelect(
- api_url='/api/dcim/{{termination_b_type}}s/?device_id={{termination_b_device}}',
+ api_url='/api/dcim/{{termination_b_type}}s/',
disabled_indicator='cable',
- url_conditional_append={
- 'termination_b_type__interface': '&type=physical',
+ conditional_query_params={
+ 'termination_b_type__interface': 'type=physical',
}
)
)
@@ -2310,7 +2306,7 @@ class CableCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
class Meta:
model = Cable
fields = [
- 'termination_b_site', 'termination_b_rack', 'termination_b_device', 'livesearch', 'termination_b_type',
+ 'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_type',
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit',
]
diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js
index fa23f1592..981310a75 100644
--- a/netbox/project-static/js/forms.js
+++ b/netbox/project-static/js/forms.js
@@ -62,135 +62,158 @@ $(document).ready(function() {
form.submit();
});
- // API select widget
- $('select[filter-for]').change(function() {
-
- // Resolve child field by ID specified in parent
- var child_names = $(this).attr('filter-for');
- var parent = this;
-
- // allow more than one child
- $.each(child_names.split(" "), function(_, child_name){
-
- var child_field = $('#id_' + child_name);
- var child_selected = child_field.val();
-
- // Wipe out any existing options within the child field and create a default option
- child_field.empty();
- if (!child_field.attr('multiple')) {
- child_field.append($("").attr("value", "").text("---------"));
+ function parseURL(url) {
+ var filter_regex = /\{\{([a-z_]+)\}\}/g;
+ var match;
+ var rendered_url = url;
+ var filter_field;
+ while (match = filter_regex.exec(url)) {
+ filter_field = $('#id_' + match[1]);
+ var custom_attr = $('option:selected', filter_field).attr('api-value');
+ if (custom_attr) {
+ rendered_url = rendered_url.replace(match[0], custom_attr);
+ } else if (filter_field.val()) {
+ rendered_url = rendered_url.replace(match[0], filter_field.val());
+ } else if (filter_field.attr('nullable') == 'true') {
+ rendered_url = rendered_url.replace(match[0], 'null');
}
+ }
+ return rendered_url
+ }
- if ($(parent).val() || $(parent).attr('nullable') == 'true') {
- var api_url = child_field.attr('api-url') + '&limit=0&brief=1';
- var disabled_indicator = child_field.attr('disabled-indicator');
- var initial_value = child_field.attr('initial');
- var display_field = child_field.attr('display-field') || 'name';
-
- // Determine the filter fields needed to make an API call
- var filter_regex = /\{\{([a-z_]+)\}\}/g;
- var match;
- var rendered_url = api_url;
- var filter_field;
- while (match = filter_regex.exec(api_url)) {
- filter_field = $('#id_' + match[1]);
- var custom_attr = $('option:selected', filter_field).attr('api-value');
- if (custom_attr) {
- rendered_url = rendered_url.replace(match[0], custom_attr);
- } else if (filter_field.val()) {
- rendered_url = rendered_url.replace(match[0], filter_field.val());
- } else if (filter_field.attr('nullable') == 'true') {
- rendered_url = rendered_url.replace(match[0], 'null');
- }
+ // API backed single selection
+ // Includes live search and chained fields
+ $('.netbox-select2-api').select2({
+ ajax: {
+ delay: 500,
+ url: function(params) {
+ var element = this[0];
+ var url = element.getAttribute("data-url");
+ url = parseURL(url);
+ if (url.includes("{{")) {
+ // URL is not furry rendered yet, abort the request
+ return null;
}
-
- // Account for any conditional URL append strings
- $.each(child_field[0].attributes, function(index, attr){
- if (attr.name.includes("data-url-conditional-append-")){
- var conditional = attr.name.split("data-url-conditional-append-")[1].split("__");
+ return url;
+ },
+ data: function(params) {
+ var element = this[0];
+ // Paging
+ var offset = params.page * 50 || 0;
+ // Base query params
+ var parameters = {
+ q: params.term,
+ brief: 1,
+ 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();
+ }
+ });
+ // 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){
- rendered_url = rendered_url + attr.value;
+ var _val = attr.value.split("=");
+ parameters[_val[0]] = _val[1];
}
}
})
-
- // If all URL variables have been replaced, make the API call
- if (rendered_url.search('{{') < 0) {
- console.log(child_name + ": Fetching " + rendered_url);
- $.ajax({
- url: rendered_url,
- dataType: 'json',
- success: function(response, status) {
- $.each(response.results, function(index, choice) {
- var option = $("").attr("value", choice.id).text(choice[display_field]);
- if (disabled_indicator && choice[disabled_indicator] && choice.id != initial_value) {
- option.attr("disabled", "disabled");
- } else if (choice.id == child_selected) {
- option.attr("selected", "selected");
- }
- child_field.append(option);
- });
- }
- });
- }
-
+ // Additional query params
+ $.each(element.attributes, function(index, attr){
+ if (attr.name.includes("data-additional-query-param-")){
+ var param_name = attr.name.split("data-additional-query-param-")[1]
+ parameters[param_name] = attr.value;
+ }
+ })
+ return parameters;
+ },
+ processResults: function (data) {
+ var element = this.$element[0];
+ var results = $.map(data.results, function (obj) {
+ obj.text = obj.name || obj[element.getAttribute('display-field')];
+ return obj;
+ });
+ // Check if there are more results to page
+ var page = data.next !== null;
+ return {
+ results: results,
+ pagination: {
+ more: page
+ }
+ };
}
-
- // Trigger change event in case the child field is the parent of another field
- child_field.change();
- });
-
+ }
});
- // Auto-complete tags
- function split_tags(val) {
- return val.split(/,\s*/);
- }
- $("#id_tags")
- .on("keydown", function(event) {
- if (event.keyCode === $.ui.keyCode.TAB &&
- $(this).autocomplete("instance").menu.active) {
- event.preventDefault();
+ // API backed tags
+ var tags = $('#id_tags').val() === "" ? [] : $('#id_tags').val().split(/,\s*/);
+ tag_objs = $.map(tags, function (tag) {
+ return {
+ id: tag,
+ text: tag,
}
- })
- .autocomplete({
- source: function(request, response) {
- $.ajax({
- type: 'GET',
- url: netbox_api_path + 'extras/tags/',
- data: 'q=' + split_tags(request.term).pop(),
- success: function(data) {
- var choices = [];
- $.each(data.results, function (index, choice) {
- choices.push(choice.name);
- });
- response(choices);
- }
- });
+ });
+ // Replace the django issued text input with a select element
+ $('#id_tags').replaceWith('');
+ $('#id_tags').select2({
+ tags: tag_objs,
+ data: function(params) {
+ // paging
+ var offset = params.page * 50 || 0;
+ var parameters = {
+ q: params.term,
+ brief: 1,
+ limit: 50,
+ offset: offset,
+ };
+ return parameters;
},
- search: function() {
- // Need 3 or more characters to begin searching
- var term = split_tags(this.value).pop();
- if (term.length < 3) {
- return false;
- }
- },
- focus: function() {
- // prevent value inserted on focus
- return false;
- },
- select: function(event, ui) {
- var terms = split_tags(this.value);
- // remove the current input
- terms.pop();
- // add the selected item
- terms.push(ui.item.value);
- // add placeholder to get the comma-and-space at the end
- terms.push("");
- this.value = terms.join(", ");
- return false;
+ multiple: true,
+ allowClear: true,
+ placeholder: "Tags",
+ ajax: {
+ delay: 250,
+ url: "/api/extras/tags/",
+ processResults: function (data) {
+ var results = $.map(data.results, function (obj) {
+ return {
+ id: obj.name,
+ text: obj.name
+ }
+ });
+ // Check if there are more results to page
+ var page = data.next !== null;
+ return {
+ results: results,
+ pagination: {
+ more: page
+ }
+ };
+ }
}
- });
+ });
+ $('#id_tags').val(tags).trigger("change");
+ $('#id_tags').closest('form').submit(function(event){
+ // django-taggit can only accept a single comma seperated string value
+ var value = $('#id_tags').val();
+ var final_tags = "";
+ if (value.length > 0){
+ final_tags = value.join(', ');
+ }
+ $('#id_tags').val(null);
+ var option = new Option(final_tags, final_tags, true, true);
+ $('#id_tags').append(option);
+ });
});
diff --git a/netbox/project-static/js/netbox-select2.js b/netbox/project-static/js/netbox-select2.js
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/netbox/project-static/js/netbox-select2.js
@@ -0,0 +1 @@
+
diff --git a/netbox/project-static/select2-4.0.5/LICENSE.md b/netbox/project-static/select2-4.0.5/LICENSE.md
new file mode 100755
index 000000000..8cb8a2b12
--- /dev/null
+++ b/netbox/project-static/select2-4.0.5/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/netbox/project-static/select2-4.0.5/README.md b/netbox/project-static/select2-4.0.5/README.md
new file mode 100755
index 000000000..6ee975d6e
--- /dev/null
+++ b/netbox/project-static/select2-4.0.5/README.md
@@ -0,0 +1,123 @@
+Select2
+=======
+[![Build Status][travis-ci-image]][travis-ci-status]
+
+Select2 is a jQuery-based replacement for select boxes. It supports searching,
+remote data sets, and pagination of results.
+
+To get started, checkout examples and documentation at
+https://select2.org/
+
+Use cases
+---------
+* Enhancing native selects with search.
+* Enhancing native selects with a better multi-select interface.
+* Loading data from JavaScript: easily load items via AJAX and have them
+ searchable.
+* Nesting optgroups: native selects only support one level of nesting. Select2
+ does not have this restriction.
+* Tagging: ability to add new items on the fly.
+* Working with large, remote datasets: ability to partially load a dataset based
+ on the search term.
+* Paging of large datasets: easy support for loading more pages when the results
+ are scrolled to the end.
+* Templating: support for custom rendering of results and selections.
+
+Browser compatibility
+---------------------
+* IE 8+
+* Chrome 8+
+* Firefox 10+
+* Safari 3+
+* Opera 10.6+
+
+Select2 is automatically tested on the following browsers.
+
+[![Sauce Labs Test Status][saucelabs-matrix]][saucelabs-status]
+
+Usage
+-----
+You can source Select2 directly from a CDN like [JSDliver][jsdelivr] or
+[CDNJS][cdnjs], [download it from this GitHub repo][releases], or use one of
+the integrations below.
+
+Integrations
+------------
+Third party developers have created plugins for platforms which allow Select2 to be integrated more natively and quickly. For many platforms, additional plugins are not required because Select2 acts as a standard `