Wednesday, March 26, 2008

Собственные фильтры в админке django (Custom FilterSpecs)

Обновление от 04.05.2012. Данная статья была написана довольно давно, но до сих пор вызывает некоторый интерес. Спешу сообщить, что с 23-го марта 2012 года с релизом Django 1.4 начинать поиск информации про custom фильтры стоит с официальной документации

Админка django - одна из самых убойных фичей этого фреймворка, как признаются и сами авторы. Она позволяет автоматически подключить к вашему сайту функционал по добавлению, редактированию и изменению как встроенных, так и пользователских моделей. Конечно, если логика добавления и изменения информации слишком сложна, то эта админка не подходит, однако, это не означает, что ее не нужно использовать - она вполне может быть дополнением к существующему функционалу. Представьте, что создавая сайт-блог вы добавили функциональность, которая позволяет быстро создавать и редактировать записи. Но это не мешает вам использовать джанговскую админку, когда вам нужно изменить какие-то параметры записи, которые нельзя изменить иначе(подробней об админке можно прочитать в 6ой главе Djangobook)

Существуют также случаи, когда админка Django предоставляет практически весь функционал, который нужен на сайте. Например, если вам нужно приложение для внутреннего использования, которое показывает сохранённую в базе информацию по каким-нибудь событиям(пример из повседневной жизни :) ), то лучше встроенной админки не найдёшь - можно легко настроить, какие поля показывать, по каким полям вести поиск, по каким фильтровать - и всё это - лишь пара строчек кода!
Фильтры в админке - более чем удобная вещь. Например, если у вас есть поля, которые могут принимать значения из фиксированного набора(успех/не успех, номер ошибки, и т.д.), то фильтры позволяют быстро отфильтровать, например, все "успешные" записи, или все записи с упоминанием ошибки 302. Однако, может случиться, что стандартных наборов фильтров вам не хватит, и вы пожелаете добавить свой собственный.

Я постараюсь показать весь процесс добавления нового фильтра на весьма полезном примере - на фильтре, который позволит фильтровать записи по заданному промежутку времени. Например, если у вас есть поле "Время создания", то можно будет отфильтровать все записи, которые были созданы в промежутке между 25 марта 2008 года 00:00 и 26 марта 2008 года 15:00. Итак, начнём.

Все фильтры, которые есть в админке, расположены в django.contrib.admin.filterspecs, все фильтры являются наследниками FilterSpec, у которого есть два особо интересных метода - create и register. Новый фильтр нужно будет зарегистрировать, вызвав функцию FilterSpec.register(test,factory), где test - это функция, принимающая объект ..Field (фильтры создаются автоматически через интроспекцию существующих полей модели) и возвращающая True, если данный фильтр применим для данного поля, а factory - это обычно класс фильтра. Функция register() просто сохраняет эту пару во внутренний список. Функция create() вызывается, когда для какого-нибудь поля нужно создать фильтр - данная функция просматривает список, запускает test функции, и когда находит подходящий фильтр - использует factory для создания объекта и возвращает его. После объявления класса FilterSpec идут сами фильтры, которые сразу же и регистрируются.
Здесь следует важное замечание: фильтры добавляются в список append'ом, поэтому последний добавленный фильтр и будет при create вызван последним. А так как в уже рассмотренном файлике последним добавляется фильтр, который умеет фильтровать любые поля, что означает, что если мы зарегистрируем новый фильтр, то до него при create просто никогда не дойдёт очередь. Такое поведение мне не очень понятно(я привык, что последние добавленные обработчики обрабатываются первыми:) ), к тому же оно сводит на нет прямолинейные попытки добавить свой собственный фильтр.

Однако есть как минимум два способа обойти это недоразумение:
- Тикет #5883 содержит патч, который меняет порядок добавления. Он очень простой и вполне разумный. Я надеюсь, что когда-нибудь он войдёт в основную ветку :)
- Вместо register() можно вставлять нужную запись напрямую в список: FilterSpec.filter_specs.insert(-1, (test, factory))
Что использовать - ваш выбор. Я для себя выбрал путь патча, ибо хоть второй и не требует изменения кода django, однако он противоречит дзену :)

Разобравшись в том, как регистрировать фильтр, перейдём к тому, как он функционирует. Все фильтры рендерятся через шаблон filters.html (django/contrib/admin/templates/admin/filters.html). Для того, чтобы фильтр нормально отобразился, он должен уметь возвращать список опций выбора(choices), которые и будут отображены автоматически. Естественно, это не подходит для нашей цели(как вы помните, мы пытаемся сделать фильтрование по промежутку времени, для задания этого промежутка нам понадобится два текстовых поля), поэтому мы поменяем поведение шаблона(как подменить шаблон админки на свой можно прочитать всё в той же 6ой главе Djangobook). Текст шаблона filters.html ниже:

