Я очень большой фанат фреймворка 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-кодинга.