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": ( "
{% block main %}Default main content.{% endblock 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&", "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 content with Inside Content
", 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( "
Default main content. Main content with Inside Content
", 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": ( "
Before partial
" "{% partialdef unclosed_partial %}" "

This partial has no closing tag

" "
After partial content
" ) } ) 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": ( "

Title

\n" "{% partialdef testing-name %}\n" "

{{ nonexistent|default:alsonotthere }}

\n" "{% endpartialdef %}\n" "

Sub Title

\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": ( "

Title

\n" "{% partialdef testing-name %}\n" "

Content

\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": ( "

Header

\n" "{% partial undefined %}\n" "

After undefined partial

\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": ( "

Header

This template has no partials defined

" ), } ) 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": ( "

Title

\n" "{% partialdef syntax_error_partial %}\n" " {% if user %}\n" "

User: {{ user.name }}

\n" " {% endif\n" "

Missing closing tag above

\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": ( "

Title

\n" "{% load bad_tag %}\n" "{% partialdef runtime_error_partial %}\n" "

This will raise an error:

\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": ( "

Title

\n" "{% partialdef outer_partial %}\n" '
\n' " {% partialdef inner_partial %}\n" "

{{ undefined_var }}

\n" " {% endpartialdef %}\n" " {% partial inner_partial %}\n" "
\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("

", output) self.assertIn("

", output) @setup( { "parent.html": ( "\n" "\n" "{% block title %}Default Title{% endblock %}\n" "\n" " {% block content %}{% endblock %}\n" "\n" "\n" ), "child.html": ( "{% extends 'parent.html' %}\n" "{% block content %}\n" " {% partialdef content_partial %}\n" "

{{ missing_variable|undefined_filter }}

\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": ( "
Before partial
\n" "{% partialdef outer %}\n" "{% partialdef inner %}...{% endpartialdef outer %}\n" "{% endpartialdef inner %}\n" "
After partial content
" ) } ) 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": ( "
Before partial
\n" "{% partialdef outer %}\n" "{% partialdef inner %}...{% endpartialdef %}\n" "{% endpartialdef inner %}\n" "
After partial content
" ) } ) 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))