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