Django

Code

root/django/trunk/django/template/defaulttags.py

Revision 11163, 40.4 kB (checked in by russellm, 1 day ago)

Fixed #11413 -- Added notes on the cycle and firstof tag detailing that variables output by those tags will not be escaped by default. Thanks to krystal for the report and draft patch.

  • Property svn:eol-style set to native
  • Property svn:keywords set to LastChangedRevision
Line 
1 """Default tags used by the template system, available to all templates."""
2
3 import sys
4 import re
5 from itertools import cycle as itertools_cycle
6 try:
7     reversed
8 except NameError:
9     from django.utils.itercompat import reversed     # Python 2.3 fallback
10
11 from django.template import Node, NodeList, Template, Context, Variable
12 from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END
13 from django.template import get_library, Library, InvalidTemplateLibrary
14 from django.conf import settings
15 from django.utils.encoding import smart_str, smart_unicode
16 from django.utils.itercompat import groupby
17 from django.utils.safestring import mark_safe
18
19 register = Library()
20
21 class AutoEscapeControlNode(Node):
22     """Implements the actions of the autoescape tag."""
23     def __init__(self, setting, nodelist):
24         self.setting, self.nodelist = setting, nodelist
25
26     def render(self, context):
27         old_setting = context.autoescape
28         context.autoescape = self.setting
29         output = self.nodelist.render(context)
30         context.autoescape = old_setting
31         if self.setting:
32             return mark_safe(output)
33         else:
34             return output
35
36 class CommentNode(Node):
37     def render(self, context):
38         return ''
39
40 class CycleNode(Node):
41     def __init__(self, cyclevars, variable_name=None):
42         self.cycle_iter = itertools_cycle(cyclevars)
43         self.variable_name = variable_name
44
45     def render(self, context):
46         value = self.cycle_iter.next().resolve(context)
47         if self.variable_name:
48             context[self.variable_name] = value
49         return value
50
51 class DebugNode(Node):
52     def render(self, context):
53         from pprint import pformat
54         output = [pformat(val) for val in context]
55         output.append('\n\n')
56         output.append(pformat(sys.modules))
57         return ''.join(output)
58
59 class FilterNode(Node):
60     def __init__(self, filter_expr, nodelist):
61         self.filter_expr, self.nodelist = filter_expr, nodelist
62
63     def render(self, context):
64         output = self.nodelist.render(context)
65         # Apply filters.
66         context.update({'var': output})
67         filtered = self.filter_expr.resolve(context)
68         context.pop()
69         return filtered
70
71 class FirstOfNode(Node):
72     def __init__(self, vars):
73         self.vars = vars
74
75     def render(self, context):
76         for var in self.vars:
77             value = var.resolve(context, True)
78             if value:
79                 return smart_unicode(value)
80         return u''
81
82 class ForNode(Node):
83     def __init__(self, loopvars, sequence, is_reversed, nodelist_loop, nodelist_empty=None):
84         self.loopvars, self.sequence = loopvars, sequence
85         self.is_reversed = is_reversed
86         self.nodelist_loop = nodelist_loop
87         if nodelist_empty is None:
88             self.nodelist_empty = NodeList()
89         else:
90             self.nodelist_empty = nodelist_empty
91
92     def __repr__(self):
93         reversed_text = self.is_reversed and ' reversed' or ''
94         return "<For Node: for %s in %s, tail_len: %d%s>" % \
95             (', '.join(self.loopvars), self.sequence, len(self.nodelist_loop),
96              reversed_text)
97
98     def __iter__(self):
99         for node in self.nodelist_loop:
100             yield node
101         for node in self.nodelist_empty:
102             yield node
103
104     def get_nodes_by_type(self, nodetype):
105         nodes = []
106         if isinstance(self, nodetype):
107             nodes.append(self)
108         nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype))
109         nodes.extend(self.nodelist_empty.get_nodes_by_type(nodetype))
110         return nodes
111
112     def render(self, context):
113         if 'forloop' in context:
114             parentloop = context['forloop']
115         else:
116             parentloop = {}
117         context.push()
118         try:
119             values = self.sequence.resolve(context, True)
120         except VariableDoesNotExist:
121             values = []
122         if values is None:
123             values = []
124         if not hasattr(values, '__len__'):
125             values = list(values)
126         len_values = len(values)
127         if len_values < 1:
128             context.pop()
129             return self.nodelist_empty.render(context)
130         nodelist = NodeList()
131         if self.is_reversed:
132             values = reversed(values)
133         unpack = len(self.loopvars) > 1
134         # Create a forloop value in the context.  We'll update counters on each
135         # iteration just below.
136         loop_dict = context['forloop'] = {'parentloop': parentloop}
137         for i, item in enumerate(values):
138             # Shortcuts for current loop iteration number.
139             loop_dict['counter0'] = i
140             loop_dict['counter'] = i+1
141             # Reverse counter iteration numbers.
142             loop_dict['revcounter'] = len_values - i
143             loop_dict['revcounter0'] = len_values - i - 1
144             # Boolean values designating first and last times through loop.
145             loop_dict['first'] = (i == 0)
146             loop_dict['last'] = (i == len_values - 1)
147
148             if unpack:
149                 # If there are multiple loop variables, unpack the item into
150                 # them.
151                 context.update(dict(zip(self.loopvars, item)))
152             else:
153                 context[self.loopvars[0]] = item
154             for node in self.nodelist_loop:
155                 nodelist.append(node.render(context))
156             if unpack:
157                 # The loop variables were pushed on to the context so pop them
158                 # off again. This is necessary because the tag lets the length
159                 # of loopvars differ to the length of each set of items and we
160                 # don't want to leave any vars from the previous loop on the
161                 # context.
162                 context.pop()
163         context.pop()
164         return nodelist.render(context)
165
166 class IfChangedNode(Node):
167     def __init__(self, nodelist_true, nodelist_false, *varlist):
168         self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
169         self._last_seen = None
170         self._varlist = varlist
171         self._id = str(id(self))
172
173     def render(self, context):
174         if 'forloop' in context and self._id not in context['forloop']:
175             self._last_seen = None
176             context['forloop'][self._id] = 1
177         try:
178             if self._varlist:
179                 # Consider multiple parameters.  This automatically behaves
180                 # like an OR evaluation of the multiple variables.
181                 compare_to = [var.resolve(context, True) for var in self._varlist]
182             else:
183                 compare_to = self.nodelist_true.render(context)
184         except VariableDoesNotExist:
185             compare_to = None
186
187         if compare_to != self._last_seen:
188             firstloop = (self._last_seen == None)
189             self._last_seen = compare_to
190             content = self.nodelist_true.render(context)
191             return content
192         elif self.nodelist_false:
193             return self.nodelist_false.render(context)
194         return ''
195
196 class IfEqualNode(Node):
197     def __init__(self, var1, var2, nodelist_true, nodelist_false, negate):
198         self.var1, self.var2 = var1, var2
199         self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
200         self.negate = negate
201
202     def __repr__(self):
203         return "<IfEqualNode>"
204
205     def render(self, context):
206         val1 = self.var1.resolve(context, True)
207         val2 = self.var2.resolve(context, True)
208         if (self.negate and val1 != val2) or (not self.negate and val1 == val2):
209             return self.nodelist_true.render(context)
210         return self.nodelist_false.render(context)
211
212 class IfNode(Node):
213     def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type):
214         self.bool_exprs = bool_exprs
215         self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
216         self.link_type = link_type
217
218     def __repr__(self):
219         return "<If node>"
220
221     def __iter__(self):
222         for node in self.nodelist_true:
223             yield node
224         for node in self.nodelist_false:
225             yield node
226
227     def get_nodes_by_type(self, nodetype):
228         nodes = []
229         if isinstance(self, nodetype):
230             nodes.append(self)
231         nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))
232         nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
233         return nodes
234
235     def render(self, context):
236         if self.link_type == IfNode.LinkTypes.or_:
237             for ifnot, bool_expr in self.bool_exprs:
238                 try:
239                     value = bool_expr.resolve(context, True)
240                 except VariableDoesNotExist:
241                     value = None
242                 if (value and not ifnot) or (ifnot and not value):
243                     return self.nodelist_true.render(context)
244             return self.nodelist_false.render(context)
245         else:
246             for ifnot, bool_expr in self.bool_exprs:
247                 try:
248                     value = bool_expr.resolve(context, True)
249                 except VariableDoesNotExist:
250                     value = None
251                 if not ((value and not ifnot) or (ifnot and not value)):
252                     return self.nodelist_false.render(context)
253             return self.nodelist_true.render(context)
254
255     class LinkTypes:
256         and_ = 0,
257         or_ = 1
258
259 class RegroupNode(Node):
260     def __init__(self, target, expression, var_name):
261         self.target, self.expression = target, expression
262         self.var_name = var_name
263
264     def render(self, context):
265         obj_list = self.target.resolve(context, True)
266         if obj_list == None:
267             # target variable wasn't found in context; fail silently.
268             context[self.var_name] = []
269             return ''
270         # List of dictionaries in the format:
271         # {'grouper': 'key', 'list': [list of contents]}.
272         context[self.var_name] = [
273             {'grouper': key, 'list': list(val)}
274             for key, val in
275             groupby(obj_list, lambda v, f=self.expression.resolve: f(v, True))
276         ]
277         return ''
278
279 def include_is_allowed(filepath):
280     for root in settings.ALLOWED_INCLUDE_ROOTS:
281         if filepath.startswith(root):
282             return True
283     return False
284
285 class SsiNode(Node):
286     def __init__(self, filepath, parsed):
287         self.filepath, self.parsed = filepath, parsed
288
289     def render(self, context):
290         if not include_is_allowed(self.filepath):
291             if settings.DEBUG:
292                 return "[Didn't have permission to include file]"
293             else:
294                 return '' # Fail silently for invalid includes.
295         try:
296             fp = open(self.filepath, 'r')
297             output = fp.read()
298             fp.close()
299         except IOError:
300             output = ''
301         if self.parsed:
302             try:
303                 t = Template(output, name=self.filepath)
304                 return t.render(context)
305             except TemplateSyntaxError, e:
306                 if settings.DEBUG:
307                     return "[Included template had syntax error: %s]" % e
308                 else:
309                     return '' # Fail silently for invalid included templates.
310         return output
311
312 class LoadNode(Node):
313     def render(self, context):
314         return ''
315
316 class NowNode(Node):
317     def __init__(self, format_string):
318         self.format_string = format_string
319
320     def render(self, context):
321         from datetime import datetime
322         from django.utils.dateformat import DateFormat
323         df = DateFormat(datetime.now())
324         return df.format(self.format_string)
325
326 class SpacelessNode(Node):
327     def __init__(self, nodelist):
328         self.nodelist = nodelist
329
330     def render(self, context):
331         from django.utils.html import strip_spaces_between_tags
332         return strip_spaces_between_tags(self.nodelist.render(context).strip())
333
334 class TemplateTagNode(Node):
335     mapping = {'openblock': BLOCK_TAG_START,
336                'closeblock': BLOCK_TAG_END,
337                'openvariable': VARIABLE_TAG_START,
338                'closevariable': VARIABLE_TAG_END,
339                'openbrace': SINGLE_BRACE_START,
340                'closebrace': SINGLE_BRACE_END,
341                'opencomment': COMMENT_TAG_START,
342                'closecomment': COMMENT_TAG_END,
343                }
344
345     def __init__(self, tagtype):
346         self.tagtype = tagtype
347
348     def render(self, context):
349         return self.mapping.get(self.tagtype, '')
350
351 class URLNode(Node):
352     def __init__(self, view_name, args, kwargs, asvar):
353         self.view_name = view_name
354         self.args = args
355         self.kwargs = kwargs
356         self.asvar = asvar
357
358     def render(self, context):
359         from django.core.urlresolvers import reverse, NoReverseMatch
360         args = [arg.resolve(context) for arg in self.args]
361         kwargs = dict([(smart_str(k,'ascii'), v.resolve(context))
362                        for k, v in self.kwargs.items()])
363
364         # Try to look up the URL twice: once given the view name, and again
365         # relative to what we guess is the "main" app. If they both fail,
366         # re-raise the NoReverseMatch unless we're using the
367         # {% url ... as var %} construct in which cause return nothing.
368         url = ''
369         try:
370             url = reverse(self.view_name, args=args, kwargs=kwargs)
371         except NoReverseMatch, e:
372             if settings.SETTINGS_MODULE:
373                 project_name = settings.SETTINGS_MODULE.split('.')[0]
374                 try:
375                     url = reverse(project_name + '.' + self.view_name,
376                               args=args, kwargs=kwargs)
377                 except NoReverseMatch:
378                     if self.asvar is None:
379                         # Re-raise the original exception, not the one with
380                         # the path relative to the project. This makes a
381                         # better error message.
382                         raise e
383             else:
384                 if self.asvar is None:
385                     raise e
386
387         if self.asvar:
388             context[self.asvar] = url
389             return ''
390         else:
391             return url
392
393 class WidthRatioNode(Node):
394     def __init__(self, val_expr, max_expr, max_width):
395         self.val_expr = val_expr
396         self.max_expr = max_expr
397         self.max_width = max_width
398
399     def render(self, context):
400         try:
401             value = self.val_expr.resolve(context)
402             maxvalue = self.max_expr.resolve(context)
403             max_width = int(self.max_width.resolve(context))
404         except VariableDoesNotExist:
405             return ''
406         except ValueError:
407             raise TemplateSyntaxError("widthratio final argument must be an number")
408         try:
409             value = float(value)
410             maxvalue = float(maxvalue)
411             ratio = (value / maxvalue) * max_width
412         except (ValueError, ZeroDivisionError):
413             return ''
414         return str(int(round(ratio)))
415
416 class WithNode(Node):
417     def __init__(self, var, name, nodelist):
418         self.var = var
419         self.name = name
420         self.nodelist = nodelist
421
422     def __repr__(self):
423         return "<WithNode>"
424
425     def render(self, context):
426         val = self.var.resolve(context)
427         context.push()
428         context[self.name] = val
429         output = self.nodelist.render(context)
430         context.pop()
431         return output
432
433 #@register.tag
434 def autoescape(parser, token):
435     """
436     Force autoescape behaviour for this block.
437     """
438     args = token.contents.split()
439     if len(args) != 2:
440         raise TemplateSyntaxError("'Autoescape' tag requires exactly one argument.")
441     arg = args[1]
442     if arg not in (u'on', u'off'):
443         raise TemplateSyntaxError("'Autoescape' argument should be 'on' or 'off'")
444     nodelist = parser.parse(('endautoescape',))
445     parser.delete_first_token()
446     return AutoEscapeControlNode((arg == 'on'), nodelist)
447 autoescape = register.tag(autoescape)
448
449 #@register.tag
450 def comment(parser, token):
451     """
452     Ignores everything between ``{% comment %}`` and ``{% endcomment %}``.
453     """
454     parser.skip_past('endcomment')
455     return CommentNode()
456 comment = register.tag(comment)
457
458 #@register.tag
459 def cycle(parser, token):
460     """
461     Cycles among the given strings each time this tag is encountered.
462
463     Within a loop, cycles among the given strings each time through
464     the loop::
465
466         {% for o in some_list %}
467             <tr class="{% cycle 'row1' 'row2' %}">
468                 ...
469             </tr>
470         {% endfor %}
471
472     Outside of a loop, give the values a unique name the first time you call
473     it, then use that name each sucessive time through::
474
475             <tr class="{% cycle 'row1' 'row2' 'row3' as rowcolors %}">...</tr>
476             <tr class="{% cycle rowcolors %}">...</tr>
477             <tr class="{% cycle rowcolors %}">...</tr>
478
479     You can use any number of values, separated by spaces. Commas can also
480     be used to separate values; if a comma is used, the cycle values are
481     interpreted as literal strings.
482     """
483
484     # Note: This returns the exact same node on each {% cycle name %} call;
485     # that is, the node object returned from {% cycle a b c as name %} and the
486     # one returned from {% cycle name %} are the exact same object. This
487     # shouldn't cause problems (heh), but if it does, now you know.
488     #
489     # Ugly hack warning: This stuffs the named template dict into parser so
490     # that names are only unique within each template (as opposed to using
491     # a global variable, which would make cycle names have to be unique across
492     # *all* templates.
493
494     args = token.split_contents()
495
496     if len(args) < 2:
497         raise TemplateSyntaxError("'cycle' tag requires at least two arguments")
498
499     if ',' in args[1]:
500         # Backwards compatibility: {% cycle a,b %} or {% cycle a,b as foo %}
501         # case.
502         args[1:2] = ['"%s"' % arg for arg in args[1].split(",")]
503
504     if len(args) == 2:
505         # {% cycle foo %} case.
506         name = args[1]
507         if not hasattr(parser, '_namedCycleNodes'):
508             raise TemplateSyntaxError("No named cycles in template. '%s' is not defined" % name)
509         if not name in parser._namedCycleNodes:
510             raise TemplateSyntaxError("Named cycle '%s' does not exist" % name)
511         return parser._namedCycleNodes[name]
512
513     if len(args) > 4 and args[-2] == 'as':
514         name = args[-1]
515         values = [parser.compile_filter(arg) for arg in args[1:-2]]
516         node = CycleNode(values, name)
517         if not hasattr(parser, '_namedCycleNodes'):
518             parser._namedCycleNodes = {}
519         parser._namedCycleNodes[name] = node
520     else:
521         values = [parser.compile_filter(arg) for arg in args[1:]]
522         node = CycleNode(values)
523     return node
524 cycle = register.tag(cycle)
525
526 def debug(parser, token):
527     """
528     Outputs a whole load of debugging information, including the current
529     context and imported modules.
530
531     Sample usage::
532
533         <pre>
534             {% debug %}
535         </pre>
536     """
537     return DebugNode()
538 debug = register.tag(debug)
539
540 #@register.tag(name="filter")
541 def do_filter(parser, token):
542     """
543     Filters the contents of the block through variable filters.
544
545     Filters can also be piped through each other, and they can have
546     arguments -- just like in variable syntax.
547
548     Sample usage::
549
550         {% filter force_escape|lower %}
551             This text will be HTML-escaped, and will appear in lowercase.
552         {% endfilter %}
553     """
554     _, rest = token.contents.split(None, 1)
555     filter_expr = parser.compile_filter("var|%s" % (rest))
556     for func, unused in filter_expr.filters:
557         if getattr(func, '_decorated_function', func).__name__ in ('escape', 'safe'):
558             raise TemplateSyntaxError('"filter %s" is not permitted.  Use the "autoescape" tag instead.' % func.__name__)
559     nodelist = parser.parse(('endfilter',))
560     parser.delete_first_token()
561     return FilterNode(filter_expr, nodelist)
562 do_filter = register.tag("filter", do_filter)
563
564 #@register.tag
565 def firstof(parser, token):
566     """
567     Outputs the first variable passed that is not False, without escaping.
568
569     Outputs nothing if all the passed variables are False.
570
571     Sample usage::
572
573         {% firstof var1 var2 var3 %}
574
575     This is equivalent to::
576
577         {% if var1 %}
578             {{ var1|safe }}
579         {% else %}{% if var2 %}
580             {{ var2|safe }}
581         {% else %}{% if var3 %}
582             {{ var3|safe }}
583         {% endif %}{% endif %}{% endif %}
584
585     but obviously much cleaner!
586
587     You can also use a literal string as a fallback value in case all
588     passed variables are False::
589
590         {% firstof var1 var2 var3 "fallback value" %}
591
592     If you want to escape the output, use a filter tag::
593
594         {% filter force_escape %}
595             {% firstof var1 var2 var3 "fallback value" %}
596         {% endfilter %}
597
598     """
599     bits = token.split_contents()[1:]
600     if len(bits) < 1:
601         raise TemplateSyntaxError("'firstof' statement requires at least one"
602                                   " argument")
603     return FirstOfNode([parser.compile_filter(bit) for bit in bits])
604 firstof = register.tag(firstof)
605
606 #@register.tag(name="for")
607 def do_for(parser, token):
608     """
609     Loops over each item in an array.
610
611     For example, to display a list of athletes given ``athlete_list``::
612
613         <ul>
614         {% for athlete in athlete_list %}
615             <li>{{ athlete.name }}</li>
616         {% endfor %}
617         </ul>
618
619     You can loop over a list in reverse by using
620     ``{% for obj in list reversed %}``.
621
622     You can also unpack multiple values from a two-dimensional array::
623
624         {% for key,value in dict.items %}
625             {{ key }}: {{ value }}
626         {% endfor %}
627
628     The ``for`` tag can take an optional ``{% empty %}`` clause that will
629     be displayed if the given array is empty or could not be found::
630
631         <ul>
632           {% for athlete in athlete_list %}
633             <li>{{ athlete.name }}</li>
634           {% empty %}
635             <li>Sorry, no athletes in this list.</li>
636           {% endfor %}
637         <ul>
638
639     The above is equivalent to -- but shorter, cleaner, and possibly faster
640     than -- the following::
641
642         <ul>
643           {% if althete_list %}
644             {% for athlete in athlete_list %}
645               <li>{{ athlete.name }}</li>
646             {% endfor %}
647           {% else %}
648             <li>Sorry, no athletes in this list.</li>
649           {% endif %}
650         </ul>
651
652     The for loop sets a number of variables available within the loop:
653
654         ==========================  ================================================
655         Variable                    Description
656         ==========================  ================================================
657         ``forloop.counter``         The current iteration of the loop (1-indexed)
658         ``forloop.counter0``        The current iteration of the loop (0-indexed)
659         ``forloop.revcounter``      The number of iterations from the end of the
660                                     loop (1-indexed)
661         ``forloop.revcounter0``     The number of iterations from the end of the
662                                     loop (0-indexed)
663         ``forloop.first``           True if this is the first time through the loop
664         ``forloop.last``            True if this is the last time through the loop
665         ``forloop.parentloop``      For nested loops, this is the loop "above" the
666                                     current one
667         ==========================  ================================================
668
669     """
670     bits = token.contents.split()
671     if len(bits) < 4:
672         raise TemplateSyntaxError("'for' statements should have at least four"
673                                   " words: %s" % token.contents)
674
675     is_reversed = bits[-1] == 'reversed'
676     in_index = is_reversed and -3 or -2
677     if bits[in_index] != 'in':
678         raise TemplateSyntaxError("'for' statements should use the format"
679                                   " 'for x in y': %s" % token.contents)
680
681     loopvars = re.sub(r' *, *', ',', ' '.join(bits[1:in_index])).split(',')
682     for var in loopvars:
683         if not var or ' ' in var:
684             raise TemplateSyntaxError("'for' tag received an invalid argument:"
685                                       " %s" % token.contents)
686
687     sequence = parser.compile_filter(bits[in_index+1])
688     nodelist_loop = parser.parse(('empty', 'endfor',))
689     token = parser.next_token()
690     if token.contents == 'empty':
691         nodelist_empty = parser.parse(('endfor',))
692         parser.delete_first_token()
693     else:
694         nodelist_empty = None
695     return ForNode(loopvars, sequence, is_reversed, nodelist_loop, nodelist_empty)
696 do_for = register.tag("for", do_for)
697
698 def do_ifequal(parser, token, negate):
699     bits = list(token.split_contents())
700     if len(bits) != 3:
701         raise TemplateSyntaxError, "%r takes two arguments" % bits[0]
702     end_tag = 'end' + bits[0]
703     nodelist_true = parser.parse(('else', end_tag))
704     token = parser.next_token()
705     if token.contents == 'else':
706         nodelist_false = parser.parse((end_tag,))
707         parser.delete_first_token()
708     else:
709         nodelist_false = NodeList()
710     val1 = parser.compile_filter(bits[1])
711     val2 = parser.compile_filter(bits[2])
712     return IfEqualNode(val1, val2, nodelist_true, nodelist_false, negate)
713
714 #@register.tag
715 def ifequal(parser, token):
716     """
717     Outputs the contents of the block if the two arguments equal each other.
718
719     Examples::
720
721         {% ifequal user.id comment.user_id %}
722             ...
723         {% endifequal %}
724
725         {% ifnotequal user.id comment.user_id %}
726             ...
727         {% else %}
728             ...
729         {% endifnotequal %}
730     """
731     return do_ifequal(parser, token, False)
732 ifequal = register.tag(ifequal)
733
734 #@register.tag
735 def ifnotequal(parser, token):
736     """
737     Outputs the contents of the block if the two arguments are not equal.
738     See ifequal.
739     """
740     return do_ifequal(parser, token, True)
741 ifnotequal = register.tag(ifnotequal)
742
743 #@register.tag(name="if")
744 def do_if(parser, token):
745     """
746     The ``{% if %}`` tag evaluates a variable, and if that variable is "true"
747     (i.e., exists, is not empty, and is not a false boolean value), the
748     contents of the block are output:
749
750     ::
751
752         {% if athlete_list %}
753             Number of athletes: {{ athlete_list|count }}
754         {% else %}
755             No athletes.
756         {% endif %}
757
758     In the above, if ``athlete_list`` is not empty, the number of athletes will
759     be displayed by the ``{{ athlete_list|count }}`` variable.
760
761     As you can see, the ``if`` tag can take an option ``{% else %}`` clause
762     that will be displayed if the test fails.
763
764     ``if`` tags may use ``or``, ``and`` or ``not`` to test a number of
765     variables or to negate a given variable::
766
767         {% if not athlete_list %}
768             There are no athletes.
769         {% endif %}
770
771         {% if athlete_list or coach_list %}
772             There are some athletes or some coaches.
773         {% endif %}
774
775         {% if athlete_list and coach_list %}
776             Both atheletes and coaches are available.
777         {% endif %}
778
779         {% if not athlete_list or coach_list %}
780             There are no athletes, or there are some coaches.
781         {% endif %}
782
783         {% if athlete_list and not coach_list %}
784             There are some athletes and absolutely no coaches.
785         {% endif %}
786
787     ``if`` tags do not allow ``and`` and ``or`` clauses with the same tag,
788     because the order of logic would be ambigous. For example, this is
789     invalid::
790
791         {% if athlete_list and coach_list or cheerleader_list %}
792
793     If you need to combine ``and`` and ``or`` to do advanced logic, just use
794     nested if tags. For example::
795
796         {% if athlete_list %}
797             {% if coach_list or cheerleader_list %}
798                 We have athletes, and either coaches or cheerleaders!
799             {% endif %}
800         {% endif %}
801     """
802     bits = token.contents.split()
803     del bits[0]
804     if not bits:
805         raise TemplateSyntaxError("'if' statement requires at least one argument")
806     # Bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d']
807     bitstr = ' '.join(bits)
808     boolpairs = bitstr.split(' and ')
809     boolvars = []
810     if len(boolpairs) == 1:
811         link_type = IfNode.LinkTypes.or_
812         boolpairs = bitstr.split(' or ')
813     else:
814         link_type = IfNode.LinkTypes.and_
815         if ' or ' in bitstr:
816             raise TemplateSyntaxError, "'if' tags can't mix 'and' and 'or'"
817     for boolpair in boolpairs:
818         if ' ' in boolpair:
819             try:
820                 not_, boolvar = boolpair.split()
821             except ValueError:
822                 raise TemplateSyntaxError, "'if' statement improperly formatted"
823             if not_ != 'not':
824                 raise TemplateSyntaxError, "Expected 'not' in if statement"
825             boolvars.append((True, parser.compile_filter(boolvar)))
826         else:
827             boolvars.append((False, parser.compile_filter(boolpair)))
828     nodelist_true = parser.parse(('else', 'endif'))
829     token = parser.next_token()
830     if token.contents == 'else':
831         nodelist_false = parser.parse(('endif',))
832         parser.delete_first_token()
833     else:
834         nodelist_false = NodeList()
835     return IfNode(boolvars, nodelist_true, nodelist_false, link_type)
836 do_if = register.tag("if", do_if)
837
838 #@register.tag
839 def ifchanged(parser, token):
840     """
841     Checks if a value has changed from the last iteration of a loop.
842
843     The 'ifchanged' block tag is used within a loop. It has two possible uses.
844
845     1. Checks its own rendered contents against its previous state and only
846        displays the content if it has changed. For example, this displays a
847        list of days, only displaying the month if it changes::
848
849             <h1>Archive for {{ year }}</h1>
850
851             {% for date in days %}
852                 {% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %}
853                 <a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a>
854             {% endfor %}
855
856     2. If given a variable, check whether that variable has changed.
857        For example, the following shows the date every time it changes, but
858        only shows the hour if both the hour and the date have changed::
859
860             {% for date in days %}
861                 {% ifchanged date.date %} {{ date.date }} {% endifchanged %}
862                 {% ifchanged date.hour date.date %}
863                     {{ date.hour }}
864                 {% endifchanged %}
865             {% endfor %}
866     """
867     bits = token.contents.split()
868     nodelist_true = parser.parse(('else', 'endifchanged'))
869     token = parser.next_token()
870     if token.contents == 'else':
871         nodelist_false = parser.parse(('endifchanged',))
872         parser.delete_first_token()
873     else:
874         nodelist_false = NodeList()
875     values = [parser.compile_filter(bit) for bit in bits[1:]]
876     return IfChangedNode(nodelist_true, nodelist_false, *values)
877 ifchanged = register.tag(ifchanged)
878
879 #@register.tag
880 def ssi(parser, token):
881     """
882     Outputs the contents of a given file into the page.
883
884     Like a simple "include" tag, the ``ssi`` tag includes the contents
885     of another file -- which must be specified using an absolute path --
886     in the current page::
887
888         {% ssi /home/html/ljworld.com/includes/right_generic.html %}
889
890     If the optional "parsed" parameter is given, the contents of the included
891     file are evaluated as template code, with the current context::
892
893         {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %}
894     """
895     bits = token.contents.split()
896     parsed = False
897     if len(bits) not in (2, 3):
898         raise TemplateSyntaxError("'ssi' tag takes one argument: the path to"
899                                   " the file to be included")
900     if len(bits) == 3:
901         if bits[2] == 'parsed':
902             parsed = True
903         else:
904             raise TemplateSyntaxError("Second (optional) argument to %s tag"
905                                       " must be 'parsed'" % bits[0])
906     return SsiNode(bits[1], parsed)
907 ssi = register.tag(ssi)
908
909 #@register.tag
910 def load(parser, token):
911     """
912     Loads a custom template tag set.
913
914     For example, to load the template tags in
915     ``django/templatetags/news/photos.py``::
916
917         {% load news.photos %}
918     """
919     bits = token.contents.split()
920     for taglib in bits[1:]:
921         # add the library to the parser
922         try:
923             lib = get_library("django.templatetags.%s" % taglib)
924             parser.add_library(lib)
925         except InvalidTemplateLibrary, e:
926             raise TemplateSyntaxError("'%s' is not a valid tag library: %s" %
927                                       (taglib, e))
928     return LoadNode()
929 load = register.tag(load)
930
931 #@register.tag
932 def now(parser, token):
933     """
934     Displays the date, formatted according to the given string.
935
936     Uses the same format as PHP's ``date()`` function; see http://php.net/date
937     for all the possible values.
938
939     Sample usage::
940
941         It is {% now "jS F Y H:i" %}
942     """
943     bits = token.contents.split('"')
944     if len(bits) != 3:
945         raise TemplateSyntaxError, "'now' statement takes one argument"
946     format_string = bits[1]
947     return NowNode(format_string)
948 now = register.tag(now)
949
950 #@register.tag
951 def regroup(parser, token):
952     """
953     Regroups a list of alike objects by a common attribute.
954
955     This complex tag is best illustrated by use of an example:  say that
956     ``people`` is a list of ``Person`` objects that have ``first_name``,
957     ``last_name``, and ``gender`` attributes, and you'd like to display a list
958     that looks like:
959
960         * Male:
961             * George Bush
962             * Bill Clinton
963         * Female:
964             * Margaret Thatcher
965             * Colendeeza Rice
966         * Unknown:
967             * Pat Smith
968
969     The following snippet of template code would accomplish this dubious task::
970
971         {% regroup people by gender as grouped %}
972         <ul>
973         {% for group in grouped %}
974             <li>{{ group.grouper }}
975             <ul>
976                 {% for item in group.list %}
977                 <li>{{ item }}</li>
978                 {% endfor %}
979             </ul>
980         {% endfor %}
981         </ul>
982
983     As you can see, ``{% regroup %}`` populates a variable with a list of
984     objects with ``grouper`` and ``list`` attributes.  ``grouper`` contains the
985     item that was grouped by; ``list`` contains the list of objects that share
986     that ``grouper``.  In this case, ``grouper`` would be ``Male``, ``Female``
987     and ``Unknown``, and ``list`` is the list of people with those genders.
988
989     Note that ``{% regroup %}`` does not work when the list to be grouped is not
990     sorted by the key you are grouping by!  This means that if your list of
991     people was not sorted by gender, you'd need to make sure it is sorted
992     before using it, i.e.::
993
994         {% regroup people|dictsort:"gender" by gender as grouped %}
995
996     """
997     firstbits = token.contents.split(None, 3)
998     if len(firstbits) != 4:
999         raise TemplateSyntaxError, "'regroup' tag takes five arguments"
1000     target = parser.compile_filter(firstbits[1])
1001     if firstbits[2] != 'by':
1002         raise TemplateSyntaxError("second argument to 'regroup' tag must be 'by'")
1003     lastbits_reversed = firstbits[3][::-1].split(None, 2)
1004     if lastbits_reversed[1][::-1] != 'as':
1005         raise TemplateSyntaxError("next-to-last argument to 'regroup' tag must"
1006                                   " be 'as'")
1007
1008     expression = parser.compile_filter(lastbits_reversed[2][::-1])
1009
1010     var_name = lastbits_reversed[0][::-1]
1011     return RegroupNode(target, expression, var_name)
1012 regroup = register.tag(regroup)
1013
1014 def spaceless(parser, token):
1015     """
1016     Removes whitespace between HTML tags, including tab and newline characters.
1017
1018     Example usage::
1019
1020         {% spaceless %}
1021             <p>
1022                 <a href="foo/">Foo</a>
1023             </p>
1024         {% endspaceless %}
1025
1026     This example would return this HTML::
1027
1028         <p><a href="foo/">Foo</a></p>
1029
1030     Only space between *tags* is normalized -- not space between tags and text.
1031     In this example, the space around ``Hello`` won't be stripped::
1032
1033         {% spaceless %}
1034             <strong>
1035                 Hello
1036             </strong>
1037         {% endspaceless %}
1038     """
1039     nodelist = parser.parse(('endspaceless',))
1040     parser.delete_first_token()
1041     return SpacelessNode(nodelist)
1042 spaceless = register.tag(spaceless)
1043
1044 #@register.tag
1045 def templatetag(parser, token):
1046     """
1047     Outputs one of the bits used to compose template tags.
1048
1049     Since the template system has no concept of "escaping", to display one of
1050     the bits used in template tags, you must use the ``{% templatetag %}`` tag.
1051
1052     The argument tells which template bit to output:
1053
1054         ==================  =======
1055         Argument            Outputs
1056         ==================  =======
1057         ``openblock``       ``{%``
1058         ``closeblock``      ``%}``
1059         ``openvariable``    ``{{``
1060         ``closevariable``   ``}}``
1061         ``openbrace``       ``{``
1062         ``closebrace``      ``}``
1063         ``opencomment``     ``{#``
1064         ``closecomment``    ``#}``
1065         ==================  =======
1066     """
1067     bits = token.contents.split()
1068     if len(bits) != 2:
1069         raise TemplateSyntaxError, "'templatetag' statement takes one argument"
1070     tag = bits[1]
1071     if tag not in TemplateTagNode.mapping:
1072         raise TemplateSyntaxError("Invalid templatetag argument: '%s'."
1073                                   " Must be one of: %s" %
1074                                   (tag, TemplateTagNode.mapping.keys()))
1075     return TemplateTagNode(tag)
1076 templatetag = register.tag(templatetag)
1077
1078 def url(parser, token):
1079     """
1080     Returns an absolute URL matching given view with its parameters.
1081
1082     This is a way to define links that aren't tied to a particular URL
1083     configuration::
1084
1085         {% url path.to.some_view arg1,arg2,name1=value1 %}
1086
1087     The first argument is a path to a view. It can be an absolute python path
1088     or just ``app_name.view_name`` without the project name if the view is
1089     located inside the project.  Other arguments are comma-separated values
1090     that will be filled in place of positional and keyword arguments in the
1091     URL. All arguments for the URL should be present.
1092
1093     For example if you have a view ``app_name.client`` taking client's id and
1094     the corresponding line in a URLconf looks like this::
1095
1096         ('^client/(\d+)/$', 'app_name.client')
1097
1098     and this app's URLconf is included into the project's URLconf under some
1099     path::
1100
1101         ('^clients/', include('project_name.app_name.urls'))
1102
1103     then in a template you can create a link for a certain client like this::
1104
1105         {% url app_name.client client.id %}
1106
1107     The URL will look like ``/clients/client/123/``.
1108     """
1109     bits = token.split_contents()
1110     if len(bits) < 2:
1111         raise TemplateSyntaxError("'%s' takes at least one argument"
1112                                   " (path to a view)" % bits[0])
1113     viewname = bits[1]
1114     args = []
1115     kwargs = {}
1116     asvar = None
1117
1118     if len(bits) > 2:
1119         bits = iter(bits[2:])
1120         for bit in bits:
1121             if bit == 'as':
1122                 asvar = bits.next()
1123                 break
1124             else:
1125                 for arg in bit.split(","):
1126                     if '=' in arg:
1127                         k, v = arg.split('=', 1)
1128                         k = k.strip()
1129                         kwargs[k] = parser.compile_filter(v)
1130                     elif arg:
1131                         args.append(parser.compile_filter(arg))
1132     return URLNode(viewname, args, kwargs, asvar)
1133 url = register.tag(url)
1134
1135 #@register.tag
1136 def widthratio(parser, token):
1137     """
1138     For creating bar charts and such, this tag calculates the ratio of a given
1139     value to a maximum value, and then applies that ratio to a constant.
1140
1141     For example::
1142
1143         <img src='bar.gif' height='10' width='{% widthratio this_value max_value 100 %}' />
1144
1145     Above, if ``this_value`` is 175 and ``max_value`` is 200, the image in
1146     the above example will be 88 pixels wide (because 175/200 = .875;
1147     .875 * 100 = 87.5 which is rounded up to 88).
1148     """
1149     bits = token.contents.split()
1150     if len(bits) != 4:
1151         raise TemplateSyntaxError("widthratio takes three arguments")
1152     tag, this_value_expr, max_value_expr, max_width = bits
1153
1154     return WidthRatioNode(parser.compile_filter(this_value_expr),
1155                           parser.compile_filter(max_value_expr),
1156                           parser.compile_filter(max_width))
1157 widthratio = register.tag(widthratio)
1158
1159 #@register.tag
1160 def do_with(parser, token):
1161     """
1162     Adds a value to the context (inside of this block) for caching and easy
1163     access.
1164
1165     For example::
1166
1167         {% with person.some_sql_method as total %}
1168             {{ total }} object{{ total|pluralize }}
1169         {% endwith %}
1170     """
1171     bits = list(token.split_contents())
1172     if len(bits) != 4 or bits[2] != "as":
1173         raise TemplateSyntaxError("%r expected format is 'value as name'" %
1174                                   bits[0])
1175     var = parser.compile_filter(bits[1])
1176     name = bits[3]
1177     nodelist = parser.parse(('endwith',))
1178     parser.delete_first_token()
1179     return WithNode(var, name, nodelist)
1180 do_with = register.tag('with', do_with)
Note: See TracBrowser for help on using the browser.