Pull to refresh

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

Reading time 5 min
Views 6.2K
Для начала всех хотелось бы поздравить с наступающими праздниками!

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

Несколько недель назад мне потребовалась сделать выпадающие списки в 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.
Tags:
Hubs:
+21
Comments 13
Comments Comments 13

Articles