Ticket #626: 626_working.patch
File 626_working.patch, 122.9 KB (added by , 19 years ago) |
---|
-
template_loader.py
1 1 "Wrapper for loading templates from storage of some sort (e.g. files or db)" 2 import template3 from template_file import load_template_source4 2 3 from django.core import template 4 from django.core.template.loaders.filesystem import load_template_source 5 5 6 class ExtendsError(Exception): 6 7 pass 7 8 … … 24 25 Loads the given template_name and renders it with the given dictionary as 25 26 context. The template_name may be a string to load a single template using 26 27 get_template, or it may be a tuple to use select_template to find one of 27 the templates in the list. Returns a string. 28 the templates in the list. Returns a string. 28 29 """ 29 30 dictionary = dictionary or {} 30 31 if isinstance(template_name, (list, tuple)): -
defaulttags.py
1 "Default tags used by the template system, available to all templates."2 3 import sys4 import template5 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 = cyclevars13 self.cyclevars_len = len(cyclevars)14 self.counter = -115 16 def render(self, context):17 self.counter += 118 return self.cyclevars[self.counter % self.cyclevars_len]19 20 class DebugNode(template.Node):21 def render(self, context):22 from pprint import pformat23 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, nodelist31 32 def render(self, context):33 output = self.nodelist.render(context)34 # apply filters35 for f in self.filters:36 output = template.registered_filters[f[0]][0](output, f[1])37 return output38 39 class FirstOfNode(template.Node):40 def __init__(self, vars):41 self.vars = vars42 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, sequence53 self.reversed = reversed54 self.nodelist_loop = nodelist_loop55 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 node67 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 nodes74 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.html91 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 number98 'counter0': i,99 'counter': i+1,100 # reverse counter iteration numbers101 'revcounter': len_values - i,102 'revcounter0': len_values - i - 1,103 # boolean values designating first and last times through loop104 'first': (i == 0),105 'last': (i == len_values - 1),106 'parentloop': parentloop,107 }108 context[self.loopvar] = item109 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 = nodelist117 self._last_seen = None118 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 = content124 context.push()125 context['ifchanged'] = {'firstloop': firstloop}126 content = self.nodelist.render(context)127 context.pop()128 return content129 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, var2135 self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false136 self.negate = negate137 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 = boolvars151 self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false152 153 def __repr__(self):154 return "<If node>"155 156 def __iter__(self):157 for node in self.nodelist_true:158 yield node159 for node in self.nodelist_false:160 yield node161 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 nodes169 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 = None176 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, expression183 self.var_name = var_name184 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 silently188 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] = output199 return ''200 201 def include_is_allowed(filepath):202 from django.conf.settings import ALLOWED_INCLUDE_ROOTS203 for root in ALLOWED_INCLUDE_ROOTS:204 if filepath.startswith(root):205 return True206 return False207 208 class SsiNode(template.Node):209 def __init__(self, filepath, parsed):210 self.filepath, self.parsed = filepath, parsed211 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 output228 229 class LoadNode(template.Node):230 def __init__(self, taglib):231 self.taglib = taglib232 233 def load_taglib(taglib):234 mod = __import__("django.templatetags.%s" % taglib.split('.')[-1], '', '', [''])235 reload(mod)236 return mod237 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_string250 251 def render(self, context):252 from datetime import datetime253 from django.utils.dateformat import DateFormat254 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 = tagtype265 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_var272 self.max_var = max_var273 self.max_width = max_width274 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 encountered300 301 Within a loop, cycles among the given strings each time through302 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 call311 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 to318 put spaces between the values -- only commas.319 """320 321 # Note: This returns the exact same node on each {% cycle name %} call; that322 # is, the node object returned from {% cycle a,b,c as name %} and the one323 # returned from {% cycle name %} are the exact same object. This shouldn't324 # cause problems (heh), but if it does, now you know.325 #326 # Ugly hack warning: this stuffs the named template dict into parser so327 # that names are only unique within each template (as opposed to using328 # a global variable, which would make cycle names have to be unique across329 # *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 blanks338 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 blanks352 name = args[3]353 node = CycleNode(cyclevars)354 355 if not hasattr(parser, '_namedCycleNodes'):356 parser._namedCycleNodes = {}357 358 parser._namedCycleNodes[name] = node359 return node360 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 have373 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 using428 ``{% for obj in list reversed %}``.429 430 The for loop sets a number of variables available within the loop:431 432 ========================== ================================================433 Variable Description434 ========================== ================================================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 the438 loop (1-indexed)439 ``forloop.revcounter0`` The number of iterations from the end of the440 loop (0-indexed)441 ``forloop.first`` True if this is the first time through the loop442 ``forloop.last`` True if this is the last time through the loop443 ``forloop.parentloop`` For nested loops, this is the loop "above" the444 current one445 ========================== ================================================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.contents451 if len(bits) not in (4, 5):452 raise template.TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents453 if bits[2] != 'in':454 raise template.TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents455 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 contents495 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 will506 be displayed by the ``{{ athlete_list|count }}`` variable.507 508 As you can see, the ``if`` tag can take an option ``{% else %}`` clause that509 will be displayed if the test fails.510 511 ``if`` tags may use ``or`` or ``not`` to test a number of variables or to512 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, so524 writing English translations of boolean logic sounds525 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 rendered567 contents against its previous state and only displays its content if the568 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 contents589 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 included595 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 = False601 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 = True606 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 imported623 try:624 LoadNode.load_taglib(taglib)625 except ImportError:626 raise template.TemplateSyntaxError, "'%s' is not a valid tag library" % taglib627 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/date634 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 that651 ``people`` is a list of ``Person`` objects that have ``first_name``,652 ``last_name``, and ``gender`` attributes, and you'd like to display a list653 that looks like:654 655 * Male:656 * George Bush657 * Bill Clinton658 * Female:659 * Margaret Thatcher660 * Colendeeza Rice661 * Unknown:662 * Pat Smith663 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 of679 objects with ``grouper`` and ``list`` attributes. ``grouper`` contains the680 item that was grouped by; ``list`` contains the list of objects that share681 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 not685 sorted by the key you are grouping by! This means that if your list of686 people was not sorted by gender, you'd need to make sure it is sorted before687 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 of710 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 Outputs716 ================== =======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 given735 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 in742 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 = bits749 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) -
template_file.py
1 # Wrapper for loading templates from files2 3 from django.conf.settings import TEMPLATE_DIRS, TEMPLATE_FILE_EXTENSION4 from django.core.template import TemplateDoesNotExist5 import os6 7 def load_template_source(template_name, template_dirs=None):8 if not template_dirs:9 template_dirs = TEMPLATE_DIRS10 tried = []11 for template_dir in template_dirs:12 filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION13 try:14 return open(filepath).read()15 except IOError:16 tried.append(filepath)17 if template_dirs:18 error_msg = "Tried %s" % tried19 else:20 error_msg = "Your TEMPLATE_DIRS settings is empty. Change it to point to at least one template directory."21 raise TemplateDoesNotExist, error_msg -
template.py
1 """2 This is the Django template system.3 4 How it works:5 6 The tokenize() function converts a template string (i.e., a string containing7 markup with custom template tags) to tokens, which can be either plain text8 (TOKEN_TEXT), variables (TOKEN_VAR) or block statements (TOKEN_BLOCK).9 10 The Parser() class takes a list of tokens in its constructor, and its parse()11 method returns a compiled template -- which is, under the hood, a list of12 Node objects.13 14 Each Node is responsible for creating some sort of output -- e.g. simple text15 (TextNode), variable values in a given context (VariableNode), results of basic16 logic (IfNode), results of looping (ForNode), or anything else. The core Node17 types are TextNode, VariableNode, IfNode and ForNode, but plugin modules can18 define their own custom node types.19 20 Each Node has a render() method, which takes a Context and returns a string of21 the rendered node. For example, the render() method of a Variable Node returns22 the variable's value as a string. The render() method of an IfNode returns the23 rendered output of whatever was inside the loop, recursively.24 25 The Template class is a convenient wrapper that takes care of template26 compilation and rendering.27 28 Usage:29 30 The only thing you should ever use directly in this file is the Template class.31 Create a compiled template object with a template_string, then call render()32 with a context. In the compilation stage, the TemplateSyntaxError exception33 will be raised if the template doesn't have proper syntax.34 35 Sample code:36 37 >>> import template38 >>> s = '''39 ... <html>40 ... {% if test %}41 ... <h1>{{ varvalue }}</h1>42 ... {% endif %}43 ... </html>44 ... '''45 >>> t = template.Template(s)46 47 (t is now a compiled template, and its render() method can be called multiple48 times with multiple contexts)49 50 >>> c = template.Context({'test':True, 'varvalue': 'Hello'})51 >>> t.render(c)52 '\n<html>\n\n <h1>Hello</h1>\n\n</html>\n'53 >>> c = template.Context({'test':False, 'varvalue': 'Hello'})54 >>> t.render(c)55 '\n<html>\n\n</html>\n'56 """57 import re58 from django.conf.settings import DEFAULT_CHARSET59 60 __all__ = ('Template','Context','compile_string')61 62 TOKEN_TEXT = 063 TOKEN_VAR = 164 TOKEN_BLOCK = 265 66 # template syntax constants67 FILTER_SEPARATOR = '|'68 FILTER_ARGUMENT_SEPARATOR = ':'69 VARIABLE_ATTRIBUTE_SEPARATOR = '.'70 BLOCK_TAG_START = '{%'71 BLOCK_TAG_END = '%}'72 VARIABLE_TAG_START = '{{'73 VARIABLE_TAG_END = '}}'74 75 ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.'76 77 # match a variable or block tag and capture the entire tag, including start/end delimiters78 tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),79 re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END)))80 81 # global dict used by register_tag; maps custom tags to callback functions82 registered_tags = {}83 84 # global dict used by register_filter; maps custom filters to callback functions85 registered_filters = {}86 87 class TemplateSyntaxError(Exception):88 pass89 90 class ContextPopException(Exception):91 "pop() has been called more times than push()"92 pass93 94 class TemplateDoesNotExist(Exception):95 pass96 97 class VariableDoesNotExist(Exception):98 pass99 100 class SilentVariableFailure(Exception):101 "Any function raising this exception will be ignored by resolve_variable"102 pass103 104 class Template:105 def __init__(self, template_string):106 "Compilation stage"107 self.nodelist = compile_string(template_string)108 109 def __iter__(self):110 for node in self.nodelist:111 for subnode in node:112 yield subnode113 114 def render(self, context):115 "Display stage -- can be called many times"116 return self.nodelist.render(context)117 118 def compile_string(template_string):119 "Compiles template_string into NodeList ready for rendering"120 tokens = tokenize(template_string)121 parser = Parser(tokens)122 return parser.parse()123 124 class Context:125 "A stack container for variable context"126 def __init__(self, dict=None):127 dict = dict or {}128 self.dicts = [dict]129 130 def __repr__(self):131 return repr(self.dicts)132 133 def __iter__(self):134 for d in self.dicts:135 yield d136 137 def push(self):138 self.dicts = [{}] + self.dicts139 140 def pop(self):141 if len(self.dicts) == 1:142 raise ContextPopException143 del self.dicts[0]144 145 def __setitem__(self, key, value):146 "Set a variable in the current context"147 self.dicts[0][key] = value148 149 def __getitem__(self, key):150 "Get a variable's value, starting at the current context and going upward"151 for dict in self.dicts:152 if dict.has_key(key):153 return dict[key]154 return ''155 156 def __delitem__(self, key):157 "Delete a variable from the current context"158 del self.dicts[0][key]159 160 def has_key(self, key):161 for dict in self.dicts:162 if dict.has_key(key):163 return True164 return False165 166 def update(self, other_dict):167 "Like dict.update(). Pushes an entire dictionary's keys and values onto the context."168 self.dicts = [other_dict] + self.dicts169 170 class Token:171 def __init__(self, token_type, contents):172 "The token_type must be TOKEN_TEXT, TOKEN_VAR or TOKEN_BLOCK"173 self.token_type, self.contents = token_type, contents174 175 def __str__(self):176 return '<%s token: "%s...">' % (177 {TOKEN_TEXT:'Text', TOKEN_VAR:'Var', TOKEN_BLOCK:'Block'}[self.token_type],178 self.contents[:20].replace('\n', '')179 )180 181 def tokenize(template_string):182 "Return a list of tokens from a given template_string"183 # remove all empty strings, because the regex has a tendency to add them184 bits = filter(None, tag_re.split(template_string))185 return map(create_token, bits)186 187 def create_token(token_string):188 "Convert the given token string into a new Token object and return it"189 if token_string.startswith(VARIABLE_TAG_START):190 return Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip())191 elif token_string.startswith(BLOCK_TAG_START):192 return Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip())193 else:194 return Token(TOKEN_TEXT, token_string)195 196 class Parser:197 def __init__(self, tokens):198 self.tokens = tokens199 200 def parse(self, parse_until=[]):201 nodelist = NodeList()202 while self.tokens:203 token = self.next_token()204 if token.token_type == TOKEN_TEXT:205 nodelist.append(TextNode(token.contents))206 elif token.token_type == TOKEN_VAR:207 if not token.contents:208 raise TemplateSyntaxError, "Empty variable tag"209 nodelist.append(VariableNode(token.contents))210 elif token.token_type == TOKEN_BLOCK:211 if token.contents in parse_until:212 # put token back on token list so calling code knows why it terminated213 self.prepend_token(token)214 return nodelist215 try:216 command = token.contents.split()[0]217 except IndexError:218 raise TemplateSyntaxError, "Empty block tag"219 try:220 # execute callback function for this tag and append resulting node221 nodelist.append(registered_tags[command](self, token))222 except KeyError:223 raise TemplateSyntaxError, "Invalid block tag: '%s'" % command224 if parse_until:225 raise TemplateSyntaxError, "Unclosed tag(s): '%s'" % ', '.join(parse_until)226 return nodelist227 228 def next_token(self):229 return self.tokens.pop(0)230 231 def prepend_token(self, token):232 self.tokens.insert(0, token)233 234 def delete_first_token(self):235 del self.tokens[0]236 237 class FilterParser:238 """Parse a variable token and its optional filters (all as a single string),239 and return a list of tuples of the filter name and arguments.240 Sample:241 >>> token = 'variable|default:"Default value"|date:"Y-m-d"'242 >>> p = FilterParser(token)243 >>> p.filters244 [('default', 'Default value'), ('date', 'Y-m-d')]245 >>> p.var246 'variable'247 248 This class should never be instantiated outside of the249 get_filters_from_token helper function.250 """251 def __init__(self, s):252 self.s = s253 self.i = -1254 self.current = ''255 self.filters = []256 self.current_filter_name = None257 self.current_filter_arg = None258 # First read the variable part259 self.var = self.read_alphanumeric_token()260 if not self.var:261 raise TemplateSyntaxError, "Could not read variable name: '%s'" % self.s262 if self.var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or self.var[0] == '_':263 raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % self.var264 # Have we reached the end?265 if self.current is None:266 return267 if self.current != FILTER_SEPARATOR:268 raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current)269 # We have a filter separator; start reading the filters270 self.read_filters()271 272 def next_char(self):273 self.i = self.i + 1274 try:275 self.current = self.s[self.i]276 except IndexError:277 self.current = None278 279 def read_alphanumeric_token(self):280 """Read a variable name or filter name, which are continuous strings of281 alphanumeric characters + the underscore"""282 var = ''283 while 1:284 self.next_char()285 if self.current is None:286 break287 if self.current not in ALLOWED_VARIABLE_CHARS:288 break289 var += self.current290 return var291 292 def read_filters(self):293 while 1:294 filter_name, arg = self.read_filter()295 if not registered_filters.has_key(filter_name):296 raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name297 if registered_filters[filter_name][1] == True and arg is None:298 raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name299 if registered_filters[filter_name][1] == False and arg is not None:300 raise TemplateSyntaxError, "Filter '%s' should not have an argument (argument is %r)" % (filter_name, arg)301 self.filters.append((filter_name, arg))302 if self.current is None:303 break304 305 def read_filter(self):306 self.current_filter_name = self.read_alphanumeric_token()307 self.current_filter_arg = None308 # Have we reached the end?309 if self.current is None:310 return (self.current_filter_name, None)311 # Does the filter have an argument?312 if self.current == FILTER_ARGUMENT_SEPARATOR:313 self.current_filter_arg = self.read_arg()314 return (self.current_filter_name, self.current_filter_arg)315 # Next thing MUST be a pipe316 if self.current != FILTER_SEPARATOR:317 raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current)318 return (self.current_filter_name, self.current_filter_arg)319 320 def read_arg(self):321 # First read a "322 self.next_char()323 if self.current != '"':324 raise TemplateSyntaxError, "Bad character (expecting '\"') '%s'" % self.current325 self.escaped = False326 arg = ''327 while 1:328 self.next_char()329 if self.current == '"' and not self.escaped:330 break331 if self.current == '\\' and not self.escaped:332 self.escaped = True333 continue334 if self.current == '\\' and self.escaped:335 arg += '\\'336 self.escaped = False337 continue338 if self.current == '"' and self.escaped:339 arg += '"'340 self.escaped = False341 continue342 if self.escaped and self.current not in '\\"':343 raise TemplateSyntaxError, "Unescaped backslash in '%s'" % self.s344 if self.current is None:345 raise TemplateSyntaxError, "Unexpected end of argument in '%s'" % self.s346 arg += self.current347 # self.current must now be '"'348 self.next_char()349 return arg350 351 def get_filters_from_token(token):352 "Convenient wrapper for FilterParser"353 p = FilterParser(token)354 return (p.var, p.filters)355 356 def resolve_variable(path, context):357 """358 Returns the resolved variable, which may contain attribute syntax, within359 the given context. The variable may be a hard-coded string (if it begins360 and ends with single or double quote marks).361 362 >>> c = {'article': {'section':'News'}}363 >>> resolve_variable('article.section', c)364 'News'365 >>> resolve_variable('article', c)366 {'section': 'News'}367 >>> class AClass: pass368 >>> c = AClass()369 >>> c.article = AClass()370 >>> c.article.section = 'News'371 >>> resolve_variable('article.section', c)372 'News'373 374 (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.')375 """376 if path[0] in ('"', "'") and path[0] == path[-1]:377 current = path[1:-1]378 else:379 current = context380 bits = path.split(VARIABLE_ATTRIBUTE_SEPARATOR)381 while bits:382 try: # dictionary lookup383 current = current[bits[0]]384 except (TypeError, AttributeError, KeyError):385 try: # attribute lookup386 current = getattr(current, bits[0])387 if callable(current):388 if getattr(current, 'alters_data', False):389 current = ''390 else:391 try: # method call (assuming no args required)392 current = current()393 except SilentVariableFailure:394 current = ''395 except TypeError: # arguments *were* required396 current = '' # invalid method call397 except (TypeError, AttributeError):398 try: # list-index lookup399 current = current[int(bits[0])]400 except (IndexError, ValueError, KeyError):401 raise VariableDoesNotExist, "Failed lookup for key [%s] in %r" % (bits[0], current) # missing attribute402 del bits[0]403 return current404 405 def resolve_variable_with_filters(var_string, context):406 """407 var_string is a full variable expression with optional filters, like:408 a.b.c|lower|date:"y/m/d"409 This function resolves the variable in the context, applies all filters and410 returns the object.411 """412 var, filters = get_filters_from_token(var_string)413 try:414 obj = resolve_variable(var, context)415 except VariableDoesNotExist:416 obj = ''417 for name, arg in filters:418 obj = registered_filters[name][0](obj, arg)419 return obj420 421 class Node:422 def render(self, context):423 "Return the node rendered as a string"424 pass425 426 def __iter__(self):427 yield self428 429 def get_nodes_by_type(self, nodetype):430 "Return a list of all nodes (within this node and its nodelist) of the given type"431 nodes = []432 if isinstance(self, nodetype):433 nodes.append(self)434 if hasattr(self, 'nodelist'):435 nodes.extend(self.nodelist.get_nodes_by_type(nodetype))436 return nodes437 438 class NodeList(list):439 def render(self, context):440 bits = []441 for node in self:442 if isinstance(node, Node):443 bits.append(node.render(context))444 else:445 bits.append(node)446 return ''.join(bits)447 448 def get_nodes_by_type(self, nodetype):449 "Return a list of all nodes of the given type"450 nodes = []451 for node in self:452 nodes.extend(node.get_nodes_by_type(nodetype))453 return nodes454 455 class TextNode(Node):456 def __init__(self, s):457 self.s = s458 459 def __repr__(self):460 return "<Text Node: '%s'>" % self.s[:25]461 462 def render(self, context):463 return self.s464 465 class VariableNode(Node):466 def __init__(self, var_string):467 self.var_string = var_string468 469 def __repr__(self):470 return "<Variable Node: %s>" % self.var_string471 472 def render(self, context):473 output = resolve_variable_with_filters(self.var_string, context)474 # Check type so that we don't run str() on a Unicode object475 if not isinstance(output, basestring):476 output = str(output)477 elif isinstance(output, unicode):478 output = output.encode(DEFAULT_CHARSET)479 return output480 481 def register_tag(token_command, callback_function):482 registered_tags[token_command] = callback_function483 484 def unregister_tag(token_command):485 del registered_tags[token_command]486 487 def register_filter(filter_name, callback_function, has_arg):488 registered_filters[filter_name] = (callback_function, has_arg)489 490 def unregister_filter(filter_name):491 del registered_filters[filter_name]492 493 import defaulttags494 import defaultfilters -
defaultfilters.py
1 "Default variable filters"2 3 import template, re4 import random as random_module5 6 ###################7 # STRINGS #8 ###################9 10 def addslashes(value, _):11 "Adds slashes - useful for passing strings to JavaScript, for example."12 return value.replace('"', '\\"').replace("'", "\\'")13 14 def capfirst(value, _):15 "Capitalizes the first character of the value"16 value = str(value)17 return value and value[0].upper() + value[1:]18 19 def fix_ampersands(value, _):20 "Replaces ampersands with ``&`` entities"21 from django.utils.html import fix_ampersands22 return fix_ampersands(value)23 24 def floatformat(text, _):25 """26 Displays a floating point number as 34.2 (with one decimal place) - but27 only if there's a point to be displayed28 """29 from math import modf30 if not text:31 return ''32 if modf(float(text))[0] < 0.1:33 return text34 return "%.1f" % float(text)35 36 def linenumbers(value, _):37 "Displays text with line numbers"38 from django.utils.html import escape39 lines = value.split('\n')40 # Find the maximum width of the line count, for use with zero padding string format command41 width = str(len(str(len(lines))))42 for i, line in enumerate(lines):43 lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line))44 return '\n'.join(lines)45 46 def lower(value, _):47 "Converts a string into all lowercase"48 return value.lower()49 50 def make_list(value, _):51 """52 Returns the value turned into a list. For an integer, it's a list of53 digits. For a string, it's a list of characters.54 """55 return list(str(value))56 57 def slugify(value, _):58 "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens"59 value = re.sub('[^\w\s-]', '', value).strip().lower()60 return re.sub('\s+', '-', value)61 62 def stringformat(value, arg):63 """64 Formats the variable according to the argument, a string formatting specifier.65 This specifier uses Python string formating syntax, with the exception that66 the leading "%" is dropped.67 68 See http://docs.python.org/lib/typesseq-strings.html for documentation69 of Python string formatting70 """71 try:72 return ("%" + arg) % value73 except (ValueError, TypeError):74 return ""75 76 def title(value, _):77 "Converts a string into titlecase"78 return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())79 80 def truncatewords(value, arg):81 """82 Truncates a string after a certain number of words83 84 Argument: Number of words to truncate after85 """86 from django.utils.text import truncate_words87 try:88 length = int(arg)89 except ValueError: # invalid literal for int()90 return value # Fail silently.91 if not isinstance(value, basestring):92 value = str(value)93 return truncate_words(value, length)94 95 def upper(value, _):96 "Converts a string into all uppercase"97 return value.upper()98 99 def urlencode(value, _):100 "Escapes a value for use in a URL"101 import urllib102 return urllib.quote(value)103 104 def urlize(value, _):105 "Converts URLs in plain text into clickable links"106 from django.utils.html import urlize107 return urlize(value, nofollow=True)108 109 def urlizetrunc(value, limit):110 """111 Converts URLs into clickable links, truncating URLs to the given character limit112 113 Argument: Length to truncate URLs to.114 """115 from django.utils.html import urlize116 return urlize(value, trim_url_limit=int(limit), nofollow=True)117 118 def wordcount(value, _):119 "Returns the number of words"120 return len(value.split())121 122 def wordwrap(value, arg):123 """124 Wraps words at specified line length125 126 Argument: number of words to wrap the text at.127 """128 from django.utils.text import wrap129 return wrap(value, int(arg))130 131 def ljust(value, arg):132 """133 Left-aligns the value in a field of a given width134 135 Argument: field size136 """137 return str(value).ljust(int(arg))138 139 def rjust(value, arg):140 """141 Right-aligns the value in a field of a given width142 143 Argument: field size144 """145 return str(value).rjust(int(arg))146 147 def center(value, arg):148 "Centers the value in a field of a given width"149 return str(value).center(int(arg))150 151 def cut(value, arg):152 "Removes all values of arg from the given string"153 return value.replace(arg, '')154 155 ###################156 # HTML STRINGS #157 ###################158 159 def escape(value, _):160 "Escapes a string's HTML"161 from django.utils.html import escape162 return escape(value)163 164 def linebreaks(value, _):165 "Converts newlines into <p> and <br />s"166 from django.utils.html import linebreaks167 return linebreaks(value)168 169 def linebreaksbr(value, _):170 "Converts newlines into <br />s"171 return value.replace('\n', '<br />')172 173 def removetags(value, tags):174 "Removes a space separated list of [X]HTML tags from the output"175 tags = [re.escape(tag) for tag in tags.split()]176 tags_re = '(%s)' % '|'.join(tags)177 starttag_re = re.compile('<%s(>|(\s+[^>]*>))' % tags_re)178 endtag_re = re.compile('</%s>' % tags_re)179 value = starttag_re.sub('', value)180 value = endtag_re.sub('', value)181 return value182 183 def striptags(value, _):184 "Strips all [X]HTML tags"185 from django.utils.html import strip_tags186 if not isinstance(value, basestring):187 value = str(value)188 return strip_tags(value)189 190 ###################191 # LISTS #192 ###################193 194 def dictsort(value, arg):195 """196 Takes a list of dicts, returns that list sorted by the property given in197 the argument.198 """199 decorated = [(template.resolve_variable('var.' + arg, {'var' : item}), item) for item in value]200 decorated.sort()201 return [item[1] for item in decorated]202 203 def dictsortreversed(value, arg):204 """205 Takes a list of dicts, returns that list sorted in reverse order by the206 property given in the argument.207 """208 decorated = [(template.resolve_variable('var.' + arg, {'var' : item}), item) for item in value]209 decorated.sort()210 decorated.reverse()211 return [item[1] for item in decorated]212 213 def first(value, _):214 "Returns the first item in a list"215 try:216 return value[0]217 except IndexError:218 return ''219 220 def join(value, arg):221 "Joins a list with a string, like Python's ``str.join(list)``"222 try:223 return arg.join(map(str, value))224 except AttributeError: # fail silently but nicely225 return value226 227 def length(value, _):228 "Returns the length of the value - useful for lists"229 return len(value)230 231 def length_is(value, arg):232 "Returns a boolean of whether the value's length is the argument"233 return len(value) == int(arg)234 235 def random(value, _):236 "Returns a random item from the list"237 return random_module.choice(value)238 239 def slice_(value, arg):240 """241 Returns a slice of the list.242 243 Uses the same syntax as Python's list slicing; see244 http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice245 for an introduction.246 """247 try:248 return value[slice(*[x and int(x) or None for x in arg.split(':')])]249 except (ValueError, TypeError):250 return value # Fail silently.251 252 def unordered_list(value, _):253 """254 Recursively takes a self-nested list and returns an HTML unordered list --255 WITHOUT opening and closing <ul> tags.256 257 The list is assumed to be in the proper format. For example, if ``var`` contains258 ``['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]``,259 then ``{{ var|unordered_list }}`` would return::260 261 <li>States262 <ul>263 <li>Kansas264 <ul>265 <li>Lawrence</li>266 <li>Topeka</li>267 </ul>268 </li>269 <li>Illinois</li>270 </ul>271 </li>272 """273 def _helper(value, tabs):274 indent = '\t' * tabs275 if value[1]:276 return '%s<li>%s\n%s<ul>\n%s\n%s</ul>\n%s</li>' % (indent, value[0], indent,277 '\n'.join([unordered_list(v, tabs+1) for v in value[1]]), indent, indent)278 else:279 return '%s<li>%s</li>' % (indent, value[0])280 return _helper(value, 1)281 282 ###################283 # INTEGERS #284 ###################285 286 def add(value, arg):287 "Adds the arg to the value"288 return int(value) + int(arg)289 290 def get_digit(value, arg):291 """292 Given a whole number, returns the requested digit of it, where 1 is the293 right-most digit, 2 is the second-right-most digit, etc. Returns the294 original value for invalid input (if input or argument is not an integer,295 or if argument is less than 1). Otherwise, output is always an integer.296 """297 try:298 arg = int(arg)299 value = int(value)300 except ValueError:301 return value # Fail silently for an invalid argument302 if arg < 1:303 return value304 try:305 return int(str(value)[-arg])306 except IndexError:307 return 0308 309 ###################310 # DATES #311 ###################312 313 def date(value, arg):314 "Formats a date according to the given format"315 from django.utils.dateformat import format316 return format(value, arg)317 318 def time(value, arg):319 "Formats a time according to the given format"320 from django.utils.dateformat import time_format321 return time_format(value, arg)322 323 def timesince(value, _):324 'Formats a date as the time since that date (i.e. "4 days, 6 hours")'325 from django.utils.timesince import timesince326 return timesince(value)327 328 ###################329 # LOGIC #330 ###################331 332 def default(value, arg):333 "If value is unavailable, use given default"334 return value or arg335 336 def default_if_none(value, arg):337 "If value is None, use given default"338 if value is None:339 return arg340 return value341 342 def divisibleby(value, arg):343 "Returns true if the value is devisible by the argument"344 return int(value) % int(arg) == 0345 346 def yesno(value, arg):347 """348 Given a string mapping values for true, false and (optionally) None,349 returns one of those strings accoding to the value:350 351 ========== ====================== ==================================352 Value Argument Outputs353 ========== ====================== ==================================354 ``True`` ``"yeah,no,maybe"`` ``yeah``355 ``False`` ``"yeah,no,maybe"`` ``no``356 ``None`` ``"yeah,no,maybe"`` ``maybe``357 ``None`` ``"yeah,no"`` ``"no"`` (converts None to False358 if no mapping for None is given.359 ========== ====================== ==================================360 """361 bits = arg.split(',')362 if len(bits) < 2:363 return value # Invalid arg.364 try:365 yes, no, maybe = bits366 except ValueError: # unpack list of wrong size (no "maybe" value provided)367 yes, no, maybe = bits[0], bits[1], bits[1]368 if value is None:369 return maybe370 if value:371 return yes372 return no373 374 ###################375 # MISC #376 ###################377 378 def filesizeformat(bytes, _):379 """380 Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102381 bytes, etc).382 """383 bytes = float(bytes)384 if bytes < 1024:385 return "%d byte%s" % (bytes, bytes != 1 and 's' or '')386 if bytes < 1024 * 1024:387 return "%.1f KB" % (bytes / 1024)388 if bytes < 1024 * 1024 * 1024:389 return "%.1f MB" % (bytes / (1024 * 1024))390 return "%.1f GB" % (bytes / (1024 * 1024 * 1024))391 392 def pluralize(value, _):393 "Returns 's' if the value is not 1, for '1 vote' vs. '2 votes'"394 try:395 if int(value) != 1:396 return 's'397 except ValueError: # invalid string that's not a number398 pass399 except TypeError: # value isn't a string or a number; maybe it's a list?400 try:401 if len(value) != 1:402 return 's'403 except TypeError: # len() of unsized object404 pass405 return ''406 407 def phone2numeric(value, _):408 "Takes a phone number and converts it in to its numerical equivalent"409 from django.utils.text import phone2numeric410 return phone2numeric(value)411 412 def pprint(value, _):413 "A wrapper around pprint.pprint -- for debugging, really"414 from pprint import pformat415 return pformat(value)416 417 # Syntax: template.register_filter(name of filter, callback, has_argument)418 template.register_filter('add', add, True)419 template.register_filter('addslashes', addslashes, False)420 template.register_filter('capfirst', capfirst, False)421 template.register_filter('center', center, True)422 template.register_filter('cut', cut, True)423 template.register_filter('date', date, True)424 template.register_filter('default', default, True)425 template.register_filter('dictsort', dictsort, True)426 template.register_filter('dictsortreversed', dictsortreversed, True)427 template.register_filter('divisibleby', divisibleby, True)428 template.register_filter('escape', escape, False)429 template.register_filter('filesizeformat', filesizeformat, False)430 template.register_filter('first', first, False)431 template.register_filter('fix_ampersands', fix_ampersands, False)432 template.register_filter('floatformat', floatformat, False)433 template.register_filter('get_digit', get_digit, True)434 template.register_filter('join', join, True)435 template.register_filter('length', length, False)436 template.register_filter('length_is', length_is, True)437 template.register_filter('linebreaks', linebreaks, False)438 template.register_filter('linebreaksbr', linebreaksbr, False)439 template.register_filter('linenumbers', linenumbers, False)440 template.register_filter('ljust', ljust, True)441 template.register_filter('lower', lower, False)442 template.register_filter('make_list', make_list, False)443 template.register_filter('phone2numeric', phone2numeric, False)444 template.register_filter('pluralize', pluralize, False)445 template.register_filter('pprint', pprint, False)446 template.register_filter('removetags', removetags, True)447 template.register_filter('random', random, False)448 template.register_filter('rjust', rjust, True)449 template.register_filter('slice', slice_, True)450 template.register_filter('slugify', slugify, False)451 template.register_filter('stringformat', stringformat, True)452 template.register_filter('striptags', striptags, False)453 template.register_filter('time', time, True)454 template.register_filter('timesince', timesince, False)455 template.register_filter('title', title, False)456 template.register_filter('truncatewords', truncatewords, True)457 template.register_filter('unordered_list', unordered_list, False)458 template.register_filter('upper', upper, False)459 template.register_filter('urlencode', urlencode, False)460 template.register_filter('urlize', urlize, False)461 template.register_filter('urlizetrunc', urlizetrunc, True)462 template.register_filter('wordcount', wordcount, False)463 template.register_filter('wordwrap', wordwrap, True)464 template.register_filter('yesno', yesno, True) -
template/defaultfilters.py
1 "Default variable filters" 2 3 from django.core.template import resolve_variable 4 from django.core.template import register_filter 5 # from django.core import template 6 import re 7 import random as random_module 8 9 ################### 10 # STRINGS # 11 ################### 12 13 def addslashes(value, _): 14 "Adds slashes - useful for passing strings to JavaScript, for example." 15 return value.replace('"', '\\"').replace("'", "\\'") 16 17 def capfirst(value, _): 18 "Capitalizes the first character of the value" 19 value = str(value) 20 return value and value[0].upper() + value[1:] 21 22 def fix_ampersands(value, _): 23 "Replaces ampersands with ``&`` entities" 24 from django.utils.html import fix_ampersands 25 return fix_ampersands(value) 26 27 def floatformat(text, _): 28 """ 29 Displays a floating point number as 34.2 (with one decimal place) - but 30 only if there's a point to be displayed 31 """ 32 from math import modf 33 if not text: 34 return '' 35 if modf(float(text))[0] < 0.1: 36 return text 37 return "%.1f" % float(text) 38 39 def linenumbers(value, _): 40 "Displays text with line numbers" 41 from django.utils.html import escape 42 lines = value.split('\n') 43 # Find the maximum width of the line count, for use with zero padding string format command 44 width = str(len(str(len(lines)))) 45 for i, line in enumerate(lines): 46 lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line)) 47 return '\n'.join(lines) 48 49 def lower(value, _): 50 "Converts a string into all lowercase" 51 return value.lower() 52 53 def make_list(value, _): 54 """ 55 Returns the value turned into a list. For an integer, it's a list of 56 digits. For a string, it's a list of characters. 57 """ 58 return list(str(value)) 59 60 def slugify(value, _): 61 "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens" 62 value = re.sub('[^\w\s-]', '', value).strip().lower() 63 return re.sub('\s+', '-', value) 64 65 def stringformat(value, arg): 66 """ 67 Formats the variable according to the argument, a string formatting specifier. 68 This specifier uses Python string formating syntax, with the exception that 69 the leading "%" is dropped. 70 71 See http://docs.python.org/lib/typesseq-strings.html for documentation 72 of Python string formatting 73 """ 74 try: 75 return ("%" + arg) % value 76 except (ValueError, TypeError): 77 return "" 78 79 def title(value, _): 80 "Converts a string into titlecase" 81 return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) 82 83 def truncatewords(value, arg): 84 """ 85 Truncates a string after a certain number of words 86 87 Argument: Number of words to truncate after 88 """ 89 from django.utils.text import truncate_words 90 try: 91 length = int(arg) 92 except ValueError: # invalid literal for int() 93 return value # Fail silently. 94 if not isinstance(value, basestring): 95 value = str(value) 96 return truncate_words(value, length) 97 98 def upper(value, _): 99 "Converts a string into all uppercase" 100 return value.upper() 101 102 def urlencode(value, _): 103 "Escapes a value for use in a URL" 104 import urllib 105 return urllib.quote(value) 106 107 def urlize(value, _): 108 "Converts URLs in plain text into clickable links" 109 from django.utils.html import urlize 110 return urlize(value, nofollow=True) 111 112 def urlizetrunc(value, limit): 113 """ 114 Converts URLs into clickable links, truncating URLs to the given character limit 115 116 Argument: Length to truncate URLs to. 117 """ 118 from django.utils.html import urlize 119 return urlize(value, trim_url_limit=int(limit), nofollow=True) 120 121 def wordcount(value, _): 122 "Returns the number of words" 123 return len(value.split()) 124 125 def wordwrap(value, arg): 126 """ 127 Wraps words at specified line length 128 129 Argument: number of words to wrap the text at. 130 """ 131 from django.utils.text import wrap 132 return wrap(value, int(arg)) 133 134 def ljust(value, arg): 135 """ 136 Left-aligns the value in a field of a given width 137 138 Argument: field size 139 """ 140 return str(value).ljust(int(arg)) 141 142 def rjust(value, arg): 143 """ 144 Right-aligns the value in a field of a given width 145 146 Argument: field size 147 """ 148 return str(value).rjust(int(arg)) 149 150 def center(value, arg): 151 "Centers the value in a field of a given width" 152 return str(value).center(int(arg)) 153 154 def cut(value, arg): 155 "Removes all values of arg from the given string" 156 return value.replace(arg, '') 157 158 ################### 159 # HTML STRINGS # 160 ################### 161 162 def escape(value, _): 163 "Escapes a string's HTML" 164 from django.utils.html import escape 165 return escape(value) 166 167 def linebreaks(value, _): 168 "Converts newlines into <p> and <br />s" 169 from django.utils.html import linebreaks 170 return linebreaks(value) 171 172 def linebreaksbr(value, _): 173 "Converts newlines into <br />s" 174 return value.replace('\n', '<br />') 175 176 def removetags(value, tags): 177 "Removes a space separated list of [X]HTML tags from the output" 178 tags = [re.escape(tag) for tag in tags.split()] 179 tags_re = '(%s)' % '|'.join(tags) 180 starttag_re = re.compile('<%s(>|(\s+[^>]*>))' % tags_re) 181 endtag_re = re.compile('</%s>' % tags_re) 182 value = starttag_re.sub('', value) 183 value = endtag_re.sub('', value) 184 return value 185 186 def striptags(value, _): 187 "Strips all [X]HTML tags" 188 from django.utils.html import strip_tags 189 if not isinstance(value, basestring): 190 value = str(value) 191 return strip_tags(value) 192 193 ################### 194 # LISTS # 195 ################### 196 197 def dictsort(value, arg): 198 """ 199 Takes a list of dicts, returns that list sorted by the property given in 200 the argument. 201 """ 202 decorated = [(template.resolve_variable('var.' + arg, {'var' : item}), item) for item in value] 203 decorated.sort() 204 return [item[1] for item in decorated] 205 206 def dictsortreversed(value, arg): 207 """ 208 Takes a list of dicts, returns that list sorted in reverse order by the 209 property given in the argument. 210 """ 211 decorated = [(template.resolve_variable('var.' + arg, {'var' : item}), item) for item in value] 212 decorated.sort() 213 decorated.reverse() 214 return [item[1] for item in decorated] 215 216 def first(value, _): 217 "Returns the first item in a list" 218 try: 219 return value[0] 220 except IndexError: 221 return '' 222 223 def join(value, arg): 224 "Joins a list with a string, like Python's ``str.join(list)``" 225 try: 226 return arg.join(map(str, value)) 227 except AttributeError: # fail silently but nicely 228 return value 229 230 def length(value, _): 231 "Returns the length of the value - useful for lists" 232 return len(value) 233 234 def length_is(value, arg): 235 "Returns a boolean of whether the value's length is the argument" 236 return len(value) == int(arg) 237 238 def random(value, _): 239 "Returns a random item from the list" 240 return random_module.choice(value) 241 242 def slice_(value, arg): 243 """ 244 Returns a slice of the list. 245 246 Uses the same syntax as Python's list slicing; see 247 http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice 248 for an introduction. 249 """ 250 try: 251 return value[slice(*[x and int(x) or None for x in arg.split(':')])] 252 except (ValueError, TypeError): 253 return value # Fail silently. 254 255 def unordered_list(value, _): 256 """ 257 Recursively takes a self-nested list and returns an HTML unordered list -- 258 WITHOUT opening and closing <ul> tags. 259 260 The list is assumed to be in the proper format. For example, if ``var`` contains 261 ``['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]``, 262 then ``{{ var|unordered_list }}`` would return:: 263 264 <li>States 265 <ul> 266 <li>Kansas 267 <ul> 268 <li>Lawrence</li> 269 <li>Topeka</li> 270 </ul> 271 </li> 272 <li>Illinois</li> 273 </ul> 274 </li> 275 """ 276 def _helper(value, tabs): 277 indent = '\t' * tabs 278 if value[1]: 279 return '%s<li>%s\n%s<ul>\n%s\n%s</ul>\n%s</li>' % (indent, value[0], indent, 280 '\n'.join([unordered_list(v, tabs+1) for v in value[1]]), indent, indent) 281 else: 282 return '%s<li>%s</li>' % (indent, value[0]) 283 return _helper(value, 1) 284 285 ################### 286 # INTEGERS # 287 ################### 288 289 def add(value, arg): 290 "Adds the arg to the value" 291 return int(value) + int(arg) 292 293 def get_digit(value, arg): 294 """ 295 Given a whole number, returns the requested digit of it, where 1 is the 296 right-most digit, 2 is the second-right-most digit, etc. Returns the 297 original value for invalid input (if input or argument is not an integer, 298 or if argument is less than 1). Otherwise, output is always an integer. 299 """ 300 try: 301 arg = int(arg) 302 value = int(value) 303 except ValueError: 304 return value # Fail silently for an invalid argument 305 if arg < 1: 306 return value 307 try: 308 return int(str(value)[-arg]) 309 except IndexError: 310 return 0 311 312 ################### 313 # DATES # 314 ################### 315 316 def date(value, arg): 317 "Formats a date according to the given format" 318 from django.utils.dateformat import format 319 return format(value, arg) 320 321 def time(value, arg): 322 "Formats a time according to the given format" 323 from django.utils.dateformat import time_format 324 return time_format(value, arg) 325 326 def timesince(value, _): 327 'Formats a date as the time since that date (i.e. "4 days, 6 hours")' 328 from django.utils.timesince import timesince 329 return timesince(value) 330 331 ################### 332 # LOGIC # 333 ################### 334 335 def default(value, arg): 336 "If value is unavailable, use given default" 337 return value or arg 338 339 def default_if_none(value, arg): 340 "If value is None, use given default" 341 if value is None: 342 return arg 343 return value 344 345 def divisibleby(value, arg): 346 "Returns true if the value is devisible by the argument" 347 return int(value) % int(arg) == 0 348 349 def yesno(value, arg): 350 """ 351 Given a string mapping values for true, false and (optionally) None, 352 returns one of those strings accoding to the value: 353 354 ========== ====================== ================================== 355 Value Argument Outputs 356 ========== ====================== ================================== 357 ``True`` ``"yeah,no,maybe"`` ``yeah`` 358 ``False`` ``"yeah,no,maybe"`` ``no`` 359 ``None`` ``"yeah,no,maybe"`` ``maybe`` 360 ``None`` ``"yeah,no"`` ``"no"`` (converts None to False 361 if no mapping for None is given. 362 ========== ====================== ================================== 363 """ 364 bits = arg.split(',') 365 if len(bits) < 2: 366 return value # Invalid arg. 367 try: 368 yes, no, maybe = bits 369 except ValueError: # unpack list of wrong size (no "maybe" value provided) 370 yes, no, maybe = bits[0], bits[1], bits[1] 371 if value is None: 372 return maybe 373 if value: 374 return yes 375 return no 376 377 ################### 378 # MISC # 379 ################### 380 381 def filesizeformat(bytes, _): 382 """ 383 Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 384 bytes, etc). 385 """ 386 bytes = float(bytes) 387 if bytes < 1024: 388 return "%d byte%s" % (bytes, bytes != 1 and 's' or '') 389 if bytes < 1024 * 1024: 390 return "%.1f KB" % (bytes / 1024) 391 if bytes < 1024 * 1024 * 1024: 392 return "%.1f MB" % (bytes / (1024 * 1024)) 393 return "%.1f GB" % (bytes / (1024 * 1024 * 1024)) 394 395 def pluralize(value, _): 396 "Returns 's' if the value is not 1, for '1 vote' vs. '2 votes'" 397 try: 398 if int(value) != 1: 399 return 's' 400 except ValueError: # invalid string that's not a number 401 pass 402 except TypeError: # value isn't a string or a number; maybe it's a list? 403 try: 404 if len(value) != 1: 405 return 's' 406 except TypeError: # len() of unsized object 407 pass 408 return '' 409 410 def phone2numeric(value, _): 411 "Takes a phone number and converts it in to its numerical equivalent" 412 from django.utils.text import phone2numeric 413 return phone2numeric(value) 414 415 def pprint(value, _): 416 "A wrapper around pprint.pprint -- for debugging, really" 417 from pprint import pformat 418 return pformat(value) 419 420 # Syntax: template.register_filter(name of filter, callback, has_argument) 421 register_filter('add', add, True) 422 register_filter('addslashes', addslashes, False) 423 register_filter('capfirst', capfirst, False) 424 register_filter('center', center, True) 425 register_filter('cut', cut, True) 426 register_filter('date', date, True) 427 register_filter('default', default, True) 428 register_filter('dictsort', dictsort, True) 429 register_filter('dictsortreversed', dictsortreversed, True) 430 register_filter('divisibleby', divisibleby, True) 431 register_filter('escape', escape, False) 432 register_filter('filesizeformat', filesizeformat, False) 433 register_filter('first', first, False) 434 register_filter('fix_ampersands', fix_ampersands, False) 435 register_filter('floatformat', floatformat, False) 436 register_filter('get_digit', get_digit, True) 437 register_filter('join', join, True) 438 register_filter('length', length, False) 439 register_filter('length_is', length_is, True) 440 register_filter('linebreaks', linebreaks, False) 441 register_filter('linebreaksbr', linebreaksbr, False) 442 register_filter('linenumbers', linenumbers, False) 443 register_filter('ljust', ljust, True) 444 register_filter('lower', lower, False) 445 register_filter('make_list', make_list, False) 446 register_filter('phone2numeric', phone2numeric, False) 447 register_filter('pluralize', pluralize, False) 448 register_filter('pprint', pprint, False) 449 register_filter('removetags', removetags, True) 450 register_filter('random', random, False) 451 register_filter('rjust', rjust, True) 452 register_filter('slice', slice_, True) 453 register_filter('slugify', slugify, False) 454 register_filter('stringformat', stringformat, True) 455 register_filter('striptags', striptags, False) 456 register_filter('time', time, True) 457 register_filter('timesince', timesince, False) 458 register_filter('title', title, False) 459 register_filter('truncatewords', truncatewords, True) 460 register_filter('unordered_list', unordered_list, False) 461 register_filter('upper', upper, False) 462 register_filter('urlencode', urlencode, False) 463 register_filter('urlize', urlize, False) 464 register_filter('urlizetrunc', urlizetrunc, True) 465 register_filter('wordcount', wordcount, False) 466 register_filter('wordwrap', wordwrap, True) 467 register_filter('yesno', yesno, True) -
template/__init__.py
1 """ 2 This is the Django template system. 3 4 How it works: 5 6 The tokenize() function converts a template string (i.e., a string containing 7 markup with custom template tags) to tokens, which can be either plain text 8 (TOKEN_TEXT), variables (TOKEN_VAR) or block statements (TOKEN_BLOCK). 9 10 The Parser() class takes a list of tokens in its constructor, and its parse() 11 method returns a compiled template -- which is, under the hood, a list of 12 Node objects. 13 14 Each Node is responsible for creating some sort of output -- e.g. simple text 15 (TextNode), variable values in a given context (VariableNode), results of basic 16 logic (IfNode), results of looping (ForNode), or anything else. The core Node 17 types are TextNode, VariableNode, IfNode and ForNode, but plugin modules can 18 define their own custom node types. 19 20 Each Node has a render() method, which takes a Context and returns a string of 21 the rendered node. For example, the render() method of a Variable Node returns 22 the variable's value as a string. The render() method of an IfNode returns the 23 rendered output of whatever was inside the loop, recursively. 24 25 The Template class is a convenient wrapper that takes care of template 26 compilation and rendering. 27 28 Usage: 29 30 The only thing you should ever use directly in this file is the Template class. 31 Create a compiled template object with a template_string, then call render() 32 with a context. In the compilation stage, the TemplateSyntaxError exception 33 will be raised if the template doesn't have proper syntax. 34 35 Sample code: 36 37 >>> import template 38 >>> s = ''' 39 ... <html> 40 ... {% if test %} 41 ... <h1>{{ varvalue }}</h1> 42 ... {% endif %} 43 ... </html> 44 ... ''' 45 >>> t = template.Template(s) 46 47 (t is now a compiled template, and its render() method can be called multiple 48 times with multiple contexts) 49 50 >>> c = template.Context({'test':True, 'varvalue': 'Hello'}) 51 >>> t.render(c) 52 '\n<html>\n\n <h1>Hello</h1>\n\n</html>\n' 53 >>> c = template.Context({'test':False, 'varvalue': 'Hello'}) 54 >>> t.render(c) 55 '\n<html>\n\n</html>\n' 56 """ 57 import re 58 from django.conf.settings import DEFAULT_CHARSET 59 60 __all__ = ('Template','Context','compile_string') 61 62 TOKEN_TEXT = 0 63 TOKEN_VAR = 1 64 TOKEN_BLOCK = 2 65 66 # template syntax constants 67 FILTER_SEPARATOR = '|' 68 FILTER_ARGUMENT_SEPARATOR = ':' 69 VARIABLE_ATTRIBUTE_SEPARATOR = '.' 70 BLOCK_TAG_START = '{%' 71 BLOCK_TAG_END = '%}' 72 VARIABLE_TAG_START = '{{' 73 VARIABLE_TAG_END = '}}' 74 75 ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.' 76 77 # match a variable or block tag and capture the entire tag, including start/end delimiters 78 tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), 79 re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END))) 80 81 # global dict used by register_tag; maps custom tags to callback functions 82 registered_tags = {} 83 84 # global dict used by register_filter; maps custom filters to callback functions 85 registered_filters = {} 86 87 class TemplateSyntaxError(Exception): 88 pass 89 90 class ContextPopException(Exception): 91 "pop() has been called more times than push()" 92 pass 93 94 class TemplateDoesNotExist(Exception): 95 pass 96 97 class VariableDoesNotExist(Exception): 98 pass 99 100 class SilentVariableFailure(Exception): 101 "Any function raising this exception will be ignored by resolve_variable" 102 pass 103 104 class Template: 105 def __init__(self, template_string): 106 "Compilation stage" 107 self.nodelist = compile_string(template_string) 108 109 def __iter__(self): 110 for node in self.nodelist: 111 for subnode in node: 112 yield subnode 113 114 def render(self, context): 115 "Display stage -- can be called many times" 116 return self.nodelist.render(context) 117 118 def compile_string(template_string): 119 "Compiles template_string into NodeList ready for rendering" 120 tokens = tokenize(template_string) 121 parser = Parser(tokens) 122 return parser.parse() 123 124 class Context: 125 "A stack container for variable context" 126 def __init__(self, dict=None): 127 dict = dict or {} 128 self.dicts = [dict] 129 130 def __repr__(self): 131 return repr(self.dicts) 132 133 def __iter__(self): 134 for d in self.dicts: 135 yield d 136 137 def push(self): 138 self.dicts = [{}] + self.dicts 139 140 def pop(self): 141 if len(self.dicts) == 1: 142 raise ContextPopException 143 del self.dicts[0] 144 145 def __setitem__(self, key, value): 146 "Set a variable in the current context" 147 self.dicts[0][key] = value 148 149 def __getitem__(self, key): 150 "Get a variable's value, starting at the current context and going upward" 151 for dict in self.dicts: 152 if dict.has_key(key): 153 return dict[key] 154 return '' 155 156 def __delitem__(self, key): 157 "Delete a variable from the current context" 158 del self.dicts[0][key] 159 160 def has_key(self, key): 161 for dict in self.dicts: 162 if dict.has_key(key): 163 return True 164 return False 165 166 def update(self, other_dict): 167 "Like dict.update(). Pushes an entire dictionary's keys and values onto the context." 168 self.dicts = [other_dict] + self.dicts 169 170 class Token: 171 def __init__(self, token_type, contents): 172 "The token_type must be TOKEN_TEXT, TOKEN_VAR or TOKEN_BLOCK" 173 self.token_type, self.contents = token_type, contents 174 175 def __str__(self): 176 return '<%s token: "%s...">' % ( 177 {TOKEN_TEXT:'Text', TOKEN_VAR:'Var', TOKEN_BLOCK:'Block'}[self.token_type], 178 self.contents[:20].replace('\n', '') 179 ) 180 181 def tokenize(template_string): 182 "Return a list of tokens from a given template_string" 183 # remove all empty strings, because the regex has a tendency to add them 184 bits = filter(None, tag_re.split(template_string)) 185 return map(create_token, bits) 186 187 def create_token(token_string): 188 "Convert the given token string into a new Token object and return it" 189 if token_string.startswith(VARIABLE_TAG_START): 190 return Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) 191 elif token_string.startswith(BLOCK_TAG_START): 192 return Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip()) 193 else: 194 return Token(TOKEN_TEXT, token_string) 195 196 class Parser: 197 def __init__(self, tokens): 198 self.tokens = tokens 199 200 def parse(self, parse_until=[]): 201 nodelist = NodeList() 202 while self.tokens: 203 token = self.next_token() 204 if token.token_type == TOKEN_TEXT: 205 nodelist.append(TextNode(token.contents)) 206 elif token.token_type == TOKEN_VAR: 207 if not token.contents: 208 raise TemplateSyntaxError, "Empty variable tag" 209 nodelist.append(VariableNode(token.contents)) 210 elif token.token_type == TOKEN_BLOCK: 211 if token.contents in parse_until: 212 # put token back on token list so calling code knows why it terminated 213 self.prepend_token(token) 214 return nodelist 215 try: 216 command = token.contents.split()[0] 217 except IndexError: 218 raise TemplateSyntaxError, "Empty block tag" 219 try: 220 # execute callback function for this tag and append resulting node 221 nodelist.append(registered_tags[command](self, token)) 222 except KeyError: 223 raise TemplateSyntaxError, "Invalid block tag: '%s'" % command 224 if parse_until: 225 raise TemplateSyntaxError, "Unclosed tag(s): '%s'" % ', '.join(parse_until) 226 return nodelist 227 228 def next_token(self): 229 return self.tokens.pop(0) 230 231 def prepend_token(self, token): 232 self.tokens.insert(0, token) 233 234 def delete_first_token(self): 235 del self.tokens[0] 236 237 class FilterParser: 238 """Parse a variable token and its optional filters (all as a single string), 239 and return a list of tuples of the filter name and arguments. 240 Sample: 241 >>> token = 'variable|default:"Default value"|date:"Y-m-d"' 242 >>> p = FilterParser(token) 243 >>> p.filters 244 [('default', 'Default value'), ('date', 'Y-m-d')] 245 >>> p.var 246 'variable' 247 248 This class should never be instantiated outside of the 249 get_filters_from_token helper function. 250 """ 251 def __init__(self, s): 252 self.s = s 253 self.i = -1 254 self.current = '' 255 self.filters = [] 256 self.current_filter_name = None 257 self.current_filter_arg = None 258 # First read the variable part 259 self.var = self.read_alphanumeric_token() 260 if not self.var: 261 raise TemplateSyntaxError, "Could not read variable name: '%s'" % self.s 262 if self.var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or self.var[0] == '_': 263 raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % self.var 264 # Have we reached the end? 265 if self.current is None: 266 return 267 if self.current != FILTER_SEPARATOR: 268 raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current) 269 # We have a filter separator; start reading the filters 270 self.read_filters() 271 272 def next_char(self): 273 self.i = self.i + 1 274 try: 275 self.current = self.s[self.i] 276 except IndexError: 277 self.current = None 278 279 def read_alphanumeric_token(self): 280 """Read a variable name or filter name, which are continuous strings of 281 alphanumeric characters + the underscore""" 282 var = '' 283 while 1: 284 self.next_char() 285 if self.current is None: 286 break 287 if self.current not in ALLOWED_VARIABLE_CHARS: 288 break 289 var += self.current 290 return var 291 292 def read_filters(self): 293 while 1: 294 filter_name, arg = self.read_filter() 295 if not registered_filters.has_key(filter_name): 296 raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name 297 if registered_filters[filter_name][1] == True and arg is None: 298 raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name 299 if registered_filters[filter_name][1] == False and arg is not None: 300 raise TemplateSyntaxError, "Filter '%s' should not have an argument (argument is %r)" % (filter_name, arg) 301 self.filters.append((filter_name, arg)) 302 if self.current is None: 303 break 304 305 def read_filter(self): 306 self.current_filter_name = self.read_alphanumeric_token() 307 self.current_filter_arg = None 308 # Have we reached the end? 309 if self.current is None: 310 return (self.current_filter_name, None) 311 # Does the filter have an argument? 312 if self.current == FILTER_ARGUMENT_SEPARATOR: 313 self.current_filter_arg = self.read_arg() 314 return (self.current_filter_name, self.current_filter_arg) 315 # Next thing MUST be a pipe 316 if self.current != FILTER_SEPARATOR: 317 raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current) 318 return (self.current_filter_name, self.current_filter_arg) 319 320 def read_arg(self): 321 # First read a " 322 self.next_char() 323 if self.current != '"': 324 raise TemplateSyntaxError, "Bad character (expecting '\"') '%s'" % self.current 325 self.escaped = False 326 arg = '' 327 while 1: 328 self.next_char() 329 if self.current == '"' and not self.escaped: 330 break 331 if self.current == '\\' and not self.escaped: 332 self.escaped = True 333 continue 334 if self.current == '\\' and self.escaped: 335 arg += '\\' 336 self.escaped = False 337 continue 338 if self.current == '"' and self.escaped: 339 arg += '"' 340 self.escaped = False 341 continue 342 if self.escaped and self.current not in '\\"': 343 raise TemplateSyntaxError, "Unescaped backslash in '%s'" % self.s 344 if self.current is None: 345 raise TemplateSyntaxError, "Unexpected end of argument in '%s'" % self.s 346 arg += self.current 347 # self.current must now be '"' 348 self.next_char() 349 return arg 350 351 def get_filters_from_token(token): 352 "Convenient wrapper for FilterParser" 353 p = FilterParser(token) 354 return (p.var, p.filters) 355 356 def resolve_variable(path, context): 357 """ 358 Returns the resolved variable, which may contain attribute syntax, within 359 the given context. The variable may be a hard-coded string (if it begins 360 and ends with single or double quote marks). 361 362 >>> c = {'article': {'section':'News'}} 363 >>> resolve_variable('article.section', c) 364 'News' 365 >>> resolve_variable('article', c) 366 {'section': 'News'} 367 >>> class AClass: pass 368 >>> c = AClass() 369 >>> c.article = AClass() 370 >>> c.article.section = 'News' 371 >>> resolve_variable('article.section', c) 372 'News' 373 374 (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.') 375 """ 376 if path[0] in ('"', "'") and path[0] == path[-1]: 377 current = path[1:-1] 378 else: 379 current = context 380 bits = path.split(VARIABLE_ATTRIBUTE_SEPARATOR) 381 while bits: 382 try: # dictionary lookup 383 current = current[bits[0]] 384 except (TypeError, AttributeError, KeyError): 385 try: # attribute lookup 386 current = getattr(current, bits[0]) 387 if callable(current): 388 if getattr(current, 'alters_data', False): 389 current = '' 390 else: 391 try: # method call (assuming no args required) 392 current = current() 393 except SilentVariableFailure: 394 current = '' 395 except TypeError: # arguments *were* required 396 current = '' # invalid method call 397 except (TypeError, AttributeError): 398 try: # list-index lookup 399 current = current[int(bits[0])] 400 except (IndexError, ValueError, KeyError): 401 raise VariableDoesNotExist, "Failed lookup for key [%s] in %r" % (bits[0], current) # missing attribute 402 del bits[0] 403 return current 404 405 def resolve_variable_with_filters(var_string, context): 406 """ 407 var_string is a full variable expression with optional filters, like: 408 a.b.c|lower|date:"y/m/d" 409 This function resolves the variable in the context, applies all filters and 410 returns the object. 411 """ 412 var, filters = get_filters_from_token(var_string) 413 try: 414 obj = resolve_variable(var, context) 415 except VariableDoesNotExist: 416 obj = '' 417 for name, arg in filters: 418 obj = registered_filters[name][0](obj, arg) 419 return obj 420 421 class Node: 422 def render(self, context): 423 "Return the node rendered as a string" 424 pass 425 426 def __iter__(self): 427 yield self 428 429 def get_nodes_by_type(self, nodetype): 430 "Return a list of all nodes (within this node and its nodelist) of the given type" 431 nodes = [] 432 if isinstance(self, nodetype): 433 nodes.append(self) 434 if hasattr(self, 'nodelist'): 435 nodes.extend(self.nodelist.get_nodes_by_type(nodetype)) 436 return nodes 437 438 class NodeList(list): 439 def render(self, context): 440 bits = [] 441 for node in self: 442 if isinstance(node, Node): 443 bits.append(node.render(context)) 444 else: 445 bits.append(node) 446 return ''.join(bits) 447 448 def get_nodes_by_type(self, nodetype): 449 "Return a list of all nodes of the given type" 450 nodes = [] 451 for node in self: 452 nodes.extend(node.get_nodes_by_type(nodetype)) 453 return nodes 454 455 class TextNode(Node): 456 def __init__(self, s): 457 self.s = s 458 459 def __repr__(self): 460 return "<Text Node: '%s'>" % self.s[:25] 461 462 def render(self, context): 463 return self.s 464 465 class VariableNode(Node): 466 def __init__(self, var_string): 467 self.var_string = var_string 468 469 def __repr__(self): 470 return "<Variable Node: %s>" % self.var_string 471 472 def render(self, context): 473 output = resolve_variable_with_filters(self.var_string, context) 474 # Check type so that we don't run str() on a Unicode object 475 if not isinstance(output, basestring): 476 output = str(output) 477 elif isinstance(output, unicode): 478 output = output.encode(DEFAULT_CHARSET) 479 return output 480 481 def register_tag(token_command, callback_function): 482 registered_tags[token_command] = callback_function 483 484 def unregister_tag(token_command): 485 del registered_tags[token_command] 486 487 def register_filter(filter_name, callback_function, has_arg): 488 registered_filters[filter_name] = (callback_function, has_arg) 489 490 def unregister_filter(filter_name): 491 del registered_filters[filter_name] 492 493 import defaulttags 494 import defaultfilters -
template/defaulttags.py
1 "Default tags used by the template system, available to all templates." 2 3 from django.core.template import Node, NodeList, Template, Context, \ 4 resolve_variable, resolve_variable_with_filters, get_filters_from_token, registered_filters, \ 5 TemplateSyntaxError, VariableDoesNotExist, \ 6 BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, \ 7 register_tag 8 import sys 9 10 class CommentNode(Node): 11 def render(self, context): 12 return '' 13 14 class CycleNode(Node): 15 def __init__(self, cyclevars): 16 self.cyclevars = cyclevars 17 self.cyclevars_len = len(cyclevars) 18 self.counter = -1 19 20 def render(self, context): 21 self.counter += 1 22 return self.cyclevars[self.counter % self.cyclevars_len] 23 24 class DebugNode(Node): 25 def render(self, context): 26 from pprint import pformat 27 output = [pformat(val) for val in context] 28 output.append('\n\n') 29 output.append(pformat(sys.modules)) 30 return ''.join(output) 31 32 class FilterNode(Node): 33 def __init__(self, filters, nodelist): 34 self.filters, self.nodelist = filters, nodelist 35 36 def render(self, context): 37 output = self.nodelist.render(context) 38 # apply filters 39 for f in self.filters: 40 output = registered_filters[f[0]][0](output, f[1]) 41 return output 42 43 class FirstOfNode(Node): 44 def __init__(self, vars): 45 self.vars = vars 46 47 def render(self, context): 48 for var in self.vars: 49 value = resolve_variable(var, context) 50 if value: 51 return str(value) 52 return '' 53 54 class ForNode(Node): 55 def __init__(self, loopvar, sequence, reversed, nodelist_loop): 56 self.loopvar, self.sequence = loopvar, sequence 57 self.reversed = reversed 58 self.nodelist_loop = nodelist_loop 59 60 def __repr__(self): 61 if self.reversed: 62 reversed = ' reversed' 63 else: 64 reversed = '' 65 return "<For Node: for %s in %s, tail_len: %d%s>" % \ 66 (self.loopvar, self.sequence, len(self.nodelist_loop), reversed) 67 68 def __iter__(self): 69 for node in self.nodelist_loop: 70 yield node 71 72 def get_nodes_by_type(self, nodetype): 73 nodes = [] 74 if isinstance(self, nodetype): 75 nodes.append(self) 76 nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype)) 77 return nodes 78 79 def render(self, context): 80 nodelist = NodeList() 81 if context.has_key('forloop'): 82 parentloop = context['forloop'] 83 else: 84 parentloop = {} 85 context.push() 86 try: 87 values = resolve_variable_with_filters(self.sequence, context) 88 except VariableDoesNotExist: 89 values = [] 90 if values is None: 91 values = [] 92 len_values = len(values) 93 if self.reversed: 94 # From http://www.python.org/doc/current/tut/node11.html 95 def reverse(data): 96 for index in range(len(data)-1, -1, -1): 97 yield data[index] 98 values = reverse(values) 99 for i, item in enumerate(values): 100 context['forloop'] = { 101 # shortcuts for current loop iteration number 102 'counter0': i, 103 'counter': i+1, 104 # reverse counter iteration numbers 105 'revcounter': len_values - i, 106 'revcounter0': len_values - i - 1, 107 # boolean values designating first and last times through loop 108 'first': (i == 0), 109 'last': (i == len_values - 1), 110 'parentloop': parentloop, 111 } 112 context[self.loopvar] = item 113 for node in self.nodelist_loop: 114 nodelist.append(node.render(context)) 115 context.pop() 116 return nodelist.render(context) 117 118 class IfChangedNode(Node): 119 def __init__(self, nodelist): 120 self.nodelist = nodelist 121 self._last_seen = None 122 123 def render(self, context): 124 content = self.nodelist.render(context) 125 if content != self._last_seen: 126 firstloop = (self._last_seen == None) 127 self._last_seen = content 128 context.push() 129 context['ifchanged'] = {'firstloop': firstloop} 130 content = self.nodelist.render(context) 131 context.pop() 132 return content 133 else: 134 return '' 135 136 class IfEqualNode(Node): 137 def __init__(self, var1, var2, nodelist_true, nodelist_false, negate): 138 self.var1, self.var2 = var1, var2 139 self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false 140 self.negate = negate 141 142 def __repr__(self): 143 return "<IfEqualNode>" 144 145 def render(self, context): 146 val1 = resolve_variable(self.var1, context) 147 val2 = resolve_variable(self.var2, context) 148 if (self.negate and val1 != val2) or (not self.negate and val1 == val2): 149 return self.nodelist_true.render(context) 150 return self.nodelist_false.render(context) 151 152 class IfNode(Node): 153 def __init__(self, boolvars, nodelist_true, nodelist_false): 154 self.boolvars = boolvars 155 self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false 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 for ifnot, boolvar in self.boolvars: 176 try: 177 value = resolve_variable_with_filters(boolvar, context) 178 except VariableDoesNotExist: 179 value = None 180 if (value and not ifnot) or (ifnot and not value): 181 return self.nodelist_true.render(context) 182 return self.nodelist_false.render(context) 183 184 class RegroupNode(Node): 185 def __init__(self, target_var, expression, var_name): 186 self.target_var, self.expression = target_var, expression 187 self.var_name = var_name 188 189 def render(self, context): 190 obj_list = resolve_variable_with_filters(self.target_var, context) 191 if obj_list == '': # target_var wasn't found in context; fail silently 192 context[self.var_name] = [] 193 return '' 194 output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]} 195 for obj in obj_list: 196 grouper = resolve_variable_with_filters('var.%s' % self.expression, \ 197 Context({'var': obj})) 198 if output and repr(output[-1]['grouper']) == repr(grouper): 199 output[-1]['list'].append(obj) 200 else: 201 output.append({'grouper': grouper, 'list': [obj]}) 202 context[self.var_name] = output 203 return '' 204 205 def include_is_allowed(filepath): 206 from django.conf.settings import ALLOWED_INCLUDE_ROOTS 207 for root in ALLOWED_INCLUDE_ROOTS: 208 if filepath.startswith(root): 209 return True 210 return False 211 212 class SsiNode(Node): 213 def __init__(self, filepath, parsed): 214 self.filepath, self.parsed = filepath, parsed 215 216 def render(self, context): 217 if not include_is_allowed(self.filepath): 218 return '' # Fail silently for invalid includes. 219 try: 220 fp = open(self.filepath, 'r') 221 output = fp.read() 222 fp.close() 223 except IOError: 224 output = '' 225 if self.parsed: 226 try: 227 t = Template(output) 228 return t.render(context) 229 except TemplateSyntaxError: 230 return '' # Fail silently for invalid included templates. 231 return output 232 233 class LoadNode(Node): 234 def __init__(self, taglib): 235 self.taglib = taglib 236 237 def load_taglib(taglib): 238 mod = __import__("django.templatetags.%s" % taglib.split('.')[-1], '', '', ['']) 239 reload(mod) 240 return mod 241 load_taglib = staticmethod(load_taglib) 242 243 def render(self, context): 244 "Import the relevant module" 245 try: 246 self.__class__.load_taglib(self.taglib) 247 except ImportError: 248 pass # Fail silently for invalid loads. 249 return '' 250 251 class NowNode(Node): 252 def __init__(self, format_string): 253 self.format_string = format_string 254 255 def render(self, context): 256 from datetime import datetime 257 from django.utils.dateformat import DateFormat 258 df = DateFormat(datetime.now()) 259 return df.format(self.format_string) 260 261 class TemplateTagNode(Node): 262 mapping = {'openblock': BLOCK_TAG_START, 263 'closeblock': BLOCK_TAG_END, 264 'openvariable': VARIABLE_TAG_START, 265 'closevariable': VARIABLE_TAG_END} 266 267 def __init__(self, tagtype): 268 self.tagtype = tagtype 269 270 def render(self, context): 271 return self.mapping.get(self.tagtype, '') 272 273 class WidthRatioNode(Node): 274 def __init__(self, val_var, max_var, max_width): 275 self.val_var = val_var 276 self.max_var = max_var 277 self.max_width = max_width 278 279 def render(self, context): 280 try: 281 value = resolve_variable_with_filters(self.val_var, context) 282 maxvalue = resolve_variable_with_filters(self.max_var, context) 283 except VariableDoesNotExist: 284 return '' 285 try: 286 value = float(value) 287 maxvalue = float(maxvalue) 288 ratio = (value / maxvalue) * int(self.max_width) 289 except (ValueError, ZeroDivisionError): 290 return '' 291 return str(int(round(ratio))) 292 293 def do_comment(parser, token): 294 """ 295 Ignore everything between ``{% comment %}`` and ``{% endcomment %}`` 296 """ 297 nodelist = parser.parse(('endcomment',)) 298 parser.delete_first_token() 299 return CommentNode() 300 301 def do_cycle(parser, token): 302 """ 303 Cycle among the given strings each time this tag is encountered 304 305 Within a loop, cycles among the given strings each time through 306 the loop:: 307 308 {% for o in some_list %} 309 <tr class="{% cycle row1,row2 %}"> 310 ... 311 </tr> 312 {% endfor %} 313 314 Outside of a loop, give the values a unique name the first time you call 315 it, then use that name each sucessive time through:: 316 317 <tr class="{% cycle row1,row2,row3 as rowcolors %}">...</tr> 318 <tr class="{% cycle rowcolors %}">...</tr> 319 <tr class="{% cycle rowcolors %}">...</tr> 320 321 You can use any number of values, seperated by commas. Make sure not to 322 put spaces between the values -- only commas. 323 """ 324 325 # Note: This returns the exact same node on each {% cycle name %} call; that 326 # is, the node object returned from {% cycle a,b,c as name %} and the one 327 # returned from {% cycle name %} are the exact same object. This shouldn't 328 # cause problems (heh), but if it does, now you know. 329 # 330 # Ugly hack warning: this stuffs the named template dict into parser so 331 # that names are only unique within each template (as opposed to using 332 # a global variable, which would make cycle names have to be unique across 333 # *all* templates. 334 335 args = token.contents.split() 336 if len(args) < 2: 337 raise TemplateSyntaxError("'Cycle' statement requires at least two arguments") 338 339 elif len(args) == 2 and "," in args[1]: 340 # {% cycle a,b,c %} 341 cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks 342 return CycleNode(cyclevars) 343 # {% cycle name %} 344 345 elif len(args) == 2: 346 name = args[1] 347 if not parser._namedCycleNodes.has_key(name): 348 raise TemplateSyntaxError("Named cycle '%s' does not exist" % name) 349 return parser._namedCycleNodes[name] 350 351 elif len(args) == 4: 352 # {% cycle a,b,c as name %} 353 if args[2] != 'as': 354 raise TemplateSyntaxError("Second 'cycle' argument must be 'as'") 355 cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks 356 name = args[3] 357 node = CycleNode(cyclevars) 358 359 if not hasattr(parser, '_namedCycleNodes'): 360 parser._namedCycleNodes = {} 361 362 parser._namedCycleNodes[name] = node 363 return node 364 365 else: 366 raise TemplateSyntaxError("Invalid arguments to 'cycle': %s" % args) 367 368 def do_debug(parser, token): 369 "Print a whole load of debugging information, including the context and imported modules" 370 return DebugNode() 371 372 def do_filter(parser, token): 373 """ 374 Filter the contents of the blog through variable filters. 375 376 Filters can also be piped through each other, and they can have 377 arguments -- just like in variable syntax. 378 379 Sample usage:: 380 381 {% filter escape|lower %} 382 This text will be HTML-escaped, and will appear in lowercase. 383 {% endfilter %} 384 """ 385 _, rest = token.contents.split(None, 1) 386 _, filters = get_filters_from_token('var|%s' % rest) 387 nodelist = parser.parse(('endfilter',)) 388 parser.delete_first_token() 389 return FilterNode(filters, nodelist) 390 391 def do_firstof(parser, token): 392 """ 393 Outputs the first variable passed that is not False. 394 395 Outputs nothing if all the passed variables are False. 396 397 Sample usage:: 398 399 {% firstof var1 var2 var3 %} 400 401 This is equivalent to:: 402 403 {% if var1 %} 404 {{ var1 }} 405 {% else %}{% if var2 %} 406 {{ var2 }} 407 {% else %}{% if var3 %} 408 {{ var3 }} 409 {% endif %}{% endif %}{% endif %} 410 411 but obviously much cleaner! 412 """ 413 bits = token.contents.split()[1:] 414 if len(bits) < 1: 415 raise TemplateSyntaxError, "'firstof' statement requires at least one argument" 416 return FirstOfNode(bits) 417 418 419 def do_for(parser, token): 420 """ 421 Loop over each item in an array. 422 423 For example, to display a list of athletes given ``athlete_list``:: 424 425 <ul> 426 {% for athlete in athlete_list %} 427 <li>{{ athlete.name }}</li> 428 {% endfor %} 429 </ul> 430 431 You can also loop over a list in reverse by using 432 ``{% for obj in list reversed %}``. 433 434 The for loop sets a number of variables available within the loop: 435 436 ========================== ================================================ 437 Variable Description 438 ========================== ================================================ 439 ``forloop.counter`` The current iteration of the loop (1-indexed) 440 ``forloop.counter0`` The current iteration of the loop (0-indexed) 441 ``forloop.revcounter`` The number of iterations from the end of the 442 loop (1-indexed) 443 ``forloop.revcounter0`` The number of iterations from the end of the 444 loop (0-indexed) 445 ``forloop.first`` True if this is the first time through the loop 446 ``forloop.last`` True if this is the last time through the loop 447 ``forloop.parentloop`` For nested loops, this is the loop "above" the 448 current one 449 ========================== ================================================ 450 451 """ 452 bits = token.contents.split() 453 if len(bits) == 5 and bits[4] != 'reversed': 454 raise TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents 455 if len(bits) not in (4, 5): 456 raise TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents 457 if bits[2] != 'in': 458 raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents 459 loopvar = bits[1] 460 sequence = bits[3] 461 reversed = (len(bits) == 5) 462 nodelist_loop = parser.parse(('endfor',)) 463 parser.delete_first_token() 464 return ForNode(loopvar, sequence, reversed, nodelist_loop) 465 466 def do_ifequal(parser, token, negate): 467 """ 468 Output the contents of the block if the two arguments equal/don't equal each other. 469 470 Examples:: 471 472 {% ifequal user.id comment.user_id %} 473 ... 474 {% endifequal %} 475 476 {% ifnotequal user.id comment.user_id %} 477 ... 478 {% else %} 479 ... 480 {% endifnotequal %} 481 """ 482 bits = token.contents.split() 483 if len(bits) != 3: 484 raise TemplateSyntaxError, "%r takes two arguments" % bits[0] 485 end_tag = 'end' + bits[0] 486 nodelist_true = parser.parse(('else', end_tag)) 487 token = parser.next_token() 488 if token.contents == 'else': 489 nodelist_false = parser.parse((end_tag,)) 490 parser.delete_first_token() 491 else: 492 nodelist_false = NodeList() 493 return IfEqualNode(bits[1], bits[2], nodelist_true, nodelist_false, negate) 494 495 def do_if(parser, token): 496 """ 497 The ``{% if %}`` tag evaluates a variable, and if that variable is "true" 498 (i.e. exists, is not empty, and is not a false boolean value) the contents 499 of the block are output: 500 501 :: 502 503 {% if althlete_list %} 504 Number of athletes: {{ althete_list|count }} 505 {% else %} 506 No athletes. 507 {% endif %} 508 509 In the above, if ``athlete_list`` is not empty, the number of athletes will 510 be displayed by the ``{{ athlete_list|count }}`` variable. 511 512 As you can see, the ``if`` tag can take an option ``{% else %}`` clause that 513 will be displayed if the test fails. 514 515 ``if`` tags may use ``or`` or ``not`` to test a number of variables or to 516 negate a given variable:: 517 518 {% if not athlete_list %} 519 There are no athletes. 520 {% endif %} 521 522 {% if athlete_list or coach_list %} 523 There are some athletes or some coaches. 524 {% endif %} 525 526 {% if not athlete_list or coach_list %} 527 There are no athletes or there are some coaches (OK, so 528 writing English translations of boolean logic sounds 529 stupid; it's not my fault). 530 {% endif %} 531 532 For simplicity, ``if`` tags do not allow ``and`` clauses; use nested ``if`` 533 tags instead:: 534 535 {% if athlete_list %} 536 {% if coach_list %} 537 Number of athletes: {{ athlete_list|count }}. 538 Number of coaches: {{ coach_list|count }}. 539 {% endif %} 540 {% endif %} 541 """ 542 bits = token.contents.split() 543 del bits[0] 544 if not bits: 545 raise TemplateSyntaxError, "'if' statement requires at least one argument" 546 # bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d'] 547 boolpairs = ' '.join(bits).split(' or ') 548 boolvars = [] 549 for boolpair in boolpairs: 550 if ' ' in boolpair: 551 not_, boolvar = boolpair.split() 552 if not_ != 'not': 553 raise TemplateSyntaxError, "Expected 'not' in if statement" 554 boolvars.append((True, boolvar)) 555 else: 556 boolvars.append((False, boolpair)) 557 nodelist_true = parser.parse(('else', 'endif')) 558 token = parser.next_token() 559 if token.contents == 'else': 560 nodelist_false = parser.parse(('endif',)) 561 parser.delete_first_token() 562 else: 563 nodelist_false = NodeList() 564 return IfNode(boolvars, nodelist_true, nodelist_false) 565 566 def do_ifchanged(parser, token): 567 """ 568 Check if a value has changed from the last iteration of a loop. 569 570 The 'ifchanged' block tag is used within a loop. It checks its own rendered 571 contents against its previous state and only displays its content if the 572 value has changed:: 573 574 <h1>Archive for {{ year }}</h1> 575 576 {% for date in days %} 577 {% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %} 578 <a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a> 579 {% endfor %} 580 """ 581 bits = token.contents.split() 582 if len(bits) != 1: 583 raise TemplateSyntaxError, "'ifchanged' tag takes no arguments" 584 nodelist = parser.parse(('endifchanged',)) 585 parser.delete_first_token() 586 return IfChangedNode(nodelist) 587 588 def do_ssi(parser, token): 589 """ 590 Output the contents of a given file into the page. 591 592 Like a simple "include" tag, the ``ssi`` tag includes the contents 593 of another file -- which must be specified using an absolute page -- 594 in the current page:: 595 596 {% ssi /home/html/ljworld.com/includes/right_generic.html %} 597 598 If the optional "parsed" parameter is given, the contents of the included 599 file are evaluated as template code, with the current context:: 600 601 {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %} 602 """ 603 bits = token.contents.split() 604 parsed = False 605 if len(bits) not in (2, 3): 606 raise TemplateSyntaxError, "'ssi' tag takes one argument: the path to the file to be included" 607 if len(bits) == 3: 608 if bits[2] == 'parsed': 609 parsed = True 610 else: 611 raise TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0] 612 return SsiNode(bits[1], parsed) 613 614 def do_load(parser, token): 615 """ 616 Load a custom template tag set. 617 618 For example, to load the template tags in ``django/templatetags/news/photos.py``:: 619 620 {% load news.photos %} 621 """ 622 bits = token.contents.split() 623 if len(bits) != 2: 624 raise TemplateSyntaxError, "'load' statement takes one argument" 625 taglib = bits[1] 626 # check at compile time that the module can be imported 627 try: 628 LoadNode.load_taglib(taglib) 629 except ImportError: 630 raise TemplateSyntaxError, "'%s' is not a valid tag library" % taglib 631 return LoadNode(taglib) 632 633 def do_now(parser, token): 634 """ 635 Display the date, formatted according to the given string. 636 637 Uses the same format as PHP's ``date()`` function; see http://php.net/date 638 for all the possible values. 639 640 Sample usage:: 641 642 It is {% now "jS F Y H:i" %} 643 """ 644 bits = token.contents.split('"') 645 if len(bits) != 3: 646 raise TemplateSyntaxError, "'now' statement takes one argument" 647 format_string = bits[1] 648 return NowNode(format_string) 649 650 def do_regroup(parser, token): 651 """ 652 Regroup a list of alike objects by a common attribute. 653 654 This complex tag is best illustrated by use of an example: say that 655 ``people`` is a list of ``Person`` objects that have ``first_name``, 656 ``last_name``, and ``gender`` attributes, and you'd like to display a list 657 that looks like: 658 659 * Male: 660 * George Bush 661 * Bill Clinton 662 * Female: 663 * Margaret Thatcher 664 * Colendeeza Rice 665 * Unknown: 666 * Pat Smith 667 668 The following snippet of template code would accomplish this dubious task:: 669 670 {% regroup people by gender as grouped %} 671 <ul> 672 {% for group in grouped %} 673 <li>{{ group.grouper }} 674 <ul> 675 {% for item in group.list %} 676 <li>{{ item }}</li> 677 {% endfor %} 678 </ul> 679 {% endfor %} 680 </ul> 681 682 As you can see, ``{% regroup %}`` populates a variable with a list of 683 objects with ``grouper`` and ``list`` attributes. ``grouper`` contains the 684 item that was grouped by; ``list`` contains the list of objects that share 685 that ``grouper``. In this case, ``grouper`` would be ``Male``, ``Female`` 686 and ``Unknown``, and ``list`` is the list of people with those genders. 687 688 Note that `{% regroup %}`` does not work when the list to be grouped is not 689 sorted by the key you are grouping by! This means that if your list of 690 people was not sorted by gender, you'd need to make sure it is sorted before 691 using it, i.e.:: 692 693 {% regroup people|dictsort:"gender" by gender as grouped %} 694 695 """ 696 firstbits = token.contents.split(None, 3) 697 if len(firstbits) != 4: 698 raise TemplateSyntaxError, "'regroup' tag takes five arguments" 699 target_var = firstbits[1] 700 if firstbits[2] != 'by': 701 raise TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'" 702 lastbits_reversed = firstbits[3][::-1].split(None, 2) 703 if lastbits_reversed[1][::-1] != 'as': 704 raise TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be 'as'" 705 expression = lastbits_reversed[2][::-1] 706 var_name = lastbits_reversed[0][::-1] 707 return RegroupNode(target_var, expression, var_name) 708 709 def do_templatetag(parser, token): 710 """ 711 Output one of the bits used to compose template tags. 712 713 Since the template system has no concept of "escaping", to display one of 714 the bits used in template tags, you must use the ``{% templatetag %}`` tag. 715 716 The argument tells which template bit to output: 717 718 ================== ======= 719 Argument Outputs 720 ================== ======= 721 ``openblock`` ``{%`` 722 ``closeblock`` ``%}`` 723 ``openvariable`` ``{{`` 724 ``closevariable`` ``}}`` 725 ================== ======= 726 """ 727 bits = token.contents.split() 728 if len(bits) != 2: 729 raise TemplateSyntaxError, "'templatetag' statement takes one argument" 730 tag = bits[1] 731 if not TemplateTagNode.mapping.has_key(tag): 732 raise TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \ 733 (tag, TemplateTagNode.mapping.keys()) 734 return TemplateTagNode(tag) 735 736 def do_widthratio(parser, token): 737 """ 738 For creating bar charts and such, this tag calculates the ratio of a given 739 value to a maximum value, and then applies that ratio to a constant. 740 741 For example:: 742 743 <img src='bar.gif' height='10' width='{% widthratio this_value max_value 100 %}' /> 744 745 Above, if ``this_value`` is 175 and ``max_value`` is 200, the the image in 746 the above example will be 88 pixels wide (because 175/200 = .875; .875 * 747 100 = 87.5 which is rounded up to 88). 748 """ 749 bits = token.contents.split() 750 if len(bits) != 4: 751 raise TemplateSyntaxError("widthratio takes three arguments") 752 tag, this_value_var, max_value_var, max_width = bits 753 try: 754 max_width = int(max_width) 755 except ValueError: 756 raise TemplateSyntaxError("widthratio final argument must be an integer") 757 return WidthRatioNode(this_value_var, max_value_var, max_width) 758 759 register_tag('comment', do_comment) 760 register_tag('cycle', do_cycle) 761 register_tag('debug', do_debug) 762 register_tag('filter', do_filter) 763 register_tag('firstof', do_firstof) 764 register_tag('for', do_for) 765 register_tag('ifequal', lambda parser, token: do_ifequal(parser, token, False)) 766 register_tag('ifnotequal', lambda parser, token: do_ifequal(parser, token, True)) 767 register_tag('if', do_if) 768 register_tag('ifchanged', do_ifchanged) 769 register_tag('regroup', do_regroup) 770 register_tag('ssi', do_ssi) 771 register_tag('load', do_load) 772 register_tag('now', do_now) 773 register_tag('templatetag', do_templatetag) 774 register_tag('widthratio', do_widthratio)