Ticket #626: 626_working.patch

File 626_working.patch, 122.9 KB (added by rjwittams, 19 years ago)

working patch

  • template_loader.py

     
    11"Wrapper for loading templates from storage of some sort (e.g. files or db)"
    2 import template
    3 from template_file import load_template_source
    42
     3from django.core import template
     4from django.core.template.loaders.filesystem import load_template_source
     5
    56class ExtendsError(Exception):
    67    pass
    78
     
    2425    Loads the given template_name and renders it with the given dictionary as
    2526    context. The template_name may be a string to load a single template using
    2627    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.
    2829    """
    2930    dictionary = dictionary or {}
    3031    if isinstance(template_name, (list, tuple)):
  • defaulttags.py

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

     
    1 # Wrapper for loading templates from files
    2 
    3 from django.conf.settings import TEMPLATE_DIRS, TEMPLATE_FILE_EXTENSION
    4 from django.core.template import TemplateDoesNotExist
    5 import os
    6 
    7 def load_template_source(template_name, template_dirs=None):
    8     if not template_dirs:
    9         template_dirs = TEMPLATE_DIRS
    10     tried = []
    11     for template_dir in template_dirs:
    12         filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION
    13         try:
    14             return open(filepath).read()
    15         except IOError:
    16             tried.append(filepath)
    17     if template_dirs:
    18         error_msg = "Tried %s" % tried
    19     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 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
  • defaultfilters.py

     
    1 "Default variable filters"
    2 
    3 import template, re
    4 import random as random_module
    5 
    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 ``&amp;`` entities"
    21     from django.utils.html import fix_ampersands
    22     return fix_ampersands(value)
    23 
    24 def floatformat(text, _):
    25     """
    26     Displays a floating point number as 34.2 (with one decimal place) - but
    27     only if there's a point to be displayed
    28     """
    29     from math import modf
    30     if not text:
    31         return ''
    32     if modf(float(text))[0] < 0.1:
    33         return text
    34     return "%.1f" % float(text)
    35 
    36 def linenumbers(value, _):
    37     "Displays text with line numbers"
    38     from django.utils.html import escape
    39     lines = value.split('\n')
    40     # Find the maximum width of the line count, for use with zero padding string format command
    41     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 of
    53     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 that
    66     the leading "%" is dropped.
    67 
    68     See http://docs.python.org/lib/typesseq-strings.html for documentation
    69     of Python string formatting
    70     """
    71     try:
    72         return ("%" + arg) % value
    73     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 words
    83 
    84     Argument: Number of words to truncate after
    85     """
    86     from django.utils.text import truncate_words
    87     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 urllib
    102     return urllib.quote(value)
    103 
    104 def urlize(value, _):
    105     "Converts URLs in plain text into clickable links"
    106     from django.utils.html import urlize
    107     return urlize(value, nofollow=True)
    108 
    109 def urlizetrunc(value, limit):
    110     """
    111     Converts URLs into clickable links, truncating URLs to the given character limit
    112 
    113     Argument: Length to truncate URLs to.
    114     """
    115     from django.utils.html import urlize
    116     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 length
    125 
    126     Argument: number of words to wrap the text at.
    127     """
    128     from django.utils.text import wrap
    129     return wrap(value, int(arg))
    130 
    131 def ljust(value, arg):
    132     """
    133     Left-aligns the value in a field of a given width
    134 
    135     Argument: field size
    136     """
    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 width
    142 
    143     Argument: field size
    144     """
    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 escape
    162     return escape(value)
    163 
    164 def linebreaks(value, _):
    165     "Converts newlines into <p> and <br />s"
    166     from django.utils.html import linebreaks
    167     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 value
    182 
    183 def striptags(value, _):
    184     "Strips all [X]HTML tags"
    185     from django.utils.html import strip_tags
    186     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 in
    197     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 the
    206     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 nicely
    225         return value
    226 
    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; see
    244     http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice
    245     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`` contains
    258     ``['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]``,
    259     then ``{{ var|unordered_list }}`` would return::
    260 
    261         <li>States
    262         <ul>
    263                 <li>Kansas
    264                 <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' * tabs
    275         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 the
    293     right-most digit, 2 is the second-right-most digit, etc. Returns the
    294     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 argument
    302     if arg < 1:
    303         return value
    304     try:
    305         return int(str(value)[-arg])
    306     except IndexError:
    307         return 0
    308 
    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 format
    316     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_format
    321     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 timesince
    326     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 arg
    335 
    336 def default_if_none(value, arg):
    337     "If value is None, use given default"
    338     if value is None:
    339         return arg
    340     return value
    341 
    342 def divisibleby(value, arg):
    343     "Returns true if the value is devisible by the argument"
    344     return int(value) % int(arg) == 0
    345 
    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                Outputs
    353     ==========  ======================  ==================================
    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 False
    358                                         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 = bits
    366     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 maybe
    370     if value:
    371         return yes
    372     return no
    373 
    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, 102
    381     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 number
    398         pass
    399     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 object
    404             pass
    405     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 phone2numeric
    410     return phone2numeric(value)
    411 
    412 def pprint(value, _):
    413     "A wrapper around pprint.pprint -- for debugging, really"
    414     from pprint import pformat
    415     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
     3from django.core.template import resolve_variable
     4from django.core.template import register_filter
     5# from django.core import template
     6import re
     7import random as random_module
     8
     9###################
     10# STRINGS         #
     11###################
     12
     13def addslashes(value, _):
     14    "Adds slashes - useful for passing strings to JavaScript, for example."
     15    return value.replace('"', '\\"').replace("'", "\\'")
     16
     17def capfirst(value, _):
     18    "Capitalizes the first character of the value"
     19    value = str(value)
     20    return value and value[0].upper() + value[1:]
     21
     22def fix_ampersands(value, _):
     23    "Replaces ampersands with ``&amp;`` entities"
     24    from django.utils.html import fix_ampersands
     25    return fix_ampersands(value)
     26
     27def 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
     39def 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
     49def lower(value, _):
     50    "Converts a string into all lowercase"
     51    return value.lower()
     52
     53def 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
     60def 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
     65def 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
     79def 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
     83def 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
     98def upper(value, _):
     99    "Converts a string into all uppercase"
     100    return value.upper()
     101
     102def urlencode(value, _):
     103    "Escapes a value for use in a URL"
     104    import urllib
     105    return urllib.quote(value)
     106
     107def 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
     112def 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
     121def wordcount(value, _):
     122    "Returns the number of words"
     123    return len(value.split())
     124
     125def 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
     134def 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
     142def 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
     150def center(value, arg):
     151    "Centers the value in a field of a given width"
     152    return str(value).center(int(arg))
     153
     154def 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
     162def escape(value, _):
     163    "Escapes a string's HTML"
     164    from django.utils.html import escape
     165    return escape(value)
     166
     167def linebreaks(value, _):
     168    "Converts newlines into <p> and <br />s"
     169    from django.utils.html import linebreaks
     170    return linebreaks(value)
     171
     172def linebreaksbr(value, _):
     173    "Converts newlines into <br />s"
     174    return value.replace('\n', '<br />')
     175
     176def 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
     186def 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
     197def 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
     206def 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
     216def first(value, _):
     217    "Returns the first item in a list"
     218    try:
     219        return value[0]
     220    except IndexError:
     221        return ''
     222
     223def 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
     230def length(value, _):
     231    "Returns the length of the value - useful for lists"
     232    return len(value)
     233
     234def length_is(value, arg):
     235    "Returns a boolean of whether the value's length is the argument"
     236    return len(value) == int(arg)
     237
     238def random(value, _):
     239    "Returns a random item from the list"
     240    return random_module.choice(value)
     241
     242def 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
     255def 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
     289def add(value, arg):
     290    "Adds the arg to the value"
     291    return int(value) + int(arg)
     292
     293def 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
     316def 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
     321def 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
     326def 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
     335def default(value, arg):
     336    "If value is unavailable, use given default"
     337    return value or arg
     338
     339def 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
     345def divisibleby(value, arg):
     346    "Returns true if the value is devisible by the argument"
     347    return int(value) % int(arg) == 0
     348
     349def 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
     381def 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
     395def 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
     410def 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
     415def 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)
     421register_filter('add', add, True)
     422register_filter('addslashes', addslashes, False)
     423register_filter('capfirst', capfirst, False)
     424register_filter('center', center, True)
     425register_filter('cut', cut, True)
     426register_filter('date', date, True)
     427register_filter('default', default, True)
     428register_filter('dictsort', dictsort, True)
     429register_filter('dictsortreversed', dictsortreversed, True)
     430register_filter('divisibleby', divisibleby, True)
     431register_filter('escape', escape, False)
     432register_filter('filesizeformat', filesizeformat, False)
     433register_filter('first', first, False)
     434register_filter('fix_ampersands', fix_ampersands, False)
     435register_filter('floatformat', floatformat, False)
     436register_filter('get_digit', get_digit, True)
     437register_filter('join', join, True)
     438register_filter('length', length, False)
     439register_filter('length_is', length_is, True)
     440register_filter('linebreaks', linebreaks, False)
     441register_filter('linebreaksbr', linebreaksbr, False)
     442register_filter('linenumbers', linenumbers, False)
     443register_filter('ljust', ljust, True)
     444register_filter('lower', lower, False)
     445register_filter('make_list', make_list, False)
     446register_filter('phone2numeric', phone2numeric, False)
     447register_filter('pluralize', pluralize, False)
     448register_filter('pprint', pprint, False)
     449register_filter('removetags', removetags, True)
     450register_filter('random', random, False)
     451register_filter('rjust', rjust, True)
     452register_filter('slice', slice_, True)
     453register_filter('slugify', slugify, False)
     454register_filter('stringformat', stringformat, True)
     455register_filter('striptags', striptags, False)
     456register_filter('time', time, True)
     457register_filter('timesince', timesince, False)
     458register_filter('title', title, False)
     459register_filter('truncatewords', truncatewords, True)
     460register_filter('unordered_list', unordered_list, False)
     461register_filter('upper', upper, False)
     462register_filter('urlencode', urlencode, False)
     463register_filter('urlize', urlize, False)
     464register_filter('urlizetrunc', urlizetrunc, True)
     465register_filter('wordcount', wordcount, False)
     466register_filter('wordwrap', wordwrap, True)
     467register_filter('yesno', yesno, True)
  • template/__init__.py

     
     1"""
     2This is the Django template system.
     3
     4How it works:
     5
     6The tokenize() function converts a template string (i.e., a string containing
     7markup with custom template tags) to tokens, which can be either plain text
     8(TOKEN_TEXT), variables (TOKEN_VAR) or block statements (TOKEN_BLOCK).
     9
     10The Parser() class takes a list of tokens in its constructor, and its parse()
     11method returns a compiled template -- which is, under the hood, a list of
     12Node objects.
     13
     14Each 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
     16logic (IfNode), results of looping (ForNode), or anything else. The core Node
     17types are TextNode, VariableNode, IfNode and ForNode, but plugin modules can
     18define their own custom node types.
     19
     20Each Node has a render() method, which takes a Context and returns a string of
     21the rendered node. For example, the render() method of a Variable Node returns
     22the variable's value as a string. The render() method of an IfNode returns the
     23rendered output of whatever was inside the loop, recursively.
     24
     25The Template class is a convenient wrapper that takes care of template
     26compilation and rendering.
     27
     28Usage:
     29
     30The only thing you should ever use directly in this file is the Template class.
     31Create a compiled template object with a template_string, then call render()
     32with a context. In the compilation stage, the TemplateSyntaxError exception
     33will be raised if the template doesn't have proper syntax.
     34
     35Sample 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
     48times 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"""
     57import re
     58from django.conf.settings import DEFAULT_CHARSET
     59
     60__all__ = ('Template','Context','compile_string')
     61
     62TOKEN_TEXT = 0
     63TOKEN_VAR = 1
     64TOKEN_BLOCK = 2
     65
     66# template syntax constants
     67FILTER_SEPARATOR = '|'
     68FILTER_ARGUMENT_SEPARATOR = ':'
     69VARIABLE_ATTRIBUTE_SEPARATOR = '.'
     70BLOCK_TAG_START = '{%'
     71BLOCK_TAG_END = '%}'
     72VARIABLE_TAG_START = '{{'
     73VARIABLE_TAG_END = '}}'
     74
     75ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.'
     76
     77# match a variable or block tag and capture the entire tag, including start/end delimiters
     78tag_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
     82registered_tags = {}
     83
     84# global dict used by register_filter; maps custom filters to callback functions
     85registered_filters = {}
     86
     87class TemplateSyntaxError(Exception):
     88    pass
     89
     90class ContextPopException(Exception):
     91    "pop() has been called more times than push()"
     92    pass
     93
     94class TemplateDoesNotExist(Exception):
     95    pass
     96
     97class VariableDoesNotExist(Exception):
     98    pass
     99
     100class SilentVariableFailure(Exception):
     101    "Any function raising this exception will be ignored by resolve_variable"
     102    pass
     103
     104class 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
     118def 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
     124class 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
     170class 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
     181def 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
     187def 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
     196class 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
     237class 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
     351def get_filters_from_token(token):
     352    "Convenient wrapper for FilterParser"
     353    p = FilterParser(token)
     354    return (p.var, p.filters)
     355
     356def 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
     405def 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
     421class 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
     438class 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
     455class 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
     465class 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
     481def register_tag(token_command, callback_function):
     482    registered_tags[token_command] = callback_function
     483
     484def unregister_tag(token_command):
     485    del registered_tags[token_command]
     486
     487def register_filter(filter_name, callback_function, has_arg):
     488    registered_filters[filter_name] = (callback_function, has_arg)
     489
     490def unregister_filter(filter_name):
     491    del registered_filters[filter_name]
     492
     493import defaulttags
     494import defaultfilters
  • template/defaulttags.py

     
     1"Default tags used by the template system, available to all templates."
     2
     3from 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
     8import sys
     9
     10class CommentNode(Node):
     11    def render(self, context):
     12        return ''
     13
     14class 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
     24class 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
     32class 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
     43class 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
     54class 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
     118class 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
     136class 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
     152class 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
     184class 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
     205def 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
     212class 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
     233class 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
     251class 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
     261class 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
     273class 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
     293def 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
     301def 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
     368def do_debug(parser, token):
     369    "Print a whole load of debugging information, including the context and imported modules"
     370    return DebugNode()
     371
     372def 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
     391def 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
     419def 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
     466def 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
     495def 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
     566def 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
     588def 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
     614def 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
     633def 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
     650def 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
     709def 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
     736def 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
     759register_tag('comment', do_comment)
     760register_tag('cycle', do_cycle)
     761register_tag('debug', do_debug)
     762register_tag('filter', do_filter)
     763register_tag('firstof', do_firstof)
     764register_tag('for', do_for)
     765register_tag('ifequal', lambda parser, token: do_ifequal(parser, token, False))
     766register_tag('ifnotequal', lambda parser, token: do_ifequal(parser, token, True))
     767register_tag('if', do_if)
     768register_tag('ifchanged', do_ifchanged)
     769register_tag('regroup', do_regroup)
     770register_tag('ssi', do_ssi)
     771register_tag('load', do_load)
     772register_tag('now', do_now)
     773register_tag('templatetag', do_templatetag)
     774register_tag('widthratio', do_widthratio)
Back to Top