X-Git-Url: http://git.ithinksw.org/philo.git/blobdiff_plain/82b08f79564159d7acbcaf255ed1ac1fb4882e64..d19e216035b14d8f60b24dda0c0670e6997f16ce:/philo/templatetags/embed.py diff --git a/philo/templatetags/embed.py b/philo/templatetags/embed.py new file mode 100644 index 0000000..eb4cd68 --- /dev/null +++ b/philo/templatetags/embed.py @@ -0,0 +1,336 @@ +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 + """ + ct = embed.get_content_type(context) + embeds = self.embeds[ct] + 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[ct][: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(embed_list, context): + embeds = {} + for e in embed_list: + ct = e.get_content_type(context) + if ct is None: + # Then the embed doesn't exist for this context. + continue + if ct not in embeds: + embeds[ct] = [e] + else: + embeds[ct].append(e) + return embeds + + +def extends_node_init(self, nodelist, *args, **kwargs): + self.embed_list = nodelist.get_nodes_by_type(ConstantEmbedNode) + old_extends_node_init(self, nodelist, *args, **kwargs) + + +def render_extends_node(self, context): + compiled_parent = self.get_parent(context) + embeds = get_embed_dict(self.embed_list, 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(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.get_nodes_by_type(ConstantEmbedNode), context) + embed_context.add_embeds(embeds) + break + + # Explicitly render all direct embed children of this node. + if self.embed_list: + for node in self.nodelist: + if isinstance(node, ConstantEmbedNode): + node.render(context) + + # 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, 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 + + kwargs = kwargs or {} + for k, v in kwargs.items(): + kwargs[k] = v + self.kwargs = kwargs + + if object_pk is not None: + self.instance = self.compile_instance(object_pk) + else: + self.instance = None + + if template_name is not None: + self.template = self.compile_template(template_name[1:-1]) + else: + self.template = None + + def compile_instance(self, object_pk): + model = self.content_type.model_class() + try: + 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 + return False + + def get_instance(self, context): + return self.instance + + def compile_template(self, template_name): + try: + return template.loader.get_template(template_name) + except template.TemplateDoesNotExist: + if hasattr(self, 'template') and settings.TEMPLATE_DEBUG: + # Then it's a constant node. + raise + return False + + def get_template(self, context): + return self.template + + def get_content_type(self, context): + return self.content_type + + 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] + + ct = self.get_content_type(context) + if ct not in embed_context.embeds: + embed_context.embeds[ct] = [self] + elif self not in embed_context.embeds[ct]: + embed_context.embeds[ct].append(self) + + def mark_rendered_for(self, context): + context.render_context[EMBED_CONTEXT_KEY].rendered.append(self) + + def render(self, context): + self.check_context(context) + + template = self.get_template(context) + if template is not None: + self.mark_rendered_for(context) + if template is False: + return settings.TEMPLATE_STRING_IF_INVALID + return '' + + # Otherwise an instance should be available. Render the instance with the appropriate template! + instance = self.get_instance(context) + if instance is None or instance is False: + self.mark_rendered_for(context) + return settings.TEMPLATE_STRING_IF_INVALID + + return self.render_instance(context, instance) + + def render_instance(self, context, instance): + try: + t = context.render_context[EMBED_CONTEXT_KEY].get_embed_template(self, context) + except (KeyError, IndexError): + self.mark_rendered_for(context) + return settings.TEMPLATE_STRING_IF_INVALID + + context.push() + context['embedded'] = instance + for k, v in self.kwargs.items(): + context[k] = v.resolve(context) + t_rendered = t.render(context) + context.pop() + self.mark_rendered_for(context) + return t_rendered + + +class EmbedNode(ConstantEmbedNode): + 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.kwargs = kwargs or {} + + if object_pk is not None: + self.object_pk = object_pk + else: + self.object_pk = None + + if template_name is not None: + self.template_name = template_name + else: + self.template_name = None + + def get_instance(self, context): + if self.object_pk is None: + return None + return self.compile_instance(self.object_pk.resolve(context)) + + def get_template(self, context): + if self.template_name is None: + return None + return self.compile_template(self.template_name.resolve(context)) + + +class InstanceEmbedNode(EmbedNode): + def __init__(self, instance, kwargs=None): + self.instance = instance + self.kwargs = kwargs or {} + + def get_template(self, context): + return None + + def get_instance(self, context): + return self.instance.resolve(context) + + def get_content_type(self, context): + instance = self.get_instance(context) + if not instance: + return None + return ContentType.objects.get_for_model(instance) + + +def get_embedded(self): + return self.template + + +setattr(ConstantEmbedNode, LOADED_TEMPLATE_ATTR, property(get_embedded)) + + +def parse_content_type(bit, tagname): + try: + app_label, model = bit.split('.') + except ValueError: + raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tagname) + try: + ct = ContentType.objects.get(app_label=app_label, model=model) + except ContentType.DoesNotExist: + raise template.TemplateSyntaxError('"%s" template tag requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tagname) + return ct + + +def do_embed(parser, token): + """ + The {% embed %} tag can be used in two ways: + {% embed . with