{% load admin_list %}
{% load i18n %}
{% load filter_tags %}
{% if cl.has_filters %}<div id="changelist-filter">
<h2>{% trans 'Filter' %} </h2>
{% for spec in cl.filter_specs %}
{% if spec.is_datetime_interval_filter %}
{% datetime_interval_filter cl spec %}
{% else%}
{% filter cl spec %}
{% endif %}
{% endfor %}</div>{% endif %}
Хочется обратить внимания на отличия от стандартного файла - подгружаются filter_tags - именно в нём у нас будет находиться inclusion tag datetime_interval_filter, который будет использоваться для всех фильтров, для которых is_datetime_interval_filter есть True(т.е., для нашего фильтра). Теперь посмотрим на новый тэг:
from django import template
register = template.Library()

@register.inclusion_tag('datetime_interval_filter.html')
def datetime_interval_filter(cl, spec):
return spec.get_output_dict(cl)
Ничего интересного в нём нет - он просто возвращает нужный для рендеринга контекст, оставляя процесс генерации его нашему объекту-наследнику FilterSpec(схожим образом действует и стандартный тэг filter). Как видно, он использует темплейт datetime_interval_filter.html - его текст ниже:
<script language='javascript'>
function set_new_location_{{field_name}}()
{
var lte = document.getElementById("{{field_name}}_lte_edit").value;
var gte = document.getElementById("{{field_name}}_gte_edit").value;
document.location.href = '{{query_str}}&{{field_name}}__gte='+gte+'&{{field_name}}__lte='+lte;
}
</script>
<h3>By {{field_title|escape}}:</h3>
<ul>
<li><input id="{{field_name}}_gte_edit" value="{{gte_old_value}}" /></li>
<li><input id="{{field_name}}_lte_edit" value="{{lte_old_value}}" /></li>
<li><a onClick='javascript:set_new_location_{{field_name}}();' style="cursor:pointer;">Filter</a></li>
</ul>
Что есть в темплейте - для каждого нашего фильтра создаётся небольшой скрипт, который займётся обработкой переходов на новую страницу с отфильтрованными данными. Также выводятся два текстовых поля и ссылка для перехода. Для того, чтобы корректно отрендерить этот темплейт, нужно передать:

field_name - имя поля, по которому будем фильтровать.
query_str - строка запроса, которая может содержать и другие значения фильтров.
field_title - название поля.
gte_old_value/lte_old_value - сохранённые значения текстовых полей. Выводятся в те же поля после применения фильтра, что очень удобно для редактирования.

Вероятно, код можно было бы упростить, если заметить, что функцию можно использовать одну для всех таких фильтров, или еще каким-нибудь образом, но он работает - а это всё, что нужно от клиентской части.

Ну, и наконец, сам объект-фильтр:
from django.contrib.admin.filterspecs import FilterSpec
from django.utils.encoding import iri_to_uri

class DateFieldIntervalFilterSpec(FilterSpec):
def __init__(self, f, request, params, model):
super(DateFieldIntervalFilterSpec, self).__init__(f, request, params, model)

is_datetime_interval_filter = True

def get_output_dict(self,cl):
p = cl.params.copy()
return {
'field_name': self.field.name,
'query_str': iri_to_uri(cl.get_query_string(remove=["%s__lte"%self.field.name,"%s__gte"%self.field.name])),
'gte_old_value': p.get('%s__gte'%self.field.name,''),
'lte_old_value': p.get('%s__lte'%self.field.name,''),
'field_title':self.field.verbose_name
}
Небольшие пояснения по коду - как я уже говорил выше - мы наследуемся от FilterSpec. Конструктор не представляет никакого интереса. Флажок is_datetime_interval_filter мы уже упоминали - когда рассматривали темплейт filters.html. Единственная функция, которая требует рассмотрения - это get_output_dict(), которая возвращает словарь, используемый для рендеринга конечного html кода. field_name и field_title не представляют особого интереса - мы просто возвращаем имена поля, для которого работает фильтр. query_str мы получаем из объекта ChangeList - это основной объект, который содержит все фильтры, а также предоставляет доступ к нужным этим фильтрам параметрам(найти его можно в django.contrib.admin.views.main). В качестве параметров функции мы передаём список remove - в нём находятся те значения, которые возвращать не нужно, даже если они указаны. Это позволит нам избежать повторений вида &time__lte=smth1&time__lte=smth2. gte_old_value/lte_old_value мы получаем также из ChangeList - из текущих параметров.

После этого осталось зарегистрировать новый шаблон(для тестирования это можно сделать даже в Urlconf, хотя это и может вызвать некоторые проблемы при автоматической перезагрузке девелопмент сервера):
FilterSpec.register(lambda f: isinstance(f, models.DateField),DateFieldIntervalFilterSpec)
Хочу заметить, что я подменяю все DateTimeField(мне откровенно не нравится стандартный фильтр для DateTime), в то время, как можно добавить проверку на наличие у поля дополнительного атрибута, который добавлять только тем полям в модели, для которых нужен такой фильтр.

