Last active
August 29, 2015 14:06
-
-
Save samwillis/af3992f69c2597a16252 to your computer and use it in GitHub Desktop.
'use' Django template tag, a cross between 'include' and 'extends'.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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