Skip to content

Instantly share code, notes, and snippets.

@ninapavlich
Last active July 26, 2022 04:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save ninapavlich/9c056532624094f24bb53752c3ad294f to your computer and use it in GitHub Desktop.
Save ninapavlich/9c056532624094f24bb53752c3ad294f to your computer and use it in GitHub Desktop.
Custom configuration for django-autocomplete-light plugin
from django import forms
from django.contrib import admin
from dal import autocomplete, forward
class AdminAutocompleteFormMixin(forms.ModelForm):
class Media:
js = ('admin/autocomplete/forward.js',
'admin/autocomplete/select_admin_autocomplete.js')
class AdminModelSelect2(autocomplete.ModelSelect2):
autocomplete_function = 'admin-autocomplete'
# THIS IS WHERE YOU CUSTOMIZE:
class CustomModelAdminForm(AdminAutocompleteFormMixin):
class Meta:
model = CustomModel
fields = ('__all__')
widgets = {
'myotherfield': AdminModelSelect2(
url='admin-autocomplete',
attrs={'data-html': True},
forward=(forward.Const(val="appname.MyOtherModel", dst="model"),)
)
}
@admin.register(CustomModel)
class CustomModelAdmin(admin.ModelAdmin):
form = CustomModelAdminForm
;(function($, yl) {
yl.forwardHandlerRegistry = yl.forwardHandlerRegistry || {};
yl.registerForwardHandler = function(name, handler) {
yl.forwardHandlerRegistry[name] = handler;
};
yl.getForwardHandler = function(name) {
return yl.forwardHandlerRegistry[name];
};
function getForwardStrategy(element) {
var checkForCheckboxes = function() {
var all = true;
$.each(element, function(ix, e) {
if ($(e).attr("type") !== "checkbox") {
all = false;
}
});
return all;
};
if (element.length === 1 &&
element.attr("type") === "checkbox" &&
element.attr("value") === undefined) {
// Single checkbox without 'value' attribute
// Boolean field
return "exists";
} else if (element.length === 1 &&
element.attr("multiple") !== undefined) {
// Multiple by HTML semantics. E. g. multiple select
// Multiple choice field
return "multiple";
} else if (checkForCheckboxes()) {
// Multiple checkboxes or one checkbox with 'value' attribute.
// Multiple choice field represented by checkboxes
return "multiple";
} else {
// Other cases
return "single";
}
}
/**
* Get fields with name `name` relative to `element` with considering form
* prefixes.
* @param element the element
* @param name name of the field
* @returns jQuery object with found fields or empty jQuery object if no
* field was found
*/
yl.getFieldRelativeTo = function(element, name) {
var prefixes = $(element).getFormPrefixes();
for (var i = 0; i < prefixes.length; i++) {
var fieldSelector = "[name=" + prefixes[i] + name + "]";
var field = $(fieldSelector);
if (field.length) {
return field;
}
}
return $();
};
/**
* Get field value which is put to forwarded dictionary
* @param field the field
* @returns forwarded value
*/
yl.getValueFromField = function(field) {
var strategy = getForwardStrategy(field);
var serializedField = $(field).serializeArray();
var getSerializedFieldElementAt = function (index) {
// Return serializedField[index]
// or null if something went wrong
if (serializedField.length > index) {
return serializedField[index];
} else {
return null;
}
};
var getValueOf = function (elem) {
// Return elem.value
// or null if something went wrong
if (elem.hasOwnProperty("value") &&
elem.value !== undefined
) {
return elem.value;
} else {
return null;
}
};
var getSerializedFieldValueAt = function (index) {
// Return serializedField[index].value
// or null if something went wrong
var elem = getSerializedFieldElementAt(index);
if (elem !== null) {
return getValueOf(elem);
} else {
return null;
}
};
if (strategy === "multiple") {
return serializedField.map(
function (item) {
return getValueOf(item);
}
);
} else if (strategy === "exists") {
return serializedField.length > 0;
} else {
return getSerializedFieldValueAt(0);
}
};
yl.getForwards = function(element) {
var forwardElem,
forwardList,
forwardedData,
divSelector,
form;
divSelector = "div.dal-forward-conf#dal-forward-conf-for-" +
element.attr("id");
form = element.length > 0 ? $(element[0].form) : $();
forwardElem =
form.find(divSelector).find('script');
if (forwardElem.length === 0) {
return;
}
try {
forwardList = JSON.parse(forwardElem.text());
} catch (e) {
return;
}
if (!Array.isArray(forwardList)) {
return;
}
forwardedData = {};
$.each(forwardList, function(ix, field) {
var srcName, dstName;
if (field.type === "const") {
forwardedData[field.dst] = field.val;
} else if (field.type === "self") {
if (field.hasOwnProperty("dst")) {
dstName = field.dst;
} else {
dstName = "self";
}
forwardedData[dstName] = yl.getValueFromField(element);
} else if (field.type === "field") {
srcName = field.src;
if (field.hasOwnProperty("dst")) {
dstName = field.dst;
} else {
dstName = srcName;
}
var forwardedField = yl.getFieldRelativeTo(element, srcName);
if (!forwardedField.length) {
return;
}
forwardedData[dstName] = yl.getValueFromField(forwardedField);
} else if (field.type === "javascript") {
var handler = yl.getForwardHandler(field.handler);
forwardedData[field.dst || field.handler] = handler(element);
}
});
return JSON.stringify(forwardedData);
};
})(yl.jQuery, yl);
$(document).on('autocompleteLightInitialize', '[data-autocomplete-light-function=admin-autocomplete]', function() {
var element = $(this);
// Templating helper
function template(text, is_html) {
if (is_html) {
var $result = $('<span>');
$result.html(text);
return $result;
} else {
return text;
}
}
function result_template(item) {
return template(item.text,
element.attr('data-html') !== undefined || element.attr('data-result-html') !== undefined
);
}
function selected_template(item) {
console.log("SELECTED TEMPLATE?")
if (item.selected_text !== undefined) {
return template(item.selected_text,
element.attr('data-html') !== undefined || element.attr('data-selected-html') !== undefined
);
} else {
return result_template(item);
}
return
}
var ajax = null;
if ($(this).attr('data-autocomplete-light-url')) {
ajax = {
url: $(this).attr('data-autocomplete-light-url'),
dataType: 'json',
delay: 250,
data: function (params) {
var data = {
q: params.term, // search term
page: params.page,
create: element.attr('data-autocomplete-light-create') && !element.attr('data-tags'),
forward: yl.getForwards(element)
};
return data;
},
processResults: function (data, page) {
if (element.attr('data-tags')) {
$.each(data.results, function(index, value) {
value.id = value.text;
});
}
return data;
},
cache: true
};
}
$(this).select2({
tokenSeparators: element.attr('data-tags') ? [','] : null,
debug: true,
placeholder: element.attr('data-placeholder') || '',
language: element.attr('data-autocomplete-light-language'),
minimumInputLength: element.attr('data-minimum-input-length') || 0,
allowClear: ! $(this).is('[required]'),
templateResult: result_template,
templateSelection: selected_template,
ajax: ajax,
tags: Boolean(element.attr('data-tags')),
});
$(this).on('select2:selecting', function (e) {
var data = e.params.args.data;
if (data.create_id !== true)
return;
e.preventDefault();
var select = $(this);
$.ajax({
url: $(this).attr('data-autocomplete-light-url'),
type: 'POST',
dataType: 'json',
data: {
text: data.id,
forward: yl.getForwards($(this))
},
beforeSend: function(xhr, settings) {
xhr.setRequestHeader("X-CSRFToken", document.csrftoken);
},
success: function(data, textStatus, jqXHR ) {
select.append(
$('<option>', {value: data.id, text: data.text, selected: true})
);
select.trigger('change');
select.select2('close');
}
});
});
});
INSTALLED_APPS = [
'dal',
'dal_select2',
...
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
...
]
# Also make sure static JS files are available:
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'path', 'to', 'custom', 'static'),
)
from django.urls import path, re_path, include
from .admin import AdminAutocomplete
urlpatterns = [
re_path(
r'^admin/admin-autocomplete/$',
AdminAutocomplete.as_view(),
name='admin-autocomplete',
)
]
from django.apps import apps
from django.contrib.admin.views.decorators import staff_member_required
from django.utils.decorators import method_decorator
from django.db.models import Q
from dal import autocomplete
class AdminAutocomplete(autocomplete.Select2QuerySetView):
autocomplete_class = None
@method_decorator(staff_member_required)
def dispatch(self, *args, **kwargs):
return super(AdminAutocomplete, self).dispatch(*args, **kwargs)
def get_queryset(self):
model = self.forwarded.get('model', None)
if not model:
return []
app_label = model.split('.')[0]
model_name = model.split('.')[1]
if not app_label or not model_name:
return []
# Determine which class we're working with
self.autocomplete_class = apps.get_model(app_label, model_name)
# Dynamically create query based on class's autocomplete_lookup_fields:
qs = self.autocomplete_class.objects.all()
if self.q:
q = Q()
if hasattr(self.autocomplete_class, 'get_autocomplete_lookup_fields'):
autocomplete_fields = self.autocomplete_class.get_autocomplete_lookup_fields()
else:
autocomplete_fields = ['pk']
for autocomplete_field in autocomplete_fields:
kwargs = {'%s__icontains' % (autocomplete_field): self.q}
print(kwargs)
q = q | Q(**kwargs)
qs = qs.filter(q)
return qs
def get_result_html(self, item):
if hasattr(self.autocomplete_class, 'get_autocomplete_result_html'):
return self.autocomplete_class.get_autocomplete_result_html(item)
return super(AdminAutocomplete, self).get_result_label(item)
def get_result_label(self, item):
if hasattr(self.autocomplete_class, 'get_autocomplete_result_plaintext'):
return self.autocomplete_class.get_autocomplete_result_plaintext(item)
return super(AdminAutocomplete, self).get_result_label(item)
def get_results(self, context):
"""Return data for the 'results' key of the response."""
return [
{
'id': self.get_result_value(result),
'selected_text': self.get_result_label(result),
'text': self.get_result_html(result),
} for result in context['object_list']
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment