среда, 2 декабря 2009 г.

Используем встроенные строковые методы Python'а в Django шаблонах

Вместо предисловия

Привет! Давно здесь не отписывался. Почему? Наверное главная причина, что после выхода 1.1 версии уже не так активно слежу за развитием Django. Может быть в ближайшее время меня пробъет на творчество и я выдам пару-тройку новых постов, но не обещаю ;)


Но это я отвлекся от темы поста. А она заключается вот в чем. Надо было сегодня в шаблонах Django для некоторых урлов убрать конечные слеши, т.е. просто вызвать url.rstrip('/'). Просмотрев в который раз список всторенных шаблонных фильтров и не обнаружив там нужного, я задумался: как быть? Создавать простой фильтр, типа:

from django.template import Library
from django.template.defaultfilters import stringfilter


register = Library()


@register.filter
@stringfilter
def rstrip(text, chars=None):
    return text.rstrip(chars)

совершенно не хотелось. Ибо вдруг мне в будущем захочется добавить поддержку lstrip или strip метода. Что надо будет морочиться с Ctrl+C, Ctrl+V и минимальными исправлениями отяжеляя эту темплейт библиотеку?

Потому не долго думая и вроде бы не отыскав необходимого мне решения в гугле, я решил повелосипедить и добавить возможность вызова всех строковых методов Python в Django шаблоне.

Первая моя идея была очень проста, перебрать все публичные методы пустой строки и зарегестрировать их как шаблонный фильтр, используя lambda. Переходя на код, это приобрело примерно следующий вид:

from django.template import Library
from django.template.defaultfilters import stringfilter


register = Library()


for name in dir(u''):
    if name.startswith('_'): continue

    filter = lambda value, *args: getattr(unicode(value), name)(*args)
    filter = stringfilter(filter)

    register.filter(name, filter)

Написав простой тест-кейс я уже было приготовился доставать шампанское и переходить к следующей задаче, но не тут-то было :( TemplateSyntaxError для {{ url|strip:"/" }} заставила притормозить коней. Почему же так произошло? Я пошел к месту ошибки и понял, что проблема в *args, точнее в том, что django.template.FilterExpression.args_check ожидает определенный набор аргументов, а не, наоборот, не определенный заранее.

Что ж, пришлось усложнять код. Для начала я добавил поддержку только трех аргументов, ибо это максимальноя кол-во используемых аргументов для любого строкового метода, кроме format. Затем переписал предыдущую безымянную функцию в:

@stringfilter
def make_filter(name):
    def filter(value, first=None, second=None, third=None):
        args = [first, second, third]
        method = getattr(force_unicode(value), name)

        while True:
            try:
                return method(*args)
            except TypeError:
                args.pop(len(args) - 1)

    return filter

и заодно переписал регистрацию этого фильтра. Запустил тест-кейс, получил Ran OK! и на свою голову решил усложнить тест-кейс, проверив работу фильтра с двумя аргументами, например, {{ "abcdef"|replace:"abc":"def" }}.

Каково же было мое удивление, когда Django сказала, нет много аргументов для фильтров - это не хорошо. И сгенерировала очередную TemplateSyntaxError. Что ж, пришлось реализовывать это в виде отдельного простого тега {% stringmethod %}. Принцип его работы простейший, как видно из кода:

@register.simple_tag
def stringmethod(name, value, first=None, second=None, third=None):
    return make_filter(name)(value, first, second, third)

В свою очередь это повлекло за собой обновление теста, и {{ "abcdef"|replace:"abc":"def" }} превратилось в {% stringmethod "replace" "abcdef" "abc" "def" %}. Монструозно, соглашусь, но что поделаешь. В итоге, получив Ran OK! я немного подправил документацию и выложил это все дело, как отдельный гист на гитхаб.

Пользуйтесь, возможно вам это пригодится!

зы. На последок упомяну еще о пару особенностях stringmethods. Во-первых, она не переписывает встроенный join фильтр, во-вторых, она плохо справляется с format.

зыы. Если вы знаете reusable apps решающие похожие проблемы - не стесняйтесь писать в комменты :)

вторник, 12 мая 2009 г.

Sphinx documentation + GitHub pages = <3

Вообщем буду краток ;)

Для документирования питоньих проектов очень удобно использовать Sphinx. Это всем известно. А если не известно, то и Python, и Django документированы именно им.

На GitHub'е есть возможность размещения HTML страниц проекта и последующего доступа к ним по адресу: http://username.github.com/repo_name/.

Само собой разумеется, было бы очень неплохо подружить их. И посему я на коленке написал следующую Makefile-команду:

project=YOUR_PROJECT_NAME
docs_dir=$(TMPDIR)/$(project)-docs

