From e2f06226ea4a38377cdb69f2f5768e4e00c2d88e Mon Sep 17 00:00:00 2001 From: Curtis Maloney Date: Thu, 29 Aug 2013 18:58:56 +1000 Subject: [PATCH] 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 --- django/template/loader_tags.py | 51 +++++-------------- docs/releases/1.7.txt | 2 + .../templates/recursive_include.html | 7 +++ tests/template_tests/tests.py | 34 +++++++++++++ 4 files changed, 56 insertions(+), 38 deletions(-) create mode 100644 tests/template_tests/templates/recursive_include.html diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index d7908acf7f..fafd9204a6 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -121,55 +121,34 @@ class ExtendsNode(Node): # the same. return compiled_parent._render(context) -class BaseIncludeNode(Node): - def __init__(self, *args, **kwargs): +class IncludeNode(Node): + def __init__(self, template, *args, **kwargs): + self.template = template self.extra_context = kwargs.pop('extra_context', {}) 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) - self.template_name = template_name def render(self, context): try: - template = self.template_name.resolve(context) + template = self.template.resolve(context) # Does this quack like a Template? if not callable(getattr(template, 'render', None)): # If not, we'll try get_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: if settings.TEMPLATE_DEBUG: raise return '' + @register.tag('block') def do_block(parser, token): """ @@ -258,9 +237,5 @@ def do_include(parser, token): options[option] = value isolated_context = options.get('only', False) 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, isolated_context=isolated_context) diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 67dc263a0f..3c478c249a 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -263,6 +263,8 @@ Templates arguments will be looked up using :func:`~django.template.loader.get_template` as always. +* It is now possible to :ttag:`include` templates recursively. + Backwards incompatible changes in 1.7 ===================================== diff --git a/tests/template_tests/templates/recursive_include.html b/tests/template_tests/templates/recursive_include.html new file mode 100644 index 0000000000..dc848f32af --- /dev/null +++ b/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 %} diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index e9c0a0f7c4..74aec32394 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -349,6 +349,40 @@ class TemplateLoaderTests(TestCase): output = outer_tmpl.render(ctx) 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):