Fixed #17906 - Autoescaping {% cycle %} and {% firstof %} templatetags.
This commit adds "future" version of these two tags with auto-escaping enabled.
This commit is contained in:
parent
a61dbd6219
commit
f49e9a517f
|
@ -5,13 +5,15 @@ import sys
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from itertools import groupby, cycle as itertools_cycle
|
from itertools import groupby, cycle as itertools_cycle
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.base import (Node, NodeList, Template, Context, Library,
|
from django.template.base import (Node, NodeList, Template, Context, Library,
|
||||||
TemplateSyntaxError, VariableDoesNotExist, InvalidTemplateLibrary,
|
TemplateSyntaxError, VariableDoesNotExist, InvalidTemplateLibrary,
|
||||||
BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END,
|
BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END,
|
||||||
SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END,
|
SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END,
|
||||||
VARIABLE_ATTRIBUTE_SEPARATOR, get_library, token_kwargs, kwarg_re)
|
VARIABLE_ATTRIBUTE_SEPARATOR, get_library, token_kwargs, kwarg_re,
|
||||||
|
_render_value_in_context)
|
||||||
from django.template.smartif import IfParser, Literal
|
from django.template.smartif import IfParser, Literal
|
||||||
from django.template.defaultfilters import date
|
from django.template.defaultfilters import date
|
||||||
from django.utils.encoding import smart_text
|
from django.utils.encoding import smart_text
|
||||||
|
@ -54,15 +56,15 @@ class CsrfTokenNode(Node):
|
||||||
# misconfiguration, so we raise a warning
|
# misconfiguration, so we raise a warning
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
import warnings
|
|
||||||
warnings.warn("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.")
|
warnings.warn("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.")
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
class CycleNode(Node):
|
class CycleNode(Node):
|
||||||
def __init__(self, cyclevars, variable_name=None, silent=False):
|
def __init__(self, cyclevars, variable_name=None, silent=False, escape=False):
|
||||||
self.cyclevars = cyclevars
|
self.cyclevars = cyclevars
|
||||||
self.variable_name = variable_name
|
self.variable_name = variable_name
|
||||||
self.silent = silent
|
self.silent = silent
|
||||||
|
self.escape = escape # only while the "future" version exists
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
if self not in context.render_context:
|
if self not in context.render_context:
|
||||||
|
@ -74,7 +76,9 @@ class CycleNode(Node):
|
||||||
context[self.variable_name] = value
|
context[self.variable_name] = value
|
||||||
if self.silent:
|
if self.silent:
|
||||||
return ''
|
return ''
|
||||||
return value
|
if not self.escape:
|
||||||
|
value = mark_safe(value)
|
||||||
|
return _render_value_in_context(value, context)
|
||||||
|
|
||||||
class DebugNode(Node):
|
class DebugNode(Node):
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
|
@ -97,14 +101,17 @@ class FilterNode(Node):
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
class FirstOfNode(Node):
|
class FirstOfNode(Node):
|
||||||
def __init__(self, vars):
|
def __init__(self, variables, escape=False):
|
||||||
self.vars = vars
|
self.vars = variables
|
||||||
|
self.escape = escape # only while the "future" version exists
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
for var in self.vars:
|
for var in self.vars:
|
||||||
value = var.resolve(context, True)
|
value = var.resolve(context, True)
|
||||||
if value:
|
if value:
|
||||||
return smart_text(value)
|
if not self.escape:
|
||||||
|
value = mark_safe(value)
|
||||||
|
return _render_value_in_context(value, context)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
class ForNode(Node):
|
class ForNode(Node):
|
||||||
|
@ -508,7 +515,7 @@ def comment(parser, token):
|
||||||
return CommentNode()
|
return CommentNode()
|
||||||
|
|
||||||
@register.tag
|
@register.tag
|
||||||
def cycle(parser, token):
|
def cycle(parser, token, escape=False):
|
||||||
"""
|
"""
|
||||||
Cycles among the given strings each time this tag is encountered.
|
Cycles among the given strings each time this tag is encountered.
|
||||||
|
|
||||||
|
@ -541,6 +548,11 @@ def cycle(parser, token):
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not escape:
|
||||||
|
warnings.warn(
|
||||||
|
"'The syntax for the `cycle` template tag is changing. Load it "
|
||||||
|
"from the `future` tag library to start using the new behavior.",
|
||||||
|
PendingDeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
# Note: This returns the exact same node on each {% cycle name %} call;
|
# Note: This returns the exact same node on each {% cycle name %} call;
|
||||||
# that is, the node object returned from {% cycle a b c as name %} and the
|
# that is, the node object returned from {% cycle a b c as name %} and the
|
||||||
|
@ -588,13 +600,13 @@ def cycle(parser, token):
|
||||||
if as_form:
|
if as_form:
|
||||||
name = args[-1]
|
name = args[-1]
|
||||||
values = [parser.compile_filter(arg) for arg in args[1:-2]]
|
values = [parser.compile_filter(arg) for arg in args[1:-2]]
|
||||||
node = CycleNode(values, name, silent=silent)
|
node = CycleNode(values, name, silent=silent, escape=escape)
|
||||||
if not hasattr(parser, '_namedCycleNodes'):
|
if not hasattr(parser, '_namedCycleNodes'):
|
||||||
parser._namedCycleNodes = {}
|
parser._namedCycleNodes = {}
|
||||||
parser._namedCycleNodes[name] = node
|
parser._namedCycleNodes[name] = node
|
||||||
else:
|
else:
|
||||||
values = [parser.compile_filter(arg) for arg in args[1:]]
|
values = [parser.compile_filter(arg) for arg in args[1:]]
|
||||||
node = CycleNode(values)
|
node = CycleNode(values, escape=escape)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
@register.tag
|
@register.tag
|
||||||
|
@ -643,7 +655,7 @@ def do_filter(parser, token):
|
||||||
return FilterNode(filter_expr, nodelist)
|
return FilterNode(filter_expr, nodelist)
|
||||||
|
|
||||||
@register.tag
|
@register.tag
|
||||||
def firstof(parser, token):
|
def firstof(parser, token, escape=False):
|
||||||
"""
|
"""
|
||||||
Outputs the first variable passed that is not False, without escaping.
|
Outputs the first variable passed that is not False, without escaping.
|
||||||
|
|
||||||
|
@ -657,11 +669,11 @@ def firstof(parser, token):
|
||||||
|
|
||||||
{% if var1 %}
|
{% if var1 %}
|
||||||
{{ var1|safe }}
|
{{ var1|safe }}
|
||||||
{% else %}{% if var2 %}
|
{% elif var2 %}
|
||||||
{{ var2|safe }}
|
{{ var2|safe }}
|
||||||
{% else %}{% if var3 %}
|
{% elif var3 %}
|
||||||
{{ var3|safe }}
|
{{ var3|safe }}
|
||||||
{% endif %}{% endif %}{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
but obviously much cleaner!
|
but obviously much cleaner!
|
||||||
|
|
||||||
|
@ -677,10 +689,16 @@ def firstof(parser, token):
|
||||||
{% endfilter %}
|
{% endfilter %}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not escape:
|
||||||
|
warnings.warn(
|
||||||
|
"'The syntax for the `firstof` template tag is changing. Load it "
|
||||||
|
"from the `future` tag library to start using the new behavior.",
|
||||||
|
PendingDeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
bits = token.split_contents()[1:]
|
bits = token.split_contents()[1:]
|
||||||
if len(bits) < 1:
|
if len(bits) < 1:
|
||||||
raise TemplateSyntaxError("'firstof' statement requires at least one argument")
|
raise TemplateSyntaxError("'firstof' statement requires at least one argument")
|
||||||
return FirstOfNode([parser.compile_filter(bit) for bit in bits])
|
return FirstOfNode([parser.compile_filter(bit) for bit in bits], escape=escape)
|
||||||
|
|
||||||
@register.tag('for')
|
@register.tag('for')
|
||||||
def do_for(parser, token):
|
def do_for(parser, token):
|
||||||
|
|
|
@ -1,14 +1,65 @@
|
||||||
from django.template import Library
|
from django.template import Library
|
||||||
from django.template.defaulttags import url as default_url, ssi as default_ssi
|
from django.template import defaulttags
|
||||||
|
|
||||||
register = Library()
|
register = Library()
|
||||||
|
|
||||||
|
|
||||||
@register.tag
|
@register.tag
|
||||||
def ssi(parser, token):
|
def ssi(parser, token):
|
||||||
# Used for deprecation path during 1.3/1.4, will be removed in 2.0
|
# Used for deprecation path during 1.3/1.4, will be removed in 2.0
|
||||||
return default_ssi(parser, token)
|
return defaulttags.ssi(parser, token)
|
||||||
|
|
||||||
|
|
||||||
@register.tag
|
@register.tag
|
||||||
def url(parser, token):
|
def url(parser, token):
|
||||||
# Used for deprecation path during 1.3/1.4, will be removed in 2.0
|
# Used for deprecation path during 1.3/1.4, will be removed in 2.0
|
||||||
return default_url(parser, token)
|
return defaulttags.url(parser, token)
|
||||||
|
|
||||||
|
|
||||||
|
@register.tag
|
||||||
|
def cycle(parser, token):
|
||||||
|
"""
|
||||||
|
This is the future version of `cycle` with auto-escaping.
|
||||||
|
|
||||||
|
By default all strings are escaped.
|
||||||
|
|
||||||
|
If you want to disable auto-escaping of variables you can use:
|
||||||
|
|
||||||
|
{% autoescape off %}
|
||||||
|
{% cycle var1 var2 var3 as somecycle %}
|
||||||
|
{% autoescape %}
|
||||||
|
|
||||||
|
Or if only some variables should be escaped, you can use:
|
||||||
|
|
||||||
|
{% cycle var1 var2|safe var3|safe as somecycle %}
|
||||||
|
"""
|
||||||
|
return defaulttags.cycle(parser, token, escape=True)
|
||||||
|
|
||||||
|
|
||||||
|
@register.tag
|
||||||
|
def firstof(parser, token):
|
||||||
|
"""
|
||||||
|
This is the future version of `firstof` with auto-escaping.
|
||||||
|
|
||||||
|
This is equivalent to:
|
||||||
|
|
||||||
|
{% if var1 %}
|
||||||
|
{{ var1 }}
|
||||||
|
{% elif var2 %}
|
||||||
|
{{ var2 }}
|
||||||
|
{% elif var3 %}
|
||||||
|
{{ var3 }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
If you want to disable auto-escaping of variables you can use:
|
||||||
|
|
||||||
|
{% autoescape off %}
|
||||||
|
{% firstof var1 var2 var3 "<strong>fallback value</strong>" %}
|
||||||
|
{% autoescape %}
|
||||||
|
|
||||||
|
Or if only some variables should be escaped, you can use:
|
||||||
|
|
||||||
|
{% firstof var1 var2|safe var3 "<strong>fallback value</strong>"|safe %}
|
||||||
|
|
||||||
|
"""
|
||||||
|
return defaulttags.firstof(parser, token, escape=True)
|
||||||
|
|
|
@ -323,6 +323,10 @@ these changes.
|
||||||
1.8
|
1.8
|
||||||
---
|
---
|
||||||
|
|
||||||
|
* The :ttag:`cycle` and :ttag:`firstof` template tags will auto-escape their
|
||||||
|
arguments. In 1.6 and 1.7, this behavior is provided by the version of these
|
||||||
|
tags in the ``future`` template tag library.
|
||||||
|
|
||||||
* The ``SEND_BROKEN_LINK_EMAILS`` setting will be removed. Add the
|
* The ``SEND_BROKEN_LINK_EMAILS`` setting will be removed. Add the
|
||||||
:class:`django.middleware.common.BrokenLinkEmailsMiddleware` middleware to
|
:class:`django.middleware.common.BrokenLinkEmailsMiddleware` middleware to
|
||||||
your :setting:`MIDDLEWARE_CLASSES` setting instead.
|
your :setting:`MIDDLEWARE_CLASSES` setting instead.
|
||||||
|
|
|
@ -147,9 +147,8 @@ You can use any number of values in a ``{% cycle %}`` tag, separated by spaces.
|
||||||
Values enclosed in single (``'``) or double quotes (``"``) are treated as
|
Values enclosed in single (``'``) or double quotes (``"``) are treated as
|
||||||
string literals, while values without quotes are treated as template variables.
|
string literals, while values without quotes are treated as template variables.
|
||||||
|
|
||||||
Note that the variables included in the cycle will not be escaped.
|
Note that currently the variables included in the cycle will not be escaped.
|
||||||
This is because template tags do not escape their content. Any HTML or
|
Any HTML or Javascript code contained in the printed variable will be rendered
|
||||||
Javascript code contained in the printed variable will be rendered
|
|
||||||
as-is, which could potentially lead to security issues.
|
as-is, which could potentially lead to security issues.
|
||||||
|
|
||||||
For backwards compatibility, the ``{% cycle %}`` tag supports the much inferior
|
For backwards compatibility, the ``{% cycle %}`` tag supports the much inferior
|
||||||
|
@ -190,6 +189,22 @@ call to ``{% cycle %}`` doesn't specify silent::
|
||||||
{% cycle 'row1' 'row2' as rowcolors silent %}
|
{% cycle 'row1' 'row2' as rowcolors silent %}
|
||||||
{% cycle rowcolors %}
|
{% cycle rowcolors %}
|
||||||
|
|
||||||
|
.. versionchanged:: 1.6
|
||||||
|
|
||||||
|
To improve safety, future versions of ``cycle`` will automatically escape
|
||||||
|
their output. You're encouraged to activate this behavior by loading
|
||||||
|
``cycle`` from the ``future`` template library::
|
||||||
|
|
||||||
|
{% load cycle from future %}
|
||||||
|
|
||||||
|
When using the ``future`` version, you can disable auto-escaping with::
|
||||||
|
|
||||||
|
{% for o in some_list %}
|
||||||
|
<tr class="{% autoescape off %}{% cycle rowvalue1 rowvalue2 %}{% endautoescape %}">
|
||||||
|
...
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
.. templatetag:: debug
|
.. templatetag:: debug
|
||||||
|
|
||||||
debug
|
debug
|
||||||
|
@ -257,28 +272,44 @@ This is equivalent to::
|
||||||
|
|
||||||
{% if var1 %}
|
{% if var1 %}
|
||||||
{{ var1|safe }}
|
{{ var1|safe }}
|
||||||
{% else %}{% if var2 %}
|
{% elif var2 %}
|
||||||
{{ var2|safe }}
|
{{ var2|safe }}
|
||||||
{% else %}{% if var3 %}
|
{% elif var3 %}
|
||||||
{{ var3|safe }}
|
{{ var3|safe }}
|
||||||
{% endif %}{% endif %}{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
You can also use a literal string as a fallback value in case all
|
You can also use a literal string as a fallback value in case all
|
||||||
passed variables are False::
|
passed variables are False::
|
||||||
|
|
||||||
{% firstof var1 var2 var3 "fallback value" %}
|
{% firstof var1 var2 var3 "fallback value" %}
|
||||||
|
|
||||||
Note that the variables included in the firstof tag will not be
|
Note that currently the variables included in the firstof tag will not be
|
||||||
escaped. This is because template tags do not escape their content.
|
escaped. Any HTML or Javascript code contained in the printed variable will be
|
||||||
Any HTML or Javascript code contained in the printed variable will be
|
rendered as-is, which could potentially lead to security issues. If you need
|
||||||
rendered as-is, which could potentially lead to security issues. If you
|
to escape the variables in the firstof tag, you must do so explicitly::
|
||||||
need to escape the variables in the firstof tag, you must do so
|
|
||||||
explicitly::
|
|
||||||
|
|
||||||
{% filter force_escape %}
|
{% filter force_escape %}
|
||||||
{% firstof var1 var2 var3 "fallback value" %}
|
{% firstof var1 var2 var3 "fallback value" %}
|
||||||
{% endfilter %}
|
{% endfilter %}
|
||||||
|
|
||||||
|
.. versionchanged:: 1.6
|
||||||
|
|
||||||
|
To improve safety, future versions of ``firstof`` will automatically escape
|
||||||
|
their output. You're encouraged to activate this behavior by loading
|
||||||
|
``firstof`` from the ``future`` template library::
|
||||||
|
|
||||||
|
{% load firstof from future %}
|
||||||
|
|
||||||
|
When using the ``future`` version, you can disable auto-escaping with::
|
||||||
|
|
||||||
|
{% autoescape off %}
|
||||||
|
{% firstof var1 var2 var3 "<strong>fallback value</strong>" %}
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
|
Or if only some variables should be escaped, you can use::
|
||||||
|
|
||||||
|
{% firstof var1 var2|safe var3 "<strong>fallback value</strong>"|safe %}
|
||||||
|
|
||||||
.. templatetag:: for
|
.. templatetag:: for
|
||||||
|
|
||||||
for
|
for
|
||||||
|
|
|
@ -160,6 +160,34 @@ Backwards incompatible changes in 1.6
|
||||||
Features deprecated in 1.6
|
Features deprecated in 1.6
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
|
Changes to :ttag:`cycle` and :ttag:`firstof`
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The template system generally escapes all variables to avoid XSS attacks.
|
||||||
|
However, due to an accident of history, the :ttag:`cycle` and :ttag:`firstof`
|
||||||
|
tags render their arguments as-is.
|
||||||
|
|
||||||
|
Django 1.6 starts a process to correct this inconsistency. The ``future``
|
||||||
|
template library provides alternate implementations of :ttag:`cycle` and
|
||||||
|
:ttag:`firstof` that autoescape their inputs. If you're using these tags,
|
||||||
|
you're encourage to include the following line at the top of your templates to
|
||||||
|
enable the new behavior::
|
||||||
|
|
||||||
|
{% load cycle from future %}
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
{% load firstof from future %}
|
||||||
|
|
||||||
|
The tags implementing the old behavior have been deprecated, and in Django
|
||||||
|
1.8, the old behavior will be replaced with the new behavior. To ensure
|
||||||
|
compatibility with future versions of Django, existing templates should be
|
||||||
|
modified to use the ``future`` versions.
|
||||||
|
|
||||||
|
If necessary, you can temporarily disable auto-escaping with
|
||||||
|
:func:`~django.utils.safestring.mark_safe` or :ttag:`{% autoescape off %}
|
||||||
|
<autoescape>`.
|
||||||
|
|
||||||
``SEND_BROKEN_LINK_EMAILS`` setting
|
``SEND_BROKEN_LINK_EMAILS`` setting
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -773,6 +773,11 @@ class Templates(TestCase):
|
||||||
'cycle23': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{{ abc }}{{ x }}{% endfor %}", {'values': [1,2,3,4]}, "a1b2c3a4"),
|
'cycle23': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{{ abc }}{{ x }}{% endfor %}", {'values': [1,2,3,4]}, "a1b2c3a4"),
|
||||||
'included-cycle': ('{{ abc }}', {'abc': 'xxx'}, 'xxx'),
|
'included-cycle': ('{{ abc }}', {'abc': 'xxx'}, 'xxx'),
|
||||||
'cycle24': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{% include 'included-cycle' %}{% endfor %}", {'values': [1,2,3,4]}, "abca"),
|
'cycle24': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{% include 'included-cycle' %}{% endfor %}", {'values': [1,2,3,4]}, "abca"),
|
||||||
|
'cycle25': ('{% cycle a as abc %}', {'a': '<'}, '<'),
|
||||||
|
|
||||||
|
'cycle26': ('{% load cycle from future %}{% cycle a b as ab %}{% cycle ab %}', {'a': '<', 'b': '>'}, '<>'),
|
||||||
|
'cycle27': ('{% load cycle from future %}{% autoescape off %}{% cycle a b as ab %}{% cycle ab %}{% endautoescape %}', {'a': '<', 'b': '>'}, '<>'),
|
||||||
|
'cycle28': ('{% load cycle from future %}{% cycle a|safe b as ab %}{% cycle ab %}', {'a': '<', 'b': '>'}, '<>'),
|
||||||
|
|
||||||
### EXCEPTIONS ############################################################
|
### EXCEPTIONS ############################################################
|
||||||
|
|
||||||
|
@ -804,7 +809,12 @@ class Templates(TestCase):
|
||||||
'firstof07': ('{% firstof a b "c" %}', {'a':0}, 'c'),
|
'firstof07': ('{% firstof a b "c" %}', {'a':0}, 'c'),
|
||||||
'firstof08': ('{% firstof a b "c and d" %}', {'a':0,'b':0}, 'c and d'),
|
'firstof08': ('{% firstof a b "c and d" %}', {'a':0,'b':0}, 'c and d'),
|
||||||
'firstof09': ('{% firstof %}', {}, template.TemplateSyntaxError),
|
'firstof09': ('{% firstof %}', {}, template.TemplateSyntaxError),
|
||||||
'firstof10': ('{% firstof a %}', {'a': '<'}, '<'), # Variables are NOT auto-escaped.
|
'firstof10': ('{% firstof a %}', {'a': '<'}, '<'),
|
||||||
|
|
||||||
|
'firstof11': ('{% load firstof from future %}{% firstof a b %}', {'a': '<', 'b': '>'}, '<'),
|
||||||
|
'firstof12': ('{% load firstof from future %}{% firstof a b %}', {'a': '', 'b': '>'}, '>'),
|
||||||
|
'firstof13': ('{% load firstof from future %}{% autoescape off %}{% firstof a %}{% endautoescape %}', {'a': '<'}, '<'),
|
||||||
|
'firstof14': ('{% load firstof from future %}{% firstof a|safe b %}', {'a': '<'}, '<'),
|
||||||
|
|
||||||
### FOR TAG ###############################################################
|
### FOR TAG ###############################################################
|
||||||
'for-tag01': ("{% for val in values %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "123"),
|
'for-tag01': ("{% for val in values %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "123"),
|
||||||
|
|
Loading…
Reference in New Issue