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),
+ "StartBefore
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),
+ "StartBefore"
+ "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()