📜 ⬆️ ⬇️

Django + Select2 = select autocomplete

Good day.


Recently, I am writing on django.

There was a need to display in the lists a sufficiently large number of options.
If you just leave the field types.ForeignKey with a standard widget (Select, SelectMultiple),
load and database and application server.
Let's try to access this data only when necessary.
')
On the Internet, I did not find a ready-made solution (just to install it and it worked).
There are comment sets like “probably you need this or that” or “this one”
In this regard, I decided to lay out what happened.

I post a small application under django containing


Article is focused on the beginning developers who were not in time "to acquit" libraries of functions on django.
I think that experienced developers it will not be interesting.


For a hierarchical widget, you need to insert a modal window into the template
{% include 'forms_custom / tree_widget_modal.html'%}.
If someone is interested to learn more about him, I will write in a personal or a separate post.

I will describe only what concerns the Select and SelectMultiple lists.
In this project, there is no TextAutocomplete field, because I haven’t needed it yet.
I think there will be enough examples to make it yourself,
benefit widgets and form fields are expanded quite simply.
The widget is based on the popular plugin Select2 ivaynberg.imtqy.com/select2

Installation


Scripts and styles
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}select2/select2.css"/> <!--    ,        ( ..) --> <script type="text/javascript" src="{{ STATIC_URL }}jquery/jquery.min.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}jstree/jquery.jstree.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}select2/select2.js"></script> <!--      ,   ,     -  --> <script type="text/javascript" src="{{ STATIC_URL }}forms_custom/tree_widget.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}forms_custom/autocomplete.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}forms_custom/autocomplete_multiple.js"></script> 



Copy the package and connect it in settings.py (to search for statics and templates)
Connect urls (for sharing content via AJAX)
  url(r'^forms_custom/', include('lib.forms_custom.urls', namespace='forms_custom')), 


Using


 from django import forms from django.contrib.auth import get_user_model from lib.forms_custom.widgets import SelectMultipleAutocomplete users_active_qs = get_user_model().objects.filter(is_active=True) class MessageCreateForm(forms.Form): recepients = forms.ModelMultipleChoiceField(label=u'', queryset=users_active_qs, widget=SelectMultipleAutocomplete(queryset=users_active_qs, expression="last_name__startswith")) subject = forms.CharField(label=u'', required=False) body = forms.CharField(label=u'', required=False, widget=forms.Textarea) back_url = forms.CharField(widget=forms.HiddenInput) 

The widget requires QuerySet and search expression arguments to search.
Filters overlaid on QuerySet supported.

With SelectAutocomplete, everything is the same, only it is used with the ModelChoiceField field.
Next, how it all works.

Widget


The "render" method returns everything that will be displayed on the form
that is, a hidden field that contains everything needed to set the select2 script on it

The "value_from_datadict" method, retrieves from the POST array, the widget data,
converts them and passes them further to the field for validation.
Here we need to replace the scalar values ​​of the identifiers listed by comma
to the list of identifiers (as expected by ModelMultipleChoiceField from SelectMultiple),
because select2 stores identifiers in a hidden text field, separated by commas.

Of the features, I can say that the imposed filters are obtained through an object of the class WhereNode,
which we get from QuerySet:
  where_node = self._queryset.query.__dict__['where'] where, where_params = where_node.as_sql(connection.ops.quote_name, connection) 


where_params is packaged with pickle and with where are sent as parameters via ajax handler

