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!

22 Февраль 2009 г.

Джанго дб моделс кью - ай лав ю!

Значится, дано следующий фрагмент models.py:

class Attribute(models.Model):
    ATTRIBUTE_PERMISSIONS = (
        ('public', _('Public')),
        ('semi-private', _('Semi-Private')),
        ('private', _('Private'))
    )

    property_ = models.ForeignKey('Property')
    type_ = models.ForeignKey('AttributeType')
    value = models.CharField(max_length=255)
    permission = models.PositiveIntegerField(choices=ATTRIBUTE_PERMISSIONS,
        default=ATTRIBUTE_PUBLIC)
    granted_users = models.ManyToManyField('auth.User', blank=True, null=True)

class AttributeType(models.Model):
    slug = models.SlugField(max_length=32)
    """
    Other implementation of AttributeType class was stripped
    """

class Property(models.Model):
    owner = models.ForeignKey('auth.User')
    """
    Other implementation of Property class was stripped
    """

Теперь, внимание, задание: надо найти все объекты Property, у которых:

  • значение публичного аттрибута attr1 равно "Public";
  • значение полу-приватного аттрибута attr2 равно "Semi-Private";
  • значение приватного аттрибута attr2 равно "Private".

Казалось бы, как сложно и тут будет много запросо к базе данных, ведь это ж Django, это ж никому не нужный ORM. Вот если бы настрочить большой и сложный SQL, эх. Но, вот именно тут на арену и выходит django.db.models.Q.

Судите сами,

  • для того чтобы отфильтровать проперти по значению публичного аттрибута, нам надо просто отфильтровать проперти по .filter(attribute_type___slug='attr1', attribute__permission='public', attribute__value='Public');
  • для фильтра по значению полу-приватного аттрибута, надо дополнительно ввести фильтр проверки на вхождение пользователя в список разрешенных пользователей или этот пользователь является автором проперти: .filter(attribute_type___slug='attr2', attribute__permission='semi-private', attribute__value='Semi-Private', attribute__granted_users=USER).filter(attribute_type___slug='attr2', attribute__permission='semi-private', attribute__value='Semi-Private', owner=USER)
  • для фильтра же по значению приватного аттрибута, мы просто оставляем вторую часть предыдущего фильтра: .filter(attribute_type___slug='attr2', attribute__permission='private', attribute__value='Private', owner=USER)

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

def filter_by_attribute(queryset, **kwargs):
    # Public attributes filter
    basequery = Q(attribute__permission='public')

    if 'user' in kwargs:
        user = kwargs.pop('user')

        # Semi-private attributes filter
        basequery |= Q(attribute__permission='semi-private') & \
                     Q(Q(attribute__granted_users=user) | \
                       Q(owner=user))

        # Private attributes filter
        basequery |= Q(attribute__permission='private') & Q(owner=user)

    for slug, value in kwargs.items():
        query = basequery & \
                Q(attribute__type___slug=slug) & \
                Q(attribute__value=value)

        queryset = queryset.filter(query)

    return queryset

queryset = Property.objects.all()
queryset = filter_by_attribute(queryset, attr1='Public', attr2='Semi-Private', user=USER)
queryset = filter_by_attribute(queryset, attr2='Private', user=USER)

Вот и все, мы отфильтровали все проперти по необходимым значениям аттрибутов, в то же время сохранив для этих аттрибутов сменные права доступа. И да, выборку всех этих проперти выполнил один большой запрос, сгенерированный джанговским ORM.

зы. Я знаю, что filter_by_attribute лучше было бы прицепить к кастомному PropertyManager'у, но это выходит за рамки моей сегодняшней темы ;)