"""Default tags used by the template system, available to all templates.""" import re import sys import warnings from collections import namedtuple from datetime import datetime from itertools import cycle as itertools_cycle, groupby from django.conf import settings from django.utils import timezone from django.utils.html import conditional_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, Context, Node, NodeList, TemplateSyntaxError, VariableDoesNotExist, kwarg_re, render_value_in_context, token_kwargs, ) 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, self.nodelist = setting, 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): def render(self, context): return '' class CsrfTokenNode(Node): 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): from pprint import pformat output = [pformat(val) for val in context] output.append('\n\n') output.append(pformat(sys.modules)) return ''.join(output) class FilterNode(Node): def __init__(self, filter_expr, nodelist): self.filter_expr, self.nodelist = filter_expr, 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, self.sequence = loopvars, 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} 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, self.nodelist_false = nodelist_true, 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 IfEqualNode(Node): child_nodelists = ('nodelist_true', 'nodelist_false') def __init__(self, var1, var2, nodelist_true, nodelist_false, negate): self.var1, self.var2 = var1, var2 self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false self.negate = negate def __repr__(self): return '<%s>' % self.__class__.__name__ def render(self, context): val1 = self.var1.resolve(context, ignore_failures=True) val2 = self.var2.resolve(context, ignore_failures=True) if (self.negate and val1 != val2) or (not self.negate and val1 == val2): return self.nodelist_true.render(context) return self.nodelist_false.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, self.method, self.common = count, method, 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, self.expression = target, 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): 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 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): def __init__(self, view_name, args, kwargs, asvar): self.view_name = view_name self.args = args self.kwargs = kwargs self.asvar = asvar def render(self, context): from django.urls import reverse, NoReverseMatch 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 %} ... {% endfor %} Outside of a loop, give the values a unique name the first time you call it, then use that name each successive time through:: ... ... ... You can use any number of values, separated by spaces. Commas can also be used to separate values; if a comma is used, the cycle values are interpreted as literal strings. The optional flag "silent" can be used to prevent the cycle declaration from returning any value:: {% for o in some_list %} {% cycle 'row1' 'row2' as rowcolors silent %} {% include "subtemplate.html " %} {% endfor %} """ # 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 # one returned from {% cycle name %} are the exact same object. This # shouldn't cause problems (heh), but if it does, now you know. # # Ugly hack warning: This stuffs the named template dict into parser so # that names are only unique within each template (as opposed to using # a global variable, which would make cycle names have to be unique across # *all* templates. # # It keeps the last node in the parser to be able to reset it with # {% resetcycle %}. args = token.split_contents() if len(args) < 2: raise TemplateSyntaxError("'cycle' tag requires at least two arguments") if len(args) == 2: # {% cycle foo %} case. name = args[1] if not hasattr(parser, '_named_cycle_nodes'): raise TemplateSyntaxError("No named cycles in template. '%s' is not defined" % name) if name not in parser._named_cycle_nodes: raise TemplateSyntaxError("Named cycle '%s' does not exist" % name) return parser._named_cycle_nodes[name] as_form = False if len(args) > 4: # {% cycle ... as foo [silent] %} case. if args[-3] == "as": if args[-1] != "silent": raise TemplateSyntaxError("Only 'silent' flag is allowed after cycle's name, not '%s'." % args[-1]) as_form = True silent = True args = args[:-1] elif args[-2] == "as": as_form = True silent = False if as_form: name = args[-1] values = [parser.compile_filter(arg) for arg in args[1:-2]] node = CycleNode(values, name, silent=silent) if not hasattr(parser, '_named_cycle_nodes'): parser._named_cycle_nodes = {} parser._named_cycle_nodes[name] = node else: values = [parser.compile_filter(arg) for arg in args[1:]] node = CycleNode(values) parser._last_cycle_node = node return node @register.tag def csrf_token(parser, token): return CsrfTokenNode() @register.tag def debug(parser, token): """ Output a whole load of debugging information, including the current context and imported modules. Sample usage::
            {% 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 obviously 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 value"|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``:: You can loop over a list in reverse by using ``{% for obj in list reversed %}``. You can also unpack multiple values from a two-dimensional array:: {% for key,value in dict.items %} {{ key }}: {{ value }} {% endfor %} The ``for`` tag can take an optional ``{% empty %}`` clause that will be displayed if the given array is empty or could not be found::