| | 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([Variable(v) for v in 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 = map(Variable, vars) |
| | 74 | |
| | 75 | def render(self, context): |
| | 76 | for var in self.vars: |
| | 77 | try: |
| | 78 | value = var.resolve(context) |
| | 79 | except VariableDoesNotExist: |
| | 80 | continue |
| | 81 | if value: |
| | 82 | return smart_unicode(value) |
| | 83 | return u'' |
| | 84 | |
| | 85 | class ForNode(Node): |
| | 86 | def __init__(self, loopvars, sequence, is_reversed, nodelist_loop): |
| | 87 | self.loopvars, self.sequence = loopvars, sequence |
| | 88 | self.is_reversed = is_reversed |
| | 89 | self.nodelist_loop = nodelist_loop |
| | 90 | |
| | 91 | def __repr__(self): |
| | 92 | reversed_text = self.is_reversed and ' reversed' or '' |
| | 93 | return "<For Node: for %s in %s, tail_len: %d%s>" % \ |
| | 94 | (', '.join(self.loopvars), self.sequence, len(self.nodelist_loop), |
| | 95 | reversed_text) |
| | 96 | |
| | 97 | def __iter__(self): |
| | 98 | for node in self.nodelist_loop: |
| | 99 | yield node |
| | 100 | |
| | 101 | def get_nodes_by_type(self, nodetype): |
| | 102 | nodes = [] |
| | 103 | if isinstance(self, nodetype): |
| | 104 | nodes.append(self) |
| | 105 | nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype)) |
| | 106 | return nodes |
| | 107 | |
| | 108 | def render(self, context): |
| | 109 | nodelist = NodeList() |
| | 110 | if 'forloop' in context: |
| | 111 | parentloop = context['forloop'] |
| | 112 | else: |
| | 113 | parentloop = {} |
| | 114 | context.push() |
| | 115 | try: |
| | 116 | values = self.sequence.resolve(context, True) |
| | 117 | except VariableDoesNotExist: |
| | 118 | values = [] |
| | 119 | if values is None: |
| | 120 | values = [] |
| | 121 | if not hasattr(values, '__len__'): |
| | 122 | values = list(values) |
| | 123 | len_values = len(values) |
| | 124 | if self.is_reversed: |
| | 125 | values = reversed(values) |
| | 126 | unpack = len(self.loopvars) > 1 |
| | 127 | # Create a forloop value in the context. We'll update counters on each |
| | 128 | # iteration just below. |
| | 129 | loop_dict = context['forloop'] = {'parentloop': parentloop} |
| | 130 | for i, item in enumerate(values): |
| | 131 | # Shortcuts for current loop iteration number. |
| | 132 | loop_dict['counter0'] = i |
| | 133 | loop_dict['counter'] = i+1 |
| | 134 | # Reverse counter iteration numbers. |
| | 135 | loop_dict['revcounter'] = len_values - i |
| | 136 | loop_dict['revcounter0'] = len_values - i - 1 |
| | 137 | # Boolean values designating first and last times through loop. |
| | 138 | loop_dict['first'] = (i == 0) |
| | 139 | loop_dict['last'] = (i == len_values - 1) |
| | 140 | |
| | 141 | if unpack: |
| | 142 | # If there are multiple loop variables, unpack the item into |
| | 143 | # them. |
| | 144 | context.update(dict(zip(self.loopvars, item))) |
| | 145 | else: |
| | 146 | context[self.loopvars[0]] = item |
| | 147 | for node in self.nodelist_loop: |
| | 148 | nodelist.append(node.render(context)) |
| | 149 | if unpack: |
| | 150 | # The loop variables were pushed on to the context so pop them |
| | 151 | # off again. This is necessary because the tag lets the length |
| | 152 | # of loopvars differ to the length of each set of items and we |
| | 153 | # don't want to leave any vars from the previous loop on the |
| | 154 | # context. |
| | 155 | context.pop() |
| | 156 | context.pop() |
| | 157 | return nodelist.render(context) |
| | 158 | |
| | 159 | class IfChangedNode(Node): |
| | 160 | def __init__(self, nodelist, *varlist): |
| | 161 | self.nodelist = nodelist |
| | 162 | self._last_seen = None |
| | 163 | self._varlist = map(Variable, varlist) |
| | 164 | self._id = str(id(self)) |
| | 165 | |
| | 166 | def render(self, context): |
| | 167 | if 'forloop' in context and self._id not in context['forloop']: |
| | 168 | self._last_seen = None |
| | 169 | context['forloop'][self._id] = 1 |
| | 170 | try: |
| | 171 | if self._varlist: |
| | 172 | # Consider multiple parameters. This automatically behaves |
| | 173 | # like an OR evaluation of the multiple variables. |
| | 174 | compare_to = [var.resolve(context) for var in self._varlist] |
| | 175 | else: |
| | 176 | compare_to = self.nodelist.render(context) |
| | 177 | except VariableDoesNotExist: |
| | 178 | compare_to = None |
| | 179 | |
| | 180 | if compare_to != self._last_seen: |
| | 181 | firstloop = (self._last_seen == None) |
| | 182 | self._last_seen = compare_to |
| | 183 | context.push() |
| | 184 | context['ifchanged'] = {'firstloop': firstloop} |
| | 185 | content = self.nodelist.render(context) |
| | 186 | context.pop() |
| | 187 | return content |
| | 188 | else: |
| | 189 | return '' |
| | 190 | |
| | 191 | class IfEqualNode(Node): |
| | 192 | def __init__(self, var1, var2, nodelist_true, nodelist_false, negate): |
| | 193 | self.var1, self.var2 = Variable(var1), Variable(var2) |
| | 194 | self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false |
| | 195 | self.negate = negate |
| | 196 | |
| | 197 | def __repr__(self): |
| | 198 | return "<IfEqualNode>" |
| | 199 | |
| | 200 | def render(self, context): |
| | 201 | try: |
| | 202 | val1 = self.var1.resolve(context) |
| | 203 | except VariableDoesNotExist: |
| | 204 | val1 = None |
| | 205 | try: |
| | 206 | val2 = self.var2.resolve(context) |
| | 207 | except VariableDoesNotExist: |
| | 208 | val2 = None |
| | 209 | if (self.negate and val1 != val2) or (not self.negate and val1 == val2): |
| | 210 | return self.nodelist_true.render(context) |
| | 211 | return self.nodelist_false.render(context) |
| | 212 | |
| | 213 | class IfNode(Node): |
| | 214 | def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type): |
| | 215 | self.bool_exprs = bool_exprs |
| | 216 | self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false |
| | 217 | self.link_type = link_type |
| | 218 | |
| | 219 | def __repr__(self): |
| | 220 | return "<If node>" |
| | 221 | |
| | 222 | def __iter__(self): |
| | 223 | for node in self.nodelist_true: |
| | 224 | yield node |
| | 225 | for node in self.nodelist_false: |
| | 226 | yield node |
| | 227 | |
| | 228 | def get_nodes_by_type(self, nodetype): |
| | 229 | nodes = [] |
| | 230 | if isinstance(self, nodetype): |
| | 231 | nodes.append(self) |
| | 232 | nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype)) |
| | 233 | nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype)) |
| | 234 | return nodes |
| | 235 | |
| | 236 | def render(self, context): |
| | 237 | if self.link_type == IfNode.LinkTypes.or_: |
| | 238 | for ifnot, bool_expr in self.bool_exprs: |
| | 239 | try: |
| | 240 | value = bool_expr.resolve(context, True) |
| | 241 | except VariableDoesNotExist: |
| | 242 | value = None |
| | 243 | if (value and not ifnot) or (ifnot and not value): |
| | 244 | return self.nodelist_true.render(context) |
| | 245 | return self.nodelist_false.render(context) |
| | 246 | else: |
| | 247 | for ifnot, bool_expr in self.bool_exprs: |
| | 248 | try: |
| | 249 | value = bool_expr.resolve(context, True) |
| | 250 | except VariableDoesNotExist: |
| | 251 | value = None |
| | 252 | if not ((value and not ifnot) or (ifnot and not value)): |
| | 253 | return self.nodelist_false.render(context) |
| | 254 | return self.nodelist_true.render(context) |
| | 255 | |
| | 256 | class LinkTypes: |
| | 257 | and_ = 0, |
| | 258 | or_ = 1 |
| | 259 | |
| | 260 | class RegroupNode(Node): |
| | 261 | def __init__(self, target, expression, var_name): |
| | 262 | self.target, self.expression = target, expression |
| | 263 | self.var_name = var_name |
| | 264 | |
| | 265 | def render(self, context): |
| | 266 | obj_list = self.target.resolve(context, True) |
| | 267 | if obj_list == None: |
| | 268 | # target variable wasn't found in context; fail silently. |
| | 269 | context[self.var_name] = [] |
| | 270 | return '' |
| | 271 | # List of dictionaries in the format: |
| | 272 | # {'grouper': 'key', 'list': [list of contents]}. |
| | 273 | context[self.var_name] = [ |
| | 274 | {'grouper': key, 'list': list(val)} |
| | 275 | for key, val in |
| | 276 | groupby(obj_list, lambda v, f=self.expression.resolve: f(v, True)) |
| | 277 | ] |
| | 278 | return '' |
| | 279 | |
| | 280 | def include_is_allowed(filepath): |
| | 281 | for root in settings.ALLOWED_INCLUDE_ROOTS: |
| | 282 | if filepath.startswith(root): |
| | 283 | return True |
| | 284 | return False |
| | 285 | |
| | 286 | class SsiNode(Node): |
| | 287 | def __init__(self, filepath, parsed): |
| | 288 | self.filepath, self.parsed = filepath, parsed |
| | 289 | |
| | 290 | def render(self, context): |
| | 291 | if not include_is_allowed(self.filepath): |
| | 292 | if settings.DEBUG: |
| | 293 | return "[Didn't have permission to include file]" |
| | 294 | else: |
| | 295 | return '' # Fail silently for invalid includes. |
| | 296 | try: |
| | 297 | fp = open(self.filepath, 'r') |
| | 298 | output = fp.read() |
| | 299 | fp.close() |
| | 300 | except IOError: |
| | 301 | output = '' |
| | 302 | if self.parsed: |
| | 303 | try: |
| | 304 | t = Template(output, name=self.filepath) |
| | 305 | return t.render(context) |
| | 306 | except TemplateSyntaxError, e: |
| | 307 | if settings.DEBUG: |
| | 308 | return "[Included template had syntax error: %s]" % e |
| | 309 | else: |
| | 310 | return '' # Fail silently for invalid included templates. |
| | 311 | return output |
| | 312 | |
| | 313 | class LoadNode(Node): |
| | 314 | def render(self, context): |
| | 315 | return '' |
| | 316 | |
| | 317 | class NowNode(Node): |
| | 318 | def __init__(self, format_string): |
| | 319 | self.format_string = format_string |
| | 320 | |
| | 321 | def render(self, context): |
| | 322 | from datetime import datetime |
| | 323 | from django.utils.dateformat import DateFormat |
| | 324 | df = DateFormat(datetime.now()) |
| | 325 | return df.format(self.format_string) |
| | 326 | |
| | 327 | class SpacelessNode(Node): |
| | 328 | def __init__(self, nodelist): |
| | 329 | self.nodelist = nodelist |
| | 330 | |
| | 331 | def render(self, context): |
| | 332 | from django.utils.html import strip_spaces_between_tags |
| | 333 | return strip_spaces_between_tags(self.nodelist.render(context).strip()) |
| | 334 | |
| | 335 | class TemplateTagNode(Node): |
| | 336 | mapping = {'openblock': BLOCK_TAG_START, |
| | 337 | 'closeblock': BLOCK_TAG_END, |
| | 338 | 'openvariable': VARIABLE_TAG_START, |
| | 339 | 'closevariable': VARIABLE_TAG_END, |
| | 340 | 'openbrace': SINGLE_BRACE_START, |
| | 341 | 'closebrace': SINGLE_BRACE_END, |
| | 342 | 'opencomment': COMMENT_TAG_START, |
| | 343 | 'closecomment': COMMENT_TAG_END, |
| | 344 | } |
| | 345 | |
| | 346 | def __init__(self, tagtype): |
| | 347 | self.tagtype = tagtype |
| | 348 | |
| | 349 | def render(self, context): |
| | 350 | return self.mapping.get(self.tagtype, '') |
| | 351 | |
| | 352 | class URLNode(Node): |
| | 353 | def __init__(self, view_name, args, kwargs): |
| | 354 | self.view_name = view_name |
| | 355 | self.args = args |
| | 356 | self.kwargs = kwargs |
| | 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 | try: |
| | 364 | return reverse(self.view_name, args=args, kwargs=kwargs) |
| | 365 | except NoReverseMatch: |
| | 366 | try: |
| | 367 | project_name = settings.SETTINGS_MODULE.split('.')[0] |
| | 368 | return reverse(project_name + '.' + self.view_name, |
| | 369 | args=args, kwargs=kwargs) |
| | 370 | except NoReverseMatch: |
| | 371 | return '' |
| | 372 | |
| | 373 | class WidthRatioNode(Node): |
| | 374 | def __init__(self, val_expr, max_expr, max_width): |
| | 375 | self.val_expr = val_expr |
| | 376 | self.max_expr = max_expr |
| | 377 | self.max_width = max_width |
| | 378 | |
| | 379 | def render(self, context): |
| | 380 | try: |
| | 381 | value = self.val_expr.resolve(context) |
| | 382 | maxvalue = self.max_expr.resolve(context) |
| | 383 | except VariableDoesNotExist: |
| | 384 | return '' |
| | 385 | try: |
| | 386 | value = float(value) |
| | 387 | maxvalue = float(maxvalue) |
| | 388 | ratio = (value / maxvalue) * int(self.max_width) |
| | 389 | except (ValueError, ZeroDivisionError): |
| | 390 | return '' |
| | 391 | return str(int(round(ratio))) |
| | 392 | |
| | 393 | class WithNode(Node): |
| | 394 | def __init__(self, var, name, nodelist): |
| | 395 | self.var = var |
| | 396 | self.name = name |
| | 397 | self.nodelist = nodelist |
| | 398 | |
| | 399 | def __repr__(self): |
| | 400 | return "<WithNode>" |
| | 401 | |
| | 402 | def render(self, context): |
| | 403 | val = self.var.resolve(context) |
| | 404 | context.push() |
| | 405 | context[self.name] = val |
| | 406 | output = self.nodelist.render(context) |
| | 407 | context.pop() |
| | 408 | return output |
| | 409 | |
| | 410 | #@register.tag |
| | 411 | def autoescape(parser, token): |
| | 412 | """ |
| | 413 | Force autoescape behaviour for this block. |
| | 414 | """ |
| | 415 | args = token.contents.split() |
| | 416 | if len(args) != 2: |
| | 417 | raise TemplateSyntaxError("'Autoescape' tag requires exactly one argument.") |
| | 418 | arg = args[1] |
| | 419 | if arg not in (u'on', u'off'): |
| | 420 | raise TemplateSyntaxError("'Autoescape' argument should be 'on' or 'off'") |
| | 421 | nodelist = parser.parse(('endautoescape',)) |
| | 422 | parser.delete_first_token() |
| | 423 | return AutoEscapeControlNode((arg == 'on'), nodelist) |
| | 424 | autoescape = register.tag(autoescape) |
| | 425 | |
| | 426 | #@register.tag |
| | 427 | def comment(parser, token): |
| | 428 | """ |
| | 429 | Ignores everything between ``{% comment %}`` and ``{% endcomment %}``. |
| | 430 | """ |
| | 431 | parser.skip_past('endcomment') |
| | 432 | return CommentNode() |
| | 433 | comment = register.tag(comment) |
| | 434 | |
| | 435 | #@register.tag |
| | 436 | def cycle(parser, token): |
| | 437 | """ |
| | 438 | Cycles among the given strings each time this tag is encountered. |
| | 439 | |
| | 440 | Within a loop, cycles among the given strings each time through |
| | 441 | the loop:: |
| | 442 | |
| | 443 | {% for o in some_list %} |
| | 444 | <tr class="{% cycle 'row1' 'row2' %}"> |
| | 445 | ... |
| | 446 | </tr> |
| | 447 | {% endfor %} |
| | 448 | |
| | 449 | Outside of a loop, give the values a unique name the first time you call |
| | 450 | it, then use that name each sucessive time through:: |
| | 451 | |
| | 452 | <tr class="{% cycle 'row1' 'row2' 'row3' as rowcolors %}">...</tr> |
| | 453 | <tr class="{% cycle rowcolors %}">...</tr> |
| | 454 | <tr class="{% cycle rowcolors %}">...</tr> |
| | 455 | |
| | 456 | You can use any number of values, separated by spaces. Commas can also |
| | 457 | be used to separate values; if a comma is used, the cycle values are |
| | 458 | interpreted as literal strings. |
| | 459 | """ |
| | 460 | |
| | 461 | # Note: This returns the exact same node on each {% cycle name %} call; |
| | 462 | # that is, the node object returned from {% cycle a b c as name %} and the |
| | 463 | # one returned from {% cycle name %} are the exact same object. This |
| | 464 | # shouldn't cause problems (heh), but if it does, now you know. |
| | 465 | # |
| | 466 | # Ugly hack warning: This stuffs the named template dict into parser so |
| | 467 | # that names are only unique within each template (as opposed to using |
| | 468 | # a global variable, which would make cycle names have to be unique across |
| | 469 | # *all* templates. |
| | 470 | |
| | 471 | args = token.split_contents() |
| | 472 | |
| | 473 | if len(args) < 2: |
| | 474 | raise TemplateSyntaxError("'cycle' tag requires at least two arguments") |
| | 475 | |
| | 476 | if ',' in args[1]: |
| | 477 | # Backwards compatibility: {% cycle a,b %} or {% cycle a,b as foo %} |
| | 478 | # case. |
| | 479 | args[1:2] = ['"%s"' % arg for arg in args[1].split(",")] |
| | 480 | |
| | 481 | if len(args) == 2: |
| | 482 | # {% cycle foo %} case. |
| | 483 | name = args[1] |
| | 484 | if not hasattr(parser, '_namedCycleNodes'): |
| | 485 | raise TemplateSyntaxError("No named cycles in template. '%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 |
| | 686 | contents of the block are output: |
| | 687 | |
| | 688 | :: |
| | 689 | |
| | 690 | {% if athlete_list %} |
| | 691 | Number of athletes: {{ athlete_list|count }} |
| | 692 | {% else %} |
| | 693 | No athletes. |
| | 694 | {% endif %} |
| | 695 | |
| | 696 | In the above, if ``athlete_list`` is not empty, the number of athletes will |
| | 697 | be displayed by the ``{{ athlete_list|count }}`` variable. |
| | 698 | |
| | 699 | As you can see, the ``if`` tag can take an option ``{% else %}`` clause |
| | 700 | that will be displayed if the test fails. |
| | 701 | |
| | 702 | ``if`` tags may use ``or``, ``and`` or ``not`` to test a number of |
| | 703 | variables or to negate a given variable:: |
| | 704 | |
| | 705 | {% if not athlete_list %} |
| | 706 | There are no athletes. |
| | 707 | {% endif %} |
| | 708 | |
| | 709 | {% if athlete_list or coach_list %} |
| | 710 | There are some athletes or some coaches. |
| | 711 | {% endif %} |
| | 712 | |
| | 713 | {% if athlete_list and coach_list %} |
| | 714 | Both atheletes and coaches are available. |
| | 715 | {% endif %} |
| | 716 | |
| | 717 | {% if not athlete_list or coach_list %} |
| | 718 | There are no athletes, or there are some coaches. |
| | 719 | {% endif %} |
| | 720 | |
| | 721 | {% if athlete_list and not coach_list %} |
| | 722 | There are some athletes and absolutely no coaches. |
| | 723 | {% endif %} |
| | 724 | |
| | 725 | ``if`` tags do not allow ``and`` and ``or`` clauses with the same tag, |
| | 726 | because the order of logic would be ambigous. For example, this is |
| | 727 | invalid:: |
| | 728 | |
| | 729 | {% if athlete_list and coach_list or cheerleader_list %} |
| | 730 | |
| | 731 | If you need to combine ``and`` and ``or`` to do advanced logic, just use |
| | 732 | nested if tags. For example:: |
| | 733 | |
| | 734 | {% if athlete_list %} |
| | 735 | {% if coach_list or cheerleader_list %} |
| | 736 | We have athletes, and either coaches or cheerleaders! |
| | 737 | {% endif %} |
| | 738 | {% endif %} |
| | 739 | """ |
| | 740 | bits = token.contents.split() |
| | 741 | del bits[0] |
| | 742 | if not bits: |
| | 743 | raise TemplateSyntaxError("'if' statement requires at least one argument") |
| | 744 | # Bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d'] |
| | 745 | bitstr = ' '.join(bits) |
| | 746 | boolpairs = bitstr.split(' and ') |
| | 747 | boolvars = [] |
| | 748 | if len(boolpairs) == 1: |
| | 749 | link_type = IfNode.LinkTypes.or_ |
| | 750 | boolpairs = bitstr.split(' or ') |
| | 751 | else: |
| | 752 | link_type = IfNode.LinkTypes.and_ |
| | 753 | if ' or ' in bitstr: |
| | 754 | raise TemplateSyntaxError, "'if' tags can't mix 'and' and 'or'" |
| | 755 | for boolpair in boolpairs: |
| | 756 | if ' ' in boolpair: |
| | 757 | try: |
| | 758 | not_, boolvar = boolpair.split() |
| | 759 | except ValueError: |
| | 760 | raise TemplateSyntaxError, "'if' statement improperly formatted" |
| | 761 | if not_ != 'not': |
| | 762 | raise TemplateSyntaxError, "Expected 'not' in if statement" |
| | 763 | boolvars.append((True, parser.compile_filter(boolvar))) |
| | 764 | else: |
| | 765 | boolvars.append((False, parser.compile_filter(boolpair))) |
| | 766 | nodelist_true = parser.parse(('else', 'endif')) |
| | 767 | token = parser.next_token() |
| | 768 | if token.contents == 'else': |
| | 769 | nodelist_false = parser.parse(('endif',)) |
| | 770 | parser.delete_first_token() |
| | 771 | else: |
| | 772 | nodelist_false = NodeList() |
| | 773 | return IfNode(boolvars, nodelist_true, nodelist_false, link_type) |
| | 774 | do_if = register.tag("if", do_if) |
| | 775 | |
| | 776 | #@register.tag |
| | 777 | def ifchanged(parser, token): |
| | 778 | """ |
| | 779 | Checks if a value has changed from the last iteration of a loop. |
| | 780 | |
| | 781 | The 'ifchanged' block tag is used within a loop. It has two possible uses. |
| | 782 | |
| | 783 | 1. Checks its own rendered contents against its previous state and only |
| | 784 | displays the content if it has changed. For example, this displays a |
| | 785 | list of days, only displaying the month if it changes:: |
| | 786 | |
| | 787 | <h1>Archive for {{ year }}</h1> |
| | 788 | |
| | 789 | {% for date in days %} |
| | 790 | {% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %} |
| | 791 | <a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a> |
| | 792 | {% endfor %} |
| | 793 | |
| | 794 | 2. If given a variable, check whether that variable has changed. |
| | 795 | For example, the following shows the date every time it changes, but |
| | 796 | only shows the hour if both the hour and the date have changed:: |
| | 797 | |
| | 798 | {% for date in days %} |
| | 799 | {% ifchanged date.date %} {{ date.date }} {% endifchanged %} |
| | 800 | {% ifchanged date.hour date.date %} |
| | 801 | {{ date.hour }} |
| | 802 | {% endifchanged %} |
| | 803 | {% endfor %} |
| | 804 | """ |
| | 805 | bits = token.contents.split() |
| | 806 | nodelist = parser.parse(('endifchanged',)) |
| | 807 | parser.delete_first_token() |
| | 808 | return IfChangedNode(nodelist, *bits[1:]) |
| | 809 | ifchanged = register.tag(ifchanged) |
| | 810 | |
| | 811 | #@register.tag |
| | 812 | def ssi(parser, token): |
| | 813 | """ |
| | 814 | Outputs the contents of a given file into the page. |
| | 815 | |
| | 816 | Like a simple "include" tag, the ``ssi`` tag includes the contents |
| | 817 | of another file -- which must be specified using an absolute path -- |
| | 818 | in the current page:: |
| | 819 | |
| | 820 | {% ssi /home/html/ljworld.com/includes/right_generic.html %} |
| | 821 | |
| | 822 | If the optional "parsed" parameter is given, the contents of the included |
| | 823 | file are evaluated as template code, with the current context:: |
| | 824 | |
| | 825 | {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %} |
| | 826 | """ |
| | 827 | bits = token.contents.split() |
| | 828 | parsed = False |
| | 829 | if len(bits) not in (2, 3): |
| | 830 | raise TemplateSyntaxError("'ssi' tag takes one argument: the path to" |
| | 831 | " the file to be included") |
| | 832 | if len(bits) == 3: |
| | 833 | if bits[2] == 'parsed': |
| | 834 | parsed = True |
| | 835 | else: |
| | 836 | raise TemplateSyntaxError("Second (optional) argument to %s tag" |
| | 837 | " must be 'parsed'" % bits[0]) |
| | 838 | return SsiNode(bits[1], parsed) |
| | 839 | ssi = register.tag(ssi) |
| | 840 | |
| | 841 | #@register.tag |
| | 842 | def load(parser, token): |
| | 843 | """ |
| | 844 | Loads a custom template tag set. |
| | 845 | |
| | 846 | For example, to load the template tags in |
| | 847 | ``django/templatetags/news/photos.py``:: |
| | 848 | |
| | 849 | {% load news.photos %} |
| | 850 | """ |
| | 851 | bits = token.contents.split() |
| | 852 | for taglib in bits[1:]: |
| | 853 | # add the library to the parser |
| | 854 | try: |
| | 855 | lib = get_library(taglib) |
| | 856 | parser.add_library(lib) |
| | 857 | except InvalidTemplateLibrary, e: |
| | 858 | raise TemplateSyntaxError("'%s' is not a valid tag library: %s" % |
| | 859 | (taglib, e)) |
| | 860 | return LoadNode() |
| | 861 | load = register.tag(load) |
| | 862 | |
| | 863 | #@register.tag |
| | 864 | def now(parser, token): |
| | 865 | """ |
| | 866 | Displays the date, formatted according to the given string. |
| | 867 | |
| | 868 | Uses the same format as PHP's ``date()`` function; see http://php.net/date |
| | 869 | for all the possible values. |
| | 870 | |
| | 871 | Sample usage:: |
| | 872 | |
| | 873 | It is {% now "jS F Y H:i" %} |
| | 874 | """ |
| | 875 | bits = token.contents.split('"') |
| | 876 | if len(bits) != 3: |
| | 877 | raise TemplateSyntaxError, "'now' statement takes one argument" |
| | 878 | format_string = bits[1] |
| | 879 | return NowNode(format_string) |
| | 880 | now = register.tag(now) |
| | 881 | |
| | 882 | #@register.tag |
| | 883 | def regroup(parser, token): |
| | 884 | """ |
| | 885 | Regroups a list of alike objects by a common attribute. |
| | 886 | |
| | 887 | This complex tag is best illustrated by use of an example: say that |
| | 888 | ``people`` is a list of ``Person`` objects that have ``first_name``, |
| | 889 | ``last_name``, and ``gender`` attributes, and you'd like to display a list |
| | 890 | that looks like: |
| | 891 | |
| | 892 | * Male: |
| | 893 | * George Bush |
| | 894 | * Bill Clinton |
| | 895 | * Female: |
| | 896 | * Margaret Thatcher |
| | 897 | * Colendeeza Rice |
| | 898 | * Unknown: |
| | 899 | * Pat Smith |
| | 900 | |
| | 901 | The following snippet of template code would accomplish this dubious task:: |
| | 902 | |
| | 903 | {% regroup people by gender as grouped %} |
| | 904 | <ul> |
| | 905 | {% for group in grouped %} |
| | 906 | <li>{{ group.grouper }} |
| | 907 | <ul> |
| | 908 | {% for item in group.list %} |
| | 909 | <li>{{ item }}</li> |
| | 910 | {% endfor %} |
| | 911 | </ul> |
| | 912 | {% endfor %} |
| | 913 | </ul> |
| | 914 | |
| | 915 | As you can see, ``{% regroup %}`` populates a variable with a list of |
| | 916 | objects with ``grouper`` and ``list`` attributes. ``grouper`` contains the |
| | 917 | item that was grouped by; ``list`` contains the list of objects that share |
| | 918 | that ``grouper``. In this case, ``grouper`` would be ``Male``, ``Female`` |
| | 919 | and ``Unknown``, and ``list`` is the list of people with those genders. |
| | 920 | |
| | 921 | Note that `{% regroup %}`` does not work when the list to be grouped is not |
| | 922 | sorted by the key you are grouping by! This means that if your list of |
| | 923 | people was not sorted by gender, you'd need to make sure it is sorted |
| | 924 | before using it, i.e.:: |
| | 925 | |
| | 926 | {% regroup people|dictsort:"gender" by gender as grouped %} |
| | 927 | |
| | 928 | """ |
| | 929 | firstbits = token.contents.split(None, 3) |
| | 930 | if len(firstbits) != 4: |
| | 931 | raise TemplateSyntaxError, "'regroup' tag takes five arguments" |
| | 932 | target = parser.compile_filter(firstbits[1]) |
| | 933 | if firstbits[2] != 'by': |
| | 934 | raise TemplateSyntaxError("second argument to 'regroup' tag must be 'by'") |
| | 935 | lastbits_reversed = firstbits[3][::-1].split(None, 2) |
| | 936 | if lastbits_reversed[1][::-1] != 'as': |
| | 937 | raise TemplateSyntaxError("next-to-last argument to 'regroup' tag must" |
| | 938 | " be 'as'") |
| | 939 | |
| | 940 | expression = parser.compile_filter(lastbits_reversed[2][::-1]) |
| | 941 | |
| | 942 | var_name = lastbits_reversed[0][::-1] |
| | 943 | return RegroupNode(target, expression, var_name) |
| | 944 | regroup = register.tag(regroup) |
| | 945 | |
| | 946 | def spaceless(parser, token): |
| | 947 | """ |
| | 948 | Removes whitespace between HTML tags, including tab and newline characters. |
| | 949 | |
| | 950 | Example usage:: |
| | 951 | |
| | 952 | {% spaceless %} |
| | 953 | <p> |
| | 954 | <a href="foo/">Foo</a> |
| | 955 | </p> |
| | 956 | {% endspaceless %} |
| | 957 | |
| | 958 | This example would return this HTML:: |
| | 959 | |
| | 960 | <p><a href="foo/">Foo</a></p> |
| | 961 | |
| | 962 | Only space between *tags* is normalized -- not space between tags and text. |
| | 963 | In this example, the space around ``Hello`` won't be stripped:: |
| | 964 | |
| | 965 | {% spaceless %} |
| | 966 | <strong> |
| | 967 | Hello |
| | 968 | </strong> |
| | 969 | {% endspaceless %} |
| | 970 | """ |
| | 971 | nodelist = parser.parse(('endspaceless',)) |
| | 972 | parser.delete_first_token() |
| | 973 | return SpacelessNode(nodelist) |
| | 974 | spaceless = register.tag(spaceless) |
| | 975 | |
| | 976 | #@register.tag |
| | 977 | def templatetag(parser, token): |
| | 978 | """ |
| | 979 | Outputs one of the bits used to compose template tags. |
| | 980 | |
| | 981 | Since the template system has no concept of "escaping", to display one of |
| | 982 | the bits used in template tags, you must use the ``{% templatetag %}`` tag. |
| | 983 | |
| | 984 | The argument tells which template bit to output: |
| | 985 | |
| | 986 | ================== ======= |
| | 987 | Argument Outputs |
| | 988 | ================== ======= |
| | 989 | ``openblock`` ``{%`` |
| | 990 | ``closeblock`` ``%}`` |
| | 991 | ``openvariable`` ``{{`` |
| | 992 | ``closevariable`` ``}}`` |
| | 993 | ``openbrace`` ``{`` |
| | 994 | ``closebrace`` ``}`` |
| | 995 | ``opencomment`` ``{#`` |
| | 996 | ``closecomment`` ``#}`` |
| | 997 | ================== ======= |
| | 998 | """ |
| | 999 | bits = token.contents.split() |
| | 1000 | if len(bits) != 2: |
| | 1001 | raise TemplateSyntaxError, "'templatetag' statement takes one argument" |
| | 1002 | tag = bits[1] |
| | 1003 | if tag not in TemplateTagNode.mapping: |
| | 1004 | raise TemplateSyntaxError("Invalid templatetag argument: '%s'." |
| | 1005 | " Must be one of: %s" % |
| | 1006 | (tag, TemplateTagNode.mapping.keys())) |
| | 1007 | return TemplateTagNode(tag) |
| | 1008 | templatetag = register.tag(templatetag) |
| | 1009 | |
| | 1010 | def url(parser, token): |
| | 1011 | """ |
| | 1012 | Returns an absolute URL matching given view with its parameters. |
| | 1013 | |
| | 1014 | This is a way to define links that aren't tied to a particular URL |
| | 1015 | configuration:: |
| | 1016 | |
| | 1017 | {% url path.to.some_view arg1,arg2,name1=value1 %} |
| | 1018 | |
| | 1019 | The first argument is a path to a view. It can be an absolute python path |
| | 1020 | or just ``app_name.view_name`` without the project name if the view is |
| | 1021 | located inside the project. Other arguments are comma-separated values |
| | 1022 | that will be filled in place of positional and keyword arguments in the |
| | 1023 | URL. All arguments for the URL should be present. |
| | 1024 | |
| | 1025 | For example if you have a view ``app_name.client`` taking client's id and |
| | 1026 | the corresponding line in a URLconf looks like this:: |
| | 1027 | |
| | 1028 | ('^client/(\d+)/$', 'app_name.client') |
| | 1029 | |
| | 1030 | and this app's URLconf is included into the project's URLconf under some |
| | 1031 | path:: |
| | 1032 | |
| | 1033 | ('^clients/', include('project_name.app_name.urls')) |
| | 1034 | |
| | 1035 | then in a template you can create a link for a certain client like this:: |
| | 1036 | |
| | 1037 | {% url app_name.client client.id %} |
| | 1038 | |
| | 1039 | The URL will look like ``/clients/client/123/``. |
| | 1040 | """ |
| | 1041 | bits = token.contents.split(' ', 2) |
| | 1042 | if len(bits) < 2: |
| | 1043 | raise TemplateSyntaxError("'%s' takes at least one argument" |
| | 1044 | " (path to a view)" % bits[0]) |
| | 1045 | args = [] |
| | 1046 | kwargs = {} |
| | 1047 | if len(bits) > 2: |
| | 1048 | for arg in bits[2].split(','): |
| | 1049 | if '=' in arg: |
| | 1050 | k, v = arg.split('=', 1) |
| | 1051 | k = k.strip() |
| | 1052 | kwargs[k] = parser.compile_filter(v) |
| | 1053 | else: |
| | 1054 | args.append(parser.compile_filter(arg)) |
| | 1055 | return URLNode(bits[1], args, kwargs) |
| | 1056 | url = register.tag(url) |
| | 1057 | |
| | 1058 | #@register.tag |
| | 1059 | def widthratio(parser, token): |
| | 1060 | """ |
| | 1061 | For creating bar charts and such, this tag calculates the ratio of a given |
| | 1062 | value to a maximum value, and then applies that ratio to a constant. |
| | 1063 | |
| | 1064 | For example:: |
| | 1065 | |
| | 1066 | <img src='bar.gif' height='10' width='{% widthratio this_value max_value 100 %}' /> |
| | 1067 | |
| | 1068 | Above, if ``this_value`` is 175 and ``max_value`` is 200, the the image in |
| | 1069 | the above example will be 88 pixels wide (because 175/200 = .875; |
| | 1070 | .875 * 100 = 87.5 which is rounded up to 88). |
| | 1071 | """ |
| | 1072 | bits = token.contents.split() |
| | 1073 | if len(bits) != 4: |
| | 1074 | raise TemplateSyntaxError("widthratio takes three arguments") |
| | 1075 | tag, this_value_expr, max_value_expr, max_width = bits |
| | 1076 | try: |
| | 1077 | max_width = int(max_width) |
| | 1078 | except ValueError: |
| | 1079 | raise TemplateSyntaxError("widthratio final argument must be an integer") |
| | 1080 | return WidthRatioNode(parser.compile_filter(this_value_expr), |
| | 1081 | parser.compile_filter(max_value_expr), max_width) |
| | 1082 | widthratio = register.tag(widthratio) |
| | 1083 | |
| | 1084 | #@register.tag |
| | 1085 | def do_with(parser, token): |
| | 1086 | """ |
| | 1087 | Adds a value to the context (inside of this block) for caching and easy |
| | 1088 | access. |
| | 1089 | |
| | 1090 | For example:: |
| | 1091 | |
| | 1092 | {% with person.some_sql_method as total %} |
| | 1093 | {{ total }} object{{ total|pluralize }} |
| | 1094 | {% endwith %} |
| | 1095 | """ |
| | 1096 | bits = list(token.split_contents()) |
| | 1097 | if len(bits) != 4 or bits[2] != "as": |
| | 1098 | raise TemplateSyntaxError("%r expected format is 'value as name'" % |
| | 1099 | bits[0]) |
| | 1100 | var = parser.compile_filter(bits[1]) |
| | 1101 | name = bits[3] |
| | 1102 | nodelist = parser.parse(('endwith',)) |
| | 1103 | parser.delete_first_token() |
| | 1104 | return WithNode(var, name, nodelist) |
| | 1105 | do_with = register.tag('with', do_with) |