diff --git a/django/template/library.py b/django/template/library.py index 4ee96cea89..d181caa832 100644 --- a/django/template/library.py +++ b/django/template/library.py @@ -153,6 +153,90 @@ class Library: else: raise ValueError("Invalid arguments provided to simple_tag") + def simple_block_tag(self, func=None, takes_context=None, name=None, end_name=None): + """ + Register a callable as a compiled block template tag. Example: + + @register.simple_block_tag + def hello(content): + return 'world' + """ + + def dec(func): + nonlocal end_name + + ( + params, + varargs, + varkw, + defaults, + kwonly, + kwonly_defaults, + _, + ) = getfullargspec(unwrap(func)) + function_name = name or func.__name__ + + if end_name is None: + end_name = f"end{function_name}" + + @wraps(func) + def compile_func(parser, token): + tag_params = params.copy() + + if takes_context: + if len(tag_params) >= 2 and tag_params[1] == "content": + del tag_params[1] + else: + raise TemplateSyntaxError( + f"{function_name!r} is decorated with takes_context=True so" + " it must have a first argument of 'context' and a second " + "argument of 'content'" + ) + elif tag_params and tag_params[0] == "content": + del tag_params[0] + else: + raise TemplateSyntaxError( + f"'{function_name}' must have a first argument of 'content'" + ) + + bits = token.split_contents()[1:] + target_var = None + if len(bits) >= 2 and bits[-2] == "as": + target_var = bits[-1] + bits = bits[:-2] + + nodelist = parser.parse((end_name,)) + parser.delete_first_token() + + args, kwargs = parse_bits( + parser, + bits, + tag_params, + varargs, + varkw, + defaults, + kwonly, + kwonly_defaults, + takes_context, + function_name, + ) + + return SimpleBlockNode( + nodelist, func, takes_context, args, kwargs, target_var + ) + + self.tag(function_name, compile_func) + return func + + if func is None: + # @register.simple_block_tag(...) + return dec + elif callable(func): + # @register.simple_block_tag + return dec(func) + else: + raise ValueError("Invalid arguments provided to simple_block_tag") + def inclusion_tag(self, filename, func=None, takes_context=None, name=None): """ Register a callable as an inclusion tag: @@ -243,6 +327,23 @@ class SimpleNode(TagHelperNode): return output +class SimpleBlockNode(SimpleNode): + def __init__(self, nodelist, *args, **kwargs): + super().__init__(*args, **kwargs) + self.nodelist = nodelist + + def get_resolved_arguments(self, context): + resolved_args, resolved_kwargs = super().get_resolved_arguments(context) + + # Restore the "content" argument. + # It will move depending on whether takes_context was passed. + resolved_args.insert( + 1 if self.takes_context else 0, self.nodelist.render(context) + ) + + return resolved_args, resolved_kwargs + + class InclusionNode(TagHelperNode): def __init__(self, func, takes_context, args, kwargs, filename): super().__init__(func, takes_context, args, kwargs) diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 15bef9b5fb..b5577eef7b 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -498,6 +498,195 @@ you see fit: {% current_time "%Y-%m-%d %I:%M %p" as the_time %}

The time is {{ the_time }}.

