Собственное поле формы в Django

Собственное поле формы в Django
Я очень большой фанат фреймворка Django и все свои проекты пишу исключительно на нем. Сегодня я расскажу о том, как расширить стандартную библиотеку полей формы собственным оригинальным решением. Задача статьи не в том чтобы предложить готовое решение, а в том, чтобы осветить технологию создания кастомных полей.
Небольшое отступление. Однажды я корпел над созданием базы знаний для компании, в которой в то время работал. База представляла собой набор статей, помеченных тегами. К элементу ввода тегов предъявлялись следующие требования:

  • Множественный ввод
  • Автодополнение вводимого тега
  • Теги могут содержать пробелы (состоять из нескольких слов)
  • Возможность создать новый тег, а не выбрать из списка

После недолгих поисков, я нашел jQuery-плагин Tag-It!, который полностью удовлетворял требованиям к виджету. Осталось только прикрутить это поле к Django.

Источник данных для автодополнения

Наш виджет будет брать данные из модели, куда, собственно, и сохраняются введенные теги

from django.db import models
from unicodedata import category
from django.utils.http import urlquote
import re

class Tag(models.Model):
    """ Model of Tags """

    name = models.CharField(max_length=200, null=False, verbose_name="Tag name")
    slug = models.CharField(max_length=400,
                                                 editable=False,
                                                 verbose_name=u'Slug',
                                                 unique=True,
                                                 null=False)

    def __unicode__(self):
        return self.name

    @staticmethod
    def _generate_slug(value):
        slug = ''.join(ch for ch in value[:200] if category(ch)[0] != 'P')
        return urlquote(re.sub(r'([ ]+_)|(_[ ]+)|([ ]+)', '_', slug))

    def save(self, *args, **kwargs):
        self.name = self.name.lower()
        self.slug = self._generate_slug(self.name)
        super(Tag, self).save(*args, **kwargs)

    @classmethod
    def get_or_create(cls, value):
        slug = cls._generate_slug(value.lower().strip())
        if cls.objects.filter(slug=slug).exists():
            return cls.objects.get(slug=slug)
        else:
            return cls.objects.create(name=value.lower().strip())

Так как теги могут содержать пробелы, и прочий мусор, введем в модель поле slug, четко идентифицирующее тег по содержимому, независимо от того, сколько пробелов между словами в названии тега. Введем также метод класса get_or_create, возвращающий тег, если он найден по полю slug, или создающий новый тег в обратном случае. Кроме того, перед созданием нового тега, мы приводим его к нижнему регистру в методе save для единообразия.

 

View для работы автодополнения

Набросаем небольшое представление, возвращающее список тегов, начинающихся с введенных символов.
Плагин Tag-It! передает введенную строку в переменной term.

from models import Tag
from django.http import HttpResponse
import json

def tag_autocomplete(request):
    """ url: /tag_autocomplete/"""

    value = request.GET['term']

    available_tags = Tag.objects.filter(name__startswith=value.lower())

    response = HttpResponse(json.dumps([unicode(tag) for tag in available_tags]), content_type="application/json")
    return response

Виджет и поле формы

Виджет и поле формы можно объявить непосредственно в месте их применения — в forms.py. Я так и сделал, так как не планировал его использовать где-либо еще.
Виджет я унаследовал от скрытого поля ввода, так как визуализацией занимается плагин Tag-It!..

from django import forms

class TagitWidget(forms.HiddenInput):
    """ Widget on the basis of Tag-It! http://aehlke.github.com/tag-it/"""

    class Media:
        js = (settings.STATIC_URL + 'js/tag-it.js',
              settings.STATIC_URL + 'js/tagit_widget.js',)
        css = {"all": (settings.STATIC_URL + 'css/jquery.tagit.css',)}

tag-it.js и jquery.tagit.css — файлы плагина Tag-It!.. Содержимое tagit_widget.js будет описано ниже.

class TagitField(forms.Field):
    """ Tag field """

    widget = TagitWidget

    def __init__(self, tag_model, *args, **kwargs):
        self.tag_model = tag_model
        super(TagitField, self).__init__(*args, **kwargs)

    def to_python(self, value):
        tag_strings = value.split(',')
        return [self.tag_model.get_or_create(tag_string) for tag_string in tag_strings if len(tag_string) > 0]

    def validate(self, value):
        if len(value) == 0 and self.required:
        raise ValidationError(self.error_messages['required'])

    def prepare_value(self, value):
        if value is not None and hasattr(value, '__iter__'):
            return ','.join((unicode(tag) for tag in value))
        return value

    def widget_attrs(self, widget):
        res = super(TagitField, self).widget_attrs(widget) or {}
        res["class"] = "tagit"
        return res

В объявлении поля формы указываем виджет. В конструктор кроме обычных параметров передаем модель тегов, с помощью которой список названий тегов преобразуем в список объектов-тегов в методе to_python. Метод prepare_value делает обратное преобразование. В методе widget_attrs добавляем скрытому полю атрибут «class», по которому скрипт будет находить нужные поля для применения к ним плагина Tag-It!..

Сам скрипт находится в файле tagit_widget.js и имеет следующий вид:

$(document).ready(function() {
    $(".tagit").tagit({
        allowSpaces: true,
        autocomplete: {delay: 0,
                       minLength: 2,
                       source: "/tag_autocomplete/"
        }
    });
});

О дополнительных опциях плагина можно посмотреть здесь. Скажу только, что здесь я разрешаю тегам содержать пробелы (allowSpaces), делаю автодополнение без задержки после ввода (delay), начиная со второго введенного символа (minLength) и беря варианты из нашей вьюхи (source).

Заключение

Поле готово к использованию. Применить его можно следующим образом:

from models import Tag

class SomeForm(forms.Form):
    tag = TagitField(Tag, label='Tags', required=True)

Главное, не забыть в шаблоне подключить статику из этой формы

<!doctype html>
<html>
    <head>
        <title>Tag-It!</title>
        {{some_form.media}}
    </head>
    <body>
        <form action="">
            {{some_form.as_p}}    
        </form>
    </body>
</html>

Приятного django-кодинга.