Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Improved {% include %} implementation
Merged BaseIncludeNode, ConstantIncludeNode and Include node.

This avoids raising TemplateDoesNotExist at parsing time, allows recursion
when passing a literal template name, and should make TEMPLATE_DEBUG behavior
consistant.

Thanks loic84 for help with the tests.

Fixed #3544, fixed #12064, fixed #16147
  • Loading branch information
funkybob authored and akaariai committed Aug 30, 2013
1 parent e973ee6 commit e2f0622
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 38 deletions.
51 changes: 13 additions & 38 deletions django/template/loader_tags.py
Expand Up @@ -121,55 +121,34 @@ def render(self, context):
# the same. # the same.
return compiled_parent._render(context) return compiled_parent._render(context)


class BaseIncludeNode(Node): class IncludeNode(Node):
def __init__(self, *args, **kwargs): def __init__(self, template, *args, **kwargs):
self.template = template
self.extra_context = kwargs.pop('extra_context', {}) self.extra_context = kwargs.pop('extra_context', {})
self.isolated_context = kwargs.pop('isolated_context', False) self.isolated_context = kwargs.pop('isolated_context', False)
super(BaseIncludeNode, self).__init__(*args, **kwargs)

def render_template(self, template, context):
values = dict([(name, var.resolve(context)) for name, var
in six.iteritems(self.extra_context)])
if self.isolated_context:
return template.render(context.new(values))
with context.push(**values):
return template.render(context)


class ConstantIncludeNode(BaseIncludeNode):
def __init__(self, template_path, *args, **kwargs):
super(ConstantIncludeNode, self).__init__(*args, **kwargs)
try:
t = get_template(template_path)
self.template = t
except:
if settings.TEMPLATE_DEBUG:
raise
self.template = None

def render(self, context):
if not self.template:
return ''
return self.render_template(self.template, context)

class IncludeNode(BaseIncludeNode):
def __init__(self, template_name, *args, **kwargs):
super(IncludeNode, self).__init__(*args, **kwargs) super(IncludeNode, self).__init__(*args, **kwargs)
self.template_name = template_name


def render(self, context): def render(self, context):
try: try:
template = self.template_name.resolve(context) template = self.template.resolve(context)
# Does this quack like a Template? # Does this quack like a Template?
if not callable(getattr(template, 'render', None)): if not callable(getattr(template, 'render', None)):
# If not, we'll try get_template # If not, we'll try get_template
template = get_template(template) template = get_template(template)
return self.render_template(template, context) values = {
name: var.resolve(context)
for name, var in six.iteritems(self.extra_context)
}
if self.isolated_context:
return template.render(context.new(values))
with context.push(**values):
return template.render(context)
except: except:
if settings.TEMPLATE_DEBUG: if settings.TEMPLATE_DEBUG:
raise raise
return '' return ''



@register.tag('block') @register.tag('block')
def do_block(parser, token): def do_block(parser, token):
""" """
Expand Down Expand Up @@ -258,9 +237,5 @@ def do_include(parser, token):
options[option] = value options[option] = value
isolated_context = options.get('only', False) isolated_context = options.get('only', False)
namemap = options.get('with', {}) namemap = options.get('with', {})
path = bits[1]
if path[0] in ('"', "'") and path[-1] == path[0]:
return ConstantIncludeNode(path[1:-1], extra_context=namemap,
isolated_context=isolated_context)
return IncludeNode(parser.compile_filter(bits[1]), extra_context=namemap, return IncludeNode(parser.compile_filter(bits[1]), extra_context=namemap,
isolated_context=isolated_context) isolated_context=isolated_context)
2 changes: 2 additions & 0 deletions docs/releases/1.7.txt
Expand Up @@ -263,6 +263,8 @@ Templates
arguments will be looked up using arguments will be looked up using
:func:`~django.template.loader.get_template` as always. :func:`~django.template.loader.get_template` as always.


* It is now possible to :ttag:`include` templates recursively.

Backwards incompatible changes in 1.7 Backwards incompatible changes in 1.7
===================================== =====================================


Expand Down
7 changes: 7 additions & 0 deletions tests/template_tests/templates/recursive_include.html
@@ -0,0 +1,7 @@
Recursion!
{% for comment in comments %}
{{ comment.comment }}
{% if comment.children %}
{% include "recursive_include.html" with comments=comment.children %}
{% endif %}
{% endfor %}
34 changes: 34 additions & 0 deletions tests/template_tests/tests.py
Expand Up @@ -349,6 +349,40 @@ def test_include_template_argument(self):
output = outer_tmpl.render(ctx) output = outer_tmpl.render(ctx)
self.assertEqual(output, 'This worked!') self.assertEqual(output, 'This worked!')


@override_settings(TEMPLATE_DEBUG=True)
def test_include_immediate_missing(self):
"""
Regression test for #16417 -- {% include %} tag raises TemplateDoesNotExist at compile time if TEMPLATE_DEBUG is True
Test that an {% include %} tag with a literal string referencing a
template that does not exist does not raise an exception at parse
time.
"""
ctx = Context()
tmpl = Template('{% include "this_does_not_exist.html" %}')
self.assertIsInstance(tmpl, Template)

@override_settings(TEMPLATE_DEBUG=True)
def test_include_recursive(self):
comments = [
{
'comment': 'A1',
'children': [
{'comment': 'B1', 'children': []},
{'comment': 'B2', 'children': []},
{'comment': 'B3', 'children': [
{'comment': 'C1', 'children': []}
]},
]
}
]

t = loader.get_template('recursive_include.html')
self.assertEqual(
"Recursion! A1 Recursion! B1 B2 B3 Recursion! C1",
t.render(Context({'comments': comments})).replace(' ', '').replace('\n', ' ').strip(),
)



class TemplateRegressionTests(TestCase): class TemplateRegressionTests(TestCase):


Expand Down

1 comment on commit e2f0622

@bmispelon
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, this seems to have introduced a regression: https://code.djangoproject.com/ticket/23516

Please sign in to comment.