+.. _howto-custom-template-tags-simple-block-tags: + +Simple block tags +----------------- + +.. versionadded:: 5.2 + +.. method:: django.template.Library.simple_block_tag() + +When a section of rendered template needs to be passed into a custom tag, +Django provides the ``simple_block_tag`` helper function to accomplish this. +Similar to :meth:`~django.template.Library.simple_tag()`, this function accepts +a custom tag function, but with the additional ``content`` argument, which +contains the rendered content as defined inside the tag. This allows dynamic +template sections to be easily incorporated into custom tags. + +For example, a custom block tag which creates a chart could look like this:: + + from django import template + from myapp.charts import render_chart + + register = template.Library() + + + @register.simple_block_tag + def chart(content): + return render_chart(source=content) + +The ``content`` argument contains everything in between the ``{% chart %}`` +and ``{% endchart %}`` tags: + +.. code-block:: html+django + + {% chart %} + digraph G { + label = "Chart for {{ request.user }}" + A -> {B C} + } + {% endchart %} + +If there are other template tags or variables inside the ``content`` block, +they will be rendered before being passed to the tag function. In the example +above, ``request.user`` will be resolved by the time ``render_chart`` is +called. + +Block tags are closed with ``end{name}`` (for example, ``endchart``). This can +be customized with the ``end_name`` parameter:: + + @register.simple_block_tag(end_name="endofchart") + def chart(content): + return render_chart(source=content) + +Which would require a template definition like this: + +.. code-block:: html+django + + {% chart %} + digraph G { + label = "Chart for {{ request.user }}" + A -> {B C} + } + {% endofchart %} + +A few things to note about ``simple_block_tag``: + +* The first argument must be called ``content``, and it will contain the + contents of the template tag as a rendered string. +* Variables passed to the tag are not included in the rendering context of the + content, as would be when using the ``{% with %}`` tag. + +Just like :ref:`simple_tag`, +``simple_block_tag``: + +* Validates the quantity and quality of the arguments. +* Strips quotes from arguments if necessary. +* Escapes the output accordingly. +* Supports passing ``takes_context=True`` at registration time to access + context. Note that in this case, the first argument to the custom function + *must* be called ``context``, and ``content`` must follow. +* Supports renaming the tag by passing the ``name`` argument when registering. +* Supports accepting any number of positional or keyword arguments. +* Supports storing the result in a template variable using the ``as`` variant. + +.. admonition:: Content Escaping + + ``simple_block_tag`` behaves similarly to ``simple_tag`` regarding + auto-escaping. For details on escaping and safety, refer to ``simple_tag``. + Because the ``content`` argument has already been rendered by Django, it is + already escaped. + +A complete example +~~~~~~~~~~~~~~~~~~ + +Consider a custom template tag that generates a message box that supports +multiple message levels and content beyond a simple phrase. This could be +implemented using a ``simple_block_tag`` as follows: + +.. code-block:: python + :caption: ``testapp/templatetags/testapptags.py`` + + from django import template + from django.utils.html import format_html + + + register = template.Library() + + + @register.simple_block_tag(takes_context=True) + def msgbox(context, content, level): + format_kwargs = { + "level": level.lower(), + "level_title": level.capitalize(), + "content": content, + "open": " open" if level.lower() == "error" else "", + "site": context.get("site", "My Site"), + } + result = """ +
+ + + {level_title}: Please read for {site} + +

+ {content} +

+ +
+ """ + return format_html(result, **format_kwargs) + +When combined with a minimal view and corresponding template, as shown here: + +.. code-block:: python + :caption: ``testapp/views.py`` + + from django.shortcuts import render + + + def simpleblocktag_view(request): + return render(request, "test.html", context={"site": "Important Site"}) + + +.. code-block:: html+django + :caption: ``testapp/templates/test.html`` + + {% extends "base.html" %} + + {% load testapptags %} + + {% block content %} + + {% msgbox level="error" %} + Please fix all errors. Further documentation can be found at + Docs. + {% endmsgbox %} + + {% msgbox level="info" %} + More information at: Other Site/ + {% endmsgbox %} + + {% endblock %} + +The following HTML is produced as the rendered output: + +.. code-block:: html + +
+
+ + Error: Please read for Important Site + +

+ Please fix all errors. Further documentation can be found at + Docs. +

+
+
+ +
+
+ + Info: Please read for Important Site + +

+ More information at: Other Site +

