Fixed #26402 -- Added relative path support in include/extends template tags.

This commit is contained in:
Vitaly Bogomolov 2016-03-24 11:39:37 +03:00 committed by Tim Graham
parent ad403ffa45
commit aec4f97555
20 changed files with 206 additions and 0 deletions

View File

@ -1,4 +1,5 @@
import logging import logging
import posixpath
from collections import defaultdict from collections import defaultdict
from django.utils import six from django.utils import six
@ -249,6 +250,36 @@ def do_block(parser, token):
return BlockNode(block_name, nodelist) return BlockNode(block_name, nodelist)
def construct_relative_path(current_template_name, relative_name):
"""
Convert a relative path (starting with './' or '../') to the full template
name based on the current_template_name.
"""
if not any(relative_name.startswith(x) for x in ["'./", "'../", '"./', '"../']):
# relative_name is a variable or a literal that doesn't contain a
# relative path.
return relative_name
new_name = posixpath.normpath(
posixpath.join(
posixpath.dirname(current_template_name.lstrip('/')),
relative_name.strip('\'"')
)
)
if new_name.startswith('../'):
raise TemplateSyntaxError(
"The relative path '%s' points outside the file hierarchy that "
"template '%s' is in." % (relative_name, current_template_name)
)
if current_template_name.lstrip('/') == new_name:
raise TemplateSyntaxError(
"The relative path '%s' was translated to template name '%s', the "
"same template in which the tag appears."
% (relative_name, current_template_name)
)
return '"%s"' % new_name
@register.tag('extends') @register.tag('extends')
def do_extends(parser, token): def do_extends(parser, token):
""" """
@ -263,6 +294,7 @@ def do_extends(parser, token):
bits = token.split_contents() bits = token.split_contents()
if len(bits) != 2: if len(bits) != 2:
raise TemplateSyntaxError("'%s' takes one argument" % bits[0]) raise TemplateSyntaxError("'%s' takes one argument" % bits[0])
bits[1] = construct_relative_path(parser.origin.template_name, bits[1])
parent_name = parser.compile_filter(bits[1]) parent_name = parser.compile_filter(bits[1])
nodelist = parser.parse() nodelist = parser.parse()
if nodelist.get_nodes_by_type(ExtendsNode): if nodelist.get_nodes_by_type(ExtendsNode):
@ -313,5 +345,6 @@ 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', {})
bits[1] = construct_relative_path(parser.origin.template_name, bits[1])
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)

View File

@ -212,6 +212,26 @@ This tag can be used in two ways:
See :ref:`template-inheritance` for more information. See :ref:`template-inheritance` for more information.
A string argument may be a relative path starting with ``./`` or ``../``. For
example, assume the following directory structure::
dir1/
template.html
base2.html
my/
base3.html
base1.html
In ``template.html``, the following paths would be valid::
{% extends "./base2.html" %}
{% extends "../base1.html" %}
{% extends "./my/base3.html" %}
.. versionadded:: 1.10
The ability to use relative paths was added.
.. templatetag:: filter .. templatetag:: filter
``filter`` ``filter``
@ -663,6 +683,13 @@ This example includes the contents of the template ``"foo/bar.html"``::
{% include "foo/bar.html" %} {% include "foo/bar.html" %}
A string argument may be a relative path starting with ``./`` or ``../`` as
described in the :ttag:`extends` tag.
.. versionadded:: 1.10
The ability to use a relative path was added.
This example includes the contents of the template whose name is contained in This example includes the contents of the template whose name is contained in
the variable ``template_name``:: the variable ``template_name``::

View File

@ -451,6 +451,9 @@ Templates
* The :func:`~django.template.context_processors.debug` context processor * The :func:`~django.template.context_processors.debug` context processor
contains queries for all database aliases instead of only the default alias. contains queries for all database aliases instead of only the default alias.
* Added relative path support for string arguments of the :ttag:`extends` and
:ttag:`include` template tags.
Tests Tests
~~~~~ ~~~~~

View File

@ -0,0 +1 @@
{% include "./../../three.html" %}

View File

@ -0,0 +1 @@
{% include "./include_content.html" %}

View File

