Чтобы все не решили, что я перестал писать технические посты про Django, вот маленький пост средней полезности.

Если вы какое-то время работаете с Django и используете inclusion-теги, то наверняка сталкивались с неудобством, что кроме переменных контекста, возвращаемого из тега, в шаблон не попадает ничего. В частности, там нет всяких полезных {{ user }}, {{ MEDIA_URL }} и прочих "глобальных" вещей из родительских контекстов.

Я одно время был даже пропонентом такого подхода, потому что он "чище": в контекст не сливается куча глобальных переменных, которые теоретически засоряют namespace внутреннего независимого шаблона. Но практика показывает, что все наоборот. Случаев, когда вам нужны глобальные данные в контексте, полно, а вот чтобы они мешались, я как-то не припоминаю совсем. Да и кроме того, поскольку шаблонный язык очень ограничен, прострелить себе ногу с помощью глобальных данных там довольно сложно. Средств мало :-).

Поэтому сейчас я думаю, что на самом деле inclusion-тег должен просто передавать в свой шаблон весь родительский контекст тоже. А пока этого не произошло, я хочу описать способ, которым я это делаю.

Проще всего сделать, конечно так:

@register.inclusion_tag('template.html', takes_context=True)
def my_tag(context):
    return {
         'user': context['user'],
         'MEDIA_URL': context['MEDIA_URL'],
    }

Но это скучно и нуждается все время в поддержке по дописыванию нужных переменных. Чтобы это автоматизировать, можно было бы воспользоваться методом типа items(), выдающему все содержимое, но такого метода у контекста нет. Потому что контекст — это на самом деле не dict, а стек из dictов. Поэтому нужно было бы бежать по всем стеку циклом, и у каждого dictа брать items(). Но это уже слишком многословно.

Поэтому вместо того, чтобы бороться со стековой структурой контекста, я использую ее себе на благо. Ведь вместо того, чтобы копировать по сути весь контекст, можно естественным образом добавить свои переменные в еще один уровень стека и передать этот же самый контекст в свой шаблон! Тогда шаблон увидит и свои переменные, и все остальные, что были в контексте до этого.

Жаль только, что декоратор inclusion_tag написан так, что сделать этого не позволяет. Поэтому я пишу тег руками:

class MyTagNode(template.Node):
    def render(self, context):
        context.update({
            # собственные переменные
        })
        result = template.loader.get_template('template.html').render(context)
        context.pop()
        return result

@register.tag
def my_tag(parser, token):
    return MyTagNode()

Метод context.update берет переданный dict и "пушит" его наверх стека. Соответственно context.pop его потом оттуда удаляет. Вуаля!

Хотя, повторюсь, хотелось бы, чтобы это было дефолтным поведением inclusion-тега. Потому что он все таки удобней, чем написание тега целиком.

