Login

Repeat blocks with new context / simple Jinja-like macro system

Author:
miracle2k
Posted:
July 26, 2007
Language:
Python
Version:
.96
Score:
2 (after 2 ratings)

A simple macro system that makes it possible to reuse previously defined blocks, optionally with a custom context, similar to the macro functionality in Jinja.

It requires some workarounds/hacks because we cannot reach all the data from inside the django template system that we need, but it seems to work pretty well so far. It is, however, also pretty untested at this point, so use at your own risk.

Examples:

base.html:

            <!-- 
        This is mandatory if you want to use the repeat-tag in
        a template. It should as placed as earily as possible.
        See below for how to mix with template inheritance.
    -->
            {% enablemacros %}

    <!-- Note that {{ param }} does not exist. -->

    {% block foo %}
        A standard django block that will be written to the output.
        {% if param %}{{ param }}{% endif %}
    {% endblock %}

    {% macro bar %}
        Pretty much the same thing as a django block (can even be 
        overridden via template inheritance), but it's content
        will NOT be rendered per default. Please note that it
        ends with ENDBLOCK!
        {% if param %}{{ param }}{% endif %}
    {% endblock %}

    <!-- Render foo for the second time -->
    {% repeat foo %} 
    <!-- Render foo bar the first time -->
    {% repeat bar %} 
    <!-- Render both blocks again, and pass a parameter -->
    {% repeat foo with "Hello World" as param %} 
    {% repeat bar with "Hello World" as param %}

    {% macro form %}do stuff with: {{ form }}{% endblock %}
    {% for form in all_forms %}
        {% repeat display %}  <!-- will have access to {{ form }}
    {% endfor %}

extend.html:
    <!--
        {% extends %} requires that it be the first thing in a template,
        and if it is, everything except for block tags is ignored, so
        {% enablemacros %} won't work. Instead, use:
    -->
    {% extends_with_macros 'base.html' %}

    {% block foo %}
        Will override "foo" in base.html
    {% endblock %}
    {% block bar %}
        Will override the macro block "bar" in base.html. Whether
        this is defined as block or macro doesn't matter.
    {% endblock %}

Todo:

* This (both tags used) results in infinite recursion:            
    {% extends_with_macros "somefile" %}{% enablemacros %}
  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
from django import template
from django.template import TemplateSyntaxError

register = template.Library()

"""
    The MacroRoot node (= %enablemacros% tag) functions quite similar to 
    the ExtendsNode from django.template.loader_tags. It will capture 
    everything that follows, and thus should be one of the first tags in 
    the template. Because %extends% also needs to be the first, if you are 
    using template inheritance, use %extends_with_macros% instead.
    
    This whole procedure is necessary because otherwise we would have no
    possiblity to access the blocktag referenced by a %repeat% (we could 
    do it for %macro%, but not for %block%, at least not without patching
    the django source).
    
    So what we do is add a custom attribute to the parser object and store
    a reference to the MacroRoot node there, which %repeat% object will
    later be able to access when they need to find a block.
    
    Apart from that, the node doesn't do much, except rendering it's childs.
"""
class MacroRoot(template.Node):
    def __init__(self, nodelist=[]):
        self.nodelist = nodelist
        
    def render(self, context):
        return self.nodelist.render(context)
        
    def find(self, block_name, parent_nodelist=None):        
        # parent_nodelist is internally for recusion, start with root nodelist
        if parent_nodelist is None: parent_nodelist = self.nodelist
        
        from django.template.loader_tags import BlockNode
        for node in parent_nodelist:
            if isinstance(node, (MacroNode, BlockNode)):                
                if node.name == block_name:
                    return node
            if hasattr(node, 'nodelist'):               
                result = self.find(block_name, node.nodelist)
                if result:
                    return result
        return None # nothing found
        
def do_enablemacros(parser, token):
    # check that there are no arguments
    bits = token.split_contents()
    if len(bits) != 1:
        raise TemplateSyntaxError, "'%s' takes no arguments" % bits[0]
    # create the Node object now, so we can assign it to the parser
    # before we continue with our call to parse(). this enables repeat
    # tags that follow later to already enforce at the parsing stage
    # that macros are correctly enabled.
    parser._macro_root = MacroRoot()
    # capture the rest of the template
    nodelist = parser.parse()
    if nodelist.get_nodes_by_type(MacroRoot):
        raise TemplateSyntaxError, "'%s' cannot appear more than once in the same template" % bits[0]
    # update the nodelist on the previously created MacroRoot node and
    # return  it.
    parser._macro_root.nodelist = nodelist
    return parser._macro_root
    