@ -0,0 +1 @@
dir2 include

View File

@ -0,0 +1,3 @@
{% extends "./../../one.html" %}
{% block content %}{{ block.super }} dir2 one{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends "./dir2/../looped.html" %}
{% block content %}{{ block.super }} dir1 three{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends "./../one.html" %}
{% block content %}{{ block.super }} dir1 one{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends './../one.html' %}
{% block content %}{{ block.super }} dir1 one{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends '../one.html' %}
{% block content %}{{ block.super }} dir1 one{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends "../one.html" %}
{% block content %}{{ block.super }} dir1 one{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends "./dir2/../../three.html" %}
{% block content %}{{ block.super }} dir1 three{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends "./dir2/one.html" %}
{% block content %}{{ block.super }} dir1 two{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends "./../two.html" %}
{% block content %}{{ block.super }} one{% endblock %}

View File

@ -0,0 +1 @@
{% include "./../three.html" %}

View File

@ -0,0 +1,3 @@
{% extends "./two.html" %}
{% block content %}{{ block.super }} one{% endblock %}

View File

@ -0,0 +1 @@
{% block content %}three{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends "./three.html" %}
{% block content %}{{ block.super }} two{% endblock %}

View File

@ -0,0 +1,105 @@
import os
from django.template import Context, Engine, TemplateSyntaxError
from django.test import SimpleTestCase
from .utils import ROOT
RELATIVE = os.path.join(ROOT, 'relative_templates')
class ExtendsRelativeBehaviorTests(SimpleTestCase):
def test_normal_extend(self):
engine = Engine(dirs=[RELATIVE])
template = engine.get_template('one.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'three two one')
def test_dir1_extend(self):
engine = Engine(dirs=[RELATIVE])
template = engine.get_template('dir1/one.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'three two one dir1 one')
def test_dir1_extend1(self):
engine = Engine(dirs=[RELATIVE])
template = engine.get_template('dir1/one1.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'three two one dir1 one')
def test_dir1_extend2(self):
engine = Engine(dirs=[RELATIVE])
template = engine.get_template('dir1/one2.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'three two one dir1 one')
def test_dir1_extend3(self):
engine = Engine(dirs=[RELATIVE])
template = engine.get_template('dir1/one3.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'three two one dir1 one')
def test_dir2_extend(self):
engine = Engine(dirs=[RELATIVE])
template = engine.get_template('dir1/dir2/one.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'three two one dir2 one')
def test_extend_error(self):
engine = Engine(dirs=[RELATIVE])
msg = (
"The relative path '\"./../two.html\"' points outside the file "
"hierarchy that template 'error_extends.html' is in."
)
with self.assertRaisesMessage(TemplateSyntaxError, msg):
engine.render_to_string('error_extends.html')
class IncludeRelativeBehaviorTests(SimpleTestCase):
def test_normal_include(self):
engine = Engine(dirs=[RELATIVE])
template = engine.get_template('dir1/dir2/inc2.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'dir2 include')
def test_dir2_include(self):
engine = Engine(dirs=[RELATIVE])
template = engine.get_template('dir1/dir2/inc1.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'three')
def test_include_error(self):
engine = Engine(dirs=[RELATIVE])
msg = (
"The relative path '\"./../three.html\"' points outside the file "
"hierarchy that template 'error_include.html' is in."
)
with self.assertRaisesMessage(TemplateSyntaxError, msg):
engine.render_to_string('error_include.html')
class ExtendsMixedBehaviorTests(SimpleTestCase):
def test_mixing1(self):
engine = Engine(dirs=[RELATIVE])
template = engine.get_template('dir1/two.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'three two one dir2 one dir1 two')
def test_mixing2(self):
engine = Engine(dirs=[RELATIVE])
template = engine.get_template('dir1/three.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'three dir1 three')
def test_mixing_loop(self):
engine = Engine(dirs=[RELATIVE])
msg = (
"The relative path '\"./dir2/../looped.html\"' was translated to "
"template name \'dir1/looped.html\', the same template in which "
"the tag appears."
)
with self.assertRaisesMessage(TemplateSyntaxError, msg):
engine.render_to_string('dir1/looped.html')