source
 import datetime from django import forms from django.db import connection from django.forms import widgets as widgets_django from django.forms import fields from django.template.loader import render_to_string from django.forms.widgets import HiddenInput import pickle class AutocompleteWidget(object): def _parse_queryset(self): self._application = self._queryset.model.__module__.split('.')[-0] self._model_name = self._queryset.model.__name__ where_node = self._queryset.query.__dict__['where'] where, where_params = where_node.as_sql(connection.ops.quote_name, connection) if where: self._queryset_where = where.replace('"', '\"') self._queryset_where_params = pickle.dumps(where_params) else: self._queryset_where = "" self._queryset_where_params = "" class SelectAutocomplete(widgets_django.Select, AutocompleteWidget): def __init__(self, queryset, attrs=None): super(SelectAutocomplete, self).__init__(attrs) self._queryset = queryset self._parse_queryset() def render(self, name, value, attrs=None, choices=()): application = self._queryset.model.__module__.split('.')[-0] model_name = self._queryset.model.__name__ return render_to_string('forms_custom/autocomplete.html', {'value': value, 'attrs': attrs, 'application': application, 'model_name': model_name, 'expression': 'title__startswith', 'name': name, 'where': self._queryset_where, 'where_params': self._queryset_where_params }) class SelectMultipleAutocomplete(widgets_django.SelectMultiple, AutocompleteWidget): def __init__(self, queryset, attrs=None, expression='title__startswith'): super(SelectMultipleAutocomplete, self).__init__(attrs) self._queryset = queryset self._expression = expression self._parse_queryset() def render(self, name, value, attrs=None, choices=()): return render_to_string('forms_custom/autocomplete_multiple.html', {'value': value, 'attrs': attrs, 'application': self._application, 'model_name': self._model_name, 'expression': self._expression, 'name': name, 'where': self._queryset_where, 'where_params': self._queryset_where_params }) def value_from_datadict(self, data, files, name): """ replace scalar value ("1,2,3") to list ([1,2,3])""" data_dict = super(SelectMultipleAutocomplete, self).value_from_datadict(data, files, name) value = data_dict[0] if not value: return None return value.split(",") 



Form field


Get the field that contains the desired set of data to run the script select2
 <input type="hidden" id="{{attrs.id}}" class="autocomplete_multiple_widget" value="{% if value %}{{value|join:","}}{% endif %}" name="{{name}}" data-url="{% url 'forms_custom:autocomplete_widget' application=application model_name=model_name %}" data-expression="{{expression}}" data-where="{{where}}" data-where_params="{{where_params}}"/> 


Script


We go around widgets by class autocomplete_multiple_widget and for each we call select2
The request to initialize the widget is no different from the work of the widget itself, it is simply called with parameters
id__in = current_values

source
 $(document).ready(function() { $('.autocomplete_multiple_widget').each(function() { bind_autocomplete_multiple_widget(this); }); }); function bind_autocomplete_multiple_widget(element) { var j_element = $(element); url = j_element.attr('data-url'); var expression = j_element.attr('data-expression'); var where = j_element.attr('data-where'); var where_params = j_element.attr('data-where_params'); $(element).select2({ placeholder: " ", minimumInputLength: 3, multiple: true, ajax: { url: url, quietMillis: 1000, //  1    ,    dataType: 'json', //  GET-    ,   where    data: function (term, page) { return {q: term, expression: expression, where: where, where_params: where_params}; }, results: function (data, page) { return {results: data}; } }, //       //        id (   value="1,2,3"   ) //         id__in=current_values   callback   initSelection: function(element, callback) { var id = $(element).val(); if (id !== "") { $.ajax(url, { data: {q: id, expression: 'pk__in', where: where, where_params: where_params}, dataType: "json" }).done(function(data) { callback(data); }); } }, dropdownCssClass: "bigdrop", escapeMarkup: function (m) { return m; } }); } 



Search handler


Receives ajax request with information about: application, model, conditions and filtering parameters of QuerySet.
When the widget is initialized, the values ​​for the search are passed to it in the form pk__in = "1,2,3"
To handle this, we substitute the line for the list, splitting by comma.

source
 import json import pickle from django.http import HttpResponse, HttpResponseForbidden from django.db.models.loading import get_model def autocomplete_widget(request, application, model_name): if not request.is_ajax(): return HttpResponseForbidden(u'    ajax') data = [] expression = request.GET.get('expression') token = request.GET.get('q') if expression == u'pk__in': token = token.split(",") objects = get_model(application, model_name).objects where = request.GET.get('where') if where: where_params = request.GET.get('where_params') where_params = pickle.loads(where_params) objects = objects.extra(where=[where], params=where_params) objects = objects.filter(**{expression: token})[:20] for item in objects.iterator(): data.append({"id": item.id, "text": unicode(item)}) return HttpResponse(json.dumps(data), content_type="application/json;charset=utf-8") 



We take the model from the django cache, impose filtering conditions, a filter for the search and give the list of found objects.
At the exit, we got a widget that can easily replace the standard Select and get
convenience for users (it is not very convenient to scroll through lists of thousands of items)
and reduce the load on your system.

drive.google.com/file/d/0B0GZGIoZAYTFNU9xd3dIR3FXU0k/edit?usp=sharing

Thanks for attention.
PS Successful projects in the new year, comrades!

Source: https://habr.com/ru/post/207968/


All Articles