📜 ⬆️ ⬇️

Django auto-completion multiple choice field

Hi, Habr.
In my last article I described the technology for creating a custom field for entering tags in Django. Now I would like to share a ready-made and more or less universal solution that implements the multiple choice field with AJAX auto-completion. The difference of this field from that described in the previous article is that it allows you only to select items from the directory, but not to create new ones. A wonderful jQuery-plugin Select2 will be responsible for the front-end part. The solution will be in the form of a separate Django application.


Widget


from django.forms.widgets import Widget from django.conf import settings from django.utils.safestring import mark_safe from django.template.loader import render_to_string class InfiniteChoiceWidget(Widget): '''Infinite choice widget, based on Select2 jQuery-plugin (http://ivaynberg.imtqy.com/select2/)''' class Media: js = ( settings.STATIC_URL + "select2/select2.js", ) css = { 'all': (settings.STATIC_URL + 'select2/select2.css',) } def __init__(self, data_model, multiple=False, disabled=False, attrs=None): super(InfiniteChoiceWidget, self).__init__(attrs) self.data_model = data_model self.multiple = multiple self.disabled = disabled def render(self, name, value, attrs=None): return mark_safe(render_to_string("infinite_choice_widget.html", {"disabled": self.disabled, "multiple": self.multiple, "attrs": attrs, "app_name": self.data_model._meta.app_label, "model_name": self.data_model.__name__, "input_name": name, "current_value": value if value else "", }) ) 

The widget's constructor accepts the model class, the multiple and disabled flags, which are responsible, respectively, for the selection multiplicity and the field activity. In the Media subclass, the scripts and styles of Select2 are connected. The script that initializes the Select2 plugin will be described in the infinite_choice_widget.html template.
 {% load url from future %} {#    django 1.5 #} <input type="hidden" {% for key,value in attrs.iteritems %} {{key}}="{{value}}" {% endfor %} name="{{input_name}}" value="{{current_value}}" /> {# Settings for Select2 #} <script type="text/javascript"> $("#{{attrs.id}}").select2({ multiple: {{multiple|yesno:"true,false"}}, formatInputTooShort: function (input, min) { return ",  " + (min - input.length) + "   "; }, formatSearching: function () { return "..."; }, formatNoMatches: function () { return "  "; }, minimumInputLength: 3, initSelection : function (element, callback) { $.ajax({ url: "{% url 'infinite_choice_data' app_name model_name %}", type: 'GET', dataType: 'json', data: {ids: element.val()}, success: function(data, textStatus, xhr) { callback(data); }, error: function(xhr, textStatus, errorThrown) { callback({}); } }); }, ajax: { url: "{% url 'infinite_choice_data' app_name model_name %}", dataType: 'json', data: function (term, page) { return {term: term, // search term page_limit: 10 }; }, results: function (data, page) { return {results: data}; } } }); {% if disabled %} $("#{{attrs.id}}").select2("disable"); {% endif %} </script> 

The central object here is hidden input, in which the identifiers of the selected options will be stored. And already on it, the Select2 plugin is set on by #id.
Let's run through the parameters of the plugin.

Please note that the URL that the data is requested for is the name of the application and the model name from this application. This is done to universalize the processing view: the view does not have to be clear about the model from which the data is taken, we will tell it to it every time.
At the end, we tell the plugin to be inactive if the widget passed disabled = True.

Presentation for autocompletion


As I said, the view must be universal and it does not need to know which model to take the data from. There is a standard way to get a model class by the well-known application name and the model class name, we use it by passing these strings to the view.
 from django.http import Http404, HttpResponse import json from django.db.models import get_model def infinite_choice_data(request, app_name, model_name): '''Returns data for infinite choice field''' data = [] if not request.is_ajax(): raise Http404 model = get_model(app_name, model_name) if 'term' in request.GET: term = request.GET['term'] page_limit = request.GET['page_limit'] data = model.objects.filter(title__startswith=term)[:int(page_limit)] json_data = json.dumps([{"id": item.id, "text": unicode(item.title)} for item in data]) if 'ids' in request.GET: id_list = request.GET['ids'].split(',') items = model.objects.filter(pk__in=id_list) json_data = json.dumps([{"id": item.id, "text": item.title} for item in items]) response = HttpResponse(json_data, content_type="application/json") response['Cache-Control'] = 'max-age=86400' return response 

The django.db.models.get_model function returns the model class. Further, depending on the query variables, either variants starting with the term string are selected from the model, or variants with id equal to those passed in the variable ids. The second case occurs when the plug-in is initialized with the previously entered data.
I added a Cache-Control header in response, with a cache data lifetime of 24 hours. This is so as not to pull the base of the same type of requests. It helps a lot when using a field with huge KLADR / FIAS type directories.
')
And this is what the urls.py entry looks like for our view.
 from django.conf.urls import patterns, url from views import infinite_choice_data urlpatterns = patterns('', url(r'^(?P<app_name>[\w\d_]+)/(?P<model_name>[\w\d_]+)/$', view=infinite_choice_data, name='infinite_choice_data'), ) 


Form field


As you know, the form field in django serves mainly to validate the data entered into it. Our field class will look like this:
 from django.forms import fields as f_fields from django.core.exceptions import ValidationError from django.core import validators from django.utils.translation import ugettext_lazy as _ class InfiniteChoiceField(f_fields.Field): '''Infinite choice field''' default_error_messages = { 'invalid_choice': _(u'Select a valid choice. %(value)s is not one of the available choices.'), } def __init__(self, data_model, multiple=False, disabled=False, widget_attrs=None, **kwargs): self.data_model = data_model self.disabled = disabled self.multiple = multiple widget = InfiniteChoiceWidget(data_model, multiple, disabled, widget_attrs) super(InfiniteChoiceField, self).__init__(widget=widget, **kwargs) def to_python(self, value): if value in validators.EMPTY_VALUES: return None if self.multiple: values = value.split(',') qs = self.data_model.objects.filter(pk__in=values) pks = set([i.pk for i in qs]) for val in values: if not int(val) in pks: raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) return qs else: try: return self.data_model.objects.get(pk=value) except self.data_model.DoesNotExists: raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) def prepare_value(self, value): if value is not None and hasattr(value, '__iter__') and self.multiple: return u','.join(unicode(v.pk) for v in value) return unicode(value.pk) 

