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(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
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):
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 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):
- if self.template is not None:
- if self.template is False:
+ 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
-
- if self.varname not in context:
- context[self.varname] = {}
- context[self.varname][self.content_type] = self.template
-
return ''
- # Otherwise self.instance should be set. Render the instance with the appropriate template!
- if self.instance is None or self.instance is False:
+ # 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_template(context, self.instance)
+ return self.render_instance(context, 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()
context.update(kwargs)
t_rendered = t.render(context)
context.pop()
+ self.mark_rendered_for(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)
- self.kwargs = kwargs
+ self.kwargs = kwargs or {}
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 render(self, context):
- if self.template_name is not None:
- template_name = self.template_name.resolve(context)
- self.compile_template(template_name)
-
- if self.object_pk is not None:
- object_pk = self.object_pk.resolve(context)
- self.compile_instance(object_pk)
-
- return super(EmbedNode, self).render(context)
+ 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):
+ return ContentType.objects.get_for_model(self.get_instance(context))
def get_embedded(self):
setattr(ConstantEmbedNode, LOADED_TEMPLATE_ATTR, property(get_embedded))
+def get_content_type(bit):
+ 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' % tag)
+ 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)' % tag)
+ return ct
+
+
def do_embed(parser, token):
"""
- The {% embed %} tag can be used in three ways:
- {% embed as <varname> %} :: 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 <app_label>.<model_name> with <template> %} :: Sets which template will be used to render a particular model.
- {% embed <app_label>.<model_name> <object_pk> [<argname>=<value> ...]%} :: Embeds the instance specified by the given parameters in the document with the previously-specified template. Any kwargs provided will be passed into the context of the template.
+ {% embed (<app_label>.<model_name> <object_pk> || <instance>) [<argname>=<value> ...] %} :: Embeds the instance specified by the given parameters in the document with the previously-specified template. Any kwargs provided will be passed into the context of the template.
"""
- args = token.split_contents()
- tag = args[0]
-
- if len(args) < 2:
- raise template.TemplateSyntaxError('"%s" template tag must have at least three arguments.' % tag)
- elif len(args) == 3 and args[1] == "as":
- parser._embedNodeVarName = args[2]
- return template.defaulttags.CommentNode()
- else:
- if '.' not in args[1]:
- raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tag)
-
- app_label, model = args[1].split('.')
- try:
- ct = ContentType.objects.get(app_label=app_label, model=model)
- except ContentType.DoesNotExist:
- raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tag)
-
- varname = getattr(parser, '_embedNodeVarName', 'embed')
-
- if args[2] == "with":
- if len(args) > 4:
- raise template.TemplateSyntaxError('"%s" template tag may have no more than four arguments.' % tag)
-
- if args[3][0] in ['"', "'"] and args[3][0] == args[3][-1]:
- return ConstantEmbedNode(ct, template_name=args[3], varname=varname)
-
- return EmbedNode(ct, template_name=args[3], varname=varname)
-
- object_pk = args[2]
- remaining_args = args[3:]
- kwargs = {}
- for arg in remaining_args:
- if '=' not in arg:
- raise template.TemplateSyntaxError("Invalid keyword argument for '%s' template tag: %s" % (tag, arg))
- k, v = arg.split('=')
- kwargs[k] = v
+ bits = token.split_contents()
+ tag = bits.pop(0)
+
+ if len(bits) < 1:
+ raise template.TemplateSyntaxError('"%s" template tag must have at least two arguments.' % tag)
+
+ if len(bits) == 3 and bits[-2] == 'with':
+ ct = get_content_type(bits[0])
- return EmbedNode(ct, object_pk=object_pk, varname=varname, kwargs=kwargs)
+ if bits[2][0] in ['"', "'"] and bits[2][0] == bits[2][-1]:
+ return ConstantEmbedNode(ct, template_name=bits[2])
+ return EmbedNode(ct, template_name=bits[2])
+
+ # Otherwise they're trying to embed a certain instance.
+ kwargs = {}
+ try:
+ bit = bits.pop()
+ while '=' in bit:
+ k, v = bit.split('=')
+ kwargs[k] = parser.compile_filter(v)
+ bit = bits.pop()
+ bits.append(bit)
+ except IndexError:
+ raise template.TemplateSyntaxError('"%s" template tag expects at least one non-keyword argument when embedding instances.')
+
+ if len(bits) == 1:
+ instance = parser.compile_filter(bits[0])
+ return InstanceEmbedNode(instance, kwargs)
+ elif len(bits) > 2:
+ raise template.TemplateSyntaxError('"%s" template tag expects at most 2 non-keyword arguments when embedding instances.')
+ ct = get_content_type(bits[0])
+ pk = bits[1]
+
+ try:
+ int(pk)
+ except ValueError:
+ return EmbedNode(ct, object_pk=parser.compile_filter(pk), kwargs=kwargs)
+ else:
+ return ConstantEmbedNode(ct, object_pk=pk, kwargs=kwargs)
register.tag('embed', do_embed)
\ No newline at end of file
from django import template
from django.conf import settings
from django.db import connection
+from django.template import loader
+from django.template.loaders import cached
from philo.exceptions import AncestorDoesNotExist
from philo.models import Node, Page, Template
from philo.contrib.penfield.models import Blog, BlogView, BlogEntry
+import sys, traceback
+
+
+class TemplateTestCase(TestCase):
+ fixtures = ['test_fixtures.json']
+
+ def test_templates(self):
+ "Tests to make sure that embed behaves with complex includes and extends"
+ template_tests = self.get_template_tests()
+
+ # Register our custom template loader. Shamelessly cribbed from django core regressiontests.
+ def test_template_loader(template_name, template_dirs=None):
+ "A custom template loader that loads the unit-test templates."
+ try:
+ return (template_tests[template_name][0] , "test:%s" % template_name)
+ except KeyError:
+ raise template.TemplateDoesNotExist, template_name
+
+ cache_loader = cached.Loader(('test_template_loader',))
+ cache_loader._cached_loaders = (test_template_loader,)
+
+ old_template_loaders = loader.template_source_loaders
+ loader.template_source_loaders = [cache_loader]
+
+ # Turn TEMPLATE_DEBUG off, because tests assume that.
+ old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
+
+ # Set TEMPLATE_STRING_IF_INVALID to a known string.
+ old_invalid = settings.TEMPLATE_STRING_IF_INVALID
+ expected_invalid_str = 'INVALID'
+
+ failures = []
+
+ # Run tests
+ for name, vals in template_tests.items():
+ xx, context, result = vals
+ try:
+ test_template = loader.get_template(name)
+ output = test_template.render(template.Context(context))
+ except Exception:
+ exc_type, exc_value, exc_tb = sys.exc_info()
+ if exc_type != result:
+ tb = '\n'.join(traceback.format_exception(exc_type, exc_value, exc_tb))
+ failures.append("Template test %s -- FAILED. Got %s, exception: %s\n%s" % (name, exc_type, exc_value, tb))
+ continue
+ if output != result:
+ failures.append("Template test %s -- FAILED. Expected %r, got %r" % (name, result, output))
+
+ # Cleanup
+ settings.TEMPLATE_DEBUG = old_td
+ settings.TEMPLATE_STRING_IF_INVALID = old_invalid
+ loader.template_source_loaders = old_template_loaders
+
+ self.assertEqual(failures, [], "Tests failed:\n%s\n%s" % ('-'*70, ("\n%s\n" % ('-'*70)).join(failures)))
+
+
+ def get_template_tests(self):
+ # SYNTAX --
+ # 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class)
+ blog = Blog.objects.all()[0]
+ return {
+ # EMBED INCLUSION HANDLING
+
+ 'embed01': ('{{ embedded.title|safe }}', {'embedded': blog}, blog.title),
+ 'embed02': ('{{ embedded.title|safe }}{{ var1 }}{{ var2 }}', {'embedded': blog}, blog.title),
+ 'embed03': ('{{ embedded.title|safe }} is a lie!', {'embedded': blog}, '%s is a lie!' % blog.title),
+
+ # Simple template structure with embed
+ 'simple01': ('{% embed penfield.blog with "embed01" %}{% embed penfield.blog 1 %}Simple{% block one %}{% endblock %}', {'blog': blog}, '%sSimple' % blog.title),
+ 'simple02': ('{% extends "simple01" %}', {}, '%sSimple' % blog.title),
+ 'simple03': ('{% embed penfield.blog with "embed000" %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
+ 'simple04': ('{% embed penfield.blog 1 %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
+ 'simple05': ('{% embed penfield.blog with "embed01" %}{% embed blog %}', {'blog': blog}, blog.title),
+
+ # Kwargs
+ 'kwargs01': ('{% embed penfield.blog with "embed02" %}{% embed penfield.blog 1 var1="hi" var2=lo %}', {'lo': 'lo'}, '%shilo' % blog.title),
+
+ # Filters/variables
+ 'filters01': ('{% embed penfield.blog with "embed02" %}{% embed penfield.blog 1 var1=hi|first var2=lo|slice:"3" %}', {'hi': ["These", "words"], 'lo': 'lower'}, '%sTheselow' % blog.title),
+ 'filters02': ('{% embed penfield.blog with "embed01" %}{% embed penfield.blog entry %}', {'entry': 1}, blog.title),
+
+ # Blocky structure
+ 'block01': ('{% block one %}Hello{% endblock %}', {}, 'Hello'),
+ 'block02': ('{% extends "simple01" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s" % (blog.title, blog.title)),
+ 'block03': ('{% extends "simple01" %}{% embed penfield.blog with "embed03" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s is a lie!" % (blog.title, blog.title)),
+
+ # Blocks and includes
+ 'block-include01': ('{% extends "simple01" %}{% embed penfield.blog with "embed03" %}{% block one %}{% include "simple01" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%sSimple%s is a lie!" % (blog.title, blog.title, blog.title)),
+ 'block-include02': ('{% extends "simple01" %}{% block one %}{% include "simple04" %}{% embed penfield.blog with "embed03" %}{% include "simple04" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s%s is a lie!%s is a lie!" % (blog.title, blog.title, blog.title, blog.title)),
+ }
class NodeURLTestCase(TestCase):
self.assertQueryLimit(1, 'root/second/third', callable=third.get_path)
self.assertQueryLimit(1, 'second/third', root, callable=third.get_path)
self.assertQueryLimit(1, e, third, callable=second2.get_path)
- self.assertQueryLimit(1, '? - ?', root, ' - ', 'title', callable=third.get_path)
\ No newline at end of file
+ self.assertQueryLimit(1, '? - ?', root, ' - ', 'title', callable=third.get_path)