Комментарии: 10

  1. Александр Соловьёв

    Хотя, повторюсь, хотелось бы, чтобы это было дефолтным поведением inclusion-тега.

    Что мешает декоратор свой написать? :)

  2. Иван Сагалаев

    Да я, в общем-то, не за себя беспокоюсь :-). Просто мне кажется, что если бы это было в Джанге "из коробки", это было бы более естественно.

  3. Александр Кошелев

    Эх, inclusion_tag, inclusion_tag - в нем много чего хочется допилить...

  4. Boo

    а еще *args и **kwargs в simple_tag

  5. Иван Сагалаев

    Эх, inclusion_tag, inclusion_tag - в нем много чего хочется допилить...

    А вот мне совсем не хочется видеть его допиленным. Сказать по правде, я думаю, что он просто сам по себе — уже отживший паттерн. Он решает задачу выдачи контекста в шаблон, а она не является тем, что нужно решать. В моем примере это по сути одна строка. Гораздо важнее и в simple_tag, и в inclusion_tag — это устранение boilerplate-кода по парсингу и ресолвингу переменных из контекста.

    Что бы мне хотелось — так это чего-то такого:

    @tag
    def mytag(context, nodelist=None, *args, **kwargs):
        # custom logic
        return some_str
    

    То есть чтобы мне передавали в функцию и контекст, и вложенные node'ы, если я написал тег как {% mytag %} .. {% endmytag %}, а также распарсенные и разресолвленные по нормальным правилам аргументы в тег. А уж как я буду формировать строку на выход — это уже неважно. Если надо будет шаблон вложенный подключить, всегда есть простой и короткий вызов render_to_string.

  6. Александр Кошелев

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

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

    То есть чтобы мне передавали в функцию и контекст, и вложенные node'ы, если я написал тег как {% mytag %} .. {% endmytag %}, а также распарсенные и разресолвленные по нормальным правилам аргументы в тег

    C "нормальными" правилами тоже проблема - оно не всегда надо, а когда не надо - применение этих правил вызовет синтаксическую ошибку. Взять для примера тег {% url %} у которого параметры совсем не по правилам резолвятся. Т.е. дробятся на биты как обычно, а вот логика их интерпретации отличается. Вот с битами в большинстве случаев и идет работа.

    Самая, на мой взгляд, большая проблема существующей системы расширения шаблонного движка - это её двухголовость (опять таки не рассматриваем шорткаты). Расширение стоит практически из синтаксического хендлера и более высокоуровнего нода. Если бы их соединить в некую одну сущность - то было бы счастье.

    Но это не отменяет попсовой хотелки - допилить inclusion_tag до большей гибкости.

    Вот так:-)

  7. Иван Сагалаев

    Он абстрагирует не только от загрузки шаблона, но и дает использовать вызывающий контекст (о ресолвинге параметров не говорим) - что по сути чуть ли не главное его достоинство.

    Я, на самом деле, примерно о том же говорю :-). По задумке inclusion_tag должен был решать задачу подгрузки вложенного шаблона. А используют его из-за того, что у него отросло совершенно случайно :-). При это совершенно случайно этого (контекста) нет в simple_tag, хотя оно там нужен не меньше. Мой основной поинт, что не нужно разделения на inclusion и simple. Большинство юзерских тегов укладывается в один шаблон использования, что я выше и привел.

    Взять для примера тег {% url %} у которого параметры совсем не по правилам резолвятся.

    Поскольку виноват в этом я, я могу совершшено точно сказать, что в этом нет никакой великой цели :-). Я сделал {% url %} с такими правилами парсинга только потому, что тогда они были такими же у первого попавшегося мне тогда тега, который принимал "список чего-то" — {% cycle %}, в котором одним кусочком шла литеральная строка через запятые. Если бы я делал его сейчас, то наверное бы стоило делать так:

    {% url "view_name" var1 "literal" name="value" %}
    

    То есть он должен укладываться в формат разделенных пробелами битов, каждый из которых может быть переменной или строкой, а также может быть парой name=value. Всем было бы проще.

  8. dobrych

    Столько всего уже написали блин :-) поздновато я с коментами... Скажу просто: Иван, давай еще про джангу! :D

  9. Александр Кошелев

    Поскольку виноват в этом я, я могу совершенно точно сказать, что в этом нет никакой великой цели :-).

    Негодование в зале

    А как же завет "сделать красиво"?:-)

    Вообще ты прав, надо как-то стандартизировать, наконец, обычные параметры и именованные. Добавить логику их обработки в парсер, чтобы совсем избавиться от рутины и неоднозначности.

  10. Alexey Kinyov

    Точно :) Хочется какой-то апгрейд для simple_tag + inclusion_tag. Может уже есть какое-то решение, а я и не в курсе? :)

    Как вариант - в simple_tag добавить возможность передавать контекст.

    def some_tag(context, arg1, arg2):
        return "I'm %s, so %s and %s" % (context['user'].username, arg1, arg2)
    
    register.simple_tag(some_tag, takes_context=True)
    

    Не супер-универсально, но очень просто и покрывает очень много случаев. Тот же inclusion_tag можно этим тэгом заменить.

    Спасибо - полезный пост :)

Добавить комментарий