Показаны сообщения с ярлыком django template. Показать все сообщения
Показаны сообщения с ярлыком django template. Показать все сообщения

12 сентября 2011 г.

Проверяем значение переменной на True, False, None в Django шаблонах

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

Но сегодняшний инцидент стоит просто за гранью моего понимания. Итак, есть модель с полем NullBooleanField, назовем его verified. Далее в шаблоне нужно отображать специфический класс для HTML-тега если verified is False. Что ж, это просто, за неимением is возпользуюсь проверенным ==:

<div{% if verified == False %} class="ui-state-error-text"{% endif %}>
    Some text
    Some input
</div>

Ок, обновляю страницу и думаю все ок. Однако, не тут-то было. Результата нет, в исходном тексте тоже никакого class="ui-state-error-text" нет и в помине. Странно. Ладно, дай думаю проверю в shell_plus. Проверка подтверждает мои опасения:

In [2]: t = Template('{% if verified == False %}False{% else %}Not False{% endif %}')

In [3]: t.render(Context({'verified': False}))
Out[3]: u'Not False'

In [4]: t.render(Context({'verified': True}))
Out[4]: u'Not False'

In [5]: t.render(Context({'verified': None}))
Out[5]: u'False'

Хм, ясное дело свитч на {% if not verified %} не достиг никаких результатов (теперь класс будет печататься и в случае, когда verified is None):

In [6]: t = Template('{% if not verified %}False{% else %}Not False{% endif %}')

In [7]: t.render(Context({'verified': False}))
Out[7]: u'False'

In [8]: t.render(Context({'verified': True}))
Out[8]: u'Not False'

In [9]: t.render(Context({'verified': None}))
Out[9]: u'False'

Хм, что же делать? Как же быть? Почему {% if verified == None %} работает если verified is None. Спросил совета у команды. И надо сказать их совет подействовал, надо просто проверку на False сменить на проверку на 0. И все начинает работать:

In [10]: t = Template('{% if verified == 0 %}False{% else %}Not False{% endif %}')

In [11]: t.render(Context({'verified': False}))
Out[11]: u'False'

In [12]: t.render(Context({'verified': True}))
Out[12]: u'Not False'

In [13]: t.render(Context({'verified': None}))
Out[13]: u'Not False'

Теперь вопрос, что заставило Django-девов не добавить boolean константы True и False в {% if %} темплейт тег? Где поддержка var is (None|True|False)? Уже ж сделали вроде как вменяемый if, что помешало сделать это раз и навсегда? Особенно в свете того, что поддержка конструкций item in list и item not in list была добавлена.

В целом был разочарован я изрядно, но теперь буду помнить, что для проверки на True, False в шаблонах надо сравнивать с 1 и 0. Прям как хрен знает где еще :(

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 решающие похожие проблемы - не стесняйтесь писать в комменты :)