def do_extends_with_macros(parser, token):
    from django.template.loader_tags import do_extends
    # parse it as an ExtendsNode, but also create a fake MacroRoot node
    # and add it to the parser, like we do in do_enablemacros().
    parser._macro_root = MacroRoot()
    extendsnode = do_extends(parser, token)
    parser._macro_root.nodelist = extendsnode.nodelist
    return extendsnode

"""
    %macro% is pretty much exactly like a %block%. Both can be repeated, but
    the macro does not output it's content by itself, but *only* if it is 
    called via a %repeat% tag.    
"""

from django.template.loader_tags import BlockNode, do_block

class MacroNode(BlockNode):
    def render(self, context):
        return ''
        
    # the render that actually works
    def repeat(self, context):        
        return super(MacroNode, self).render(context)

def do_macro(parser, token):
    # let the block parse itself
    result = do_block(parser, token)
    # "upgrade" the BlockNode to a MacroNode and return it. Yes, I was not 
    # completely comfortable with it either at first, but Google says it's ok.
    result.__class__ = MacroNode
    return result
      
"""
    This (the %repeast%) is the heart of the macro system. It will try to 
    find the specified %macro% or %block% tag and render it with the most 
    up-to-date context, including any number of additional parameters passed 
    to the repeat-tag itself.
"""    
class RepeatNode(template.Node):    
    def __init__(self, block_name, macro_root, extra_context):
        self.block_name = block_name
        self.macro_root = macro_root
        self.extra_context = extra_context

    def render(self, context):
        block = self.macro_root.find(self.block_name)
        if not block:
            # apparently we are not supposed to raise exceptions at rendering 
            # stage, but this is serious, and we cannot do it while parsing.
            # once again, it comes down to being able to support repeating of
            # standard blocks. If we would only support our own %macro% tags,
            # we would not need the whole %enablemacros% stuff and could do
            # things differently.
            raise TemplateSyntaxError, "cannot repeat '%s': block or macro not found" % self.block_name
        else:
            # resolve extra context variables
            resolved_context = {}
            for key, value in self.extra_context.items():
                resolved_context[key] = value.resolve(context)
            # render the block with the new context
            context.update(resolved_context)
            if isinstance(block, MacroNode):
                result = block.repeat(context)
            else:
                result = block.render(context)
            context.pop()
            return result

def do_repeat(parser, token):
    # Stolen from django.templatetags.i18n.BlockTranslateParser
    # Parses something like  "with x as y, i as j", and 
    # returns it as a  context dict.
    class RepeatTagParser(template.TokenParser):
        def top(self):
            extra_context = {}
            # first tag is the blockname
            try: block_name = self.tag()
            except TemplateSyntaxError: 
                raise TemplateSyntaxError("'%s' requires a block or macro name" % self.tagname)
            # read param bindings
            while self.more():
                tag = self.tag()
                if tag == 'with' or tag == 'and':
                    value = self.value()
                    if self.tag() != 'as':
                        raise TemplateSyntaxError, "variable bindings in %s must be 'with value as variable'" % self.tagname
                    extra_context[self.tag()] = parser.compile_filter(value)                
                else:
                    raise TemplateSyntaxError, "unknown subtag %s for '%s' found" % (tag, self.tagname)
            return self.tagname, block_name, extra_context
    
    # parse arguments
    (tag_name, block_name, extra_context) = \
        RepeatTagParser(token.contents).top()
    # return as a RepeatNode
    if not hasattr(parser, '_macro_root'):
        raise TemplateSyntaxError, "'%s' requires macros to be enabled first" % tag_name
    return RepeatNode(block_name, parser._macro_root, extra_context)

# register all our tags
register.tag('repeat', do_repeat)
register.tag('macro', do_macro)
register.tag('enablemacros', do_enablemacros)
register.tag('extends_with_macros', do_extends_with_macros)

More like this

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

Comments

diegor (on December 11, 2007):

{% load macro %} <- load the macro.py

{% enablemacros %}

{% macro bar %}

macro: {{ pm }}

{% endblock %}

{% repeat bar with pm as 'fooo' %}

{% repeat bar with pm as 'fooooo' %}

This print twice only "macro:" without 'fooo' or 'fooooo'. Any ideas about this reaction?

#

miracle2k (on December 22, 2007):

The docs on this were wrong, you have to use:

{% repeat bar with "fooooo" as bar %}

#

miracle2k (on December 22, 2007):

Sorry:

{% repeat bar with "fooooo" as pm %}

#

Please login first before commenting.