Ticket #2181: defaulttags.py

File defaulttags.py, 28.8 KB (added by James Bennett, 18 years ago)

one-half of a patch to implement this

Line 
1"Default tags used by the template system, available to all templates."
2
3from django.template import Node, NodeList, Template, Context, resolve_variable
4from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_VARIABLE_TAG_START, SINGLE_VARIABLE_TAG_END
5from django.template import get_library, Library, InvalidTemplateLibrary
6from django.conf import settings
7import sys
8
9register = Library()
10
11class CommentNode(Node):
12 def render(self, context):
13 return ''
14
15class CycleNode(Node):
16 def __init__(self, cyclevars):
17 self.cyclevars = cyclevars
18 self.cyclevars_len = len(cyclevars)
19 self.counter = -1
20
21 def render(self, context):
22 self.counter += 1
23 return self.cyclevars[self.counter % self.cyclevars_len]
24
25class DebugNode(Node):
26 def render(self, context):
27 from pprint import pformat
28 output = [pformat(val) for val in context]
29 output.append('\n\n')
30 output.append(pformat(sys.modules))
31 return ''.join(output)
32
33class FilterNode(Node):
34 def __init__(self, filter_expr, nodelist):
35 self.filter_expr, self.nodelist = filter_expr, nodelist
36
37 def render(self, context):
38 output = self.nodelist.render(context)
39 # apply filters
40 return self.filter_expr.resolve(Context({'var': output}))
41
42class FirstOfNode(Node):
43 def __init__(self, vars):
44 self.vars = vars
45
46 def render(self, context):
47 for var in self.vars:
48 value = resolve_variable(var, context)
49 if value:
50 return str(value)
51 return ''
52
53class ForNode(Node):
54 def __init__(self, loopvar, sequence, reversed, nodelist_loop):
55 self.loopvar, self.sequence = loopvar, sequence
56 self.reversed = reversed
57 self.nodelist_loop = nodelist_loop
58
59 def __repr__(self):
60 if self.reversed:
61 reversed = ' reversed'
62 else:
63 reversed = ''
64 return "<For Node: for %s in %s, tail_len: %d%s>" % \
65 (self.loopvar, self.sequence, len(self.nodelist_loop), reversed)
66
67 def __iter__(self):
68 for node in self.nodelist_loop:
69 yield node
70
71 def get_nodes_by_type(self, nodetype):
72 nodes = []
73 if isinstance(self, nodetype):
74 nodes.append(self)
75 nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype))
76 return nodes
77
78 def render(self, context):
79 nodelist = NodeList()
80 if context.has_key('forloop'):
81 parentloop = context['forloop']
82 else:
83 parentloop = {}
84 context.push()
85 try:
86 values = self.sequence.resolve(context)
87 except VariableDoesNotExist:
88 values = []
89 if values is None:
90 values = []
91 len_values = len(values)
92 if self.reversed:
93 # From http://www.python.org/doc/current/tut/node11.html
94 def reverse(data):
95 for index in range(len(data)-1, -1, -1):
96 yield data[index]
97 values = reverse(values)
98 for i, item in enumerate(values):
99 context['forloop'] = {
100 # shortcuts for current loop iteration number
101 'counter0': i,
102 'counter': i+1,
103 # reverse counter iteration numbers
104 'revcounter': len_values - i,
105 'revcounter0': len_values - i - 1,
106 # boolean values designating first and last times through loop
107 'first': (i == 0),
108 'last': (i == len_values - 1),
109 'parentloop': parentloop,
110 }
111 context[self.loopvar] = item
112 for node in self.nodelist_loop:
113 nodelist.append(node.render(context))
114 context.pop()
115 return nodelist.render(context)
116
117class IfChangedNode(Node):
118 def __init__(self, nodelist):
119 self.nodelist = nodelist
120 self._last_seen = None
121
122 def render(self, context):
123 content = self.nodelist.render(context)
124 if content != self._last_seen:
125 firstloop = (self._last_seen == None)
126 self._last_seen = content
127 context.push()
128 context['ifchanged'] = {'firstloop': firstloop}
129 content = self.nodelist.render(context)
130 context.pop()
131 return content
132 else:
133 return ''
134
135class IfEqualNode(Node):
136 def __init__(self, var1, var2, nodelist_true, nodelist_false, negate):
137 self.var1, self.var2 = var1, var2
138 self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
139 self.negate = negate
140
141 def __repr__(self):
142 return "<IfEqualNode>"
143
144 def render(self, context):
145 val1 = resolve_variable(self.var1, context)
146 val2 = resolve_variable(self.var2, context)
147 if (self.negate and val1 != val2) or (not self.negate and val1 == val2):
148 return self.nodelist_true.render(context)
149 return self.nodelist_false.render(context)
150
151class IfNode(Node):
152 def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type):
153 self.bool_exprs = bool_exprs
154 self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
155 self.link_type = link_type
156
157 def __repr__(self):
158 return "<If node>"
159
160 def __iter__(self):
161 for node in self.nodelist_true:
162 yield node
163 for node in self.nodelist_false:
164 yield node
165
166 def get_nodes_by_type(self, nodetype):
167 nodes = []
168 if isinstance(self, nodetype):
169 nodes.append(self)
170 nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))
171 nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
172 return nodes
173
174 def render(self, context):
175 if self.link_type == IfNode.LinkTypes.or_:
176 for ifnot, bool_expr in self.bool_exprs:
177 try:
178 value = bool_expr.resolve(context)
179 except VariableDoesNotExist:
180 value = None
181 if (value and not ifnot) or (ifnot and not value):
182 return self.nodelist_true.render(context)
183 return self.nodelist_false.render(context)
184 else:
185 for ifnot, bool_expr in self.bool_exprs:
186 try:
187 value = bool_expr.resolve(context)
188 except VariableDoesNotExist:
189 value = None
190 if not ((value and not ifnot) or (ifnot and not value)):
191 return self.nodelist_false.render(context)
192 return self.nodelist_true.render(context)
193
194 class LinkTypes:
195 and_ = 0,
196 or_ = 1
197
198class RegroupNode(Node):
199 def __init__(self, target, expression, var_name):
200 self.target, self.expression = target, expression
201 self.var_name = var_name
202
203 def render(self, context):
204 obj_list = self.target.resolve(context)
205 if obj_list == '': # target_var wasn't found in context; fail silently
206 context[self.var_name] = []
207 return ''
208 output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]}
209 for obj in obj_list:
210 grouper = self.expression.resolve(Context({'var': obj}))
211 # TODO: Is this a sensible way to determine equality?
212 if output and repr(output[-1]['grouper']) == repr(grouper):
213 output[-1]['list'].append(obj)
214 else:
215 output.append({'grouper': grouper, 'list': [obj]})
216 context[self.var_name] = output
217 return ''
218
219def include_is_allowed(filepath):
220 for root in settings.ALLOWED_INCLUDE_ROOTS:
221 if filepath.startswith(root):
222 return True
223 return False
224
225class SsiNode(Node):
226 def __init__(self, filepath, parsed):
227 self.filepath, self.parsed = filepath, parsed
228
229 def render(self, context):
230 if not include_is_allowed(self.filepath):
231 if settings.DEBUG:
232 return "[Didn't have permission to include file]"
233 else:
234 return '' # Fail silently for invalid includes.
235 try:
236 fp = open(self.filepath, 'r')
237 output = fp.read()
238 fp.close()
239 except IOError:
240 output = ''
241 if self.parsed:
242 try:
243 t = Template(output)
244 return t.render(context)
245 except TemplateSyntaxError, e:
246 if settings.DEBUG:
247 return "[Included template had syntax error: %s]" % e
248 else:
249 return '' # Fail silently for invalid included templates.
250 return output
251
252class LoadNode(Node):
253 def render(self, context):
254 return ''
255
256class NowNode(Node):
257 def __init__(self, format_string):
258 self.format_string = format_string
259
260 def render(self, context):
261 from datetime import datetime
262 from django.utils.dateformat import DateFormat
263 df = DateFormat(datetime.now())
264 return df.format(self.format_string)
265
266class SpacelessNode(Node):
267 def __init__(self, nodelist):
268 self.nodelist = nodelist
269
270 def render(self, context):
271 from django.utils.html import strip_spaces_between_tags
272 return strip_spaces_between_tags(self.nodelist.render(context).strip())
273
274class TemplateTagNode(Node):
275 mapping = {'openblock': BLOCK_TAG_START,
276 'closeblock': BLOCK_TAG_END,
277 'openvariable': VARIABLE_TAG_START,
278 'closevariable': VARIABLE_TAG_END,
279 'opensinglebrace': SINGLE_VARIABLE_TAG_START,
280 'closesinglebrace': SINGLE_VARIABLE_TAG_END}
281
282 def __init__(self, tagtype):
283 self.tagtype = tagtype
284
285 def render(self, context):
286 return self.mapping.get(self.tagtype, '')
287
288class WidthRatioNode(Node):
289 def __init__(self, val_expr, max_expr, max_width):
290 self.val_expr = val_expr
291 self.max_expr = max_expr
292 self.max_width = max_width
293
294 def render(self, context):
295 try:
296 value = self.val_expr.resolve(context)
297 maxvalue = self.max_expr.resolve(context)
298 except VariableDoesNotExist:
299 return ''
300 try:
301 value = float(value)
302 maxvalue = float(maxvalue)
303 ratio = (value / maxvalue) * int(self.max_width)
304 except (ValueError, ZeroDivisionError):
305 return ''
306 return str(int(round(ratio)))
307
308#@register.tag
309def comment(parser, token):
310 """
311 Ignore everything between ``{% comment %}`` and ``{% endcomment %}``
312 """
313 parser.skip_past('endcomment')
314 return CommentNode()
315comment = register.tag(comment)
316
317#@register.tag
318def cycle(parser, token):
319 """
320 Cycle among the given strings each time this tag is encountered
321
322 Within a loop, cycles among the given strings each time through
323 the loop::
324
325 {% for o in some_list %}
326 <tr class="{% cycle row1,row2 %}">
327 ...
328 </tr>
329 {% endfor %}
330
331 Outside of a loop, give the values a unique name the first time you call
332 it, then use that name each sucessive time through::
333
334 <tr class="{% cycle row1,row2,row3 as rowcolors %}">...</tr>
335 <tr class="{% cycle rowcolors %}">...</tr>
336 <tr class="{% cycle rowcolors %}">...</tr>
337
338 You can use any number of values, seperated by commas. Make sure not to
339 put spaces between the values -- only commas.
340 """
341
342 # Note: This returns the exact same node on each {% cycle name %} call; that
343 # is, the node object returned from {% cycle a,b,c as name %} and the one
344 # returned from {% cycle name %} are the exact same object. This shouldn't
345 # cause problems (heh), but if it does, now you know.
346 #
347 # Ugly hack warning: this stuffs the named template dict into parser so
348 # that names are only unique within each template (as opposed to using
349 # a global variable, which would make cycle names have to be unique across
350 # *all* templates.
351
352 args = token.contents.split()
353 if len(args) < 2:
354 raise TemplateSyntaxError("'Cycle' statement requires at least two arguments")
355
356 elif len(args) == 2 and "," in args[1]:
357 # {% cycle a,b,c %}
358 cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks
359 return CycleNode(cyclevars)
360 # {% cycle name %}
361
362 elif len(args) == 2:
363 name = args[1]
364 if not hasattr(parser, '_namedCycleNodes'):
365 raise TemplateSyntaxError("No named cycles in template: '%s' is not defined" % name)
366 if not parser._namedCycleNodes.has_key(name):
367 raise TemplateSyntaxError("Named cycle '%s' does not exist" % name)
368 return parser._namedCycleNodes[name]
369
370 elif len(args) == 4:
371 # {% cycle a,b,c as name %}
372 if args[2] != 'as':
373 raise TemplateSyntaxError("Second 'cycle' argument must be 'as'")
374 cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks
375 name = args[3]
376 node = CycleNode(cyclevars)
377
378 if not hasattr(parser, '_namedCycleNodes'):
379 parser._namedCycleNodes = {}
380
381 parser._namedCycleNodes[name] = node
382 return node
383
384 else:
385 raise TemplateSyntaxError("Invalid arguments to 'cycle': %s" % args)
386cycle = register.tag(cycle)
387
388def debug(parser, token):
389 return DebugNode()
390debug = register.tag(debug)
391
392#@register.tag(name="filter")
393def do_filter(parser, token):
394 """
395 Filter the contents of the blog through variable filters.
396
397 Filters can also be piped through each other, and they can have
398 arguments -- just like in variable syntax.
399
400 Sample usage::
401
402 {% filter escape|lower %}
403 This text will be HTML-escaped, and will appear in lowercase.
404 {% endfilter %}
405 """
406 _, rest = token.contents.split(None, 1)
407 filter_expr = parser.compile_filter("var|%s" % (rest))
408 nodelist = parser.parse(('endfilter',))
409 parser.delete_first_token()
410 return FilterNode(filter_expr, nodelist)
411filter = register.tag("filter", do_filter)
412
413#@register.tag
414def firstof(parser, token):
415 """
416 Outputs the first variable passed that is not False.
417
418 Outputs nothing if all the passed variables are False.
419
420 Sample usage::
421
422 {% firstof var1 var2 var3 %}
423
424 This is equivalent to::
425
426 {% if var1 %}
427 {{ var1 }}
428 {% else %}{% if var2 %}
429 {{ var2 }}
430 {% else %}{% if var3 %}
431 {{ var3 }}
432 {% endif %}{% endif %}{% endif %}
433
434 but obviously much cleaner!
435 """
436 bits = token.contents.split()[1:]
437 if len(bits) < 1:
438 raise TemplateSyntaxError, "'firstof' statement requires at least one argument"
439 return FirstOfNode(bits)
440firstof = register.tag(firstof)
441
442#@register.tag(name="for")
443def do_for(parser, token):
444 """
445 Loop over each item in an array.
446
447 For example, to display a list of athletes given ``athlete_list``::
448
449 <ul>
450 {% for athlete in athlete_list %}
451 <li>{{ athlete.name }}</li>
452 {% endfor %}
453 </ul>
454
455 You can also loop over a list in reverse by using
456 ``{% for obj in list reversed %}``.
457
458 The for loop sets a number of variables available within the loop:
459
460 ========================== ================================================
461 Variable Description
462 ========================== ================================================
463 ``forloop.counter`` The current iteration of the loop (1-indexed)
464 ``forloop.counter0`` The current iteration of the loop (0-indexed)
465 ``forloop.revcounter`` The number of iterations from the end of the
466 loop (1-indexed)
467 ``forloop.revcounter0`` The number of iterations from the end of the
468 loop (0-indexed)
469 ``forloop.first`` True if this is the first time through the loop
470 ``forloop.last`` True if this is the last time through the loop
471 ``forloop.parentloop`` For nested loops, this is the loop "above" the
472 current one
473 ========================== ================================================
474
475 """
476 bits = token.contents.split()
477 if len(bits) == 5 and bits[4] != 'reversed':
478 raise TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents
479 if len(bits) not in (4, 5):
480 raise TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents
481 if bits[2] != 'in':
482 raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents
483 loopvar = bits[1]
484 sequence = parser.compile_filter(bits[3])
485 reversed = (len(bits) == 5)
486 nodelist_loop = parser.parse(('endfor',))
487 parser.delete_first_token()
488 return ForNode(loopvar, sequence, reversed, nodelist_loop)
489do_for = register.tag("for", do_for)
490
491def do_ifequal(parser, token, negate):
492 """
493 Output the contents of the block if the two arguments equal/don't equal each other.
494
495 Examples::
496
497 {% ifequal user.id comment.user_id %}
498 ...
499 {% endifequal %}
500
501 {% ifnotequal user.id comment.user_id %}
502 ...
503 {% else %}
504 ...
505 {% endifnotequal %}
506 """
507 bits = list(token.split_contents())
508 if len(bits) != 3:
509 raise TemplateSyntaxError, "%r takes two arguments" % bits[0]
510 end_tag = 'end' + bits[0]
511 nodelist_true = parser.parse(('else', end_tag))
512 token = parser.next_token()
513 if token.contents == 'else':
514 nodelist_false = parser.parse((end_tag,))
515 parser.delete_first_token()
516 else:
517 nodelist_false = NodeList()
518 return IfEqualNode(bits[1], bits[2], nodelist_true, nodelist_false, negate)
519
520#@register.tag
521def ifequal(parser, token):
522 return do_ifequal(parser, token, False)
523ifequal = register.tag(ifequal)
524
525#@register.tag
526def ifnotequal(parser, token):
527 return do_ifequal(parser, token, True)
528ifnotequal = register.tag(ifnotequal)
529
530#@register.tag(name="if")
531def do_if(parser, token):
532 """
533 The ``{% if %}`` tag evaluates a variable, and if that variable is "true"
534 (i.e. exists, is not empty, and is not a false boolean value) the contents
535 of the block are output:
536
537 ::
538
539 {% if althlete_list %}
540 Number of athletes: {{ althete_list|count }}
541 {% else %}
542 No athletes.
543 {% endif %}
544
545 In the above, if ``athlete_list`` is not empty, the number of athletes will
546 be displayed by the ``{{ athlete_list|count }}`` variable.
547
548 As you can see, the ``if`` tag can take an option ``{% else %}`` clause that
549 will be displayed if the test fails.
550
551 ``if`` tags may use ``or`` or ``not`` to test a number of variables or to
552 negate a given variable::
553
554 {% if not athlete_list %}
555 There are no athletes.
556 {% endif %}
557
558 {% if athlete_list or coach_list %}
559 There are some athletes or some coaches.
560 {% endif %}
561
562 {% if not athlete_list or coach_list %}
563 There are no athletes, or there are some coaches.
564 {% endif %}
565
566 For simplicity, ``if`` tags do not allow ``and`` clauses. Use nested ``if``
567 tags instead::
568
569 {% if athlete_list %}
570 {% if coach_list %}
571 Number of athletes: {{ athlete_list|count }}.
572 Number of coaches: {{ coach_list|count }}.
573 {% endif %}
574 {% endif %}
575 """
576 bits = token.contents.split()
577 del bits[0]
578 if not bits:
579 raise TemplateSyntaxError, "'if' statement requires at least one argument"
580 # bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d']
581 bitstr = ' '.join(bits)
582 boolpairs = bitstr.split(' and ')
583 boolvars = []
584 if len(boolpairs) == 1:
585 link_type = IfNode.LinkTypes.or_
586 boolpairs = bitstr.split(' or ')
587 else:
588 link_type = IfNode.LinkTypes.and_
589 if ' or ' in bitstr:
590 raise TemplateSyntaxError, "'if' tags can't mix 'and' and 'or'"
591 for boolpair in boolpairs:
592 if ' ' in boolpair:
593 try:
594 not_, boolvar = boolpair.split()
595 except ValueError:
596 raise TemplateSyntaxError, "'if' statement improperly formatted"
597 if not_ != 'not':
598 raise TemplateSyntaxError, "Expected 'not' in if statement"
599 boolvars.append((True, parser.compile_filter(boolvar)))
600 else:
601 boolvars.append((False, parser.compile_filter(boolpair)))
602 nodelist_true = parser.parse(('else', 'endif'))
603 token = parser.next_token()
604 if token.contents == 'else':
605 nodelist_false = parser.parse(('endif',))
606 parser.delete_first_token()
607 else:
608 nodelist_false = NodeList()
609 return IfNode(boolvars, nodelist_true, nodelist_false, link_type)
610do_if = register.tag("if", do_if)
611
612#@register.tag
613def ifchanged(parser, token):
614 """
615 Check if a value has changed from the last iteration of a loop.
616
617 The 'ifchanged' block tag is used within a loop. It checks its own rendered
618 contents against its previous state and only displays its content if the
619 value has changed::
620
621 <h1>Archive for {{ year }}</h1>
622
623 {% for date in days %}
624 {% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %}
625 <a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a>
626 {% endfor %}
627 """
628 bits = token.contents.split()
629 if len(bits) != 1:
630 raise TemplateSyntaxError, "'ifchanged' tag takes no arguments"
631 nodelist = parser.parse(('endifchanged',))
632 parser.delete_first_token()
633 return IfChangedNode(nodelist)
634ifchanged = register.tag(ifchanged)
635
636#@register.tag
637def ssi(parser, token):
638 """
639 Output the contents of a given file into the page.
640
641 Like a simple "include" tag, the ``ssi`` tag includes the contents
642 of another file -- which must be specified using an absolute page --
643 in the current page::
644
645 {% ssi /home/html/ljworld.com/includes/right_generic.html %}
646
647 If the optional "parsed" parameter is given, the contents of the included
648 file are evaluated as template code, with the current context::
649
650 {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %}
651 """
652 bits = token.contents.split()
653 parsed = False
654 if len(bits) not in (2, 3):
655 raise TemplateSyntaxError, "'ssi' tag takes one argument: the path to the file to be included"
656 if len(bits) == 3:
657 if bits[2] == 'parsed':
658 parsed = True
659 else:
660 raise TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0]
661 return SsiNode(bits[1], parsed)
662ssi = register.tag(ssi)
663
664#@register.tag
665def load(parser, token):
666 """
667 Load a custom template tag set.
668
669 For example, to load the template tags in ``django/templatetags/news/photos.py``::
670
671 {% load news.photos %}
672 """
673 bits = token.contents.split()
674 for taglib in bits[1:]:
675 # add the library to the parser
676 try:
677 lib = get_library("django.templatetags.%s" % taglib.split('.')[-1])
678 parser.add_library(lib)
679 except InvalidTemplateLibrary, e:
680 raise TemplateSyntaxError, "'%s' is not a valid tag library: %s" % (taglib, e)
681 return LoadNode()
682load = register.tag(load)
683
684#@register.tag
685def now(parser, token):
686 """
687 Display the date, formatted according to the given string.
688
689 Uses the same format as PHP's ``date()`` function; see http://php.net/date
690 for all the possible values.
691
692 Sample usage::
693
694 It is {% now "jS F Y H:i" %}
695 """
696 bits = token.contents.split('"')
697 if len(bits) != 3:
698 raise TemplateSyntaxError, "'now' statement takes one argument"
699 format_string = bits[1]
700 return NowNode(format_string)
701now = register.tag(now)
702
703#@register.tag
704def regroup(parser, token):
705 """
706 Regroup a list of alike objects by a common attribute.
707
708 This complex tag is best illustrated by use of an example: say that
709 ``people`` is a list of ``Person`` objects that have ``first_name``,
710 ``last_name``, and ``gender`` attributes, and you'd like to display a list
711 that looks like:
712
713 * Male:
714 * George Bush
715 * Bill Clinton
716 * Female:
717 * Margaret Thatcher
718 * Colendeeza Rice
719 * Unknown:
720 * Pat Smith
721
722 The following snippet of template code would accomplish this dubious task::
723
724 {% regroup people by gender as grouped %}
725 <ul>
726 {% for group in grouped %}
727 <li>{{ group.grouper }}
728 <ul>
729 {% for item in group.list %}
730 <li>{{ item }}</li>
731 {% endfor %}
732 </ul>
733 {% endfor %}
734 </ul>
735
736 As you can see, ``{% regroup %}`` populates a variable with a list of
737 objects with ``grouper`` and ``list`` attributes. ``grouper`` contains the
738 item that was grouped by; ``list`` contains the list of objects that share
739 that ``grouper``. In this case, ``grouper`` would be ``Male``, ``Female``
740 and ``Unknown``, and ``list`` is the list of people with those genders.
741
742 Note that `{% regroup %}`` does not work when the list to be grouped is not
743 sorted by the key you are grouping by! This means that if your list of
744 people was not sorted by gender, you'd need to make sure it is sorted before
745 using it, i.e.::
746
747 {% regroup people|dictsort:"gender" by gender as grouped %}
748
749 """
750 firstbits = token.contents.split(None, 3)
751 if len(firstbits) != 4:
752 raise TemplateSyntaxError, "'regroup' tag takes five arguments"
753 target = parser.compile_filter(firstbits[1])
754 if firstbits[2] != 'by':
755 raise TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'"
756 lastbits_reversed = firstbits[3][::-1].split(None, 2)
757 if lastbits_reversed[1][::-1] != 'as':
758 raise TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be 'as'"
759
760 expression = parser.compile_filter('var.%s' % lastbits_reversed[2][::-1])
761
762 var_name = lastbits_reversed[0][::-1]
763 return RegroupNode(target, expression, var_name)
764regroup = register.tag(regroup)
765
766def spaceless(parser, token):
767 """
768 Normalize whitespace between HTML tags to a single space. This includes tab
769 characters and newlines.
770
771 Example usage::
772
773 {% spaceless %}
774 <p>
775 <a href="foo/">Foo</a>
776 </p>
777 {% endspaceless %}
778
779 This example would return this HTML::
780
781 <p> <a href="foo/">Foo</a> </p>
782
783 Only space between *tags* is normalized -- not space between tags and text. In
784 this example, the space around ``Hello`` won't be stripped::
785
786 {% spaceless %}
787 <strong>
788 Hello
789 </strong>
790 {% endspaceless %}
791 """
792 nodelist = parser.parse(('endspaceless',))
793 parser.delete_first_token()
794 return SpacelessNode(nodelist)
795spaceless = register.tag(spaceless)
796
797#@register.tag
798def templatetag(parser, token):
799 """
800 Output one of the bits used to compose template tags.
801
802 Since the template system has no concept of "escaping", to display one of
803 the bits used in template tags, you must use the ``{% templatetag %}`` tag.
804
805 The argument tells which template bit to output:
806
807 ================== =======
808 Argument Outputs
809 ================== =======
810 ``openblock`` ``{%``
811 ``closeblock`` ``%}``
812 ``openvariable`` ``{{``
813 ``closevariable`` ``}}``
814 ================== =======
815 """
816 bits = token.contents.split()
817 if len(bits) != 2:
818 raise TemplateSyntaxError, "'templatetag' statement takes one argument"
819 tag = bits[1]
820 if not TemplateTagNode.mapping.has_key(tag):
821 raise TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \
822 (tag, TemplateTagNode.mapping.keys())
823 return TemplateTagNode(tag)
824templatetag = register.tag(templatetag)
825
826#@register.tag
827def widthratio(parser, token):
828 """
829 For creating bar charts and such, this tag calculates the ratio of a given
830 value to a maximum value, and then applies that ratio to a constant.
831
832 For example::
833
834 <img src='bar.gif' height='10' width='{% widthratio this_value max_value 100 %}' />
835
836 Above, if ``this_value`` is 175 and ``max_value`` is 200, the the image in
837 the above example will be 88 pixels wide (because 175/200 = .875; .875 *
838 100 = 87.5 which is rounded up to 88).
839 """
840 bits = token.contents.split()
841 if len(bits) != 4:
842 raise TemplateSyntaxError("widthratio takes three arguments")
843 tag, this_value_expr, max_value_expr, max_width = bits
844 try:
845 max_width = int(max_width)
846 except ValueError:
847 raise TemplateSyntaxError("widthratio final argument must be an integer")
848 return WidthRatioNode(parser.compile_filter(this_value_expr),
849 parser.compile_filter(max_value_expr), max_width)
850widthratio = register.tag(widthratio)
Back to Top