Login

Template {% macro %} support, with context rendering

Author:
MattP
Posted:
February 13, 2013
Language:
Python
Version:
1.4
Score:
0 (after 0 ratings)

Reuse blocks of template code and content as macros.

This is a small extension of https://gist.github.com/skyl/1715202 (which was based on http://djangosnippets.org/snippets/363/) to support rendering macro output into context variables.

See comments for details.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#
# templatetags/macros.py - Support for macros in Django templates
#
# Based on snippet by
#     Michal Ludvig <[email protected]>
#     http://www.logix.cz/michal
#
# Extended for args and kwargs into templatetags/kwacro.py by
#     Skylar Saveland http://skyl.org
#     https://gist.github.com/skyl/1715202
#
# Modified to support rendering into context by [email protected]
#
"""

Usage example:

0) Save this file as <yourapp>/templatetags/macros.py

1) In your template load the library:
    {% load macros %}

2) Define a new macro called 'my_macro' that takes args and/or kwargs
   All will be optional.

    {% macro my_macro arg1 arg2 baz="Default baz" %}
        {% firstof arg1 "default_arg1" %}
        {% if arg2 %}{{ arg2 }}{% else %}default_arg2{% endif %}
        {{ baz }}
    {% endmacro %}

3) Use the macro with string parameters or context variables:

    {% usemacro my_macro "foo" "bar" baz="KW" %}
    <br>
    {% usemacro my_macro num_pages "bar" %}
    <br>
    {% setmacro my_macro %} {{ my_macro }}

    renders like

    foo bar KW
    77 bar Default baz
    default_arg1 default_arg2 Default baz

4) Alternatively save your macros in a separate file, e.g. "mymacro.html"
    and load it to the current template with:

        {% loadmacros "mymacros.html" %}

    Then use these loaded macros in as described above.

Bear in mind that defined and loaded macros are local to each template
file and are not inherited through {% extends ... %} tags.
"""

from django import template
from django.template import FilterExpression
from django.template.loader import get_template

register = template.Library()


def _setup_macros_dict(parser):
    ## Metadata of each macro are stored in a new attribute
    ## of 'parser' class. That way we can access it later
    ## in the template when processing 'usemacro' tags.
    try:
        ## Only try to access it to eventually trigger an exception
        parser._macros
    except AttributeError:
        parser._macros = {}


class DefineMacroNode(template.Node):
    def __init__(self, name, nodelist, args):

        self.name = name
        self.nodelist = nodelist
        self.args = []
        self.kwargs = {}
        for a in args:
            if "=" not in a:
                self.args.append(a)
            else:
                name, value = a.split("=")
                self.kwargs[name] = value

    def render(self, context):
        ## empty string - {% macro %} tag does no output
        return ''


@register.tag(name="macro")
def do_macro(parser, token):
    try:
        args = token.split_contents()
        tag_name, macro_name, args = args[0], args[1], args[2:]
    except IndexError:
        m = ("'%s' tag requires at least one argument (macro name)"
            % token.contents.split()[0])
        raise template.TemplateSyntaxError, m
    # TODO: could do some validations here,
    # for now, "blow your head clean off"
    nodelist = parser.parse(('endmacro', ))
    parser.delete_first_token()

    ## Metadata of each macro are stored in a new attribute
    ## of 'parser' class. That way we can access it later
    ## in the template when processing 'usemacro' tags.
    _setup_macros_dict(parser)
    parser._macros[macro_name] = DefineMacroNode(macro_name, nodelist, args)
    return parser._macros[macro_name]


class LoadMacrosNode(template.Node):
    def render(self, context):
        ## empty string - {% loadmacros %} tag does no output
        return ''


@register.tag(name="loadmacros")
def do_loadmacros(parser, token):
    try:
        tag_name, filename = token.split_contents()
    except IndexError:
        m = ("'%s' tag requires at least one argument (macro name)"
            % token.contents.split()[0])
        raise template.TemplateSyntaxError, m
    if filename[0] in ('"', "'") and filename[-1] == filename[0]:
        filename = filename[1:-1]
    t = get_template(filename)
    macros = t.nodelist.get_nodes_by_type(DefineMacroNode)
    ## Metadata of each macro are stored in a new attribute
    ## of 'parser' class. That way we can access it later
    ## in the template when processing 'usemacro' tags.
    _setup_macros_dict(parser)
    for macro in macros:
        parser._macros[macro.name] = macro
    return LoadMacrosNode()


class UseMacroNode(template.Node):

    def __init__(self, macro, fe_args, fe_kwargs, context_only):
        self.macro = macro
        self.fe_args = fe_args
        self.fe_kwargs = fe_kwargs
        self.context_only = context_only

    def render(self, context):

        for i, arg in enumerate(self.macro.args):
            try:
                fe = self.fe_args[i]
                context[arg] = fe.resolve(context)
            except IndexError:
                context[arg] = ""

        for name, default in self.macro.kwargs.iteritems():
            if name in self.fe_kwargs:
                context[name] = self.fe_kwargs[name].resolve(context)
            else:
                context[name] = FilterExpression(default,
                                                 self.macro.parser
                                                 ).resolve(context)

        # Place output into context variable
        context[self.macro.name] = self.macro.nodelist.render(context)
        return '' if self.context_only else context[self.macro.name]


def parse_usemacro(parser, token):
    try:
        args = token.split_contents()
        tag_name, macro_name, values = args[0], args[1], args[2:]
    except IndexError:
        m = ("'%s' tag requires at least one argument (macro name)"
             % token.contents.split()[0])
        raise template.TemplateSyntaxError, m

    try:
        macro = parser._macros[macro_name]
    except (AttributeError, KeyError):
        m = "Macro '%s' is not defined" % macro_name
        raise template.TemplateSyntaxError, m

    fe_kwargs = {}
    fe_args = []

    for val in values:
        if "=" in val:
            # kwarg
            name, value = val.split("=")
            fe_kwargs[name] = FilterExpression(value, parser)
        else:  # arg
            # no validation, go for it ...
            fe_args.append(FilterExpression(val, parser))

    macro.name = macro_name
    macro.parser = parser
    return macro, fe_args, fe_kwargs


@register.tag(name="usemacro")
def do_usemacro(parser, token):
    return UseMacroNode(*parse_usemacro(parser, token), context_only=False)


@register.tag(name="setmacro")
def do_setmacro(parser, token):
    return UseMacroNode(*parse_usemacro(parser, token), context_only=True)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 1 week ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 6 months ago

Comments

Twidi (on January 27, 2014):

Hi

Thanks for your work.

I created a python module/django app with this snippet:

https://github.com/twidi/django-templates-macros https://pypi.python.org/pypi/django-templates-macros

#

Please login first before commenting.