"""Default tags used by the template system, available to all templates.""" import re import sys import warnings from collections import namedtuple from collections.abc import Iterable, Mapping from datetime import datetime from itertools import cycle as itertools_cycle from itertools import groupby from django.conf import settings from django.http import QueryDict from django.utils import timezone from django.utils.datastructures import DeferredSubDict from django.utils.html import conditional_escape, escape, format_html from django.utils.lorem_ipsum import paragraphs, words from django.utils.safestring import mark_safe from .base import ( BLOCK_TAG_END, BLOCK_TAG_START, COMMENT_TAG_END, COMMENT_TAG_START, FILTER_SEPARATOR, SINGLE_BRACE_END, SINGLE_BRACE_START, VARIABLE_ATTRIBUTE_SEPARATOR, VARIABLE_TAG_END, VARIABLE_TAG_START, Node, NodeList, PartialTemplate, TemplateSyntaxError, VariableDoesNotExist, kwarg_re, render_value_in_context, token_kwargs, ) from .context import Context from .defaultfilters import date from .library import Library from .smartif import IfParser, Literal register = Library() class AutoEscapeControlNode(Node): """Implement the actions of the autoescape tag.""" def __init__(self, setting, nodelist): self.setting = setting self.nodelist = nodelist def render(self, context): old_setting = context.autoescape context.autoescape = self.setting output = self.nodelist.render(context) context.autoescape = old_setting if self.setting: return mark_safe(output) else: return output class CommentNode(Node): child_nodelists = () def render(self, context): return "" class CsrfTokenNode(Node): child_nodelists = () def render(self, context): csrf_token = context.get("csrf_token") if csrf_token: if csrf_token == "NOTPROVIDED": return format_html("") else: return format_html( '', csrf_token, ) else: # It's very probable that the token is missing because of # misconfiguration, so we raise a warning if settings.DEBUG: 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 "" class CycleNode(Node): def __init__(self, cyclevars, variable_name=None, silent=False): self.cyclevars = cyclevars self.variable_name = variable_name self.silent = silent def render(self, context): if self not in context.render_context: # First time the node is rendered in template context.render_context[self] = itertools_cycle(self.cyclevars) cycle_iter = context.render_context[self] value = next(cycle_iter).resolve(context) if self.variable_name: context.set_upward(self.variable_name, value) if self.silent: return "" return render_value_in_context(value, context) def reset(self, context): """ Reset the cycle iteration back to the beginning. """ context.render_context[self] = itertools_cycle(self.cyclevars) class DebugNode(Node): def render(self, context): if not settings.DEBUG: return "" from pprint import pformat output = [escape(pformat(val)) for val in context] output.append("\n\n") output.append(escape(pformat(sys.modules))) return "".join(output) class FilterNode(Node): def __init__(self, filter_expr, nodelist): self.filter_expr = filter_expr self.nodelist = nodelist def render(self, context): output = self.nodelist.render(context) # Apply filters. with context.push(var=output): return self.filter_expr.resolve(context) class FirstOfNode(Node): def __init__(self, variables, asvar=None): self.vars = variables self.asvar = asvar def render(self, context): first = "" for var in self.vars: value = var.resolve(context, ignore_failures=True) if value: first = render_value_in_context(value, context) break if self.asvar: context[self.asvar] = first return "" return first class ForNode(Node): child_nodelists = ("nodelist_loop", "nodelist_empty") def __init__( self, loopvars, sequence, is_reversed, nodelist_loop, nodelist_empty=None ): self.loopvars = loopvars self.sequence = sequence self.is_reversed = is_reversed self.nodelist_loop = nodelist_loop if nodelist_empty is None: self.nodelist_empty = NodeList() else: self.nodelist_empty = nodelist_empty def __repr__(self): reversed_text = " reversed" if self.is_reversed else "" return "<%s: for %s in %s, tail_len: %d%s>" % ( self.__class__.__name__, ", ".join(self.loopvars), self.sequence, len(self.nodelist_loop), reversed_text, ) def render(self, context): if "forloop" in context: parentloop = context["forloop"] else: parentloop = {} with context.push(): values = self.sequence.resolve(context, ignore_failures=True) if values is None: values = [] if not hasattr(values, "__len__"): values = list(values) len_values = len(values) if len_values < 1: return self.nodelist_empty.render(context) nodelist = [] if self.is_reversed: values = reversed(values) num_loopvars = len(self.loopvars) unpack = num_loopvars > 1 # Create a forloop value in the context. We'll update counters on # each iteration just below. loop_dict = context["forloop"] = { "parentloop": parentloop, "length": len_values, } for i, item in enumerate(values): # Shortcuts for current loop iteration number. loop_dict["counter0"] = i loop_dict["counter"] = i + 1 # Reverse counter iteration numbers. loop_dict["revcounter"] = len_values - i loop_dict["revcounter0"] = len_values - i - 1 # Boolean values designating first and last times through loop. loop_dict["first"] = i == 0 loop_dict["last"] = i == len_values - 1 pop_context = False if unpack: # If there are multiple loop variables, unpack the item # into them. try: len_item = len(item) except TypeError: # not an iterable len_item = 1 # Check loop variable count before unpacking if num_loopvars != len_item: raise ValueError( "Need {} values to unpack in for loop; got {}. ".format( num_loopvars, len_item ), ) unpacked_vars = dict(zip(self.loopvars, item)) pop_context = True context.update(unpacked_vars) else: context[self.loopvars[0]] = item for node in self.nodelist_loop: nodelist.append(node.render_annotated(context)) if pop_context: # Pop the loop variables pushed on to the context to avoid # the context ending up in an inconsistent state when other # tags (e.g., include and with) push data to context. context.pop() return mark_safe("".join(nodelist)) class IfChangedNode(Node): child_nodelists = ("nodelist_true", "nodelist_false") def __init__(self, nodelist_true, nodelist_false, *varlist): self.nodelist_true = nodelist_true self.nodelist_false = nodelist_false self._varlist = varlist def render(self, context): # Init state storage state_frame = self._get_context_stack_frame(context) state_frame.setdefault(self) nodelist_true_output = None if self._varlist: # Consider multiple parameters. This behaves like an OR evaluation # of the multiple variables. compare_to = [ var.resolve(context, ignore_failures=True) for var in self._varlist ] else: # The "{% ifchanged %}" syntax (without any variables) compares # the rendered output. compare_to = nodelist_true_output = self.nodelist_true.render(context) if compare_to != state_frame[self]: state_frame[self] = compare_to # render true block if not already rendered return nodelist_true_output or self.nodelist_true.render(context) elif self.nodelist_false: return self.nodelist_false.render(context) return "" def _get_context_stack_frame(self, context): # The Context object behaves like a stack where each template tag can # create a new scope. Find the place where to store the state to detect # changes. if "forloop" in context: # Ifchanged is bound to the local for loop. # When there is a loop-in-loop, the state is bound to the inner # loop, so it resets when the outer loop continues. return context["forloop"] else: # Using ifchanged outside loops. Effectively this is a no-op # because the state is associated with 'self'. return context.render_context class IfNode(Node): def __init__(self, conditions_nodelists): self.conditions_nodelists = conditions_nodelists def __repr__(self): return "<%s>" % self.__class__.__name__ def __iter__(self): for _, nodelist in self.conditions_nodelists: yield from nodelist @property def nodelist(self): return NodeList(self) def render(self, context): for condition, nodelist in self.conditions_nodelists: if condition is not None: # if / elif clause try: match = condition.eval(context) except VariableDoesNotExist: match = None else: # else clause match = True if match: return nodelist.render(context) return "" class LoremNode(Node): def __init__(self, count, method, common): self.count = count self.method = method self.common = common def render(self, context): try: count = int(self.count.resolve(context)) except (ValueError, TypeError): count = 1 if self.method == "w": return words(count, common=self.common) else: paras = paragraphs(count, common=self.common) if self.method == "p": paras = ["
%s
" % p for p in paras] return "\n\n".join(paras) GroupedResult = namedtuple("GroupedResult", ["grouper", "list"]) class RegroupNode(Node): def __init__(self, target, expression, var_name): self.target = target self.expression = expression self.var_name = var_name def resolve_expression(self, obj, context): # This method is called for each object in self.target. See regroup() # for the reason why we temporarily put the object in the context. context[self.var_name] = obj return self.expression.resolve(context, ignore_failures=True) def render(self, context): obj_list = self.target.resolve(context, ignore_failures=True) if obj_list is None: # target variable wasn't found in context; fail silently. context[self.var_name] = [] return "" # List of dictionaries in the format: # {'grouper': 'key', 'list': [list of contents]}. context[self.var_name] = [ GroupedResult(grouper=key, list=list(val)) for key, val in groupby( obj_list, lambda obj: self.resolve_expression(obj, context) ) ] return "" class LoadNode(Node): child_nodelists = () def render(self, context): return "" class NowNode(Node): def __init__(self, format_string, asvar=None): self.format_string = format_string self.asvar = asvar def render(self, context): tzinfo = timezone.get_current_timezone() if settings.USE_TZ else None formatted = date(datetime.now(tz=tzinfo), self.format_string) if self.asvar: context[self.asvar] = formatted return "" else: return formatted class PartialDefNode(Node): def __init__(self, partial_name, inline, nodelist): self.partial_name = partial_name self.inline = inline self.nodelist = nodelist def render(self, context): return self.nodelist.render(context) if self.inline else "" class PartialNode(Node): def __init__(self, partial_name, partial_mapping): # Defer lookup in `partial_mapping` and nodelist to runtime. self.partial_name = partial_name self.partial_mapping = partial_mapping def render(self, context): try: return self.partial_mapping[self.partial_name].render(context) except KeyError: raise TemplateSyntaxError( f"Partial '{self.partial_name}' is not defined in the current template." ) class ResetCycleNode(Node): def __init__(self, node): self.node = node def render(self, context): self.node.reset(context) return "" class SpacelessNode(Node): def __init__(self, nodelist): self.nodelist = nodelist def render(self, context): from django.utils.html import strip_spaces_between_tags return strip_spaces_between_tags(self.nodelist.render(context).strip()) class TemplateTagNode(Node): mapping = { "openblock": BLOCK_TAG_START, "closeblock": BLOCK_TAG_END, "openvariable": VARIABLE_TAG_START, "closevariable": VARIABLE_TAG_END, "openbrace": SINGLE_BRACE_START, "closebrace": SINGLE_BRACE_END, "opencomment": COMMENT_TAG_START, "closecomment": COMMENT_TAG_END, } def __init__(self, tagtype): self.tagtype = tagtype def render(self, context): return self.mapping.get(self.tagtype, "") class URLNode(Node): child_nodelists = () def __init__(self, view_name, args, kwargs, asvar): self.view_name = view_name self.args = args self.kwargs = kwargs self.asvar = asvar def __repr__(self): return "<%s view_name='%s' args=%s kwargs=%s as=%s>" % ( self.__class__.__qualname__, self.view_name, repr(self.args), repr(self.kwargs), repr(self.asvar), ) def render(self, context): from django.urls import NoReverseMatch, reverse args = [arg.resolve(context) for arg in self.args] kwargs = {k: v.resolve(context) for k, v in self.kwargs.items()} view_name = self.view_name.resolve(context) try: current_app = context.request.current_app except AttributeError: try: current_app = context.request.resolver_match.namespace except AttributeError: current_app = None # Try to look up the URL. If it fails, raise NoReverseMatch unless the # {% url ... as var %} construct is used, in which case return nothing. url = "" try: url = reverse(view_name, args=args, kwargs=kwargs, current_app=current_app) except NoReverseMatch: if self.asvar is None: raise if self.asvar: context[self.asvar] = url return "" else: if context.autoescape: url = conditional_escape(url) return url class VerbatimNode(Node): def __init__(self, content): self.content = content def render(self, context): return self.content class WidthRatioNode(Node): def __init__(self, val_expr, max_expr, max_width, asvar=None): self.val_expr = val_expr self.max_expr = max_expr self.max_width = max_width self.asvar = asvar def render(self, context): try: value = self.val_expr.resolve(context) max_value = self.max_expr.resolve(context) max_width = int(self.max_width.resolve(context)) except VariableDoesNotExist: return "" except (ValueError, TypeError): raise TemplateSyntaxError("widthratio final argument must be a number") try: value = float(value) max_value = float(max_value) ratio = (value / max_value) * max_width result = str(round(ratio)) except ZeroDivisionError: result = "0" except (ValueError, TypeError, OverflowError): result = "" if self.asvar: context[self.asvar] = result return "" else: return result class WithNode(Node): def __init__(self, var, name, nodelist, extra_context=None): self.nodelist = nodelist # var and name are legacy attributes, being left in case they are used # by third-party subclasses of this Node. self.extra_context = extra_context or {} if name: self.extra_context[name] = var def __repr__(self): return "<%s>" % self.__class__.__name__ def render(self, context): values = {key: val.resolve(context) for key, val in self.extra_context.items()} with context.push(**values): return self.nodelist.render(context) @register.tag def autoescape(parser, token): """ Force autoescape behavior for this block. """ # token.split_contents() isn't useful here because this tag doesn't accept # variable as arguments. args = token.contents.split() if len(args) != 2: raise TemplateSyntaxError("'autoescape' tag requires exactly one argument.") arg = args[1] if arg not in ("on", "off"): raise TemplateSyntaxError("'autoescape' argument should be 'on' or 'off'") nodelist = parser.parse(("endautoescape",)) parser.delete_first_token() return AutoEscapeControlNode((arg == "on"), nodelist) @register.tag def comment(parser, token): """ Ignore everything between ``{% comment %}`` and ``{% endcomment %}``. """ parser.skip_past("endcomment") return CommentNode() @register.tag def cycle(parser, token): """ Cycle among the given strings each time this tag is encountered. Within a loop, cycles among the given strings each time through the loop:: {% for o in some_list %}
{% debug %}
"""
return DebugNode()
@register.tag("filter")
def do_filter(parser, token):
"""
Filter the contents of the block through variable filters.
Filters can also be piped through each other, and they can have
arguments -- just like in variable syntax.
Sample usage::
{% filter force_escape|lower %}
This text will be HTML-escaped, and will appear in lowercase.
{% endfilter %}
Note that the ``escape`` and ``safe`` filters are not acceptable arguments.
Instead, use the ``autoescape`` tag to manage autoescaping for blocks of
template code.
"""
# token.split_contents() isn't useful here because this tag doesn't accept
# variable as arguments.
_, rest = token.contents.split(None, 1)
filter_expr = parser.compile_filter("var|%s" % (rest))
for func, unused in filter_expr.filters:
filter_name = getattr(func, "_filter_name", None)
if filter_name in ("escape", "safe"):
raise TemplateSyntaxError(
'"filter %s" is not permitted. Use the "autoescape" tag instead.'
% filter_name
)
nodelist = parser.parse(("endfilter",))
parser.delete_first_token()
return FilterNode(filter_expr, nodelist)
@register.tag
def firstof(parser, token):
"""
Output the first variable passed that is not False.
Output nothing if all the passed variables are False.
Sample usage::
{% firstof var1 var2 var3 as myvar %}
This is equivalent to::
{% if var1 %}
{{ var1 }}
{% elif var2 %}
{{ var2 }}
{% elif var3 %}
{{ var3 }}
{% endif %}
but much cleaner!
You can also use a literal string as a fallback value in case all
passed variables are False::
{% firstof var1 var2 var3 "fallback value" %}
If you want to disable auto-escaping of variables you can use::
{% autoescape off %}
{% firstof var1 var2 var3 "fallback value" %}
{% autoescape %}
Or if only some variables should be escaped, you can use::
{% firstof var1 var2|safe var3 "fallback"|safe %}
"""
bits = token.split_contents()[1:]
asvar = None
if not bits:
raise TemplateSyntaxError("'firstof' statement requires at least one argument")
if len(bits) >= 2 and bits[-2] == "as":
asvar = bits[-1]
bits = bits[:-2]
return FirstOfNode([parser.compile_filter(bit) for bit in bits], asvar)
@register.tag("for")
def do_for(parser, token):
"""
Loop over each item in an array.
For example, to display a list of athletes given ``athlete_list``::
`` tags * ``{% lorem 2 w random %}`` outputs two random latin words """ bits = list(token.split_contents()) tagname = bits[0] # Random bit common = bits[-1] != "random" if not common: bits.pop() # Method bit if bits[-1] in ("w", "p", "b"): method = bits.pop() else: method = "b" # Count bit if len(bits) > 1: count = bits.pop() else: count = "1" count = parser.compile_filter(count) if len(bits) != 1: raise TemplateSyntaxError("Incorrect format for %r tag" % tagname) return LoremNode(count, method, common) @register.tag def now(parser, token): """ Display the date, formatted according to the given string. Use the same format as PHP's ``date()`` function; see https://php.net/date for all the possible values. Sample usage:: It is {% now "jS F Y H:i" %} """ bits = token.split_contents() asvar = None if len(bits) == 4 and bits[-2] == "as": asvar = bits[-1] bits = bits[:-2] if len(bits) != 2: raise TemplateSyntaxError("'now' statement takes one argument") format_string = bits[1][1:-1] return NowNode(format_string, asvar) @register.tag(name="partialdef") def partialdef_func(parser, token): """ Declare a partial that can be used in the template. Usage:: {% partialdef partial_name %} Content goes here. {% endpartialdef %} Store the nodelist in the context under the key "partials". It can be retrieved using the ``{% partial %}`` tag. The optional ``inline`` argument renders the partial's contents immediately, at the point where it is defined. """ match token.split_contents(): case "partialdef", partial_name, "inline": inline = True case "partialdef", partial_name, _: raise TemplateSyntaxError( "The 'inline' argument does not have any parameters; either use " "'inline' or remove it completely." ) case "partialdef", partial_name: inline = False case ["partialdef"]: raise TemplateSyntaxError("'partialdef' tag requires a name") case _: raise TemplateSyntaxError("'partialdef' tag takes at most 2 arguments") # Parse the content until the end tag. valid_endpartials = ("endpartialdef", f"endpartialdef {partial_name}") pos_open = getattr(token, "position", None) source_start = pos_open[0] if isinstance(pos_open, tuple) else None nodelist = parser.parse(valid_endpartials) endpartial = parser.next_token() if endpartial.contents not in valid_endpartials: parser.invalid_block_tag(endpartial, "endpartialdef", valid_endpartials) pos_close = getattr(endpartial, "position", None) source_end = pos_close[1] if isinstance(pos_close, tuple) else None # Store the partial nodelist in the parser.extra_data attribute. partials = parser.extra_data.setdefault("partials", {}) if partial_name in partials: raise TemplateSyntaxError( f"Partial '{partial_name}' is already defined in the " f"'{parser.origin.name}' template." ) partials[partial_name] = PartialTemplate( nodelist, parser.origin, partial_name, source_start=source_start, source_end=source_end, ) return PartialDefNode(partial_name, inline, nodelist) @register.tag(name="partial") def partial_func(parser, token): """ Render a partial previously declared with the ``{% partialdef %}`` tag. Usage:: {% partial partial_name %} """ match token.split_contents(): case "partial", partial_name: extra_data = parser.extra_data partial_mapping = DeferredSubDict(extra_data, "partials") return PartialNode(partial_name, partial_mapping=partial_mapping) case _: raise TemplateSyntaxError("'partial' tag requires a single argument") @register.simple_tag(name="querystring", takes_context=True) def querystring(context, *args, **kwargs): """ Build a query string using `args` and `kwargs` arguments. This tag constructs a new query string by adding, removing, or modifying parameters from the given positional and keyword arguments. Positional arguments must be mappings (such as `QueryDict` or `dict`), and `request.GET` is used as the starting point if `args` is empty. Keyword arguments are treated as an extra, final mapping. These mappings are processed sequentially, with later arguments taking precedence. Passing `None` as a value removes the corresponding key from the result. For iterable values, `None` entries are ignored, but if all values are `None`, the key is removed. A query string prefixed with `?` is returned. Raise TemplateSyntaxError if a positional argument is not a mapping or if keys are not strings. For example:: {# Set a parameter on top of `request.GET` #} {% querystring foo=3 %} {# Remove a key from `request.GET` #} {% querystring foo=None %} {# Use with pagination #} {% querystring page=page_obj.next_page_number %} {# Use a custom ``QueryDict`` #} {% querystring my_query_dict foo=3 %} {# Use multiple positional and keyword arguments #} {% querystring my_query_dict my_dict foo=3 bar=None %} """ if not args: args = [context.request.GET] params = QueryDict(mutable=True) for d in [*args, kwargs]: if not isinstance(d, Mapping): raise TemplateSyntaxError( "querystring requires mappings for positional arguments (got " "%r instead)." % d ) items = d.lists() if isinstance(d, QueryDict) else d.items() for key, value in items: if not isinstance(key, str): raise TemplateSyntaxError( "querystring requires strings for mapping keys (got %r " "instead)." % key ) if value is None: params.pop(key, None) elif isinstance(value, Iterable) and not isinstance(value, str): # Drop None values; if no values remain, the key is removed. params.setlist(key, [v for v in value if v is not None]) else: params[key] = value query_string = params.urlencode() if params else "" return f"?{query_string}" @register.tag def regroup(parser, token): """ Regroup a list of alike objects by a common attribute. This complex tag is best illustrated by use of an example: say that ``musicians`` is a list of ``Musician`` objects that have ``name`` and ``instrument`` attributes, and you'd like to display a list that looks like: * Guitar: * Django Reinhardt * Emily Remler * Piano: * Lovie Austin * Bud Powell * Trumpet: * Duke Ellington The following snippet of template code would accomplish this dubious task:: {% regroup musicians by instrument as grouped %}