ghdocs:
    rm -rf $(docs_dir)
    $(MAKE) -C docs html
    cp -r docs/_build/html $(docs_dir)
    mv $(docs_dir)/_static $(docs_dir)/static
    mv $(docs_dir)/_sources $(docs_dir)/sources
    perl -pi -e "s/_sources/sources/g;" $(docs_dir)/*.html
    perl -pi -e "s/_static/static/g;" $(docs_dir)/*.html
    git checkout gh-pages
    rm -r sources static
    cp -rf $(docs_dir)/* .
    git add .
    git commit -a -m 'Updates $(project) documentation.'
    git checkout master
    rm -rf $(docs_dir)

Теперь по вызову make ghdocs я обновляю документацию после каждого релиза tddspry, которая теперь и впредь доступна всем ;)

зы. После коммита документацию можно было бы сразу пушить в репо, но иногда не бывает интернета под рукой, иногда очепятку найдешь уже после коммита, посему я потом вручную перехожу на gh-pages и делаю git push origin gh-pages.

пятница, 24 апреля 2009 г.

Test or die!

Сегодня я расскажу вам о тестах. Нет, я не буду рассказывать, почему это важно или зачем это нужно делать. Об этом вы прочтете у других авторов. Я же расскажу вам при помощи чего я тестирую свои Django-проекты и приложения.

Итак, знакомтесь, tddspry - альтернатива стандартному подходу в тестировании Django приложений.

Как мы знаем из документации и практики для тестирования приложения надо:

  • Собственно написать док или юнит-тест. Если это юнит-тест, то разместить его в tests.py или tests/ приложения (при этом надо помнить, что в этом приложении должен присутствовать хотя бы пустой models.py).
  • Выполнить тесты при помощи ./manage.py test app.

Вроде бы ничего сложного или религиозно неправильного нет. Но, я просто не привык к ним. Не привык и точка. Мне намного проще тестировать приложения при помощи python-nose, а вместо стандартного django.test.Сlient использовать twill-овский браузер.

Не буду вдаваться в подробности почему так произошло, но, поверьте, я рад этому до сих пор. Единственное, что меня не устраивало в последнее время это кое-какая монструозность нашей библиотеки для тестирования (которая tddspry и зовется ;) ). И посему я решил вспомнить и применить на практике значение слова рефакторинг. Прошел процесс быстро и практически безболезнено и за три неплоных дня мы имеем практически, да чего уж там таить, фактически новое лицо для нее.

Итак, что-же сейчас умеет tddspry:

  • Есть три базовых тест-кейса:
    • tddspry.NoseTestCase - этот тест-кейс предназначается для любых тестов и по-большому счету является нашим ответом Чемберлену unittest.TestCase. Его особенности: он содержит все декораторы из nose.tools как статические методы класса, а все прочие функции из того же nose.tools как методы объекта.
    • tddspry.django.DatabaseTestCase - этот тест-кейс предназначается для тестов в которых необходимо использовать базу данных. Как и в стандартном поведении unittest.TestCase он создает или просто настраивает для тестов: а) базу данных sqlite3 в оперативной памяти; б) текущую базу данных из settings файла проекта; в) тестовую базу данных. Самым быстрым для тестов является вариант создания sqlite3 базы данных в оперативной памяти и именно он используется по умолчанию. Но никто не говорит, что вы не сможете протестировать любой другой адаптер. Для этого просто укажите database_name аттрибут в вашем тест-кейсе, наследующем DatabaseTestCase. Также, аналогично к джанговским тест-кейсам, DatabaseTestCase умеет загружать фикстуры. И напоследок этот тест-кейс содержит три безумно простых метода для проверки создания, удаления и исправления модели: они же check_create, check_delete, check_update.
    • tddspry.django.HttpTestCase - этот тест-кейс предназначается для тестов серверной части вашего Django проекта при помощи twill-браузера. Почему twill? Потому что он простой и он на Python'e ;) Единственным существенным его недостатком считаю отсутствие простой функции для кастомного (не GET-запроса), посему в тестировании POST или PUT запросов вам прийдется дополнительно использовать или джанговский тест-клиент или просто напросто urllib2. Так, но это мы отвлеклись. Что еще особенного в HttpTestCase? Да то, что он содержит в себе все функции из twill.commands, а также кое-какие стоящие хелперы, как то login, login_to_admin, logout, из названия которых становится ясно, что они делают. Также упрощен интерфейс перехода по урлам, в функцию go можна передать как уже чистый урл, так и только имя его урлпаттерна, которое будет сконвертировано в чистый урл при помощи django.core.urlresolvers.reverse.
  • Есть пару воспомогательных хелперов как-то:
    • tddspry.django.helpers.create_profile
    • tddspry.django.helpers.create_staff
    • tddspry.django.helpers.create_superuser
    • tddspry.django.helpers.create_user
    из названия которых вроде бы опять становится ясно, что они делают.
  • Есть крутой воспомогательный хелпер для проверки работы регистрационного механизма django-registration. Он зовется tddspry.django.helpers.registration.registration.
  • Есть прелесный декоратор show_on_error, который автоматически включен для каждого тестового метода в HttpTestCase. Его прелесть заключается в том, что при ошибке twill'a он показывает содержимое страницы на которой произошла ошибка или при наличии переменной окружения TWILL_ERROR_DIR сохраняет в этой директории html-страницу с ошибкой.

Для установки библиотеки, достаточно просто выполнить sudo easy_install tddspry. А для непосредственного тестирования вашего Джанго-проекта или приложения необходимо присвоить переменной окружения DJANGO_SETTINGS_MODULE значение актуальных настроек и сказать nosetests :)

Для окончательного примера покажу, какой командой тестируется сам tddspry:

PYTHONPATH=/srv/projects/tddspry-github:. DJANGO_SETTINGS_MODULE=testproject.settings nosetests -w .. --with-coverage --cover-package=tddspry --exe testproject

Так что, надеюсь, что моя информация будет вам полезна и вы будете следить за дальнейшим развитием tddspry. Ведь в планах у меня есть поддержка Django 1.1, использование Selenium или Windmill для клиентских тестов и куча прочих полезностей.

зы. Совсем забыл ;) Примеры тестов, написанных при помощи tddspry, вы можете найти где-то здесь.

зыы. Особенная благодарность Almad'у, автору django-sane-testing, за стимул к полному рефакторингу tddspry. Краткое объяснение: он написал мне в личку на гитхабе, мол так и так, я уже сделал практически то же самое только с Селениумом и без Твилла, посмотри может тебе пригодится. Я посмотрел на его тесты и я окончательно понял, что надо улучшать tddspry.

суббота, 18 апреля 2009 г.

Давайте просто поищем!

Как Вы, наверное, знаете поиск и Django - понятия весьма и весьма родные. Каких только приложений не написали для этих целей: и djapian, и и solango, и даже haystack. И это все хорошо, и замечательно, но что делать если нужен простой, совсем простой, поиск по сайту без использования дополнительных поисковых движков? Ответ прост: писать свой очередной велосипед. Что я с удовольствием и сделал!

Итак, знакомтесь: kikola.contrib.basicsearch - приложение для легковесного поиска по любым моделям в вашем проекте.

Для того, чтобы быстро и ясно понять, что оно умеет и как оно работает, предлогаю следующую задачу: в проекте используются стандартная джанговская модель FlatPage и самописная SampleModel. Надо организовать поиск по им, причем так, чтобы результаты из SampleModel печатались раньше, чем результаты из FlatPage.

Решение:

  1. Добавляем kikola.contrib.basicsearch в INSTALLED_APPS;
  2. Настраиваем SEARCH_MODELS там же, в settings'ах:
    SEARCH_MODELS = {
       'flatpages.FlatPage': {
           'description': '{{ obj.content|truncatewords_html:20 }}',
           'fields': ('title', 'content'),
           'priority': 0,
           'title': '{{ obj.title }}',
       },
       'sample.SampleModel': {
           'description': '{{ obj.overview|truncatewords_html:20 }}',
           'fields': ('name', 'slug', 'overview'),
           'priority': 100,
           'trigger': lambda obj: obj.is_active,
       },
    }
    
  3. Добавляем kikola.contrib.basicsearch.urls в ROOT_URLCONF-модуль;
  4. ???
  5. Profit!

Иными словами, сейчас запустив тестовый сервер проекта и зайдя на 127.0.0.1:8000/search/ перед нами будет страничка с поисковой формой в которой мы сможем поискать и по всем FlatPage, и по только активным SampleModel.

зы. Код SampleModel:

from django.db import models
from django.db.models import permalink
from django.utils.translation import ugettext_lazy as _


class SampleModel(models.Model):

   name = models.CharField(_('name'), max_length=64)
   slug = models.CharField(_('slug'), max_length=64)
   overview = models.TextField(_('overview'), blank=True)
   is_active = models.BooleanField(_('actived'), blank=True, default=True)

   def __unicode__(self):
       return self.name

   def get_absolute_url(self):
       return ('sample_urlname', [self.slug])
   get_absolute_url = permalink(get_absolute_url)

пятница, 27 февраля 2009 г.

django-mediafiles 0.2

Сегодня оффициально вышел первый публичный релиз моего проекта, под названием django-mediafiles.

О чем этот проект уже практически понятно из его названия, а чтобы рассеять последние ваши сомнения, посвечу скриншотами того, что он умеет:

Главная страница

Создание новой директории или файла, загрузка файла на сервер

Удаление директории

Предпросмотр рисунков, кода или редактирование текстовых файлов

Надеюсь, что кому-то этот проект пригодится. Enjoy!