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