+
+
+ .. _howto-custom-template-tags-inclusion-tags: Inclusion tags diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index 3cc71b7f68..f1ffe07569 100644 --- a/docs/releases/5.2.txt +++ b/docs/releases/5.2.txt @@ -323,7 +323,9 @@ Signals Templates ~~~~~~~~~ -* ... +* The new :meth:`~django.template.Library.simple_block_tag` decorator enables + the creation of simple block tags, which can accept and use a section of the + template. Tests ~~~~~ diff --git a/tests/template_tests/templatetags/custom.py b/tests/template_tests/templatetags/custom.py index 8d1130ae78..2c0a1b7f3f 100644 --- a/tests/template_tests/templatetags/custom.py +++ b/tests/template_tests/templatetags/custom.py @@ -20,6 +20,16 @@ def make_data_div(value): return '
' % value +@register.simple_block_tag +def div(content, id="test"): + return format_html("
{}
", id, content) + + +@register.simple_block_tag(end_name="divend") +def div_custom_end(content): + return format_html("
{}
", content) + + @register.filter def noop(value, param=None): """A noop filter that always return its first argument and does nothing with @@ -51,6 +61,12 @@ def one_param(arg): one_param.anything = "Expected one_param __dict__" +@register.simple_block_tag +def one_param_block(content, arg): + """Expected one_param_block __doc__""" + return f"one_param_block - Expected result: {arg} with content {content}" + + @register.simple_tag(takes_context=False) def explicit_no_context(arg): """Expected explicit_no_context __doc__""" @@ -60,6 +76,12 @@ def explicit_no_context(arg): explicit_no_context.anything = "Expected explicit_no_context __dict__" +@register.simple_block_tag(takes_context=False) +def explicit_no_context_block(content, arg): + """Expected explicit_no_context_block __doc__""" + return f"explicit_no_context_block - Expected result: {arg} with content {content}" + + @register.simple_tag(takes_context=True) def no_params_with_context(context): """Expected no_params_with_context __doc__""" @@ -72,6 +94,15 @@ def no_params_with_context(context): no_params_with_context.anything = "Expected no_params_with_context __dict__" +@register.simple_block_tag(takes_context=True) +def no_params_with_context_block(context, content): + """Expected no_params_with_context_block __doc__""" + return ( + "no_params_with_context_block - Expected result (context value: %s) " + "(content value: %s)" % (context["value"], content) + ) + + @register.simple_tag(takes_context=True) def params_and_context(context, arg): """Expected params_and_context __doc__""" @@ -84,6 +115,20 @@ def params_and_context(context, arg): params_and_context.anything = "Expected params_and_context __dict__" +@register.simple_block_tag(takes_context=True) +def params_and_context_block(context, content, arg): + """Expected params_and_context_block __doc__""" + return ( + "params_and_context_block - Expected result (context value: %s) " + "(content value: %s): %s" + % ( + context["value"], + content, + arg, + ) + ) + + @register.simple_tag def simple_two_params(one, two): """Expected simple_two_params __doc__""" @@ -93,16 +138,48 @@ def simple_two_params(one, two): simple_two_params.anything = "Expected simple_two_params __dict__" +@register.simple_block_tag +def simple_two_params_block(content, one, two): + """Expected simple_two_params_block __doc__""" + return "simple_two_params_block - Expected result (content value: %s): %s, %s" % ( + content, + one, + two, + ) + + @register.simple_tag def simple_keyword_only_param(*, kwarg): return "simple_keyword_only_param - Expected result: %s" % kwarg +@register.simple_block_tag +def simple_keyword_only_param_block(content, *, kwarg): + return ( + "simple_keyword_only_param_block - Expected result (content value: %s): %s" + % ( + content, + kwarg, + ) + ) + + @register.simple_tag def simple_keyword_only_default(*, kwarg=42): return "simple_keyword_only_default - Expected result: %s" % kwarg +@register.simple_block_tag +def simple_keyword_only_default_block(content, *, kwarg=42): + return ( + "simple_keyword_only_default_block - Expected result (content value: %s): %s" + % ( + content, + kwarg, + ) + ) + + @register.simple_tag def simple_one_default(one, two="hi"): """Expected simple_one_default __doc__""" @@ -112,6 +189,16 @@ def simple_one_default(one, two="hi"): simple_one_default.anything = "Expected simple_one_default __dict__" +@register.simple_block_tag +def simple_one_default_block(content, one, two="hi"): + """Expected simple_one_default_block __doc__""" + return "simple_one_default_block - Expected result (content value: %s): %s, %s" % ( + content, + one, + two, + ) + + @register.simple_tag def simple_unlimited_args(one, two="hi", *args): """Expected simple_unlimited_args __doc__""" @@ -123,6 +210,15 @@ def simple_unlimited_args(one, two="hi", *args): simple_unlimited_args.anything = "Expected simple_unlimited_args __dict__" +@register.simple_block_tag +def simple_unlimited_args_block(content, one, two="hi", *args): + """Expected simple_unlimited_args_block __doc__""" + return "simple_unlimited_args_block - Expected result (content value: %s): %s" % ( + content, + ", ".join(str(arg) for arg in [one, two, *args]), + ) + + @register.simple_tag def simple_only_unlimited_args(*args): """Expected simple_only_unlimited_args __doc__""" @@ -134,6 +230,18 @@ def simple_only_unlimited_args(*args): simple_only_unlimited_args.anything = "Expected simple_only_unlimited_args __dict__" +@register.simple_block_tag +def simple_only_unlimited_args_block(content, *args): + """Expected simple_only_unlimited_args_block __doc__""" + return ( + "simple_only_unlimited_args_block - Expected result (content value: %s): %s" + % ( + content, + ", ".join(str(arg) for arg in args), + ) + ) + + @register.simple_tag def simple_unlimited_args_kwargs(one, two="hi", *args, **kwargs): """Expected simple_unlimited_args_kwargs __doc__""" @@ -146,6 +254,38 @@ def simple_unlimited_args_kwargs(one, two="hi", *args, **kwargs): simple_unlimited_args_kwargs.anything = "Expected simple_unlimited_args_kwargs __dict__" +@register.simple_block_tag +def simple_unlimited_args_kwargs_block(content, one, two="hi", *args, **kwargs): + """Expected simple_unlimited_args_kwargs_block __doc__""" + return ( + "simple_unlimited_args_kwargs_block - Expected result (content value: %s): " + "%s / %s" + % ( + content, + ", ".join(str(arg) for arg in [one, two, *args]), + ", ".join("%s=%s" % (k, v) for (k, v) in kwargs.items()), + ) + ) + + +@register.simple_block_tag(takes_context=True) +def simple_block_tag_without_context_parameter(arg): + """Expected simple_block_tag_without_context_parameter __doc__""" + return "Expected result" + + +@register.simple_block_tag +def simple_tag_without_content_parameter(arg): + """Expected simple_tag_without_content_parameter __doc__""" + return "Expected result" + + +@register.simple_block_tag(takes_context=True) +def simple_tag_with_context_without_content_parameter(context, arg): + """Expected simple_tag_with_context_without_content_parameter __doc__""" + return "Expected result" + + @register.simple_tag(takes_context=True) def simple_tag_without_context_parameter(arg): """Expected simple_tag_without_context_parameter __doc__""" @@ -157,6 +297,12 @@ simple_tag_without_context_parameter.anything = ( ) +@register.simple_block_tag(takes_context=True) +def simple_tag_takes_context_without_params_block(): + """Expected simple_tag_takes_context_without_params_block __doc__""" + return "Expected result" + + @register.simple_tag(takes_context=True) def simple_tag_takes_context_without_params(): """Expected simple_tag_takes_context_without_params __doc__""" @@ -168,24 +314,52 @@ simple_tag_takes_context_without_params.anything = ( ) +@register.simple_block_tag +def simple_block_tag_without_content(): + return "Expected result" + + +@register.simple_block_tag(takes_context=True) +def simple_block_tag_with_context_without_content(): + return "Expected result" + + @register.simple_tag(takes_context=True) def escape_naive(context): """A tag that doesn't even think about escaping issues""" return "Hello {}!".format(context["name"]) +@register.simple_block_tag(takes_context=True) +def escape_naive_block(context, content): + """A block tag that doesn't even think about escaping issues""" + return "Hello {}: {}!".format(context["name"], content) + + @register.simple_tag(takes_context=True) def escape_explicit(context): """A tag that uses escape explicitly""" return escape("Hello {}!".format(context["name"])) +@register.simple_block_tag(takes_context=True) +def escape_explicit_block(context, content): + """A block tag that uses escape explicitly""" + return escape("Hello {}: {}!".format(context["name"], content)) + + @register.simple_tag(takes_context=True) def escape_format_html(context): """A tag that uses format_html""" return format_html("Hello {0}!", context["name"]) +@register.simple_block_tag(takes_context=True) +def escape_format_html_block(context, content): + """A block tag that uses format_html""" + return format_html("Hello {0}: {1}!", context["name"], content) + + @register.simple_tag(takes_context=True) def current_app(context): return str(context.current_app) diff --git a/tests/template_tests/test_custom.py b/tests/template_tests/test_custom.py index 1697d16ef5..9ec27b481f 100644 --- a/tests/template_tests/test_custom.py +++ b/tests/template_tests/test_custom.py @@ -243,6 +243,343 @@ class SimpleTagTests(TagTestCase): ) +class SimpleBlockTagTests(TagTestCase): + def test_simple_block_tags(self): + c = Context({"value": 42}) + + templates = [ + ( + "{% load custom %}{% div %}content{% enddiv %}", + "
content
", + ), + ( + "{% load custom %}{% one_param_block 37 %}inner" + "{% endone_param_block %}", + "one_param_block - Expected result: 37 with content inner", + ), + ( + "{% load custom %}{% explicit_no_context_block 37 %}inner" + "{% endexplicit_no_context_block %}", + "explicit_no_context_block - Expected result: 37 with content inner", + ), + ( + "{% load custom %}{% no_params_with_context_block %}inner" + "{% endno_params_with_context_block %}", + "no_params_with_context_block - Expected result (context value: 42) " + "(content value: inner)", + ), + ( + "{% load custom %}{% params_and_context_block 37 %}inner" + "{% endparams_and_context_block %}", + "params_and_context_block - Expected result (context value: 42) " + "(content value: inner): 37", + ), + ( + "{% load custom %}{% simple_two_params_block 37 42 %}inner" + "{% endsimple_two_params_block %}", + "simple_two_params_block - Expected result (content value: inner): " + "37, 42", + ), + ( + "{% load custom %}{% simple_keyword_only_param_block kwarg=37 %}thirty " + "seven{% endsimple_keyword_only_param_block %}", + "simple_keyword_only_param_block - Expected result (content value: " + "thirty seven): 37", + ), + ( + "{% load custom %}{% simple_keyword_only_default_block %}forty two" + "{% endsimple_keyword_only_default_block %}", + "simple_keyword_only_default_block - Expected result (content value: " + "forty two): 42", + ), + ( + "{% load custom %}{% simple_keyword_only_default_block kwarg=37 %}" + "thirty seven{% endsimple_keyword_only_default_block %}", + "simple_keyword_only_default_block - Expected result (content value: " + "thirty seven): 37", + ), + ( + "{% load custom %}{% simple_one_default_block 37 %}inner" + "{% endsimple_one_default_block %}", + "simple_one_default_block - Expected result (content value: inner): " + "37, hi", + ), + ( + '{% load custom %}{% simple_one_default_block 37 two="hello" %}inner' + "{% endsimple_one_default_block %}", + "simple_one_default_block - Expected result (content value: inner): " + "37, hello", + ), + ( + '{% load custom %}{% simple_one_default_block one=99 two="hello" %}' + "inner{% endsimple_one_default_block %}", + "simple_one_default_block - Expected result (content value: inner): " + "99, hello", + ), + ( + "{% load custom %}{% simple_one_default_block 37 42 %}inner" + "{% endsimple_one_default_block %}", + "simple_one_default_block - Expected result (content value: inner): " + "37, 42", + ), + ( + "{% load custom %}{% simple_unlimited_args_block 37 %}thirty seven" + "{% endsimple_unlimited_args_block %}", + "simple_unlimited_args_block - Expected result (content value: thirty " + "seven): 37, hi", + ), + ( + "{% load custom %}{% simple_unlimited_args_block 37 42 56 89 %}numbers" + "{% endsimple_unlimited_args_block %}", + "simple_unlimited_args_block - Expected result " + "(content value: numbers): 37, 42, 56, 89", + ), + ( + "{% load custom %}{% simple_only_unlimited_args_block %}inner" + "{% endsimple_only_unlimited_args_block %}", + "simple_only_unlimited_args_block - Expected result (content value: " + "inner): ", + ), + ( + "{% load custom %}{% simple_only_unlimited_args_block 37 42 56 89 %}" + "numbers{% endsimple_only_unlimited_args_block %}", + "simple_only_unlimited_args_block - Expected result " + "(content value: numbers): 37, 42, 56, 89", + ), + ( + "{% load custom %}" + '{% simple_unlimited_args_kwargs_block 37 40|add:2 56 eggs="scrambled" ' + "four=1|add:3 %}inner content" + "{% endsimple_unlimited_args_kwargs_block %}", + "simple_unlimited_args_kwargs_block - Expected result (content value: " + "inner content): 37, 42, 56 / eggs=scrambled, four=4", + ), + ] + + for entry in templates: + with self.subTest(entry[0]): + t = self.engine.from_string(entry[0]) + self.assertEqual(t.render(c), entry[1]) + + def test_simple_block_tag_errors(self): + errors = [ + ( + "'simple_one_default_block' received unexpected keyword argument " + "'three'", + "{% load custom %}" + '{% simple_one_default_block 99 two="hello" three="foo" %}' + "{% endsimple_one_default_block %}", + ), + ( + "'simple_two_params_block' received too many positional arguments", + "{% load custom %}{% simple_two_params_block 37 42 56 %}" + "{% endsimple_two_params_block %}", + ), + ( + "'simple_one_default_block' received too many positional arguments", + "{% load custom %}{% simple_one_default_block 37 42 56 %}" + "{% endsimple_one_default_block %}", + ), + ( + "'simple_keyword_only_param_block' did not receive value(s) for the " + "argument(s): 'kwarg'", + "{% load custom %}{% simple_keyword_only_param_block %}" + "{% endsimple_keyword_only_param_block %}", + ), + ( + "'simple_keyword_only_param_block' received multiple values for " + "keyword argument 'kwarg'", + "{% load custom %}" + "{% simple_keyword_only_param_block kwarg=42 kwarg=37 %}" + "{% endsimple_keyword_only_param_block %}", + ), + ( + "'simple_keyword_only_default_block' received multiple values for " + "keyword argument 'kwarg'", + "{% load custom %}{% simple_keyword_only_default_block kwarg=42 " + "kwarg=37 %}{% endsimple_keyword_only_default_block %}", + ), + ( + "'simple_unlimited_args_kwargs_block' received some positional " + "argument(s) after some keyword argument(s)", + "{% load custom %}" + '{% simple_unlimited_args_kwargs_block 37 40|add:2 eggs="scrambled" 56 ' + "four=1|add:3 %}{% endsimple_unlimited_args_kwargs_block %}", + ), + ( + "'simple_unlimited_args_kwargs_block' received multiple values for " + "keyword argument 'eggs'", + "{% load custom %}" + "{% simple_unlimited_args_kwargs_block 37 " + 'eggs="scrambled" eggs="scrambled" %}' + "{% endsimple_unlimited_args_kwargs_block %}", + ), + ( + "Unclosed tag on line 1: 'div'. Looking for one of: enddiv.", + "{% load custom %}{% div %}Some content", + ), + ( + "Unclosed tag on line 1: 'simple_one_default_block'. Looking for one " + "of: endsimple_one_default_block.", + "{% load custom %}{% simple_one_default_block %}Some content", + ), + ( + "'simple_tag_without_content_parameter' must have a first argument " + "of 'content'", + "{% load custom %}{% simple_tag_without_content_parameter %}", + ), + ( + "'simple_tag_with_context_without_content_parameter' is decorated with " + "takes_context=True so it must have a first argument of 'context' and " + "a second argument of 'content'", + "{% load custom %}" + "{% simple_tag_with_context_without_content_parameter %}", + ), + ] + + for entry in errors: + with self.subTest(entry[1]): + with self.assertRaisesMessage(TemplateSyntaxError, entry[0]): + self.engine.from_string(entry[1]) + + def test_simple_block_tag_escaping_autoescape_off(self): + c = Context({"name": "Jack & Jill"}, autoescape=False) + t = self.engine.from_string( + "{% load custom %}{% escape_naive_block %}{{ name }} again" + "{% endescape_naive_block %}" + ) + self.assertEqual(t.render(c), "Hello Jack & Jill: Jack & Jill again!") + + def test_simple_block_tag_naive_escaping(self): + c = Context({"name": "Jack & Jill"}) + t = self.engine.from_string( + "{% load custom %}{% escape_naive_block %}{{ name }} again" + "{% endescape_naive_block %}" + ) + self.assertEqual( + t.render(c), "Hello Jack & Jill: Jack & Jill again!" + ) + + def test_simple_block_tag_explicit_escaping(self): + # Check we don't double escape + c = Context({"name": "Jack & Jill"}) + t = self.engine.from_string( + "{% load custom %}{% escape_explicit_block %}again" + "{% endescape_explicit_block %}" + ) + self.assertEqual(t.render(c), "Hello Jack & Jill: again!") + + def test_simple_block_tag_format_html_escaping(self): + # Check we don't double escape + c = Context({"name": "Jack & Jill"}) + t = self.engine.from_string( + "{% load custom %}{% escape_format_html_block %}again" + "{% endescape_format_html_block %}" + ) + self.assertEqual(t.render(c), "Hello Jack & Jill: again!") + + def test_simple_block_tag_missing_context(self): + # The 'context' parameter must be present when takes_context is True + msg = ( + "'simple_block_tag_without_context_parameter' is decorated with " + "takes_context=True so it must have a first argument of 'context'" + ) + with self.assertRaisesMessage(TemplateSyntaxError, msg): + self.engine.from_string( + "{% load custom %}{% simple_block_tag_without_context_parameter 123 %}" + "{% endsimple_block_tag_without_context_parameter %}" + ) + + def test_simple_block_tag_missing_context_no_params(self): + msg = ( + "'simple_tag_takes_context_without_params_block' is decorated with " + "takes_context=True so it must have a first argument of 'context'" + ) + with self.assertRaisesMessage(TemplateSyntaxError, msg): + self.engine.from_string( + "{% load custom %}{% simple_tag_takes_context_without_params_block %}" + "{% endsimple_tag_takes_context_without_params_block %}" + ) + + def test_simple_block_tag_missing_content(self): + # The 'content' parameter must be present when takes_context is True + msg = ( + "'simple_block_tag_without_content' must have a first argument of 'content'" + ) + with self.assertRaisesMessage(TemplateSyntaxError, msg): + self.engine.from_string( + "{% load custom %}{% simple_block_tag_without_content %}" + "{% endsimple_block_tag_without_content %}" + ) + + def test_simple_block_tag_with_context_missing_content(self): + # The 'content' parameter must be present when takes_context is True + msg = "'simple_block_tag_with_context_without_content' is decorated with " + "takes_context=True so it must have a first argument of 'context' and a " + "second argument of 'content'" + with self.assertRaisesMessage(TemplateSyntaxError, msg): + self.engine.from_string( + "{% load custom %}{% simple_block_tag_with_context_without_content %}" + "{% endsimple_block_tag_with_context_without_content %}" + ) + + def test_simple_block_gets_context(self): + c = Context({"name": "Jack & Jill"}) + t = self.engine.from_string("{% load custom %}{% div %}{{ name }}{% enddiv %}") + self.assertEqual(t.render(c), "
Jack & Jill
") + + def test_simple_block_capture_as(self): + c = Context({"name": "Jack & Jill"}) + t = self.engine.from_string( + "{% load custom %}{% div as div_content %}{{ name }}{% enddiv %}" + "My div is: {{ div_content }}" + ) + self.assertEqual(t.render(c), "My div is:
Jack & Jill
") + + def test_simple_block_nested(self): + c = Context({"name": "Jack & Jill"}) + t = self.engine.from_string( + "{% load custom %}Start{% div id='outer' %}Before{% div id='inner' %}" + "{{ name }}{% enddiv %}After{% enddiv %}End" + ) + self.assertEqual( + t.render(c), + "Start
Before
Jack & Jill
After" + "
End", + ) + + def test_different_simple_block_nested(self): + c = Context({"name": "Jack & Jill"}) + t = self.engine.from_string( + "{% load custom %}Start{% div id='outer' %}Before" + "{% simple_keyword_only_default_block %}Inner" + "{% endsimple_keyword_only_default_block %}" + "After{% enddiv %}End" + ) + self.assertEqual( + t.render(c), + "Start
Before" + "simple_keyword_only_default_block - Expected result (content value: " + "Inner): 42After
End", + ) + + def test_custom_end_tag(self): + c = Context({"name": "Jack & Jill"}) + t = self.engine.from_string( + "{% load custom %}{% div_custom_end %}{{ name }}{% divend %}" + ) + self.assertEqual(t.render(c), "
Jack & Jill
") + + with self.assertRaisesMessage( + TemplateSyntaxError, + "'enddiv_custom_end', expected 'divend'. Did you forget to register or " + "load this tag?", + ): + self.engine.from_string( + "{% load custom %}{% div_custom_end %}{{ name }}{% enddiv_custom_end %}" + ) + + class InclusionTagTests(TagTestCase): def test_inclusion_tags(self): c = Context({"value": 42}) diff --git a/tests/template_tests/test_library.py b/tests/template_tests/test_library.py index 7376832879..98e9d228aa 100644 --- a/tests/template_tests/test_library.py +++ b/tests/template_tests/test_library.py @@ -120,6 +120,47 @@ class SimpleTagRegistrationTests(SimpleTestCase): self.assertTrue(hasattr(func_wrapped, "cache_info")) +class SimpleBlockTagRegistrationTests(SimpleTestCase): + def setUp(self): + self.library = Library() + + def test_simple_block_tag(self): + @self.library.simple_block_tag + def func(content): + return content + + self.assertIn("func", self.library.tags) + + def test_simple_block_tag_parens(self): + @self.library.simple_tag() + def func(content): + return content + + self.assertIn("func", self.library.tags) + + def test_simple_block_tag_name_kwarg(self): + @self.library.simple_block_tag(name="name") + def func(content): + return content + + self.assertIn("name", self.library.tags) + + def test_simple_block_tag_invalid(self): + msg = "Invalid arguments provided to simple_block_tag" + with self.assertRaisesMessage(ValueError, msg): + self.library.simple_block_tag("invalid") + + def test_simple_tag_wrapped(self): + @self.library.simple_block_tag + @functools.lru_cache(maxsize=32) + def func(content): + return content + + func_wrapped = self.library.tags["func"].__wrapped__ + self.assertIs(func_wrapped, func) + self.assertTrue(hasattr(func_wrapped, "cache_info")) + + class TagRegistrationTests(SimpleTestCase): def setUp(self): self.library = Library()