Skip to content

Instantly share code, notes, and snippets.

@samwillis
Last active August 29, 2015 14:06
Show Gist options
  • Save samwillis/af3992f69c2597a16252 to your computer and use it in GitHub Desktop.
Save samwillis/af3992f69c2597a16252 to your computer and use it in GitHub Desktop.
'use' Django template tag, a cross between 'include' and 'extends'.
from django.template.base import TemplateSyntaxError, Library, Node, Variable, token_kwargs
from django.template.loader import get_template
from django.template.loader_tags import BlockNode, BLOCK_CONTEXT_KEY, BlockContext
from django.utils import six
from collections import defaultdict
import contextlib
register = Library()
GLOBAL_BLOCK_NAMESPACE = 'global'
TEMP_BLOCK_NAMESPACE = '__temp__'
class NsBlockContext(object):
"""
A block context with namespaces.
Blocks can be named as such:
{% block mynamespace.blockname %}{% endblock %}
The string before the first period is the namespace, everything after
(including any further periods) is the block name.
This is almost fully backwards compatible with existing code as blocks
already named with periods will just end up in a name space but continue
to function fully. The only exception is if they have blocks named both
'global.block_name' and 'block_name' as these would now address the
same block.
There is a special temporary name space that is emptied on each exit.
"""
def __init__(self):
# Our namespace stack, as we enter namespaces they are appended,
# as we exit they are popped and so the current name space is the last
# in the stack. Initially at the top of the stack we have our global namespace
self.ns_stack = [GLOBAL_BLOCK_NAMESPACE]
# Dictionary of FIFO queues for each namespace.
self.nsblocks = defaultdict(lambda : defaultdict(list))
def add_blocks(self, blocks):
"Add blocks to the selected namespace / current namespace."
for name, block in six.iteritems(blocks):
namespace, name = self.namespace_from_name(name)
self.nsblocks[namespace][name].insert(0, block)
def pop(self, name):
"Pop block from the selected namespace / current namespace."
namespace, name = self.namespace_from_name(name)
try:
return self.nsblocks[namespace][name].pop()
except IndexError:
return None
def push(self, name, block):
"Push block to the selected namespace / current namespace"
namespace, name = self.namespace_from_name(name)
self.nsblocks[namespace][name].append(block)
def get_block(self, name):
"Get a block from selected namespace / current namespace."
namespace, name = self.namespace_from_name(name)
try:
return self.nsblocks[namespace][name][-1]
except IndexError:
return None
def namespace_from_name(self, name):
if '.' in name:
return name.split('.', 1)
else:
return self.current_namespace, name
@property
def current_namespace(self):
return self.ns_stack[-1]
def enter_namespace(self, namespace):
self.ns_stack.append(namespace)
def exit_namespace(self):
if self.current_namespace == TEMP_BLOCK_NAMESPACE:
# Delete the temp namespace, it only lives for a single enter/exit
del self.nsblocks[TEMP_BLOCK_NAMESPACE]
self.ns_stack.pop()
@contextlib.contextmanager
def namespace(self, namespace):
self.enter_namespace(namespace)
yield
self.exit_namespace()
# This (and its use) would not be required if BlockContext was replaced with NsBlockContext in core Django.
def ensure_ns_block_context(context):
"Ensures that the block_context is a NsBlockContext."
block_context = context.render_context[BLOCK_CONTEXT_KEY]
if type(block_context) != NsBlockContext:
ns_block_context = NsBlockContext()
for block_name, block_stack in block_context.blocks.items():
if '.' in block_name:
ns, name = block_name.split('.', 1)
else:
ns, name = GLOBAL_BLOCK_NAMESPACE, block_name
ns_block_context.nsblocks[ns][name] = block_stack
context.render_context[BLOCK_CONTEXT_KEY] = ns_block_context
@register.tag("use")
def do_use(parser, token):
"""
Loads a template and renders it with the current context. You can pass
additional context using keyword arguments as well as override blocks in
the included template.
Examples::
{% use "foo/some_include" %}
{% block my_block %}Some Content{% endblock %}
{% enduse %}
{% use "foo/some_include" with bar="BAZZ!" baz="BING!" %}
{% block my_block %}Some Content{% endblock %}
{% enduse %}
Use the ``only`` argument to exclude the current context when rendering
the included template::
{% use "foo/some_include" only %}
{% block my_block %}Some Content{% endblock %}
{% enduse %}
{% use "foo/some_include" with bar="1" only %}
{% block my_block %}Some Content{% endblock %}
{% enduse %}
The included template receives an additional context variable called
``used_blocks`` which is a Dict indicating which blocks were overridden
in the use 'tag'. Using this you can conditionally show content around the
block. For example, if you had this template for generating a page heading::
<div class="page-heading">
<h1>{% block heading %}{% endblock %}</h1>
{% if used_blocks.sub_heading %}
<h2>{% block sub_heading %}{% endblock %}<h2>
{% endif %}
</div>
and included it using::
{% use "page_header.html" %}
{% block heading %}My Page Title{% endblock %}
{% enduse %}
it would exclude the ``<h2>`` tags from the empty subheading.
As syntactic sugar if you are just overriding a single block you
can express it as::
{% use "page_header.html" block heading %}
My Page Title
{% enduse %}
Initially any blocks within the 'use' block or in the template it is
including are not accessible for overriding in child templates. This is so
that you can include the same template multiple times without them
interfering with each other. If you would like to override a template in a
child template you need to assign the 'use' tag a namespace for the
contained blocks. This is done with the 'ns' option:
{% use "page_header.html" ns my_header %}
{% block heading %}My Page Title{% endblock %}
{% enduse %}
The 'heading', and 'subheading' blocks would then be accessible in child
templates as such:
{% block my_header.heading %}My New Heading{% endblock %}
"""
bits = token.split_contents()
if len(bits) < 2:
raise TemplateSyntaxError("%r tag takes at least one argument: the name of the template to be included." % bits[0])
options = {}
remaining_bits = bits[2:]
while remaining_bits:
option = remaining_bits.pop(0)
if option in options:
raise TemplateSyntaxError('The %r option was specified more '
'than once.' % option)
if option == 'with':
value = token_kwargs(remaining_bits, parser, support_legacy=False)
if not value:
raise TemplateSyntaxError('"with" in %r tag needs at least '
'one keyword argument.' % bits[0])
elif option == 'only':
value = True
elif option == 'block':
if remaining_bits:
value = remaining_bits.pop(0)
else:
raise TemplateSyntaxError('"block" in %r tag needs to be followed '
'by a block name.' % bits[0])
elif option == 'ns':
if remaining_bits:
value = remaining_bits.pop(0)
else:
raise TemplateSyntaxError('"ns" in %r tag needs to be followed by a name '
'space for the contains blocks.' % bits[0])
else:
raise TemplateSyntaxError('Unknown argument for %r tag: %r.' %
(bits[0], option))
options[option] = value
isolated_context = options.get('only', False)
namemap = options.get('with', {})
block_name = options.get('block', False)
namespace = options.get('ns', TEMP_BLOCK_NAMESPACE)
template_name = parser.compile_filter(bits[1])
# Parse the contents of the use tag, we reset the parser.__loaded_blocks
# so that we can reuse block names inside each individual use tag
current_loaded_blocks = parser.__loaded_blocks
parser.__loaded_blocks = []
nodelist = parser.parse(('end%s' % bits[0],))
parser.__loaded_blocks = current_loaded_blocks # Return original __loaded_blocks
parser.delete_first_token()
return UseNode(nodelist, template_name, extra_context=namemap, isolated_context=isolated_context,
block_name=block_name, namespace=namespace)
class UseNode(Node):
def __init__(self, nodelist, template_name, *args, **kwargs):
self.nodelist = nodelist
self.template_name = template_name
self.extra_context = kwargs.pop('extra_context', {})
self.isolated_context = kwargs.pop('isolated_context', False)
self.block_name = kwargs.pop('block_name', False)
self.namespace = kwargs.pop('namespace', TEMP_BLOCK_NAMESPACE)
self.prepare_blocks()
def __repr__(self):
return '<UseNode: uses %s>' % self.template_name.token
def get_nodes_by_type(self, nodetype):
# This stops blocks leaking out of scope.
if nodetype == BlockNode:
return []
else:
return super(UseNode, self).get_nodes_by_type(nodetype)
def prepare_blocks(self):
if self.block_name:
# We are only overriding a single block
block = BlockNode(self.block_name, self.nodelist)
self.blocks = {self.block_name: block}
else:
# We are overriding specific blocks provided
self.blocks = dict((n.name, n) for n in self.nodelist.get_nodes_by_type(BlockNode))
def get_template(self, context):
template = self.template_name.resolve(context)
if not template:
error_msg = "Invalid template name in 'use' tag: %r." % template
if self.template_name.filters or\
isinstance(self.template_name.var, Variable):
error_msg += " Got this from the '%s' variable." %\
self.template_name.token
raise TemplateSyntaxError(error_msg)
if hasattr(template, 'render'):
return template # template is a Template object rather than name
return get_template(template)
def render(self, context):
compiled_template = self.get_template(context)
# Ensure we are using an NsBlockContext, this would not be required if
# BlockContext replaced in core Django
ensure_ns_block_context(context)
# Get the block context and enter the namespace
block_context = context.render_context[BLOCK_CONTEXT_KEY]
with block_context.namespace(self.namespace):
# Add the block nodes from this node to the block context
block_context.add_blocks(self.blocks)
# Prepare our extra context values for adding to the current context
values = {
name: var.resolve(context)
for name, var in six.iteritems(self.extra_context)
}
# Create our extra context value 'used_blocks' indicating which bocks
# have been overridden.
# This will only indicate blocks overridden at this level.
values['used_blocks'] = {key: True for key in self.blocks.keys()}
# Include the extra context provided and render.
# We call Template._render explicitly so the parser context stays
# the same.
if self.isolated_context:
return compiled_template._render(context.new(values))
context.update(values)
output = compiled_template._render(context)
context.pop()
return output
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment