Django + Select2 = select autocomplete

Доброго времени суток.


В последнее время, я пишу на django.

Возникла необходимость вывода в списках достаточно большого количества опций.
Если оставлять просто поле типа models.ForeignKey со стандартным виджетом (Select, SelectMultiple),
нагружаем и базу данных и сервер приложений.
Давайте попробуем обращатся к этим данным только тогда, когда это нужно.

На просторах интернета, не обнаружил готового решения (чтобы просто установить и это заработало).
Есть наборы комментарий типа «наверное, вам нужно вот то-то» или «вот это»
В связи с этим, решил выложить то, что получилось.

Выкладываю небольшой application под django, содержащий

  • Составные числовые поля и поля с датами
  • TreeWidget для модели, основанной на MPTT
  • Виджет SelectAutocomplete
  • Виджет SelectMultipleAutocomplete

Статья ориентированно на начинающих разработчиков, не успевших «обрасти» библиотеками функций на django.
Думаю, что опытным разработчикам она не будет интересна.

Для иерархического виджета, нужно вставить в шаблон модальное окно
{% include ‘forms_custom/tree_widget_modal.html’ %}.
Если кому-то интересно узнать о нем подробнее, напишу в личку или отдельным постом.

Опишу только то, что касается списков Select и SelectMultiple.
В этом проекте нет поля TextAutocomplete, потому что мне оно пока не понадобилось.
Думаю, тут будет достаточно примеров, чтобы сделать его самостоятельно,
благо виджеты и поля форм расширяются достаточно просто.
Виджет основан на популярном плагине Select2 ivaynberg.github.io/select2/

Установка

Скрипты и стили

<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> 

Скопировать пакет и подключить его в settings.py (для поиска статики и шаблонов)
Подключить urls (для отдачи контента через AJAX)

    url(r'^forms_custom/', include('lib.forms_custom.urls', namespace='forms_custom')), 

Использование

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) 

Виджет требует аргументами QuerySet и search expression для поиска
Фильтры, наложеные на QuerySet поддерживаются.

С SelectAutocomplete все то же самое, только используется он с полем ModelChoiceField.
Далее, как все это работает.

Виджет

Метод «render» возвращает все то, что будет выведено на форме,
то есть скрытое поле, содержащее все необходимое для «натравливания» на него скрипта select2

Метод «value_from_datadict», достает из POST-массива, данные виджета,
преобразует их и передает дальше полю для валидации.
Тут нам нужно подменить скалярные значения идентификаторов, перечисленных через запятую
на список идентификаторов (как ожидает ModelMultipleChoiceField от SelectMultiple),
потому что select2 хранит идентификаторы в скрытом текстовом поле, разделенные запятыми.

Из особенностей, могу отметить, что наложенные фильтры достаем через объект класса WhereNode,
который мы получаем из QuerySet:

    where_node = self._queryset.query.__dict__['where']     where, where_params = where_node.as_sql(connection.ops.quote_name, connection) 

where_params пакуем с помощью pickle и вместе с where отправляем в виде параметров через ajax обработчику

исходный код

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(",")           

Поле в форме

Получаем поле, которое содержит нужный набор данных для запуска скрипта 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}}"/> 

Скрипт

Обходим виджеты по классу autocomplete_multiple_widget и для каждого вызываем select2
Запрос на инициализирование виджета ничем не отличается от работы самого виджета, просто вызывается с параметрами
id__in=current_values

исходный код

$(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; }     });  } 

Обработчик поиска

Получает запрос ajax с информацией о: приложении, модели, условиями и параметрами фильтрации QuerySet-а
При инициализации виджета, в него значения для поиска передаются в виде pk__in=«1,2,3»
Для обработки этого, мы подменяем строку на список, разбивая по запятой.

исходный код

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")  

Берем модель из кэша django, накладываем условия фильтрации, фильтр для поиска и отдаем список найденых объектов.
На выходе получили виджет, которым можно легко подменить стандартный Select и получить
удобство для пользователей (не особо удобно проматывать списки из тысяч элементов)
и снизит нагрузку на вашу систему.

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

Спасибо за внимание.
P.S. Успешных проектов в новом году, комрады!

ссылка на оригинал статьи http://habrahabr.ru/post/207968/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *