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