0
0
mirror of https://github.com/django/django.git synced 2024-11-21 10:59:04 +01:00

Fixed #35535 -- Added template tag decorator simple_block_tag().

Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
This commit is contained in:
Jake Howard 2024-11-19 17:35:02 +00:00 committed by GitHub
parent 9543c605c3
commit 4c452cc377
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 845 additions and 1 deletions

View File

@ -153,6 +153,90 @@ class Library:
else: else:
raise ValueError("Invalid arguments provided to simple_tag") 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): def inclusion_tag(self, filename, func=None, takes_context=None, name=None):
""" """
Register a callable as an inclusion tag: Register a callable as an inclusion tag:
@ -243,6 +327,23 @@ class SimpleNode(TagHelperNode):
return output 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): class InclusionNode(TagHelperNode):
def __init__(self, func, takes_context, args, kwargs, filename): def __init__(self, func, takes_context, args, kwargs, filename):
super().__init__(func, takes_context, args, kwargs) super().__init__(func, takes_context, args, kwargs)

View File

@ -498,6 +498,195 @@ you see fit:
{% current_time "%Y-%m-%d %I:%M %p" as the_time %} {% current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p> <p>The time is {{ the_time }}.</p>
.. _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<howto-custom-template-tags-simple-tags>`,
``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 = """
<div class="msgbox {level}">
<details{open}>
<summary>
<strong>{level_title}</strong>: Please read for <i>{site}</i>
</summary>
<p>
{content}
</p>
</details>
</div>
"""
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
<a href="http://example.com">Docs</a>.
{% endmsgbox %}
{% msgbox level="info" %}
More information at: <a href="http://othersite.com">Other Site</a>/
{% endmsgbox %}
{% endblock %}
The following HTML is produced as the rendered output:
.. code-block:: html
<div class="msgbox error">
<details open>
<summary>
<strong>Error</strong>: Please read for <i>Important Site</i>
</summary>
<p>
Please fix all errors. Further documentation can be found at
<a href="http://example.com">Docs</a>.
</p>
</details>
</div>
<div class="msgbox info">
<details>
<summary>
<strong>Info</strong>: Please read for <i>Important Site</i>
</summary>
<p>
More information at: <a href="http://othersite.com">Other Site</a>
</p>
</details>
</div>
.. _howto-custom-template-tags-inclusion-tags: .. _howto-custom-template-tags-inclusion-tags:
Inclusion tags Inclusion tags

View File

@ -323,7 +323,9 @@ Signals
Templates 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 Tests
~~~~~ ~~~~~

View File

@ -20,6 +20,16 @@ def make_data_div(value):
return '<div data-name="%s"></div>' % value return '<div data-name="%s"></div>' % value
@register.simple_block_tag
def div(content, id="test"):
return format_html("<div id='{}'>{}</div>", id, content)
@register.simple_block_tag(end_name="divend")
def div_custom_end(content):
return format_html("<div>{}</div>", content)
@register.filter @register.filter
def noop(value, param=None): def noop(value, param=None):
"""A noop filter that always return its first argument and does nothing with """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__" 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) @register.simple_tag(takes_context=False)
def explicit_no_context(arg): def explicit_no_context(arg):
"""Expected explicit_no_context __doc__""" """Expected explicit_no_context __doc__"""
@ -60,6 +76,12 @@ def explicit_no_context(arg):
explicit_no_context.anything = "Expected explicit_no_context __dict__" 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) @register.simple_tag(takes_context=True)
def no_params_with_context(context): def no_params_with_context(context):
"""Expected no_params_with_context __doc__""" """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__" 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) @register.simple_tag(takes_context=True)
def params_and_context(context, arg): def params_and_context(context, arg):
"""Expected params_and_context __doc__""" """Expected params_and_context __doc__"""
@ -84,6 +115,20 @@ def params_and_context(context, arg):
params_and_context.anything = "Expected params_and_context __dict__" 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 @register.simple_tag
def simple_two_params(one, two): def simple_two_params(one, two):
"""Expected simple_two_params __doc__""" """Expected simple_two_params __doc__"""
@ -93,16 +138,48 @@ def simple_two_params(one, two):
simple_two_params.anything = "Expected simple_two_params __dict__" 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 @register.simple_tag
def simple_keyword_only_param(*, kwarg): def simple_keyword_only_param(*, kwarg):
return "simple_keyword_only_param - Expected result: %s" % 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 @register.simple_tag
def simple_keyword_only_default(*, kwarg=42): def simple_keyword_only_default(*, kwarg=42):
return "simple_keyword_only_default - Expected result: %s" % kwarg 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 @register.simple_tag
def simple_one_default(one, two="hi"): def simple_one_default(one, two="hi"):
"""Expected simple_one_default __doc__""" """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__" 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 @register.simple_tag
def simple_unlimited_args(one, two="hi", *args): def simple_unlimited_args(one, two="hi", *args):
"""Expected simple_unlimited_args __doc__""" """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__" 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 @register.simple_tag
def simple_only_unlimited_args(*args): def simple_only_unlimited_args(*args):
"""Expected simple_only_unlimited_args __doc__""" """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__" 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 @register.simple_tag
def simple_unlimited_args_kwargs(one, two="hi", *args, **kwargs): def simple_unlimited_args_kwargs(one, two="hi", *args, **kwargs):
"""Expected simple_unlimited_args_kwargs __doc__""" """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__" 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) @register.simple_tag(takes_context=True)
def simple_tag_without_context_parameter(arg): def simple_tag_without_context_parameter(arg):
"""Expected simple_tag_without_context_parameter __doc__""" """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) @register.simple_tag(takes_context=True)
def simple_tag_takes_context_without_params(): def simple_tag_takes_context_without_params():
"""Expected simple_tag_takes_context_without_params __doc__""" """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) @register.simple_tag(takes_context=True)
def escape_naive(context): def escape_naive(context):
"""A tag that doesn't even think about escaping issues""" """A tag that doesn't even think about escaping issues"""
return "Hello {}!".format(context["name"]) 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) @register.simple_tag(takes_context=True)
def escape_explicit(context): def escape_explicit(context):
"""A tag that uses escape explicitly""" """A tag that uses escape explicitly"""
return escape("Hello {}!".format(context["name"])) 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) @register.simple_tag(takes_context=True)
def escape_format_html(context): def escape_format_html(context):
"""A tag that uses format_html""" """A tag that uses format_html"""
return format_html("Hello {0}!", context["name"]) 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) @register.simple_tag(takes_context=True)
def current_app(context): def current_app(context):
return str(context.current_app) return str(context.current_app)

View File

@ -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 %}",
"<div id='test'>content</div>",
),
(
"{% 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 &amp; Jill: Jack &amp;amp; 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 &amp; 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 &amp; 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), "<div id='test'>Jack &amp; Jill</div>")
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: <div id='test'>Jack &amp; Jill</div>")
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<div id='outer'>Before<div id='inner'>Jack &amp; Jill</div>After"
"</div>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<div id='outer'>Before"
"simple_keyword_only_default_block - Expected result (content value: "
"Inner): 42After</div>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), "<div>Jack &amp; Jill</div>")
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): class InclusionTagTests(TagTestCase):
def test_inclusion_tags(self): def test_inclusion_tags(self):
c = Context({"value": 42}) c = Context({"value": 42})

View File

@ -120,6 +120,47 @@ class SimpleTagRegistrationTests(SimpleTestCase):
self.assertTrue(hasattr(func_wrapped, "cache_info")) 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): class TagRegistrationTests(SimpleTestCase):
def setUp(self): def setUp(self):
self.library = Library() self.library = Library()