Ну, и конечно, хотелось бы посмотреть на результат. Вот скриншшот:На скриншоте - сохраненные Cdr events от Asterisk. Если как-нибудь будет время - я постараюсь осветить тот небольшой код, который позволяет сохранять евенты от Астериска в базе - это потрясающая иллюстрация возможностей Python, Django и Twisted.

Буду рад любым фидбекам и исправлениям. :)

Friday, March 21, 2008

Курс «Как стать успешным фрилансером и остаться человеком»

[info]4annel в своем ЖЖ публикует курс статей "Как стать успешным фрилансером и остаться человеком"

Как справедливо замечено здесь, хоть я и не дизайнер, но подходы к организации - одинаковые, так что можно и чему-то научиться. Надеюсь, что этот курс найдёт своих читателей - ведь качество и количество материала напрямую зависит от желания развивать идею, а желание в свою очередь напрямую зависит от количества слушателей.

P.S. Не могу не поделиться - впервые обратил на этот проект чуть больше внимания: Twisted . Посмотрите и вы - это потрясающий фреймворк!

Thursday, March 13, 2008

Прелести GIT и бэкап SVN репозитория

Натолкнувшись на неведомый доселе GIT сначала здесь, а потом здесь, я, несмотря на большую приверженность SVN, решил опробовать его при случае. Мне хотелось попробовать, как он справится с задачами, приближенными к боевым.
Также не так давно я столкнулся с небольшой проблемой, которую хотелось бы решить. Дело в том, что для одного достаточно объемного проекта(более 9 месяцев труда 5-ти программистов, 2100 ревизий в SVN репозитории) захотелось сделать бэкап. Я вообще трудно представляю себе жизнь без системы контроля версий, которая предоставляет возможность отката, бранчевания и т.д. Именно поэтому простой бэкап HEAD ревизии меня абсолютно не устраивал. Проблема усложнялась тем, что администраторского доступа к репозиторию не было - нужно было думать, как ограничиться стандартным клиентским.
Насколько я знаком с возможностями SVN, клонирование репозитория клиентом в список этих возможностей не входит. Конечно, можно написать простенький скрипт, который будет поревизионно апдейтить код, а после этого коммитить его куда-нибудь в локальный репозиторий, однако не факт, что такое решение будет быстрым и качественным. В сочетании с нехваткой времени и необязательностью подобного бэкапа это привело к тому, что скрипт я так и не написал.
Однако, когда дело дошло до тестирования возможностей GIT, я решил убить сразу двух зайцев - сделать бэкап, а также проверить насколько быстр, удобен и неприхотлив к ОЗУ/ПЗУ вышеназванный GIT, благо он умеет клонировать SVN репозитории. Так что я начал с того, что поставил себе GIT. Все указания по установке можно найти здесь. Для теста поставил на Windows XP SP2( повременю пока с установкой на Ubuntu - всё равно я уверен, что проявит GIT и там себя не менее достойно ). Установочные файлы брал отсюда. Итак, после установки я запустил команду

git-svn clone http://host/path
дабы сбэкапить нужную мне ветку(975 коммитов) и стал ждать, пока он закончит работу. Сразу скажу, что длилось это довольно долго - после 4-х часов работы он сделал только чуть больше половины. Сколько времени это заняло в целом я даже и не знаю - я не выдержал и оставил уомпьютер трудиться на ночь.
На утро можно оценить результаты. Данные из выбранной ветки занимают около 25-ти Мб. GIT в отличие от SVN не плодит множество папочек во всех каталогах, а создает локальный репозиторий, в котором и хранит все данные. Т.о., сами данные хранятся в чистом виде, тогда как в SVN дополнительные папки увеличивают объем до 51-го Мб. Теперь обратим внимание на то, сколько занимает локальный репозиторий GIT, ведь цифра действительно поражает - всего 17 Мб - а ведь здесь хранятся все изменения, которые велись в ветке с начала проекта! Т.о., данные вместе с репозиторием GIT занимают только 42 метра. Конечно, в наше время объемы винчестеров позволяют не обращать внимание на такие мелочи (к тому же, после компиляции 25 Мб всё равно превращаются в 2 Гб:) ), однако всё же приятно...
Идём далее. Оценим грубо скорость чекаута. Для SVN эта величина - 35 секунд(стоит всё же учесть, что checkout при тестировании вёлся по локальной сети). С помощью же GIT чекаут из локального репозитория был сделан за 4 секунды! Это при том, что простое копирование этих же данных идёт около 7 секунд. Впечатляет!

Вывод: GIT - это нечто новое и весьма интересное, на что стоит обратить внимание при выборе системы контроля версий для проектов. Конечно, он не так удобен и привычен(субъективное мнение) как SVN, однако это всё дело времени - ведь GIT это не просто version control system, это инфраструктура для создания идеальной такой системы.
Редакция от 27.08.08. Тем, кому нужен простой бэкап svn репозитория - могу посоветовать svnsync