Мой вариант MultipleInput + Autocomplete

    Для начала всех хотелось бы поздравить с наступающими праздниками!

    А теперь к сути моего повествования.

    Несколько недель назад мне потребовалась сделать выпадающие списки в django. Значения должны подгружаться автоматически по мере ввода и пользователь должен иметь возможность как выбрать значение из списка, так и добавить своё.

    Для начала посмотрим, какой результат мы преследуем:



    Итак, файл с моделями. Для примера я просто создал две модели и связал их с помощью ManyToManyField.

    models.py

    from django.db import models
    
    class City(models.Model):
    	name = models.CharField(max_length=150, unique=True)
    
    class Country(models.Model):
    	name = models.CharField(max_length=100)
    	cities = models.ManyToManyField(City, blank=True)
    

    Затем я полез изучать стандартые виджеты. Самым подходящим оказался MultipleHiddenInput, но он наследовался от HiddenInput и пока что не имел функции автокомплита. File>New поехали.

    widget.py

    from django.forms.util import flatatt
    from django.utils.datastructures import MultiValueDict, MergeDict
    from django.utils.encoding import force_unicode
    
    class MultipleInput(Input):
    	input_type = 'text'
    	
    	def __init__(self, attrs=None, choices=()):
    		super(MultipleInput, self).__init__(attrs)
    		self.choices = choices
    
    	def render(self, name, value, attrs=None, choices=()):
    		if value is None: value = []
    		final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
    		id_ = final_attrs.get('id', None)
    		inputs = []
    		for i, v in enumerate(value):
    			input_attrs = dict(value=force_unicode(v), **final_attrs)
    			if id_:
    				input_attrs['id'] = '%s_%s' % (id_, i)
    			inputs.append(u'<p><input%s /><a href="#" id="remove_%s">Remove</a></p>' % (flatatt(input_attrs), id_))
    		return mark_safe(u'\n'.join(inputs))
    
    	def value_from_datadict(self, data, files, name):
    		if isinstance(data, (MultiValueDict, MergeDict)):
    			return data.getlist(name)
    		return data.get(name, None)
    

    Что я сделал? Я взял стандартный MultipleHiddenInput, унаследовал его от Input и поменял inputs.append. Как видите почти ничего не изменилось. В inputs.append html-код, необходимый для удаления записей на стороне пользователя. Для чего это нужно можно понять ниже, когда я буду описывать файл forms.py.

    Теперь форма. Для поля cities уставливаем ранее написанный виджет MultipleInput. Также у виджета можно заметить атрибут 'class' со значением 'autocompleteCity'. Уже по названию понятно, что это необходимо для будущего автокомплита.
    Ввиду изменения поведения связки ManyToManyField, в форме появилась необходимость переопределить __init__. Здесь же мы проверяем наличие повторов и удаляем их, сохраняя порядок следования элементов.
    Если форма была отправлена с ошибками, то именно в этот момент нам и помогает __init__, он сохраняет все значения для cities и отправяет их обратно пользователю.

    forms.py

    from django import forms
    from myapp.widget import MultipleInput
    
    class CreateCountryForm(forms.Form):
    	name = forms.CharField(widget=forms.TextInput(), required=True)
    	cities = forms.CharField(widget=MultipleInput(attrs={'class' : 'autocompleteCity'}), required=False)
    	
    	def __init__(self, *args, **kwargs):
    		super(CreateCountryForm, self).__init__(*args, **kwargs)
    		s = kwargs.get('data', None)
    		if s:
    			cities = s.getlist('cities')
    			for i in xrange(len(cities)-1, -1, -1):
    				if cities.count(cities[i]) != 1: del cities[i]
    

    Осталось написать отображение, чтобы правильно сохранять наши модели. А также второе отображение, чтобы принимать ajax-запросы для автодополения.

    views.py

    from django.shortcuts import render_to_response
    from django.http import HttpResponseRedirect
    from django.template import RequestContext
    from myapp.forms import CreateCountryForm
    from myapp.models import City
    
    def create_country(request, form_class=None, template_name='create_country.html'):
    	form_class = CreateCountryForm
    	if request.method == 'POST':
    		form = form_class(data=request.POST, files=request.FILES)
    		if form.is_valid():
    			obj = form.save(commit=False)
    			obj.save()
    			cities = request.POST.getlist('cities')
    			obj.cities.clear()
    			for c in cities:
    				city, created = City.objects.get_or_create(c)
    				obj.cities.add(city)
    			return HttpResponseRedirect('index')
    	else:
    		form = form_class()
    	context = {
    		'form': form,
    	}
    	return render_to_response(template_name, context, context_instance=RequestContext(request))
    
    def city_autocomplete(request):
    	try:
    		cities = City.objects.filter(name__icontains=request.GET['q']).values_list('name', flat=True)
    	except MultiValueDictKeyError:
    		pass
    	return HttpResponse('\n'.join(cities), mimetype='text/plain')
    

    Ну и разумеется конфигурация url.

    urls.py

    from django.conf.urls.defaults import *
    from myapp import views
    
    urlpatterns = patterns(''
    	url(r'^city_autocomplete/$', views.city_autocomplete, name='city_autocomplete'),
    	url(r'^create_country/$', views.create_stream, name='stream_create_stream'),
    )
    

    Все самое сложное преодолели, переходим к написанию шаблона. Я специально написал один шаблон без различного рода наследований, чтобы просто отобразить суть. В примере используется jQueryAutocompletePlugin для автокомплита.

    create_country.html

    <!DOCTYPE html>
    <html lang="ru">
    <head>
    <link type="text/css" href="https://github.com/agarzola/jQueryAutocompletePlugin/blob/master/jquery.autocomplete.css" media="all" rel="stylesheet" />
    <script type="text/javascript" src="https://github.com/agarzola/jQueryAutocompletePlugin/blob/master/jquery.autocomplete.js"></script>
    <script type="text/javascript" src="{{ MEDIA_URL }}js/add_and_remove.js"></script>
    </head>
    <body>
    <form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
    <label for="id_name">Название</label>
    {{ form.name }}</br>
    <a href="#" id="addCity">Добавить город</a>
    <div id="p_cities">
    {{ form.cities }}
    </div>
    <script type="text/javascript">
    jQuery().ready(function() { jQuery(".autocompleteCity").autocomplete("/city_autocomplete/", { multiple: false }); });
    </script>
    <input class="button" type="submit" value="Отправить"/>
    </form>
    </body>
    </html>
    

    И для полноты картины привожу пример файла add_and_remove.js.

    add_and_remove.js

    $(function() {
            var CitiesDiv = $('#p_cities');
            var i = $('#p_cities p').size();
    		
            $('#addCity').live('click', function() {
                    $('<p><input class="autocompleteCity ac_input" type="text" id="id_cities_' + i +'" size="20" name="cities" placeholder="Input Value" autocomplete="off" /><a href="#" id="remove_id_cities">Remove</a></p>').appendTo(CitiesDiv);
                    $('#id_cities_' + i).focus();
                    i++;
    				jQuery(".autocompleteCity").autocomplete("/city_autocomplete/", { multiple: false });
                    return false;
            });
    		
    		$('#remove_id_cities').live('click', function() { 
                    if( i > 0 ) {
                            $(this).parents('p').remove();
                            i--;
                    }
                    return false;
            });
    });
    

    P.S. Это всего лишь мой вариант решения проблемы, с которой я столкнулся, и он не претендует на лучший. Названия моделей взяты случайно, пример может также подойти для реализации тегов на сайте. Буду очень признателен замечаниям и пожеланиям, ибо я всего 4 месяца влюблен в django и python.
    Поделиться публикацией

    Комментарии 13

      0
      М, хорошая идея завернуть эти автокомплиты в виджет, нам бы тоже так наверное следовало сделать :) Как-то отслеживаете/модифицируете введённые пользователем данные? Что если кто-то сделает город «Мсква»?
        0
        Пока что никак не отслеживаем, потому что проект еще не закончен. На начальном этапе, наверное, будем вручную все модифицировать, а дальше будет видно.
        +1
        ИМХО: а не лучше ли в модели City:
        country = ForeignKey('Country', related_name='cities')?
          0
          С городами и странами да. Можно подойти с двух сторон: ManyToManyField — одна страна принадлежит множеству городов, ForeignKey — один город принадлежит одной стране.
          С другой стороны, если нам необходима связка моделей City и Profile, к примеру, когда пользователь может указать множество городов, в которых он был. Или, как я написал в самом конце, данный подход можно использовать для реализации тегов, когда один тег принадлежит множеству сообщений. В этом подходе ForeignKey уже не подойдет.
          Или же вариант, если модель City нам необходимо использовать не только для связки с таблицей Country, но и с другими таблицами. В этом подходе хочется, чтобы модель City была максимально независима от остальной логики проекта.
          0
          Мне кажется, что лучше будет не подгружать данные с сервера на лету, а сразу брать все города и грузить в виде json + кешировать. Это будет быстрее для пользователя и меньше будет грузить сервер.
            0
            Да, вопрос быстродействия меня тоже волнует и пока что я не определился как лучше. Мельком посмотрел метки на хабре, вроде бы подгружаются по мере ввода пользователем.
            И опять же с какой стороны посмотреть, базу городов мы хотя бы примерно можем оценить по объему. А если использовать структуру, бесконечно расширяющуюся, такую как метки на хабрахабр. Не станет ли в определенный момент объем меток критичен для быстродействия?
              0
              Вам города России нужны или всего мира?
                0
                Я не хочу ставить себя в какие-то рамки, поэтому сразу рассчитываю, что города могут быть со всего мира.
                  0
                  gist.github.com/1519751 — России, если гзипом отдавать, то можно прямо в
                    0
                    Размер и правда хорош, спасибо за наводку.
              +1
              формсеты

              smart_select

              django-cities

              @media для виджетов

              схема бд неудачная, обоснования высосаны из пальца

              спасибо что делитесь опытом, многим новичкам будет полезно

              «… я в джангобуке читал что регистрацию надо самому реализовывать!...»

              плюсанул
                0
                Я надеюсь вы прочитали моё послесловие? Я использую данный подход не на том примере, что описывается в статье. Код я специально облегчил, чтобы не вводить в заблуждение. Именно поэтому я не стал называть статью «Делаем теги в django» или «Реализация поля город в django». Модели нейтральны, главное — множественный выбор с функцией автокомплита.
                0
                Схема базы неправильная. Отношения город-страна — многие к одному, и связь должна быть в City, а не Country.
                При привязке городов к профилю, связь будет в профиле, один ко многим.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое