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