summaryrefslogtreecommitdiff
path: root/tests/template_tests/syntax_tests/test_partials.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/template_tests/syntax_tests/test_partials.py')
-rw-r--r--tests/template_tests/syntax_tests/test_partials.py652
1 files changed, 652 insertions, 0 deletions
diff --git a/tests/template_tests/syntax_tests/test_partials.py b/tests/template_tests/syntax_tests/test_partials.py
new file mode 100644
index 0000000000..a2cd3ae96a
--- /dev/null
+++ b/tests/template_tests/syntax_tests/test_partials.py
@@ -0,0 +1,652 @@
+from django.template import (
+ Context,
+ TemplateDoesNotExist,
+ TemplateSyntaxError,
+ VariableDoesNotExist,
+)
+from django.template.base import Token, TokenType
+from django.test import SimpleTestCase
+from django.views.debug import ExceptionReporter
+
+from ..utils import setup
+
+partial_templates = {
+ "partial_base.html": (
+ "<main>{% block main %}Default main content.{% endblock main %}</main>"
+ ),
+ "partial_included.html": (
+ "INCLUDED TEMPLATE START\n"
+ "{% partialdef included-partial %}\n"
+ "THIS IS CONTENT FROM THE INCLUDED PARTIAL\n"
+ "{% endpartialdef %}\n\n"
+ "Now using the partial: {% partial included-partial %}\n"
+ "INCLUDED TEMPLATE END\n"
+ ),
+}
+
+valid_partialdef_names = (
+ "dot.in.name",
+ "'space in name'",
+ "exclamation!",
+ "@at",
+ "slash/something",
+ "inline",
+ "inline-inline",
+ "INLINE" "with+plus",
+ "with&amp",
+ "with%percent",
+ "with,comma",
+ "with:colon",
+ "with;semicolon",
+ "[brackets]",
+ "(parens)",
+ "{curly}",
+)
+
+
+def gen_partial_template(name, *args, **kwargs):
+ if args or kwargs:
+ extra = " ".join((args, *("{k}={v}" for k, v in kwargs.items()))) + " "
+ else:
+ extra = ""
+ return (
+ f"{{% partialdef {name} {extra}%}}TEST with {name}!{{% endpartialdef %}}"
+ f"{{% partial {name} %}}"
+ )
+
+
+class PartialTagTests(SimpleTestCase):
+ libraries = {"bad_tag": "template_tests.templatetags.bad_tag"}
+
+ @setup({name: gen_partial_template(name) for name in valid_partialdef_names})
+ def test_valid_partialdef_names(self):
+ for template_name in valid_partialdef_names:
+ with self.subTest(template_name=template_name):
+ output = self.engine.render_to_string(template_name)
+ self.assertEqual(output, f"TEST with {template_name}!")
+
+ @setup(
+ {
+ "basic": (
+ "{% partialdef testing-name %}"
+ "HERE IS THE TEST CONTENT"
+ "{% endpartialdef %}"
+ "{% partial testing-name %}"
+ ),
+ "basic_inline": (
+ "{% partialdef testing-name inline %}"
+ "HERE IS THE TEST CONTENT"
+ "{% endpartialdef %}"
+ ),
+ "inline_inline": (
+ "{% partialdef inline inline %}"
+ "HERE IS THE TEST CONTENT"
+ "{% endpartialdef %}"
+ ),
+ "with_newlines": (
+ "{% partialdef testing-name %}\n"
+ "HERE IS THE TEST CONTENT\n"
+ "{% endpartialdef testing-name %}\n"
+ "{% partial testing-name %}"
+ ),
+ }
+ )
+ def test_basic_usage(self):
+ for template_name in (
+ "basic",
+ "basic_inline",
+ "inline_inline",
+ "with_newlines",
+ ):
+ with self.subTest(template_name=template_name):
+ output = self.engine.render_to_string(template_name)
+ self.assertEqual(output.strip(), "HERE IS THE TEST CONTENT")
+
+ @setup(
+ {
+ "inline_partial_with_context": (
+ "BEFORE\n"
+ "{% partialdef testing-name inline %}"
+ "HERE IS THE TEST CONTENT"
+ "{% endpartialdef %}\n"
+ "AFTER"
+ )
+ }
+ )
+ def test_partial_inline_only_with_before_and_after_content(self):
+ output = self.engine.render_to_string("inline_partial_with_context")
+ self.assertEqual(output.strip(), "BEFORE\nHERE IS THE TEST CONTENT\nAFTER")
+
+ @setup(
+ {
+ "inline_partial_explicit_end": (
+ "{% partialdef testing-name inline %}"
+ "HERE IS THE TEST CONTENT"
+ "{% endpartialdef testing-name %}\n"
+ "{% partial testing-name %}"
+ )
+ }
+ )
+ def test_partial_inline_and_used_once(self):
+ output = self.engine.render_to_string("inline_partial_explicit_end")
+ self.assertEqual(output, "HERE IS THE TEST CONTENT\nHERE IS THE TEST CONTENT")
+
+ @setup(
+ {
+ "inline_partial_with_usage": (
+ "BEFORE\n"
+ "{% partialdef content_snippet inline %}"
+ "HERE IS THE TEST CONTENT"
+ "{% endpartialdef %}\n"
+ "AFTER\n"
+ "{% partial content_snippet %}"
+ )
+ }
+ )
+ def test_partial_inline_and_used_once_with_before_and_after_content(self):
+ output = self.engine.render_to_string("inline_partial_with_usage")
+ self.assertEqual(
+ output.strip(),
+ "BEFORE\nHERE IS THE TEST CONTENT\nAFTER\nHERE IS THE TEST CONTENT",
+ )
+
+ @setup(
+ {
+ "partial_used_before_definition": (
+ "TEMPLATE START\n"
+ "{% partial testing-name %}\n"
+ "MIDDLE CONTENT\n"
+ "{% partialdef testing-name %}\n"
+ "THIS IS THE PARTIAL CONTENT\n"
+ "{% endpartialdef %}\n"
+ "TEMPLATE END"
+ ),
+ }
+ )
+ def test_partial_used_before_definition(self):
+ output = self.engine.render_to_string("partial_used_before_definition")
+ expected = (
+ "TEMPLATE START\n\nTHIS IS THE PARTIAL CONTENT\n\n"
+ "MIDDLE CONTENT\n\nTEMPLATE END"
+ )
+ self.assertEqual(output, expected)
+
+ @setup(
+ {
+ "partial_with_extends": (
+ "{% extends 'partial_base.html' %}"
+ "{% partialdef testing-name %}Inside Content{% endpartialdef %}"
+ "{% block main %}"
+ "Main content with {% partial testing-name %}"
+ "{% endblock %}"
+ ),
+ },
+ partial_templates,
+ )
+ def test_partial_defined_outside_main_block(self):
+ output = self.engine.render_to_string("partial_with_extends")
+ self.assertIn("<main>Main content with Inside Content</main>", output)
+
+ @setup(
+ {
+ "partial_with_extends_and_block_super": (
+ "{% extends 'partial_base.html' %}"
+ "{% partialdef testing-name %}Inside Content{% endpartialdef %}"
+ "{% block main %}{{ block.super }} "
+ "Main content with {% partial testing-name %}"
+ "{% endblock %}"
+ ),
+ },
+ partial_templates,
+ )
+ def test_partial_used_with_block_super(self):
+ output = self.engine.render_to_string("partial_with_extends_and_block_super")
+ self.assertIn(
+ "<main>Default main content. Main content with Inside Content</main>",
+ output,
+ )
+
+ @setup(
+ {
+ "partial_with_include": (
+ "MAIN TEMPLATE START\n"
+ "{% include 'partial_included.html' %}\n"
+ "MAIN TEMPLATE END"
+ )
+ },
+ partial_templates,
+ )
+ def test_partial_in_included_template(self):
+ output = self.engine.render_to_string("partial_with_include")
+ expected = (
+ "MAIN TEMPLATE START\nINCLUDED TEMPLATE START\n\n\n"
+ "Now using the partial: \n"
+ "THIS IS CONTENT FROM THE INCLUDED PARTIAL\n\n"
+ "INCLUDED TEMPLATE END\n\nMAIN TEMPLATE END"
+ )
+ self.assertEqual(output, expected)
+
+ @setup(
+ {
+ "partial_as_include_in_other_template": (
+ "MAIN TEMPLATE START\n"
+ "{% include 'partial_included.html#included-partial' %}\n"
+ "MAIN TEMPLATE END"
+ )
+ },
+ partial_templates,
+ )
+ def test_partial_as_include_in_template(self):
+ output = self.engine.render_to_string("partial_as_include_in_other_template")
+ expected = (
+ "MAIN TEMPLATE START\n\n"
+ "THIS IS CONTENT FROM THE INCLUDED PARTIAL\n\n"
+ "MAIN TEMPLATE END"
+ )
+ self.assertEqual(output, expected)
+
+ @setup(
+ {
+ "nested_simple": (
+ "{% extends 'base.html' %}"
+ "{% block content %}"
+ "This is my main page."
+ "{% partialdef outer inline %}"
+ " It hosts a couple of partials.\n"
+ " {% partialdef inner inline %}"
+ " And an inner one."
+ " {% endpartialdef inner %}"
+ "{% endpartialdef outer %}"
+ "{% endblock content %}"
+ ),
+ "use_outer": "{% include 'nested_simple#outer' %}",
+ "use_inner": "{% include 'nested_simple#inner' %}",
+ }
+ )
+ def test_nested_partials(self):
+ with self.subTest(template_name="use_outer"):
+ output = self.engine.render_to_string("use_outer")
+ self.assertEqual(
+ [line.strip() for line in output.split("\n")],
+ ["It hosts a couple of partials.", "And an inner one."],
+ )
+ with self.subTest(template_name="use_inner"):
+ output = self.engine.render_to_string("use_inner")
+ self.assertEqual(output.strip(), "And an inner one.")
+
+ @setup(
+ {
+ "partial_undefined_name": "{% partial undefined %}",
+ "partial_missing_name": "{% partial %}",
+ "partial_closing_tag": (
+ "{% partialdef testing-name %}TEST{% endpartialdef %}"
+ "{% partial testing-name %}{% endpartial %}"
+ ),
+ "partialdef_missing_name": "{% partialdef %}{% endpartialdef %}",
+ "partialdef_missing_close_tag": "{% partialdef name %}TEST",
+ "partialdef_opening_closing_name_mismatch": (
+ "{% partialdef testing-name %}TEST{% endpartialdef invalid %}"
+ ),
+ "partialdef_invalid_name": gen_partial_template("with\nnewline"),
+ "partialdef_extra_params": (
+ "{% partialdef testing-name inline extra %}TEST{% endpartialdef %}"
+ ),
+ "partialdef_duplicated_names": (
+ "{% partialdef testing-name %}TEST{% endpartialdef %}"
+ "{% partialdef testing-name %}TEST{% endpartialdef %}"
+ "{% partial testing-name %}"
+ ),
+ "partialdef_duplicated_nested_names": (
+ "{% partialdef testing-name %}"
+ "TEST"
+ "{% partialdef testing-name %}TEST{% endpartialdef %}"
+ "{% endpartialdef %}"
+ "{% partial testing-name %}"
+ ),
+ },
+ )
+ def test_basic_parse_errors(self):
+ for template_name, error_msg in (
+ (
+ "partial_undefined_name",
+ "Partial 'undefined' is not defined in the current template.",
+ ),
+ ("partial_missing_name", "'partial' tag requires a single argument"),
+ ("partial_closing_tag", "Invalid block tag on line 1: 'endpartial'"),
+ ("partialdef_missing_name", "'partialdef' tag requires a name"),
+ ("partialdef_missing_close_tag", "Unclosed tag on line 1: 'partialdef'"),
+ (
+ "partialdef_opening_closing_name_mismatch",
+ "expected 'endpartialdef' or 'endpartialdef testing-name'.",
+ ),
+ ("partialdef_invalid_name", "Invalid block tag on line 3: 'endpartialdef'"),
+ ("partialdef_extra_params", "'partialdef' tag takes at most 2 arguments"),
+ (
+ "partialdef_duplicated_names",
+ "Partial 'testing-name' is already defined in the "
+ "'partialdef_duplicated_names' template.",
+ ),
+ (
+ "partialdef_duplicated_nested_names",
+ "Partial 'testing-name' is already defined in the "
+ "'partialdef_duplicated_nested_names' template.",
+ ),
+ ):
+ with (
+ self.subTest(template_name=template_name),
+ self.assertRaisesMessage(TemplateSyntaxError, error_msg),
+ ):
+ self.engine.render_to_string(template_name)
+
+ @setup(
+ {
+ "with_params": (
+ "{% partialdef testing-name inline=true %}TEST{% endpartialdef %}"
+ ),
+ "uppercase": "{% partialdef testing-name INLINE %}TEST{% endpartialdef %}",
+ }
+ )
+ def test_partialdef_invalid_inline(self):
+ error_msg = "The 'inline' argument does not have any parameters"
+ for template_name in ("with_params", "uppercase"):
+ with (
+ self.subTest(template_name=template_name),
+ self.assertRaisesMessage(TemplateSyntaxError, error_msg),
+ ):
+ self.engine.render_to_string(template_name)
+
+ @setup(
+ {
+ "partial_broken_unclosed": (
+ "<div>Before partial</div>"
+ "{% partialdef unclosed_partial %}"
+ "<p>This partial has no closing tag</p>"
+ "<div>After partial content</div>"
+ )
+ }
+ )
+ def test_broken_partial_unclosed_exception_info(self):
+ with self.assertRaises(TemplateSyntaxError) as cm:
+ self.engine.get_template("partial_broken_unclosed")
+
+ self.assertIn("endpartialdef", str(cm.exception))
+ self.assertIn("Unclosed tag", str(cm.exception))
+
+ reporter = ExceptionReporter(None, cm.exception.__class__, cm.exception, None)
+ traceback_data = reporter.get_traceback_data()
+
+ exception_value = str(traceback_data.get("exception_value", ""))
+ self.assertIn("Unclosed tag", exception_value)
+
+ @setup(
+ {
+ "partial_with_variable_error": (
+ "<h1>Title</h1>\n"
+ "{% partialdef testing-name %}\n"
+ "<p>{{ nonexistent|default:alsonotthere }}</p>\n"
+ "{% endpartialdef %}\n"
+ "<h2>Sub Title</h2>\n"
+ "{% partial testing-name %}\n"
+ ),
+ }
+ )
+ def test_partial_runtime_exception_has_debug_info(self):
+ template = self.engine.get_template("partial_with_variable_error")
+ context = Context({})
+
+ if hasattr(self.engine, "string_if_invalid") and self.engine.string_if_invalid:
+ output = template.render(context)
+ # The variable should be replaced with INVALID
+ self.assertIn("INVALID", output)
+ else:
+ with self.assertRaises(VariableDoesNotExist) as cm:
+ template.render(context)
+
+ if self.engine.debug:
+ exc_info = cm.exception.template_debug
+
+ self.assertEqual(
+ exc_info["during"], "{{ nonexistent|default:alsonotthere }}"
+ )
+ self.assertEqual(exc_info["line"], 3)
+ self.assertEqual(exc_info["name"], "partial_with_variable_error")
+ self.assertIn("Failed lookup", exc_info["message"])
+
+ @setup(
+ {
+ "partial_exception_info_test": (
+ "<h1>Title</h1>\n"
+ "{% partialdef testing-name %}\n"
+ "<p>Content</p>\n"
+ "{% endpartialdef %}\n"
+ ),
+ }
+ )
+ def test_partial_template_get_exception_info_delegation(self):
+ if self.engine.debug:
+ template = self.engine.get_template("partial_exception_info_test")
+
+ partial_template = template.extra_data["partials"]["testing-name"]
+
+ test_exc = Exception("Test exception")
+ token = Token(
+ token_type=TokenType.VAR,
+ contents="test",
+ position=(0, 4),
+ )
+
+ exc_info = partial_template.get_exception_info(test_exc, token)
+ self.assertIn("message", exc_info)
+ self.assertIn("line", exc_info)
+ self.assertIn("name", exc_info)
+ self.assertEqual(exc_info["name"], "partial_exception_info_test")
+ self.assertEqual(exc_info["message"], "Test exception")
+
+ @setup(
+ {
+ "partial_with_undefined_reference": (
+ "<h1>Header</h1>\n"
+ "{% partial undefined %}\n"
+ "<p>After undefined partial</p>\n"
+ ),
+ }
+ )
+ def test_undefined_partial_exception_info(self):
+ template = self.engine.get_template("partial_with_undefined_reference")
+ with self.assertRaises(TemplateSyntaxError) as cm:
+ template.render(Context())
+
+ self.assertIn("undefined", str(cm.exception))
+ self.assertIn("is not defined", str(cm.exception))
+
+ if self.engine.debug:
+ exc_debug = cm.exception.template_debug
+
+ self.assertEqual(exc_debug["during"], "{% partial undefined %}")
+ self.assertEqual(exc_debug["line"], 2)
+ self.assertEqual(exc_debug["name"], "partial_with_undefined_reference")
+ self.assertIn("undefined", exc_debug["message"])
+
+ @setup(
+ {
+ "existing_template": (
+ "<h1>Header</h1><p>This template has no partials defined</p>"
+ ),
+ }
+ )
+ def test_undefined_partial_exception_info_template_does_not_exist(self):
+ with self.assertRaises(TemplateDoesNotExist) as cm:
+ self.engine.get_template("existing_template#undefined")
+
+ self.assertIn("undefined", str(cm.exception))
+
+ @setup(
+ {
+ "partial_with_syntax_error": (
+ "<h1>Title</h1>\n"
+ "{% partialdef syntax_error_partial %}\n"
+ " {% if user %}\n"
+ " <p>User: {{ user.name }}</p>\n"
+ " {% endif\n"
+ " <p>Missing closing tag above</p>\n"
+ "{% endpartialdef %}\n"
+ "{% partial syntax_error_partial %}\n"
+ ),
+ }
+ )
+ def test_partial_with_syntax_error_exception_info(self):
+ with self.assertRaises(TemplateSyntaxError) as cm:
+ self.engine.get_template("partial_with_syntax_error")
+
+ self.assertIn("endif", str(cm.exception).lower())
+
+ if self.engine.debug:
+ exc_debug = cm.exception.template_debug
+
+ self.assertIn("endpartialdef", exc_debug["during"])
+ self.assertEqual(exc_debug["name"], "partial_with_syntax_error")
+ self.assertIn("endif", exc_debug["message"].lower())
+
+ @setup(
+ {
+ "partial_with_runtime_error": (
+ "<h1>Title</h1>\n"
+ "{% load bad_tag %}\n"
+ "{% partialdef runtime_error_partial %}\n"
+ " <p>This will raise an error:</p>\n"
+ " {% badsimpletag %}\n"
+ "{% endpartialdef %}\n"
+ "{% partial runtime_error_partial %}\n"
+ ),
+ }
+ )
+ def test_partial_runtime_error_exception_info(self):
+ template = self.engine.get_template("partial_with_runtime_error")
+ context = Context()
+
+ with self.assertRaises(RuntimeError) as cm:
+ template.render(context)
+
+ if self.engine.debug:
+ exc_debug = cm.exception.template_debug
+
+ self.assertIn("badsimpletag", exc_debug["during"])
+ self.assertEqual(exc_debug["line"], 5) # Line 5 is where badsimpletag is
+ self.assertEqual(exc_debug["name"], "partial_with_runtime_error")
+ self.assertIn("bad simpletag", exc_debug["message"])
+
+ @setup(
+ {
+ "nested_partial_with_undefined_var": (
+ "<h1>Title</h1>\n"
+ "{% partialdef outer_partial %}\n"
+ ' <div class="outer">\n'
+ " {% partialdef inner_partial %}\n"
+ " <p>{{ undefined_var }}</p>\n"
+ " {% endpartialdef %}\n"
+ " {% partial inner_partial %}\n"
+ " </div>\n"
+ "{% endpartialdef %}\n"
+ "{% partial outer_partial %}\n"
+ ),
+ }
+ )
+ def test_nested_partial_error_exception_info(self):
+ template = self.engine.get_template("nested_partial_with_undefined_var")
+ context = Context()
+ output = template.render(context)
+
+ # When string_if_invalid is set, it will show INVALID
+ # When not set, undefined variables just render as empty string
+ if hasattr(self.engine, "string_if_invalid") and self.engine.string_if_invalid:
+ self.assertIn("INVALID", output)
+ else:
+ self.assertIn("<p>", output)
+ self.assertIn("</p>", output)
+
+ @setup(
+ {
+ "parent.html": (
+ "<!DOCTYPE html>\n"
+ "<html>\n"
+ "<head>{% block title %}Default Title{% endblock %}</head>\n"
+ "<body>\n"
+ " {% block content %}{% endblock %}\n"
+ "</body>\n"
+ "</html>\n"
+ ),
+ "child.html": (
+ "{% extends 'parent.html' %}\n"
+ "{% block content %}\n"
+ " {% partialdef content_partial %}\n"
+ " <p>{{ missing_variable|undefined_filter }}</p>\n"
+ " {% endpartialdef %}\n"
+ " {% partial content_partial %}\n"
+ "{% endblock %}\n"
+ ),
+ }
+ )
+ def test_partial_in_extended_template_error(self):
+ with self.assertRaises(TemplateSyntaxError) as cm:
+ self.engine.get_template("child.html")
+
+ self.assertIn("undefined_filter", str(cm.exception))
+
+ if self.engine.debug:
+ exc_debug = cm.exception.template_debug
+
+ self.assertIn("undefined_filter", exc_debug["during"])
+ self.assertEqual(exc_debug["name"], "child.html")
+ self.assertIn("undefined_filter", exc_debug["message"])
+
+ @setup(
+ {
+ "partial_broken_nesting": (
+ "<div>Before partial</div>\n"
+ "{% partialdef outer %}\n"
+ "{% partialdef inner %}...{% endpartialdef outer %}\n"
+ "{% endpartialdef inner %}\n"
+ "<div>After partial content</div>"
+ )
+ }
+ )
+ def test_broken_partial_nesting(self):
+ with self.assertRaises(TemplateSyntaxError) as cm:
+ self.engine.get_template("partial_broken_nesting")
+
+ self.assertIn("endpartialdef", str(cm.exception))
+ self.assertIn("Invalid block tag", str(cm.exception))
+ self.assertIn("'endpartialdef inner'", str(cm.exception))
+
+ reporter = ExceptionReporter(None, cm.exception.__class__, cm.exception, None)
+ traceback_data = reporter.get_traceback_data()
+
+ exception_value = str(traceback_data.get("exception_value", ""))
+ self.assertIn("Invalid block tag", exception_value)
+ self.assertIn("'endpartialdef inner'", str(cm.exception))
+
+ @setup(
+ {
+ "partial_broken_nesting_mixed": (
+ "<div>Before partial</div>\n"
+ "{% partialdef outer %}\n"
+ "{% partialdef inner %}...{% endpartialdef %}\n"
+ "{% endpartialdef inner %}\n"
+ "<div>After partial content</div>"
+ )
+ }
+ )
+ def test_broken_partial_nesting_mixed(self):
+ with self.assertRaises(TemplateSyntaxError) as cm:
+ self.engine.get_template("partial_broken_nesting_mixed")
+
+ self.assertIn("endpartialdef", str(cm.exception))
+ self.assertIn("Invalid block tag", str(cm.exception))
+ self.assertIn("'endpartialdef outer'", str(cm.exception))
+
+ reporter = ExceptionReporter(None, cm.exception.__class__, cm.exception, None)
+ traceback_data = reporter.get_traceback_data()
+
+ exception_value = str(traceback_data.get("exception_value", ""))
+ self.assertIn("Invalid block tag", exception_value)
+ self.assertIn("'endpartialdef outer'", str(cm.exception))