Ticket #6587: new_template_lib_loader_10.diff

File new_template_lib_loader_10.diff, 70.0 KB (added by oyvind, 16 years ago)

fixed import to get the correct module, moved defaulttags and defaultfilters to avoid circular imports, fixes #6579

  • django/template/__init__.py

    diff --git a/django/template/__init__.py b/django/template/__init__.py
    index 5c4ab30..31a3790 100644
    a b from django.utils.encoding import smart_unicode, force_unicode  
    5959from django.utils.translation import ugettext as _
    6060from django.utils.safestring import SafeData, EscapeData, mark_safe, mark_for_escaping
    6161from django.utils.html import escape
     62from django.templatetags import get_templatetags_modules
    6263
    6364__all__ = ('Template', 'Context', 'RequestContext', 'compile_string')
    6465
    class Library(object):  
    913914            return func
    914915        return dec
    915916
    916 def get_library(module_name):
    917     lib = libraries.get(module_name, None)
     917def import_library(module_name):
     918    try:
     919        components = module_name.split('.')
     920        mod = __import__(module_name)
     921        for comp in components[1:]:
     922            mod = getattr(mod, comp)
     923    except ImportError, AttributeError:
     924        return None
     925    try:
     926        return mod.register
     927    except AttributeError:
     928        raise InvalidTemplateLibrary("Template library %s does not have a variable named 'register'" % module_name)
     929
     930def get_library(library_name):
     931    lib = libraries.get(library_name, None)
    918932    if not lib:
    919         try:
    920             mod = __import__(module_name, {}, {}, [''])
    921         except ImportError, e:
    922             raise InvalidTemplateLibrary("Could not load template library from %s, %s" % (module_name, e))
    923         try:
    924             lib = mod.register
    925             libraries[module_name] = lib
    926         except AttributeError:
    927             raise InvalidTemplateLibrary("Template library %s does not have a variable named 'register'" % module_name)
     933
     934        """
     935        If library is not already loaded loop over all templatetags modules to locate it.
     936
     937        {% load somelib %} and {% load someotherlib %} loops twice.
     938
     939        Subsequent loads eg. {% load somelib %} in the same thread will grab the cached
     940        module from libraries.
     941        """
     942        templatetags_modules = get_templatetags_modules()
     943        tried_modules = []
     944        for module in templatetags_modules:
     945            taglib_module = '%s.%s' % (module, library_name)
     946            tried_modules.append(taglib_module)
     947            lib = import_library(taglib_module)
     948            if lib:
     949                libraries[library_name] = lib
     950                break
     951        if not lib:
     952            raise InvalidTemplateLibrary("Template library %s not found, tried %s" % (library_name, ','.join(tried_modules)))
    928953    return lib
    929954
    930955def add_to_builtins(module_name):
    931     builtins.append(get_library(module_name))
     956    builtins.append(import_library(module_name))
    932957
    933 add_to_builtins('django.template.defaulttags')
    934 add_to_builtins('django.template.defaultfilters')
     958add_to_builtins('django.templatetags.defaulttags')
     959add_to_builtins('django.templatetags.defaultfilters')
  • django/template/defaulttags.py

    diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
    index 01c43ee..535457a 100644
    a b def load(parser, token):  
    852852    for taglib in bits[1:]:
    853853        # add the library to the parser
    854854        try:
    855             lib = get_library("django.templatetags.%s" % taglib)
     855            lib = get_library(taglib)
    856856            parser.add_library(lib)
    857857        except InvalidTemplateLibrary, e:
    858858            raise TemplateSyntaxError("'%s' is not a valid tag library: %s" %
  • django/templatetags/__init__.py

    diff --git a/django/templatetags/__init__.py b/django/templatetags/__init__.py
    index 9204535..6f1aaa8 100644
    a b  
    11from django.conf import settings
    22
    3 for a in settings.INSTALLED_APPS:
    4     try:
    5         __path__.extend(__import__(a + '.templatetags', {}, {}, ['']).__path__)
    6     except ImportError:
    7         pass
     3templatetags_modules= []
     4
     5def get_templatetags_modules():
     6    if not templatetags_modules:
     7        """ Populate list once per thread. """
     8        for app_module in ['django'] + list(settings.INSTALLED_APPS):
     9            try:
     10                name = '%s.templatetags' % app_module
     11                components = name.split('.')
     12                mod = __import__(name)
     13                for comp in components[1:]:
     14                    mod = getattr(mod, comp)
     15                templatetags_modules.append(name)
     16            except ImportError, AttributeError:
     17                pass
     18    return templatetags_modules
  • new file django/templatetags/defaultfilters.py

    diff --git a/django/templatetags/defaultfilters.py b/django/templatetags/defaultfilters.py
    new file mode 100644
    index 0000000..cef3143
    - +  
     1"""Default variable filters."""
     2
     3import re
     4import random as random_module
     5try:
     6    from functools import wraps
     7except ImportError:
     8    from django.utils.functional import wraps  # Python 2.3, 2.4 fallback.
     9
     10from django.template import Variable, Library
     11from django.conf import settings
     12from django.utils.translation import ugettext, ungettext
     13from django.utils.encoding import force_unicode, iri_to_uri
     14from django.utils.safestring import mark_safe, SafeData
     15
     16register = Library()
     17
     18#######################
     19# STRING DECORATOR    #
     20#######################
     21
     22def stringfilter(func):
     23    """
     24    Decorator for filters which should only receive unicode objects. The object
     25    passed as the first positional argument will be converted to a unicode
     26    object.
     27    """
     28    def _dec(*args, **kwargs):
     29        if args:
     30            args = list(args)
     31            args[0] = force_unicode(args[0])
     32            if isinstance(args[0], SafeData) and getattr(func, 'is_safe', False):
     33                return mark_safe(func(*args, **kwargs))
     34        return func(*args, **kwargs)
     35
     36    # Include a reference to the real function (used to check original
     37    # arguments by the template parser).
     38    _dec._decorated_function = getattr(func, '_decorated_function', func)
     39    for attr in ('is_safe', 'needs_autoescape'):
     40        if hasattr(func, attr):
     41            setattr(_dec, attr, getattr(func, attr))
     42    return wraps(func)(_dec)
     43
     44###################
     45# STRINGS         #
     46###################
     47
     48
     49def addslashes(value):
     50    """
     51    Adds slashes before quotes. Useful for escaping strings in CSV, for
     52    example. Less useful for escaping JavaScript; use the ``escapejs``
     53    filter instead.
     54    """
     55    return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
     56addslashes.is_safe = True
     57addslashes = stringfilter(addslashes)
     58
     59def capfirst(value):
     60    """Capitalizes the first character of the value."""
     61    return value and value[0].upper() + value[1:]
     62capfirst.is_safe=True
     63capfirst = stringfilter(capfirst)
     64
     65_js_escapes = (
     66    ('\\', '\\\\'),
     67    ('"', '\\"'),
     68    ("'", "\\'"),
     69    ('\n', '\\n'),
     70    ('\r', '\\r'),
     71    ('\b', '\\b'),
     72    ('\f', '\\f'),
     73    ('\t', '\\t'),
     74    ('\v', '\\v'),
     75    ('</', '<\\/'),
     76)
     77def escapejs(value):
     78    """Backslash-escapes characters for use in JavaScript strings."""
     79    for bad, good in _js_escapes:
     80        value = value.replace(bad, good)
     81    return value
     82escapejs = stringfilter(escapejs)
     83
     84def fix_ampersands(value):
     85    """Replaces ampersands with ``&amp;`` entities."""
     86    from django.utils.html import fix_ampersands
     87    return fix_ampersands(value)
     88fix_ampersands.is_safe=True
     89fix_ampersands = stringfilter(fix_ampersands)
     90
     91def floatformat(text, arg=-1):
     92    """
     93    Displays a float to a specified number of decimal places.
     94
     95    If called without an argument, it displays the floating point number with
     96    one decimal place -- but only if there's a decimal place to be displayed:
     97
     98    * num1 = 34.23234
     99    * num2 = 34.00000
     100    * num3 = 34.26000
     101    * {{ num1|floatformat }} displays "34.2"
     102    * {{ num2|floatformat }} displays "34"
     103    * {{ num3|floatformat }} displays "34.3"
     104
     105    If arg is positive, it will always display exactly arg number of decimal
     106    places:
     107
     108    * {{ num1|floatformat:3 }} displays "34.232"
     109    * {{ num2|floatformat:3 }} displays "34.000"
     110    * {{ num3|floatformat:3 }} displays "34.260"
     111
     112    If arg is negative, it will display arg number of decimal places -- but
     113    only if there are places to be displayed:
     114
     115    * {{ num1|floatformat:"-3" }} displays "34.232"
     116    * {{ num2|floatformat:"-3" }} displays "34"
     117    * {{ num3|floatformat:"-3" }} displays "34.260"
     118    """
     119    try:
     120        f = float(text)
     121    except (ValueError, TypeError):
     122        return u''
     123    try:
     124        d = int(arg)
     125    except ValueError:
     126        return force_unicode(f)
     127    try:
     128        m = f - int(f)
     129    except OverflowError:
     130        return force_unicode(f)
     131    if not m and d < 0:
     132        return mark_safe(u'%d' % int(f))
     133    else:
     134        formatstr = u'%%.%df' % abs(d)
     135        return mark_safe(formatstr % f)
     136floatformat.is_safe = True
     137
     138def iriencode(value):
     139    """Escapes an IRI value for use in a URL."""
     140    return force_unicode(iri_to_uri(value))
     141iriencode.is_safe = True
     142iriencode = stringfilter(iriencode)
     143
     144def linenumbers(value, autoescape=None):
     145    """Displays text with line numbers."""
     146    from django.utils.html import escape
     147    lines = value.split(u'\n')
     148    # Find the maximum width of the line count, for use with zero padding
     149    # string format command
     150    width = unicode(len(unicode(len(lines))))
     151    if not autoescape or isinstance(value, SafeData):
     152        for i, line in enumerate(lines):
     153            lines[i] = (u"%0" + width  + u"d. %s") % (i + 1, line)
     154    else:
     155        for i, line in enumerate(lines):
     156            lines[i] = (u"%0" + width  + u"d. %s") % (i + 1, escape(line))
     157    return mark_safe(u'\n'.join(lines))
     158linenumbers.is_safe = True
     159linenumbers.needs_autoescape = True
     160linenumbers = stringfilter(linenumbers)
     161
     162def lower(value):
     163    """Converts a string into all lowercase."""
     164    return value.lower()
     165lower.is_safe = True
     166lower = stringfilter(lower)
     167
     168def make_list(value):
     169    """
     170    Returns the value turned into a list.
     171
     172    For an integer, it's a list of digits.
     173    For a string, it's a list of characters.
     174    """
     175    return list(value)
     176make_list.is_safe = False
     177make_list = stringfilter(make_list)
     178
     179def slugify(value):
     180    """
     181    Normalizes string, converts to lowercase, removes non-alpha characters,
     182    and converts spaces to hyphens.
     183    """
     184    import unicodedata
     185    value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
     186    value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
     187    return mark_safe(re.sub('[-\s]+', '-', value))
     188slugify.is_safe = True
     189slugify = stringfilter(slugify)
     190
     191def stringformat(value, arg):
     192    """
     193    Formats the variable according to the arg, a string formatting specifier.
     194
     195    This specifier uses Python string formating syntax, with the exception that
     196    the leading "%" is dropped.
     197
     198    See http://docs.python.org/lib/typesseq-strings.html for documentation
     199    of Python string formatting
     200    """
     201    try:
     202        return (u"%" + unicode(arg)) % value
     203    except (ValueError, TypeError):
     204        return u""
     205stringformat.is_safe = True
     206
     207def title(value):
     208    """Converts a string into titlecase."""
     209    return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
     210title.is_safe = True
     211title = stringfilter(title)
     212
     213def truncatewords(value, arg):
     214    """
     215    Truncates a string after a certain number of words.
     216
     217    Argument: Number of words to truncate after.
     218    """
     219    from django.utils.text import truncate_words
     220    try:
     221        length = int(arg)
     222    except ValueError: # Invalid literal for int().
     223        return value # Fail silently.
     224    return truncate_words(value, length)
     225truncatewords.is_safe = True
     226truncatewords = stringfilter(truncatewords)
     227
     228def truncatewords_html(value, arg):
     229    """
     230    Truncates HTML after a certain number of words.
     231
     232    Argument: Number of words to truncate after.
     233    """
     234    from django.utils.text import truncate_html_words
     235    try:
     236        length = int(arg)
     237    except ValueError: # invalid literal for int()
     238        return value # Fail silently.
     239    return truncate_html_words(value, length)
     240truncatewords_html.is_safe = True
     241truncatewords_html = stringfilter(truncatewords_html)
     242
     243def upper(value):
     244    """Converts a string into all uppercase."""
     245    return value.upper()
     246upper.is_safe = False
     247upper = stringfilter(upper)
     248
     249def urlencode(value):
     250    """Escapes a value for use in a URL."""
     251    from django.utils.http import urlquote
     252    return urlquote(value)
     253urlencode.is_safe = False
     254urlencode = stringfilter(urlencode)
     255
     256def urlize(value, autoescape=None):
     257    """Converts URLs in plain text into clickable links."""
     258    from django.utils.html import urlize
     259    return mark_safe(urlize(value, nofollow=True, autoescape=autoescape))
     260urlize.is_safe=True
     261urlize.needs_autoescape = True
     262urlize = stringfilter(urlize)
     263
     264def urlizetrunc(value, limit, autoescape=None):
     265    """
     266    Converts URLs into clickable links, truncating URLs to the given character
     267    limit, and adding 'rel=nofollow' attribute to discourage spamming.
     268
     269    Argument: Length to truncate URLs to.
     270    """
     271    from django.utils.html import urlize
     272    return mark_safe(urlize(value, trim_url_limit=int(limit), nofollow=True,
     273                            autoescape=autoescape))
     274urlizetrunc.is_safe = True
     275urlizetrunc.needs_autoescape = True
     276urlizetrunc = stringfilter(urlizetrunc)
     277
     278def wordcount(value):
     279    """Returns the number of words."""
     280    return len(value.split())
     281wordcount.is_safe = False
     282wordcount = stringfilter(wordcount)
     283
     284def wordwrap(value, arg):
     285    """
     286    Wraps words at specified line length.
     287
     288    Argument: number of characters to wrap the text at.
     289    """
     290    from django.utils.text import wrap
     291    return wrap(value, int(arg))
     292wordwrap.is_safe = True
     293wordwrap = stringfilter(wordwrap)
     294
     295def ljust(value, arg):
     296    """
     297    Left-aligns the value in a field of a given width.
     298
     299    Argument: field size.
     300    """
     301    return value.ljust(int(arg))
     302ljust.is_safe = True
     303ljust = stringfilter(ljust)
     304
     305def rjust(value, arg):
     306    """
     307    Right-aligns the value in a field of a given width.
     308
     309    Argument: field size.
     310    """
     311    return value.rjust(int(arg))
     312rjust.is_safe = True
     313rjust = stringfilter(rjust)
     314
     315def center(value, arg):
     316    """Centers the value in a field of a given width."""
     317    return value.center(int(arg))
     318center.is_safe = True
     319center = stringfilter(center)
     320
     321def cut(value, arg):
     322    """
     323    Removes all values of arg from the given string.
     324    """
     325    safe = isinstance(value, SafeData)
     326    value = value.replace(arg, u'')
     327    if safe and arg != ';':
     328        return mark_safe(value)
     329    return value
     330cut = stringfilter(cut)
     331
     332###################
     333# HTML STRINGS    #
     334###################
     335
     336def escape(value):
     337    """
     338    Marks the value as a string that should not be auto-escaped.
     339    """
     340    from django.utils.safestring import mark_for_escaping
     341    return mark_for_escaping(value)
     342escape.is_safe = True
     343escape = stringfilter(escape)
     344
     345def force_escape(value):
     346    """
     347    Escapes a string's HTML. This returns a new string containing the escaped
     348    characters (as opposed to "escape", which marks the content for later
     349    possible escaping).
     350    """
     351    from django.utils.html import escape
     352    return mark_safe(escape(value))
     353force_escape = stringfilter(force_escape)
     354force_escape.is_safe = True
     355
     356def linebreaks(value, autoescape=None):
     357    """
     358    Replaces line breaks in plain text with appropriate HTML; a single
     359    newline becomes an HTML line break (``<br />``) and a new line
     360    followed by a blank line becomes a paragraph break (``</p>``).
     361    """
     362    from django.utils.html import linebreaks
     363    autoescape = autoescape and not isinstance(value, SafeData)
     364    return mark_safe(linebreaks(value, autoescape))
     365linebreaks.is_safe = True
     366linebreaks.needs_autoescape = True
     367linebreaks = stringfilter(linebreaks)
     368
     369def linebreaksbr(value, autoescape=None):
     370    """
     371    Converts all newlines in a piece of plain text to HTML line breaks
     372    (``<br />``).
     373    """
     374    if autoescape and not isinstance(value, SafeData):
     375        from django.utils.html import escape
     376        value = escape(value)
     377    return mark_safe(value.replace('\n', '<br />'))
     378linebreaksbr.is_safe = True
     379linebreaksbr.needs_autoescape = True
     380linebreaksbr = stringfilter(linebreaksbr)
     381
     382def safe(value):
     383    """
     384    Marks the value as a string that should not be auto-escaped.
     385    """
     386    from django.utils.safestring import mark_safe
     387    return mark_safe(value)
     388safe.is_safe = True
     389safe = stringfilter(safe)
     390
     391def removetags(value, tags):
     392    """Removes a space separated list of [X]HTML tags from the output."""
     393    tags = [re.escape(tag) for tag in tags.split()]
     394    tags_re = u'(%s)' % u'|'.join(tags)
     395    starttag_re = re.compile(ur'<%s(/?>|(\s+[^>]*>))' % tags_re, re.U)
     396    endtag_re = re.compile(u'</%s>' % tags_re)
     397    value = starttag_re.sub(u'', value)
     398    value = endtag_re.sub(u'', value)
     399    return value
     400removetags.is_safe = True
     401removetags = stringfilter(removetags)
     402
     403def striptags(value):
     404    """Strips all [X]HTML tags."""
     405    from django.utils.html import strip_tags
     406    return strip_tags(value)
     407striptags.is_safe = True
     408striptags = stringfilter(striptags)
     409
     410###################
     411# LISTS           #
     412###################
     413
     414def dictsort(value, arg):
     415    """
     416    Takes a list of dicts, returns that list sorted by the property given in
     417    the argument.
     418    """
     419    var_resolve = Variable(arg).resolve
     420    decorated = [(var_resolve(item), item) for item in value]
     421    decorated.sort()
     422    return [item[1] for item in decorated]
     423dictsort.is_safe = False
     424
     425def dictsortreversed(value, arg):
     426    """
     427    Takes a list of dicts, returns that list sorted in reverse order by the
     428    property given in the argument.
     429    """
     430    var_resolve = Variable(arg).resolve
     431    decorated = [(var_resolve(item), item) for item in value]
     432    decorated.sort()
     433    decorated.reverse()
     434    return [item[1] for item in decorated]
     435dictsortreversed.is_safe = False
     436
     437def first(value):
     438    """Returns the first item in a list."""
     439    try:
     440        return value[0]
     441    except IndexError:
     442        return u''
     443first.is_safe = False
     444
     445def join(value, arg):
     446    """Joins a list with a string, like Python's ``str.join(list)``."""
     447    try:
     448        data = arg.join(map(force_unicode, value))
     449    except AttributeError: # fail silently but nicely
     450        return value
     451    safe_args = reduce(lambda lhs, rhs: lhs and isinstance(rhs, SafeData),
     452            value, True)
     453    if safe_args:
     454        return mark_safe(data)
     455    else:
     456        return data
     457join.is_safe = True
     458
     459def last(value):
     460    "Returns the last item in a list"
     461    try:
     462        return value[-1]
     463    except IndexError:
     464        return u''
     465last.is_safe = True
     466
     467def length(value):
     468    """Returns the length of the value - useful for lists."""
     469    return len(value)
     470length.is_safe = True
     471
     472def length_is(value, arg):
     473    """Returns a boolean of whether the value's length is the argument."""
     474    return len(value) == int(arg)
     475length_is.is_safe = True
     476
     477def random(value):
     478    """Returns a random item from the list."""
     479    return random_module.choice(value)
     480random.is_safe = True
     481
     482def slice_(value, arg):
     483    """
     484    Returns a slice of the list.
     485
     486    Uses the same syntax as Python's list slicing; see
     487    http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice
     488    for an introduction.
     489    """
     490    try:
     491        bits = []
     492        for x in arg.split(u':'):
     493            if len(x) == 0:
     494                bits.append(None)
     495            else:
     496                bits.append(int(x))
     497        return value[slice(*bits)]
     498
     499    except (ValueError, TypeError):
     500        return value # Fail silently.
     501slice_.is_safe = True
     502
     503def unordered_list(value, autoescape=None):
     504    """
     505    Recursively takes a self-nested list and returns an HTML unordered list --
     506    WITHOUT opening and closing <ul> tags.
     507
     508    The list is assumed to be in the proper format. For example, if ``var``
     509    contains: ``['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']]``,
     510    then ``{{ var|unordered_list }}`` would return::
     511
     512        <li>States
     513        <ul>
     514                <li>Kansas
     515                <ul>
     516                        <li>Lawrence</li>
     517                        <li>Topeka</li>
     518                </ul>
     519                </li>
     520                <li>Illinois</li>
     521        </ul>
     522        </li>
     523    """
     524    if autoescape:
     525        from django.utils.html import conditional_escape
     526        escaper = conditional_escape
     527    else:
     528        escaper = lambda x: x
     529    def convert_old_style_list(list_):
     530        """
     531        Converts old style lists to the new easier to understand format.
     532
     533        The old list format looked like:
     534            ['Item 1', [['Item 1.1', []], ['Item 1.2', []]]
     535
     536        And it is converted to:
     537            ['Item 1', ['Item 1.1', 'Item 1.2]]
     538        """
     539        if not isinstance(list_, (tuple, list)) or len(list_) != 2:
     540            return list_, False
     541        first_item, second_item = list_
     542        if second_item == []:
     543            return [first_item], True
     544        old_style_list = True
     545        new_second_item = []
     546        for sublist in second_item:
     547            item, old_style_list = convert_old_style_list(sublist)
     548            if not old_style_list:
     549                break
     550            new_second_item.extend(item)
     551        if old_style_list:
     552            second_item = new_second_item
     553        return [first_item, second_item], old_style_list
     554    def _helper(list_, tabs=1):
     555        indent = u'\t' * tabs
     556        output = []
     557
     558        list_length = len(list_)
     559        i = 0
     560        while i < list_length:
     561            title = list_[i]
     562            sublist = ''
     563            sublist_item = None
     564            if isinstance(title, (list, tuple)):
     565                sublist_item = title
     566                title = ''
     567            elif i < list_length - 1:
     568                next_item = list_[i+1]
     569                if next_item and isinstance(next_item, (list, tuple)):
     570                    # The next item is a sub-list.
     571                    sublist_item = next_item
     572                    # We've processed the next item now too.
     573                    i += 1
     574            if sublist_item:
     575                sublist = _helper(sublist_item, tabs+1)
     576                sublist = '\n%s<ul>\n%s\n%s</ul>\n%s' % (indent, sublist,
     577                                                         indent, indent)
     578            output.append('%s<li>%s%s</li>' % (indent,
     579                    escaper(force_unicode(title)), sublist))
     580            i += 1
     581        return '\n'.join(output)
     582    value, converted = convert_old_style_list(value)
     583    return mark_safe(_helper(value))
     584unordered_list.is_safe = True
     585unordered_list.needs_autoescape = True
     586
     587###################
     588# INTEGERS        #
     589###################
     590
     591def add(value, arg):
     592    """Adds the arg to the value."""
     593    return int(value) + int(arg)
     594add.is_safe = False
     595
     596def get_digit(value, arg):
     597    """
     598    Given a whole number, returns the requested digit of it, where 1 is the
     599    right-most digit, 2 is the second-right-most digit, etc. Returns the
     600    original value for invalid input (if input or argument is not an integer,
     601    or if argument is less than 1). Otherwise, output is always an integer.
     602    """
     603    try:
     604        arg = int(arg)
     605        value = int(value)
     606    except ValueError:
     607        return value # Fail silently for an invalid argument
     608    if arg < 1:
     609        return value
     610    try:
     611        return int(str(value)[-arg])
     612    except IndexError:
     613        return 0
     614get_digit.is_safe = False
     615
     616###################
     617# DATES           #
     618###################
     619
     620def date(value, arg=None):
     621    """Formats a date according to the given format."""
     622    from django.utils.dateformat import format
     623    if not value:
     624        return u''
     625    if arg is None:
     626        arg = settings.DATE_FORMAT
     627    return format(value, arg)
     628date.is_safe = False
     629
     630def time(value, arg=None):
     631    """Formats a time according to the given format."""
     632    from django.utils.dateformat import time_format
     633    if value in (None, u''):
     634        return u''
     635    if arg is None:
     636        arg = settings.TIME_FORMAT
     637    return time_format(value, arg)
     638time.is_safe = False
     639
     640def timesince(value, arg=None):
     641    """Formats a date as the time since that date (i.e. "4 days, 6 hours")."""
     642    from django.utils.timesince import timesince
     643    if not value:
     644        return u''
     645    if arg:
     646        return timesince(arg, value)
     647    return timesince(value)
     648timesince.is_safe = False
     649
     650def timeuntil(value, arg=None):
     651    """Formats a date as the time until that date (i.e. "4 days, 6 hours")."""
     652    from django.utils.timesince import timesince
     653    from datetime import datetime
     654    if not value:
     655        return u''
     656    if arg:
     657        return timesince(arg, value)
     658    return timesince(datetime.now(), value)
     659timeuntil.is_safe = False
     660
     661###################
     662# LOGIC           #
     663###################
     664
     665def default(value, arg):
     666    """If value is unavailable, use given default."""
     667    return value or arg
     668default.is_safe = False
     669
     670def default_if_none(value, arg):
     671    """If value is None, use given default."""
     672    if value is None:
     673        return arg
     674    return value
     675default_if_none.is_safe = False
     676
     677def divisibleby(value, arg):
     678    """Returns True if the value is devisible by the argument."""
     679    return int(value) % int(arg) == 0
     680divisibleby.is_safe = False
     681
     682def yesno(value, arg=None):
     683    """
     684    Given a string mapping values for true, false and (optionally) None,
     685    returns one of those strings accoding to the value:
     686
     687    ==========  ======================  ==================================
     688    Value       Argument                Outputs
     689    ==========  ======================  ==================================
     690    ``True``    ``"yeah,no,maybe"``     ``yeah``
     691    ``False``   ``"yeah,no,maybe"``     ``no``
     692    ``None``    ``"yeah,no,maybe"``     ``maybe``
     693    ``None``    ``"yeah,no"``           ``"no"`` (converts None to False
     694                                        if no mapping for None is given.
     695    ==========  ======================  ==================================
     696    """
     697    if arg is None:
     698        arg = ugettext('yes,no,maybe')
     699    bits = arg.split(u',')
     700    if len(bits) < 2:
     701        return value # Invalid arg.
     702    try:
     703        yes, no, maybe = bits
     704    except ValueError:
     705        # Unpack list of wrong size (no "maybe" value provided).
     706        yes, no, maybe = bits[0], bits[1], bits[1]
     707    if value is None:
     708        return maybe
     709    if value:
     710        return yes
     711    return no
     712yesno.is_safe = False
     713
     714###################
     715# MISC            #
     716###################
     717
     718def filesizeformat(bytes):
     719    """
     720    Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB,
     721    102 bytes, etc).
     722    """
     723    try:
     724        bytes = float(bytes)
     725    except TypeError:
     726        return u"0 bytes"
     727
     728    if bytes < 1024:
     729        return ungettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes}
     730    if bytes < 1024 * 1024:
     731        return ugettext("%.1f KB") % (bytes / 1024)
     732    if bytes < 1024 * 1024 * 1024:
     733        return ugettext("%.1f MB") % (bytes / (1024 * 1024))
     734    return ugettext("%.1f GB") % (bytes / (1024 * 1024 * 1024))
     735filesizeformat.is_safe = True
     736
     737def pluralize(value, arg=u's'):
     738    """
     739    Returns a plural suffix if the value is not 1. By default, 's' is used as
     740    the suffix:
     741
     742    * If value is 0, vote{{ value|pluralize }} displays "0 votes".
     743    * If value is 1, vote{{ value|pluralize }} displays "1 vote".
     744    * If value is 2, vote{{ value|pluralize }} displays "2 votes".
     745
     746    If an argument is provided, that string is used instead:
     747
     748    * If value is 0, class{{ value|pluralize:"es" }} displays "0 classes".
     749    * If value is 1, class{{ value|pluralize:"es" }} displays "1 class".
     750    * If value is 2, class{{ value|pluralize:"es" }} displays "2 classes".
     751
     752    If the provided argument contains a comma, the text before the comma is
     753    used for the singular case and the text after the comma is used for the
     754    plural case:
     755
     756    * If value is 0, cand{{ value|pluralize:"y,ies" }} displays "0 candies".
     757    * If value is 1, cand{{ value|pluralize:"y,ies" }} displays "1 candy".
     758    * If value is 2, cand{{ value|pluralize:"y,ies" }} displays "2 candies".
     759    """
     760    if not u',' in arg:
     761        arg = u',' + arg
     762    bits = arg.split(u',')
     763    if len(bits) > 2:
     764        return u''
     765    singular_suffix, plural_suffix = bits[:2]
     766
     767    try:
     768        if int(value) != 1:
     769            return plural_suffix
     770    except ValueError: # Invalid string that's not a number.
     771        pass
     772    except TypeError: # Value isn't a string or a number; maybe it's a list?
     773        try:
     774            if len(value) != 1:
     775                return plural_suffix
     776        except TypeError: # len() of unsized object.
     777            pass
     778    return singular_suffix
     779pluralize.is_safe = False
     780
     781def phone2numeric(value):
     782    """Takes a phone number and converts it in to its numerical equivalent."""
     783    from django.utils.text import phone2numeric
     784    return phone2numeric(value)
     785phone2numeric.is_safe = True
     786
     787def pprint(value):
     788    """A wrapper around pprint.pprint -- for debugging, really."""
     789    from pprint import pformat
     790    try:
     791        return pformat(value)
     792    except Exception, e:
     793        return u"Error in formatting: %s" % force_unicode(e, errors="replace")
     794pprint.is_safe = True
     795
     796# Syntax: register.filter(name of filter, callback)
     797register.filter(add)
     798register.filter(addslashes)
     799register.filter(capfirst)
     800register.filter(center)
     801register.filter(cut)
     802register.filter(date)
     803register.filter(default)
     804register.filter(default_if_none)
     805register.filter(dictsort)
     806register.filter(dictsortreversed)
     807register.filter(divisibleby)
     808register.filter(escape)
     809register.filter(escapejs)
     810register.filter(filesizeformat)
     811register.filter(first)
     812register.filter(fix_ampersands)
     813register.filter(floatformat)
     814register.filter(force_escape)
     815register.filter(get_digit)
     816register.filter(iriencode)
     817register.filter(join)
     818register.filter(last)
     819register.filter(length)
     820register.filter(length_is)
     821register.filter(linebreaks)
     822register.filter(linebreaksbr)
     823register.filter(linenumbers)
     824register.filter(ljust)
     825register.filter(lower)
     826register.filter(make_list)
     827register.filter(phone2numeric)
     828register.filter(pluralize)
     829register.filter(pprint)
     830register.filter(removetags)
     831register.filter(random)
     832register.filter(rjust)
     833register.filter(safe)
     834register.filter('slice', slice_)
     835register.filter(slugify)
     836register.filter(stringformat)
     837register.filter(striptags)
     838register.filter(time)
     839register.filter(timesince)
     840register.filter(timeuntil)
     841register.filter(title)
     842register.filter(truncatewords)
     843register.filter(truncatewords_html)
     844register.filter(unordered_list)
     845register.filter(upper)
     846register.filter(urlencode)
     847register.filter(urlize)
     848register.filter(urlizetrunc)
     849register.filter(wordcount)
     850register.filter(wordwrap)
     851register.filter(yesno)
  • new file django/templatetags/defaulttags.py

    diff --git a/django/templatetags/defaulttags.py b/django/templatetags/defaulttags.py
    new file mode 100644
    index 0000000..535457a
    - +  
     1"""Default tags used by the template system, available to all templates."""
     2
     3import sys
     4import re
     5from itertools import cycle as itertools_cycle
     6try:
     7    reversed
     8except NameError:
     9    from django.utils.itercompat import reversed     # Python 2.3 fallback
     10
     11from django.template import Node, NodeList, Template, Context, Variable
     12from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END
     13from django.template import get_library, Library, InvalidTemplateLibrary
     14from django.conf import settings
     15from django.utils.encoding import smart_str, smart_unicode
     16from django.utils.itercompat import groupby
     17from django.utils.safestring import mark_safe
     18
     19register = Library()
     20
     21class AutoEscapeControlNode(Node):
     22    """Implements the actions of the autoescape tag."""
     23    def __init__(self, setting, nodelist):
     24        self.setting, self.nodelist = setting, nodelist
     25
     26    def render(self, context):
     27        old_setting = context.autoescape
     28        context.autoescape = self.setting
     29        output = self.nodelist.render(context)
     30        context.autoescape = old_setting
     31        if self.setting:
     32            return mark_safe(output)
     33        else:
     34            return output
     35
     36class CommentNode(Node):
     37    def render(self, context):
     38        return ''
     39
     40class CycleNode(Node):
     41    def __init__(self, cyclevars, variable_name=None):
     42        self.cycle_iter = itertools_cycle([Variable(v) for v in cyclevars])
     43        self.variable_name = variable_name
     44
     45    def render(self, context):
     46        value = self.cycle_iter.next().resolve(context)
     47        if self.variable_name:
     48            context[self.variable_name] = value
     49        return value
     50
     51class DebugNode(Node):
     52    def render(self, context):
     53        from pprint import pformat
     54        output = [pformat(val) for val in context]
     55        output.append('\n\n')
     56        output.append(pformat(sys.modules))
     57        return ''.join(output)
     58
     59class FilterNode(Node):
     60    def __init__(self, filter_expr, nodelist):
     61        self.filter_expr, self.nodelist = filter_expr, nodelist
     62
     63    def render(self, context):
     64        output = self.nodelist.render(context)
     65        # Apply filters.
     66        context.update({'var': output})
     67        filtered = self.filter_expr.resolve(context)
     68        context.pop()
     69        return filtered
     70
     71class FirstOfNode(Node):
     72    def __init__(self, vars):
     73        self.vars = map(Variable, vars)
     74
     75    def render(self, context):
     76        for var in self.vars:
     77            try:
     78                value = var.resolve(context)
     79            except VariableDoesNotExist:
     80                continue
     81            if value:
     82                return smart_unicode(value)
     83        return u''
     84
     85class ForNode(Node):
     86    def __init__(self, loopvars, sequence, is_reversed, nodelist_loop):
     87        self.loopvars, self.sequence = loopvars, sequence
     88        self.is_reversed = is_reversed
     89        self.nodelist_loop = nodelist_loop
     90
     91    def __repr__(self):
     92        reversed_text = self.is_reversed and ' reversed' or ''
     93        return "<For Node: for %s in %s, tail_len: %d%s>" % \
     94            (', '.join(self.loopvars), self.sequence, len(self.nodelist_loop),
     95             reversed_text)
     96
     97    def __iter__(self):
     98        for node in self.nodelist_loop:
     99            yield node
     100
     101    def get_nodes_by_type(self, nodetype):
     102        nodes = []
     103        if isinstance(self, nodetype):
     104            nodes.append(self)
     105        nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype))
     106        return nodes
     107
     108    def render(self, context):
     109        nodelist = NodeList()
     110        if 'forloop' in context:
     111            parentloop = context['forloop']
     112        else:
     113            parentloop = {}
     114        context.push()
     115        try:
     116            values = self.sequence.resolve(context, True)
     117        except VariableDoesNotExist:
     118            values = []
     119        if values is None:
     120            values = []
     121        if not hasattr(values, '__len__'):
     122            values = list(values)
     123        len_values = len(values)
     124        if self.is_reversed:
     125            values = reversed(values)
     126        unpack = len(self.loopvars) > 1
     127        # Create a forloop value in the context.  We'll update counters on each
     128        # iteration just below.
     129        loop_dict = context['forloop'] = {'parentloop': parentloop}
     130        for i, item in enumerate(values):
     131            # Shortcuts for current loop iteration number.
     132            loop_dict['counter0'] = i
     133            loop_dict['counter'] = i+1
     134            # Reverse counter iteration numbers.
     135            loop_dict['revcounter'] = len_values - i
     136            loop_dict['revcounter0'] = len_values - i - 1
     137            # Boolean values designating first and last times through loop.
     138            loop_dict['first'] = (i == 0)
     139            loop_dict['last'] = (i == len_values - 1)
     140
     141            if unpack:
     142                # If there are multiple loop variables, unpack the item into
     143                # them.
     144                context.update(dict(zip(self.loopvars, item)))
     145            else:
     146                context[self.loopvars[0]] = item
     147            for node in self.nodelist_loop:
     148                nodelist.append(node.render(context))
     149            if unpack:
     150                # The loop variables were pushed on to the context so pop them
     151                # off again. This is necessary because the tag lets the length
     152                # of loopvars differ to the length of each set of items and we
     153                # don't want to leave any vars from the previous loop on the
     154                # context.
     155                context.pop()
     156        context.pop()
     157        return nodelist.render(context)
     158
     159class IfChangedNode(Node):
     160    def __init__(self, nodelist, *varlist):
     161        self.nodelist = nodelist
     162        self._last_seen = None
     163        self._varlist = map(Variable, varlist)
     164        self._id = str(id(self))
     165
     166    def render(self, context):
     167        if 'forloop' in context and self._id not in context['forloop']:
     168            self._last_seen = None
     169            context['forloop'][self._id] = 1
     170        try:
     171            if self._varlist:
     172                # Consider multiple parameters.  This automatically behaves
     173                # like an OR evaluation of the multiple variables.
     174                compare_to = [var.resolve(context) for var in self._varlist]
     175            else:
     176                compare_to = self.nodelist.render(context)
     177        except VariableDoesNotExist:
     178            compare_to = None
     179
     180        if  compare_to != self._last_seen:
     181            firstloop = (self._last_seen == None)
     182            self._last_seen = compare_to
     183            context.push()
     184            context['ifchanged'] = {'firstloop': firstloop}
     185            content = self.nodelist.render(context)
     186            context.pop()
     187            return content
     188        else:
     189            return ''
     190
     191class IfEqualNode(Node):
     192    def __init__(self, var1, var2, nodelist_true, nodelist_false, negate):
     193        self.var1, self.var2 = Variable(var1), Variable(var2)
     194        self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
     195        self.negate = negate
     196
     197    def __repr__(self):
     198        return "<IfEqualNode>"
     199
     200    def render(self, context):
     201        try:
     202            val1 = self.var1.resolve(context)
     203        except VariableDoesNotExist:
     204            val1 = None
     205        try:
     206            val2 = self.var2.resolve(context)
     207        except VariableDoesNotExist:
     208            val2 = None
     209        if (self.negate and val1 != val2) or (not self.negate and val1 == val2):
     210            return self.nodelist_true.render(context)
     211        return self.nodelist_false.render(context)
     212
     213class IfNode(Node):
     214    def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type):
     215        self.bool_exprs = bool_exprs
     216        self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
     217        self.link_type = link_type
     218
     219    def __repr__(self):
     220        return "<If node>"
     221
     222    def __iter__(self):
     223        for node in self.nodelist_true:
     224            yield node
     225        for node in self.nodelist_false:
     226            yield node
     227
     228    def get_nodes_by_type(self, nodetype):
     229        nodes = []
     230        if isinstance(self, nodetype):
     231            nodes.append(self)
     232        nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))
     233        nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
     234        return nodes
     235
     236    def render(self, context):
     237        if self.link_type == IfNode.LinkTypes.or_:
     238            for ifnot, bool_expr in self.bool_exprs:
     239                try:
     240                    value = bool_expr.resolve(context, True)
     241                except VariableDoesNotExist:
     242                    value = None
     243                if (value and not ifnot) or (ifnot and not value):
     244                    return self.nodelist_true.render(context)
     245            return self.nodelist_false.render(context)
     246        else:
     247            for ifnot, bool_expr in self.bool_exprs:
     248                try:
     249                    value = bool_expr.resolve(context, True)
     250                except VariableDoesNotExist:
     251                    value = None
     252                if not ((value and not ifnot) or (ifnot and not value)):
     253                    return self.nodelist_false.render(context)
     254            return self.nodelist_true.render(context)
     255
     256    class LinkTypes:
     257        and_ = 0,
     258        or_ = 1
     259
     260class RegroupNode(Node):
     261    def __init__(self, target, expression, var_name):
     262        self.target, self.expression = target, expression
     263        self.var_name = var_name
     264
     265    def render(self, context):
     266        obj_list = self.target.resolve(context, True)
     267        if obj_list == None:
     268            # target variable wasn't found in context; fail silently.
     269            context[self.var_name] = []
     270            return ''
     271        # List of dictionaries in the format:
     272        # {'grouper': 'key', 'list': [list of contents]}.
     273        context[self.var_name] = [
     274            {'grouper': key, 'list': list(val)}
     275            for key, val in
     276            groupby(obj_list, lambda v, f=self.expression.resolve: f(v, True))
     277        ]
     278        return ''
     279
     280def include_is_allowed(filepath):
     281    for root in settings.ALLOWED_INCLUDE_ROOTS:
     282        if filepath.startswith(root):
     283            return True
     284    return False
     285
     286class SsiNode(Node):
     287    def __init__(self, filepath, parsed):
     288        self.filepath, self.parsed = filepath, parsed
     289
     290    def render(self, context):
     291        if not include_is_allowed(self.filepath):
     292            if settings.DEBUG:
     293                return "[Didn't have permission to include file]"
     294            else:
     295                return '' # Fail silently for invalid includes.
     296        try:
     297            fp = open(self.filepath, 'r')
     298            output = fp.read()
     299            fp.close()
     300        except IOError:
     301            output = ''
     302        if self.parsed:
     303            try:
     304                t = Template(output, name=self.filepath)
     305                return t.render(context)
     306            except TemplateSyntaxError, e:
     307                if settings.DEBUG:
     308                    return "[Included template had syntax error: %s]" % e
     309                else:
     310                    return '' # Fail silently for invalid included templates.
     311        return output
     312
     313class LoadNode(Node):
     314    def render(self, context):
     315        return ''
     316
     317class NowNode(Node):
     318    def __init__(self, format_string):
     319        self.format_string = format_string
     320
     321    def render(self, context):
     322        from datetime import datetime
     323        from django.utils.dateformat import DateFormat
     324        df = DateFormat(datetime.now())
     325        return df.format(self.format_string)
     326
     327class SpacelessNode(Node):
     328    def __init__(self, nodelist):
     329        self.nodelist = nodelist
     330
     331    def render(self, context):
     332        from django.utils.html import strip_spaces_between_tags
     333        return strip_spaces_between_tags(self.nodelist.render(context).strip())
     334
     335class TemplateTagNode(Node):
     336    mapping = {'openblock': BLOCK_TAG_START,
     337               'closeblock': BLOCK_TAG_END,
     338               'openvariable': VARIABLE_TAG_START,
     339               'closevariable': VARIABLE_TAG_END,
     340               'openbrace': SINGLE_BRACE_START,
     341               'closebrace': SINGLE_BRACE_END,
     342               'opencomment': COMMENT_TAG_START,
     343               'closecomment': COMMENT_TAG_END,
     344               }
     345
     346    def __init__(self, tagtype):
     347        self.tagtype = tagtype
     348
     349    def render(self, context):
     350        return self.mapping.get(self.tagtype, '')
     351
     352class URLNode(Node):
     353    def __init__(self, view_name, args, kwargs):
     354        self.view_name = view_name
     355        self.args = args
     356        self.kwargs = kwargs
     357
     358    def render(self, context):
     359        from django.core.urlresolvers import reverse, NoReverseMatch
     360        args = [arg.resolve(context) for arg in self.args]
     361        kwargs = dict([(smart_str(k,'ascii'), v.resolve(context))
     362                       for k, v in self.kwargs.items()])
     363        try:
     364            return reverse(self.view_name, args=args, kwargs=kwargs)
     365        except NoReverseMatch:
     366            try:
     367                project_name = settings.SETTINGS_MODULE.split('.')[0]
     368                return reverse(project_name + '.' + self.view_name,
     369                               args=args, kwargs=kwargs)
     370            except NoReverseMatch:
     371                return ''
     372
     373class WidthRatioNode(Node):
     374    def __init__(self, val_expr, max_expr, max_width):
     375        self.val_expr = val_expr
     376        self.max_expr = max_expr
     377        self.max_width = max_width
     378
     379    def render(self, context):
     380        try:
     381            value = self.val_expr.resolve(context)
     382            maxvalue = self.max_expr.resolve(context)
     383        except VariableDoesNotExist:
     384            return ''
     385        try:
     386            value = float(value)
     387            maxvalue = float(maxvalue)
     388            ratio = (value / maxvalue) * int(self.max_width)
     389        except (ValueError, ZeroDivisionError):
     390            return ''
     391        return str(int(round(ratio)))
     392
     393class WithNode(Node):
     394    def __init__(self, var, name, nodelist):
     395        self.var = var
     396        self.name = name
     397        self.nodelist = nodelist
     398
     399    def __repr__(self):
     400        return "<WithNode>"
     401
     402    def render(self, context):
     403        val = self.var.resolve(context)
     404        context.push()
     405        context[self.name] = val
     406        output = self.nodelist.render(context)
     407        context.pop()
     408        return output
     409
     410#@register.tag
     411def autoescape(parser, token):
     412    """
     413    Force autoescape behaviour for this block.
     414    """
     415    args = token.contents.split()
     416    if len(args) != 2:
     417        raise TemplateSyntaxError("'Autoescape' tag requires exactly one argument.")
     418    arg = args[1]
     419    if arg not in (u'on', u'off'):
     420        raise TemplateSyntaxError("'Autoescape' argument should be 'on' or 'off'")
     421    nodelist = parser.parse(('endautoescape',))
     422    parser.delete_first_token()
     423    return AutoEscapeControlNode((arg == 'on'), nodelist)
     424autoescape = register.tag(autoescape)
     425
     426#@register.tag
     427def comment(parser, token):
     428    """
     429    Ignores everything between ``{% comment %}`` and ``{% endcomment %}``.
     430    """
     431    parser.skip_past('endcomment')
     432    return CommentNode()
     433comment = register.tag(comment)
     434
     435#@register.tag
     436def cycle(parser, token):
     437    """
     438    Cycles among the given strings each time this tag is encountered.
     439
     440    Within a loop, cycles among the given strings each time through
     441    the loop::
     442
     443        {% for o in some_list %}
     444            <tr class="{% cycle 'row1' 'row2' %}">
     445                ...
     446            </tr>
     447        {% endfor %}
     448
     449    Outside of a loop, give the values a unique name the first time you call
     450    it, then use that name each sucessive time through::
     451
     452            <tr class="{% cycle 'row1' 'row2' 'row3' as rowcolors %}">...</tr>
     453            <tr class="{% cycle rowcolors %}">...</tr>
     454            <tr class="{% cycle rowcolors %}">...</tr>
     455
     456    You can use any number of values, separated by spaces. Commas can also
     457    be used to separate values; if a comma is used, the cycle values are
     458    interpreted as literal strings.
     459    """
     460
     461    # Note: This returns the exact same node on each {% cycle name %} call;
     462    # that is, the node object returned from {% cycle a b c as name %} and the
     463    # one returned from {% cycle name %} are the exact same object. This
     464    # shouldn't cause problems (heh), but if it does, now you know.
     465    #
     466    # Ugly hack warning: This stuffs the named template dict into parser so
     467    # that names are only unique within each template (as opposed to using
     468    # a global variable, which would make cycle names have to be unique across
     469    # *all* templates.
     470
     471    args = token.split_contents()
     472
     473    if len(args) < 2:
     474        raise TemplateSyntaxError("'cycle' tag requires at least two arguments")
     475
     476    if ',' in args[1]:
     477        # Backwards compatibility: {% cycle a,b %} or {% cycle a,b as foo %}
     478        # case.
     479        args[1:2] = ['"%s"' % arg for arg in args[1].split(",")]
     480
     481    if len(args) == 2:
     482        # {% cycle foo %} case.
     483        name = args[1]
     484        if not hasattr(parser, '_namedCycleNodes'):
     485            raise TemplateSyntaxError("No named cycles in template. '%s' is not defined" % name)
     486        if not name in parser._namedCycleNodes:
     487            raise TemplateSyntaxError("Named cycle '%s' does not exist" % name)
     488        return parser._namedCycleNodes[name]
     489
     490    if len(args) > 4 and args[-2] == 'as':
     491        name = args[-1]
     492        node = CycleNode(args[1:-2], name)
     493        if not hasattr(parser, '_namedCycleNodes'):
     494            parser._namedCycleNodes = {}
     495        parser._namedCycleNodes[name] = node
     496    else:
     497        node = CycleNode(args[1:])
     498    return node
     499cycle = register.tag(cycle)
     500
     501def debug(parser, token):
     502    """
     503    Outputs a whole load of debugging information, including the current
     504    context and imported modules.
     505
     506    Sample usage::
     507
     508        <pre>
     509            {% debug %}
     510        </pre>
     511    """
     512    return DebugNode()
     513debug = register.tag(debug)
     514
     515#@register.tag(name="filter")
     516def do_filter(parser, token):
     517    """
     518    Filters the contents of the block through variable filters.
     519
     520    Filters can also be piped through each other, and they can have
     521    arguments -- just like in variable syntax.
     522
     523    Sample usage::
     524
     525        {% filter force_escape|lower %}
     526            This text will be HTML-escaped, and will appear in lowercase.
     527        {% endfilter %}
     528    """
     529    _, rest = token.contents.split(None, 1)
     530    filter_expr = parser.compile_filter("var|%s" % (rest))
     531    for func, unused in filter_expr.filters:
     532        if getattr(func, '_decorated_function', func).__name__ in ('escape', 'safe'):
     533            raise TemplateSyntaxError('"filter %s" is not permitted.  Use the "autoescape" tag instead.' % func.__name__)
     534    nodelist = parser.parse(('endfilter',))
     535    parser.delete_first_token()
     536    return FilterNode(filter_expr, nodelist)
     537do_filter = register.tag("filter", do_filter)
     538
     539#@register.tag
     540def firstof(parser, token):
     541    """
     542    Outputs the first variable passed that is not False.
     543
     544    Outputs nothing if all the passed variables are False.
     545
     546    Sample usage::
     547
     548        {% firstof var1 var2 var3 %}
     549
     550    This is equivalent to::
     551
     552        {% if var1 %}
     553            {{ var1 }}
     554        {% else %}{% if var2 %}
     555            {{ var2 }}
     556        {% else %}{% if var3 %}
     557            {{ var3 }}
     558        {% endif %}{% endif %}{% endif %}
     559
     560    but obviously much cleaner!
     561
     562    You can also use a literal string as a fallback value in case all
     563    passed variables are False::
     564
     565        {% firstof var1 var2 var3 "fallback value" %}
     566
     567    """
     568    bits = token.split_contents()[1:]
     569    if len(bits) < 1:
     570        raise TemplateSyntaxError("'firstof' statement requires at least one"
     571                                  " argument")
     572    return FirstOfNode(bits)
     573firstof = register.tag(firstof)
     574
     575#@register.tag(name="for")
     576def do_for(parser, token):
     577    """
     578    Loops over each item in an array.
     579
     580    For example, to display a list of athletes given ``athlete_list``::
     581
     582        <ul>
     583        {% for athlete in athlete_list %}
     584            <li>{{ athlete.name }}</li>
     585        {% endfor %}
     586        </ul>
     587
     588    You can loop over a list in reverse by using
     589    ``{% for obj in list reversed %}``.
     590
     591    You can also unpack multiple values from a two-dimensional array::
     592
     593        {% for key,value in dict.items %}
     594            {{ key }}: {{ value }}
     595        {% endfor %}
     596
     597    The for loop sets a number of variables available within the loop:
     598
     599        ==========================  ================================================
     600        Variable                    Description
     601        ==========================  ================================================
     602        ``forloop.counter``         The current iteration of the loop (1-indexed)
     603        ``forloop.counter0``        The current iteration of the loop (0-indexed)
     604        ``forloop.revcounter``      The number of iterations from the end of the
     605                                    loop (1-indexed)
     606        ``forloop.revcounter0``     The number of iterations from the end of the
     607                                    loop (0-indexed)
     608        ``forloop.first``           True if this is the first time through the loop
     609        ``forloop.last``            True if this is the last time through the loop
     610        ``forloop.parentloop``      For nested loops, this is the loop "above" the
     611                                    current one
     612        ==========================  ================================================
     613
     614    """
     615    bits = token.contents.split()
     616    if len(bits) < 4:
     617        raise TemplateSyntaxError("'for' statements should have at least four"
     618                                  " words: %s" % token.contents)
     619
     620    is_reversed = bits[-1] == 'reversed'
     621    in_index = is_reversed and -3 or -2
     622    if bits[in_index] != 'in':
     623        raise TemplateSyntaxError("'for' statements should use the format"
     624                                  " 'for x in y': %s" % token.contents)
     625
     626    loopvars = re.sub(r' *, *', ',', ' '.join(bits[1:in_index])).split(',')
     627    for var in loopvars:
     628        if not var or ' ' in var:
     629            raise TemplateSyntaxError("'for' tag received an invalid argument:"
     630                                      " %s" % token.contents)
     631
     632    sequence = parser.compile_filter(bits[in_index+1])
     633    nodelist_loop = parser.parse(('endfor',))
     634    parser.delete_first_token()
     635    return ForNode(loopvars, sequence, is_reversed, nodelist_loop)
     636do_for = register.tag("for", do_for)
     637
     638def do_ifequal(parser, token, negate):
     639    bits = list(token.split_contents())
     640    if len(bits) != 3:
     641        raise TemplateSyntaxError, "%r takes two arguments" % bits[0]
     642    end_tag = 'end' + bits[0]
     643    nodelist_true = parser.parse(('else', end_tag))
     644    token = parser.next_token()
     645    if token.contents == 'else':
     646        nodelist_false = parser.parse((end_tag,))
     647        parser.delete_first_token()
     648    else:
     649        nodelist_false = NodeList()
     650    return IfEqualNode(bits[1], bits[2], nodelist_true, nodelist_false, negate)
     651
     652#@register.tag
     653def ifequal(parser, token):
     654    """
     655    Outputs the contents of the block if the two arguments equal each other.
     656
     657    Examples::
     658
     659        {% ifequal user.id comment.user_id %}
     660            ...
     661        {% endifequal %}
     662
     663        {% ifnotequal user.id comment.user_id %}
     664            ...
     665        {% else %}
     666            ...
     667        {% endifnotequal %}
     668    """
     669    return do_ifequal(parser, token, False)
     670ifequal = register.tag(ifequal)
     671
     672#@register.tag
     673def ifnotequal(parser, token):
     674    """
     675    Outputs the contents of the block if the two arguments are not equal.
     676    See ifequal.
     677    """
     678    return do_ifequal(parser, token, True)
     679ifnotequal = register.tag(ifnotequal)
     680
     681#@register.tag(name="if")
     682def do_if(parser, token):
     683    """
     684    The ``{% if %}`` tag evaluates a variable, and if that variable is "true"
     685    (i.e., exists, is not empty, and is not a false boolean value), the
     686    contents of the block are output:
     687
     688    ::
     689
     690        {% if athlete_list %}
     691            Number of athletes: {{ athlete_list|count }}
     692        {% else %}
     693            No athletes.
     694        {% endif %}
     695
     696    In the above, if ``athlete_list`` is not empty, the number of athletes will
     697    be displayed by the ``{{ athlete_list|count }}`` variable.
     698
     699    As you can see, the ``if`` tag can take an option ``{% else %}`` clause
     700    that will be displayed if the test fails.
     701
     702    ``if`` tags may use ``or``, ``and`` or ``not`` to test a number of
     703    variables or to negate a given variable::
     704
     705        {% if not athlete_list %}
     706            There are no athletes.
     707        {% endif %}
     708
     709        {% if athlete_list or coach_list %}
     710            There are some athletes or some coaches.
     711        {% endif %}
     712
     713        {% if athlete_list and coach_list %}
     714            Both atheletes and coaches are available.
     715        {% endif %}
     716
     717        {% if not athlete_list or coach_list %}
     718            There are no athletes, or there are some coaches.
     719        {% endif %}
     720
     721        {% if athlete_list and not coach_list %}
     722            There are some athletes and absolutely no coaches.
     723        {% endif %}
     724
     725    ``if`` tags do not allow ``and`` and ``or`` clauses with the same tag,
     726    because the order of logic would be ambigous. For example, this is
     727    invalid::
     728
     729        {% if athlete_list and coach_list or cheerleader_list %}
     730
     731    If you need to combine ``and`` and ``or`` to do advanced logic, just use
     732    nested if tags. For example::
     733
     734        {% if athlete_list %}
     735            {% if coach_list or cheerleader_list %}
     736                We have athletes, and either coaches or cheerleaders!
     737            {% endif %}
     738        {% endif %}
     739    """
     740    bits = token.contents.split()
     741    del bits[0]
     742    if not bits:
     743        raise TemplateSyntaxError("'if' statement requires at least one argument")
     744    # Bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d']
     745    bitstr = ' '.join(bits)
     746    boolpairs = bitstr.split(' and ')
     747    boolvars = []
     748    if len(boolpairs) == 1:
     749        link_type = IfNode.LinkTypes.or_
     750        boolpairs = bitstr.split(' or ')
     751    else:
     752        link_type = IfNode.LinkTypes.and_
     753        if ' or ' in bitstr:
     754            raise TemplateSyntaxError, "'if' tags can't mix 'and' and 'or'"
     755    for boolpair in boolpairs:
     756        if ' ' in boolpair:
     757            try:
     758                not_, boolvar = boolpair.split()
     759            except ValueError:
     760                raise TemplateSyntaxError, "'if' statement improperly formatted"
     761            if not_ != 'not':
     762                raise TemplateSyntaxError, "Expected 'not' in if statement"
     763            boolvars.append((True, parser.compile_filter(boolvar)))
     764        else:
     765            boolvars.append((False, parser.compile_filter(boolpair)))
     766    nodelist_true = parser.parse(('else', 'endif'))
     767    token = parser.next_token()
     768    if token.contents == 'else':
     769        nodelist_false = parser.parse(('endif',))
     770        parser.delete_first_token()
     771    else:
     772        nodelist_false = NodeList()
     773    return IfNode(boolvars, nodelist_true, nodelist_false, link_type)
     774do_if = register.tag("if", do_if)
     775
     776#@register.tag
     777def ifchanged(parser, token):
     778    """
     779    Checks if a value has changed from the last iteration of a loop.
     780
     781    The 'ifchanged' block tag is used within a loop. It has two possible uses.
     782
     783    1. Checks its own rendered contents against its previous state and only
     784       displays the content if it has changed. For example, this displays a
     785       list of days, only displaying the month if it changes::
     786
     787            <h1>Archive for {{ year }}</h1>
     788
     789            {% for date in days %}
     790                {% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %}
     791                <a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a>
     792            {% endfor %}
     793
     794    2. If given a variable, check whether that variable has changed.
     795       For example, the following shows the date every time it changes, but
     796       only shows the hour if both the hour and the date have changed::
     797
     798            {% for date in days %}
     799                {% ifchanged date.date %} {{ date.date }} {% endifchanged %}
     800                {% ifchanged date.hour date.date %}
     801                    {{ date.hour }}
     802                {% endifchanged %}
     803            {% endfor %}
     804    """
     805    bits = token.contents.split()
     806    nodelist = parser.parse(('endifchanged',))
     807    parser.delete_first_token()
     808    return IfChangedNode(nodelist, *bits[1:])
     809ifchanged = register.tag(ifchanged)
     810
     811#@register.tag
     812def ssi(parser, token):
     813    """
     814    Outputs the contents of a given file into the page.
     815
     816    Like a simple "include" tag, the ``ssi`` tag includes the contents
     817    of another file -- which must be specified using an absolute path --
     818    in the current page::
     819
     820        {% ssi /home/html/ljworld.com/includes/right_generic.html %}
     821
     822    If the optional "parsed" parameter is given, the contents of the included
     823    file are evaluated as template code, with the current context::
     824
     825        {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %}
     826    """
     827    bits = token.contents.split()
     828    parsed = False
     829    if len(bits) not in (2, 3):
     830        raise TemplateSyntaxError("'ssi' tag takes one argument: the path to"
     831                                  " the file to be included")
     832    if len(bits) == 3:
     833        if bits[2] == 'parsed':
     834            parsed = True
     835        else:
     836            raise TemplateSyntaxError("Second (optional) argument to %s tag"
     837                                      " must be 'parsed'" % bits[0])
     838    return SsiNode(bits[1], parsed)
     839ssi = register.tag(ssi)
     840
     841#@register.tag
     842def load(parser, token):
     843    """
     844    Loads a custom template tag set.
     845
     846    For example, to load the template tags in
     847    ``django/templatetags/news/photos.py``::
     848
     849        {% load news.photos %}
     850    """
     851    bits = token.contents.split()
     852    for taglib in bits[1:]:
     853        # add the library to the parser
     854        try:
     855            lib = get_library(taglib)
     856            parser.add_library(lib)
     857        except InvalidTemplateLibrary, e:
     858            raise TemplateSyntaxError("'%s' is not a valid tag library: %s" %
     859                                      (taglib, e))
     860    return LoadNode()
     861load = register.tag(load)
     862
     863#@register.tag
     864def now(parser, token):
     865    """
     866    Displays the date, formatted according to the given string.
     867
     868    Uses the same format as PHP's ``date()`` function; see http://php.net/date
     869    for all the possible values.
     870
     871    Sample usage::
     872
     873        It is {% now "jS F Y H:i" %}
     874    """
     875    bits = token.contents.split('"')
     876    if len(bits) != 3:
     877        raise TemplateSyntaxError, "'now' statement takes one argument"
     878    format_string = bits[1]
     879    return NowNode(format_string)
     880now = register.tag(now)
     881
     882#@register.tag
     883def regroup(parser, token):
     884    """
     885    Regroups a list of alike objects by a common attribute.
     886
     887    This complex tag is best illustrated by use of an example:  say that
     888    ``people`` is a list of ``Person`` objects that have ``first_name``,
     889    ``last_name``, and ``gender`` attributes, and you'd like to display a list
     890    that looks like:
     891
     892        * Male:
     893            * George Bush
     894            * Bill Clinton
     895        * Female:
     896            * Margaret Thatcher
     897            * Colendeeza Rice
     898        * Unknown:
     899            * Pat Smith
     900
     901    The following snippet of template code would accomplish this dubious task::
     902
     903        {% regroup people by gender as grouped %}
     904        <ul>
     905        {% for group in grouped %}
     906            <li>{{ group.grouper }}
     907            <ul>
     908                {% for item in group.list %}
     909                <li>{{ item }}</li>
     910                {% endfor %}
     911            </ul>
     912        {% endfor %}
     913        </ul>
     914
     915    As you can see, ``{% regroup %}`` populates a variable with a list of
     916    objects with ``grouper`` and ``list`` attributes.  ``grouper`` contains the
     917    item that was grouped by; ``list`` contains the list of objects that share
     918    that ``grouper``.  In this case, ``grouper`` would be ``Male``, ``Female``
     919    and ``Unknown``, and ``list`` is the list of people with those genders.
     920
     921    Note that `{% regroup %}`` does not work when the list to be grouped is not
     922    sorted by the key you are grouping by!  This means that if your list of
     923    people was not sorted by gender, you'd need to make sure it is sorted
     924    before using it, i.e.::
     925
     926        {% regroup people|dictsort:"gender" by gender as grouped %}
     927
     928    """
     929    firstbits = token.contents.split(None, 3)
     930    if len(firstbits) != 4:
     931        raise TemplateSyntaxError, "'regroup' tag takes five arguments"
     932    target = parser.compile_filter(firstbits[1])
     933    if firstbits[2] != 'by':
     934        raise TemplateSyntaxError("second argument to 'regroup' tag must be 'by'")
     935    lastbits_reversed = firstbits[3][::-1].split(None, 2)
     936    if lastbits_reversed[1][::-1] != 'as':
     937        raise TemplateSyntaxError("next-to-last argument to 'regroup' tag must"
     938                                  " be 'as'")
     939
     940    expression = parser.compile_filter(lastbits_reversed[2][::-1])
     941
     942    var_name = lastbits_reversed[0][::-1]
     943    return RegroupNode(target, expression, var_name)
     944regroup = register.tag(regroup)
     945
     946def spaceless(parser, token):
     947    """
     948    Removes whitespace between HTML tags, including tab and newline characters.
     949
     950    Example usage::
     951
     952        {% spaceless %}
     953            <p>
     954                <a href="foo/">Foo</a>
     955            </p>
     956        {% endspaceless %}
     957
     958    This example would return this HTML::
     959
     960        <p><a href="foo/">Foo</a></p>
     961
     962    Only space between *tags* is normalized -- not space between tags and text.
     963    In this example, the space around ``Hello`` won't be stripped::
     964
     965        {% spaceless %}
     966            <strong>
     967                Hello
     968            </strong>
     969        {% endspaceless %}
     970    """
     971    nodelist = parser.parse(('endspaceless',))
     972    parser.delete_first_token()
     973    return SpacelessNode(nodelist)
     974spaceless = register.tag(spaceless)
     975
     976#@register.tag
     977def templatetag(parser, token):
     978    """
     979    Outputs one of the bits used to compose template tags.
     980
     981    Since the template system has no concept of "escaping", to display one of
     982    the bits used in template tags, you must use the ``{% templatetag %}`` tag.
     983
     984    The argument tells which template bit to output:
     985
     986        ==================  =======
     987        Argument            Outputs
     988        ==================  =======
     989        ``openblock``       ``{%``
     990        ``closeblock``      ``%}``
     991        ``openvariable``    ``{{``
     992        ``closevariable``   ``}}``
     993        ``openbrace``       ``{``
     994        ``closebrace``      ``}``
     995        ``opencomment``     ``{#``
     996        ``closecomment``    ``#}``
     997        ==================  =======
     998    """
     999    bits = token.contents.split()
     1000    if len(bits) != 2:
     1001        raise TemplateSyntaxError, "'templatetag' statement takes one argument"
     1002    tag = bits[1]
     1003    if tag not in TemplateTagNode.mapping:
     1004        raise TemplateSyntaxError("Invalid templatetag argument: '%s'."
     1005                                  " Must be one of: %s" %
     1006                                  (tag, TemplateTagNode.mapping.keys()))
     1007    return TemplateTagNode(tag)
     1008templatetag = register.tag(templatetag)
     1009
     1010def url(parser, token):
     1011    """
     1012    Returns an absolute URL matching given view with its parameters.
     1013
     1014    This is a way to define links that aren't tied to a particular URL
     1015    configuration::
     1016
     1017        {% url path.to.some_view arg1,arg2,name1=value1 %}
     1018
     1019    The first argument is a path to a view. It can be an absolute python path
     1020    or just ``app_name.view_name`` without the project name if the view is
     1021    located inside the project.  Other arguments are comma-separated values
     1022    that will be filled in place of positional and keyword arguments in the
     1023    URL. All arguments for the URL should be present.
     1024
     1025    For example if you have a view ``app_name.client`` taking client's id and
     1026    the corresponding line in a URLconf looks like this::
     1027
     1028        ('^client/(\d+)/$', 'app_name.client')
     1029
     1030    and this app's URLconf is included into the project's URLconf under some
     1031    path::
     1032
     1033        ('^clients/', include('project_name.app_name.urls'))
     1034
     1035    then in a template you can create a link for a certain client like this::
     1036
     1037        {% url app_name.client client.id %}
     1038
     1039    The URL will look like ``/clients/client/123/``.
     1040    """
     1041    bits = token.contents.split(' ', 2)
     1042    if len(bits) < 2:
     1043        raise TemplateSyntaxError("'%s' takes at least one argument"
     1044                                  " (path to a view)" % bits[0])
     1045    args = []
     1046    kwargs = {}
     1047    if len(bits) > 2:
     1048        for arg in bits[2].split(','):
     1049            if '=' in arg:
     1050                k, v = arg.split('=', 1)
     1051                k = k.strip()
     1052                kwargs[k] = parser.compile_filter(v)
     1053            else:
     1054                args.append(parser.compile_filter(arg))
     1055    return URLNode(bits[1], args, kwargs)
     1056url = register.tag(url)
     1057
     1058#@register.tag
     1059def widthratio(parser, token):
     1060    """
     1061    For creating bar charts and such, this tag calculates the ratio of a given
     1062    value to a maximum value, and then applies that ratio to a constant.
     1063
     1064    For example::
     1065
     1066        <img src='bar.gif' height='10' width='{% widthratio this_value max_value 100 %}' />
     1067
     1068    Above, if ``this_value`` is 175 and ``max_value`` is 200, the the image in
     1069    the above example will be 88 pixels wide (because 175/200 = .875;
     1070    .875 * 100 = 87.5 which is rounded up to 88).
     1071    """
     1072    bits = token.contents.split()
     1073    if len(bits) != 4:
     1074        raise TemplateSyntaxError("widthratio takes three arguments")
     1075    tag, this_value_expr, max_value_expr, max_width = bits
     1076    try:
     1077        max_width = int(max_width)
     1078    except ValueError:
     1079        raise TemplateSyntaxError("widthratio final argument must be an integer")
     1080    return WidthRatioNode(parser.compile_filter(this_value_expr),
     1081                          parser.compile_filter(max_value_expr), max_width)
     1082widthratio = register.tag(widthratio)
     1083
     1084#@register.tag
     1085def do_with(parser, token):
     1086    """
     1087    Adds a value to the context (inside of this block) for caching and easy
     1088    access.
     1089
     1090    For example::
     1091
     1092        {% with person.some_sql_method as total %}
     1093            {{ total }} object{{ total|pluralize }}
     1094        {% endwith %}
     1095    """
     1096    bits = list(token.split_contents())
     1097    if len(bits) != 4 or bits[2] != "as":
     1098        raise TemplateSyntaxError("%r expected format is 'value as name'" %
     1099                                  bits[0])
     1100    var = parser.compile_filter(bits[1])
     1101    name = bits[3]
     1102    nodelist = parser.parse(('endwith',))
     1103    parser.delete_first_token()
     1104    return WithNode(var, name, nodelist)
     1105do_with = register.tag('with', do_with)
  • tests/regressiontests/templates/tests.py

    diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
    index 186b8aa..ac0912d 100644
    a b def do_echo(parser, token):  
    5151
    5252register.tag("echo", do_echo)
    5353
    54 template.libraries['django.templatetags.testtags'] = register
     54template.libraries['testtags'] = register
    5555
    5656#####################################
    5757# Helper objects for template tests #
Back to Top