I did not inherit from ModelMultipleChoiceField, since it works with the queryset, and we need to work with the model.
The constructor initializes the widget with the passed model, multiple and disabled flags, and specific attributes.
The to_python method receives as value either a single id or several id on a single line separated by commas and processes it depending on the multiple flag. In both cases, the presence of the selected id in the model is checked.
The prepare_value method prepares initialization data for display: if a single model instance is passed in the initial field parameter, then it returns the id of this instance as a string; if a list of instances or a QuerySet is passed, then returns a string with id separated by a comma.

Conclusion


The field is ready for use. The application can be downloaded here . Using the field is very easy:
 from django import forms from infinite_choice_field import InfiniteChoiceField from models import ChoiceModel class TestForm(forms.Form): choice = InfiniteChoiceField(ChoiceModel, multiple=True, disabled=False, required=False, initial=ChoiceModel.objects.filter(id__in=(7, 8, 12))) 

where ChoiceModel is an arbitrary view model
 class ChoiceModel(models.Model): title = models.CharField(max_length=100, verbose_name="Choice title") class Meta: verbose_name = 'ChoiceModel' verbose_name_plural = 'ChoiceModels' def __unicode__(self): return self.title 

It remains to not forget to connect the application in settings.py,
 INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'infinite_choice_field', ... } 

import urls
 urlpatterns = patterns('', # Examples: # url(r'^$', 'habr_test.views.home', name='home'), url(r'^', include('core.urls')), url(r'^infinite_choice_data/', include('infinite_choice_field.urls')), # Uncomment the next line to enable the admin: url(r'^admin/', include(admin.site.urls)), ) 

and display the static form of the TestForm in the template
 <!doctype html> <html> <head> <title>Test InfiniteChoiceField</title> {{test_form.media}} </head> <body> <form action=""> {{test_form}} </form> </body> </html> 

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


All Articles