From e5b488ac6b676cbac24eb5f5717f0ad071ce2957 Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Fri, 19 Nov 2010 15:06:19 -0500 Subject: [PATCH] Enabled Embed rule inheritance from parent templates and to included templates with a bit of overriding of EmbedNode methods and use of context.render_context similar to BlockNodes. Removed {% embed as %} syntax as obsolete. Added tests for {% embed %} tags in various situations. --- templatetags/embed.py | 220 ++++++++++++++++++++++++++++++++++-------- tests.py | 91 +++++++++++++++++ 2 files changed, 269 insertions(+), 42 deletions(-) diff --git a/templatetags/embed.py b/templatetags/embed.py index 8fb240d..d7a8466 100644 --- a/templatetags/embed.py +++ b/templatetags/embed.py @@ -1,75 +1,200 @@ from django import template from django.contrib.contenttypes.models import ContentType from django.conf import settings +from django.template.loader_tags import ExtendsNode, BlockContext, BLOCK_CONTEXT_KEY, TextNode, BlockNode from philo.utils import LOADED_TEMPLATE_ATTR register = template.Library() +EMBED_CONTEXT_KEY = 'embed_context' + + +class EmbedContext(object): + "Inspired by django.template.loader_tags.BlockContext." + def __init__(self): + self.embeds = {} + self.rendered = [] + + def add_embeds(self, embeds): + for content_type, embed_list in embeds.iteritems(): + if content_type in self.embeds: + self.embeds[content_type] = embed_list + self.embeds[content_type] + else: + self.embeds[content_type] = embed_list + + def get_embed_template(self, embed, context): + """To return a template for an embed node, find the node's position in the stack + and then progress up the stack until a template-defining node is found + """ + embeds = self.embeds[embed.content_type] + embeds = embeds[:embeds.index(embed)][::-1] + for e in embeds: + template = e.get_template(context) + if template: + return template + + # No template was found in the current render_context - but perhaps one level up? Or more? + # We may be in an inclusion tag. + self_found = False + for context_dict in context.render_context.dicts[::-1]: + if not self_found: + if self in context_dict.values(): + self_found = True + continue + elif EMBED_CONTEXT_KEY not in context_dict: + continue + else: + embed_context = context_dict[EMBED_CONTEXT_KEY] + # We can tell where we are in the list of embeds by which have already been rendered. + embeds = embed_context.embeds[embed.content_type][:len(embed_context.rendered)][::-1] + for e in embeds: + template = e.get_template(context) + if template: + return template + + raise IndexError + + +# Override ExtendsNode render method to have it handle EmbedNodes +# similarly to BlockNodes. +old_extends_node_init = ExtendsNode.__init__ + + +def get_embed_dict(nodelist): + embeds = {} + for n in nodelist.get_nodes_by_type(ConstantEmbedNode): + if n.content_type not in embeds: + embeds[n.content_type] = [n] + else: + embeds[n.content_type].append(n) + return embeds + + +def extends_node_init(self, nodelist, *args, **kwargs): + self.embeds = get_embed_dict(nodelist) + old_extends_node_init(self, nodelist, *args, **kwargs) + + +def render_extends_node(self, context): + compiled_parent = self.get_parent(context) + + if BLOCK_CONTEXT_KEY not in context.render_context: + context.render_context[BLOCK_CONTEXT_KEY] = BlockContext() + block_context = context.render_context[BLOCK_CONTEXT_KEY] + + if EMBED_CONTEXT_KEY not in context.render_context: + context.render_context[EMBED_CONTEXT_KEY] = EmbedContext() + embed_context = context.render_context[EMBED_CONTEXT_KEY] + + # Add the block nodes from this node to the block context + # Do the equivalent for embed nodes + block_context.add_blocks(self.blocks) + embed_context.add_embeds(self.embeds) + + # If this block's parent doesn't have an extends node it is the root, + # and its block nodes also need to be added to the block context. + for node in compiled_parent.nodelist: + # The ExtendsNode has to be the first non-text node. + if not isinstance(node, TextNode): + if not isinstance(node, ExtendsNode): + blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) + block_context.add_blocks(blocks) + embeds = get_embed_dict(compiled_parent.nodelist) + embed_context.add_embeds(embeds) + break + + # Call Template._render explicitly so the parser context stays + # the same. + return compiled_parent._render(context) + + +ExtendsNode.__init__ = extends_node_init +ExtendsNode.render = render_extends_node class ConstantEmbedNode(template.Node): """Analogous to the ConstantIncludeNode, this node precompiles several variables necessary for correct rendering - namely the referenced instance or the included template.""" - def __init__(self, content_type, varname, object_pk=None, template_name=None, kwargs=None): + def __init__(self, content_type, object_pk=None, template_name=None, kwargs=None): assert template_name is not None or object_pk is not None self.content_type = content_type - self.varname = varname kwargs = kwargs or {} for k, v in kwargs.items(): - kwargs[k] = template.Variable(v) + kwargs[k] = v self.kwargs = kwargs if object_pk is not None: - self.compile_instance(object_pk) + self.instance = self.compile_instance(object_pk) else: self.instance = None if template_name is not None: - self.compile_template(template_name[1:-1]) + self.template = self.compile_template(template_name[1:-1]) else: self.template = None - def compile_instance(self, object_pk): + def compile_instance(self, object_pk, context=None): self.object_pk = object_pk model = self.content_type.model_class() try: - self.instance = model.objects.get(pk=object_pk) + return model.objects.get(pk=object_pk) except model.DoesNotExist: if not hasattr(self, 'object_pk') and settings.TEMPLATE_DEBUG: # Then it's a constant node. raise - self.instance = False + return False + + def get_instance(self, context): + return self.instance - def compile_template(self, template_name): + def compile_template(self, template_name, context=None): try: - self.template = template.loader.get_template(template_name) + return template.loader.get_template(template_name) except template.TemplateDoesNotExist: if not hasattr(self, 'template_name') and settings.TEMPLATE_DEBUG: # Then it's a constant node. raise - self.template = False + return False + + def get_template(self, context): + return self.template + + def check_context(self, context): + if EMBED_CONTEXT_KEY not in context.render_context: + context.render_context[EMBED_CONTEXT_KEY] = EmbedContext() + embed_context = context.render_context[EMBED_CONTEXT_KEY] + + + if self.content_type not in embed_context.embeds: + embed_context.embeds[self.content_type] = [self] + elif self not in embed_context.embeds[self.content_type]: + embed_context.embeds[self.content_type].append(self) + + def mark_rendered(self, context): + context.render_context[EMBED_CONTEXT_KEY].rendered.append(self) def render(self, context): + self.check_context(context) + if self.template is not None: if self.template is False: return settings.TEMPLATE_STRING_IF_INVALID - - if self.varname not in context: - context[self.varname] = {} - context[self.varname][self.content_type] = self.template - + self.mark_rendered(context) return '' # Otherwise self.instance should be set. Render the instance with the appropriate template! if self.instance is None or self.instance is False: + self.mark_rendered(context) return settings.TEMPLATE_STRING_IF_INVALID - return self.render_template(context, self.instance) + return self.render_instance(context, self.instance) - def render_template(self, context, instance): + def render_instance(self, context, instance): try: - t = context[self.varname][self.content_type] - except KeyError: + t = context.render_context[EMBED_CONTEXT_KEY].get_embed_template(self, context) + except (KeyError, IndexError): + if settings.TEMPLATE_DEBUG: + raise return settings.TEMPLATE_STRING_IF_INVALID context.push() @@ -80,42 +205,54 @@ class ConstantEmbedNode(template.Node): context.update(kwargs) t_rendered = t.render(context) context.pop() + self.mark_rendered(context) return t_rendered class EmbedNode(ConstantEmbedNode): - def __init__(self, content_type, varname, object_pk=None, template_name=None, kwargs=None): + def __init__(self, content_type, object_pk=None, template_name=None, kwargs=None): assert template_name is not None or object_pk is not None self.content_type = content_type - self.varname = varname kwargs = kwargs or {} for k, v in kwargs.items(): - kwargs[k] = template.Variable(v) + kwargs[k] = v self.kwargs = kwargs if object_pk is not None: - self.object_pk = template.Variable(object_pk) + self.object_pk = object_pk else: self.object_pk = None self.instance = None if template_name is not None: - self.template_name = template.Variable(template_name) + self.template_name = template_name else: self.template_name = None self.template = None + def get_instance(self, context): + return self.compile_instance(self.object_pk, context) + + def get_template(self, context): + return self.compile_template(self.template_name, context) + def render(self, context): + self.check_context(context) + if self.template_name is not None: - template_name = self.template_name.resolve(context) - self.compile_template(template_name) + self.mark_rendered(context) + return '' - if self.object_pk is not None: - object_pk = self.object_pk.resolve(context) - self.compile_instance(object_pk) + if self.object_pk is None: + if settings.TEMPLATE_DEBUG: + raise ValueError("NoneType is not a valid object_pk value") + self.mark_rendered(context) + return settings.TEMPLATE_STRING_IF_INVALID - return super(EmbedNode, self).render(context) + instance = self.compile_instance(self.object_pk.resolve(context)) + + return self.render_instance(context, instance) def get_embedded(self): @@ -127,8 +264,7 @@ setattr(ConstantEmbedNode, LOADED_TEMPLATE_ATTR, property(get_embedded)) def do_embed(parser, token): """ - The {% embed %} tag can be used in three ways: - {% embed as %} :: This sets which variable will be used to track embedding template names for the current context. Default: "embed" + The {% embed %} tag can be used in two ways: {% embed . with