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
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 3 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
{% 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?
#
The docs on this were wrong, you have to use:
{% repeat bar with "fooooo" as bar %}
#
Sorry:
{% repeat bar with "fooooo" as pm %}
#
Please login first before commenting.