Code

Ticket #6587: new_template_lib_loader_12.diff

File new_template_lib_loader_12.diff, 135.7 KB (added by oyvind, 6 years ago)

new loader using imp, may need some cleaup, and decide if add_to_builtins should change, and if a import should use a list instead of the getattr solution

Line 
1diff --git a/django/template/__init__.py b/django/template/__init__.py
2index 5c4ab30..4ee3174 100644
3--- a/django/template/__init__.py
4+++ b/django/template/__init__.py
5@@ -49,6 +49,7 @@ u'<html><h1>Hello</h1></html>'
6 u'<html></html>'
7 """
8 import re
9+import imp
10 from inspect import getargspec
11 from django.conf import settings
12 from django.template.context import Context, RequestContext, ContextPopException
13@@ -59,6 +60,7 @@ from django.utils.encoding import smart_unicode, force_unicode
14 from django.utils.translation import ugettext as _
15 from django.utils.safestring import SafeData, EscapeData, mark_safe, mark_for_escaping
16 from django.utils.html import escape
17+from django.templatetags import get_templatetags_modules
18 
19 __all__ = ('Template', 'Context', 'RequestContext', 'compile_string')
20 
21@@ -913,22 +915,52 @@ class Library(object):
22             return func
23         return dec
24 
25-def get_library(module_name):
26-    lib = libraries.get(module_name, None)
27+def import_library(templatetag_module, library_name):
28+    try:
29+        components = templatetag_module.split('.')
30+        mod = __import__(templatetag_module)
31+        for comp in components[1:]:
32+            mod = getattr(mod, comp)
33+        imp.find_module(library_name, mod.__path__)
34+    except ImportError, AttributeError:
35+        return None
36+    library_module = '%s.%s' % (templatetag_module, library_name)
37+    components = library_module.split('.')
38+    mod = __import__(library_module)
39+    for comp in components[1:]:
40+        mod = getattr(mod, comp)
41+    try:
42+        return mod.register
43+    except AttributeError:
44+        raise InvalidTemplateLibrary("Template library %s does not have a variable named 'register'" % module_name)
45+
46+def get_library(library_name):
47+    lib = libraries.get(library_name, None)
48     if not lib:
49-        try:
50-            mod = __import__(module_name, {}, {}, [''])
51-        except ImportError, e:
52-            raise InvalidTemplateLibrary("Could not load template library from %s, %s" % (module_name, e))
53-        try:
54-            lib = mod.register
55-            libraries[module_name] = lib
56-        except AttributeError:
57-            raise InvalidTemplateLibrary("Template library %s does not have a variable named 'register'" % module_name)
58+
59+        """
60+        If library is not already loaded loop over all templatetags modules to locate it.
61+
62+        {% load somelib %} and {% load someotherlib %} loops twice.
63+
64+        Subsequent loads eg. {% load somelib %} in the same thread will grab the cached
65+        module from libraries.
66+        """
67+        templatetags_modules = get_templatetags_modules()
68+        tried_modules = []
69+        for module in templatetags_modules:
70+            taglib_module = '%s.%s' % (module, library_name)
71+            tried_modules.append(taglib_module)
72+            lib = import_library(module, library_name)
73+            if lib:
74+                libraries[library_name] = lib
75+                break
76+        if not lib:
77+            raise InvalidTemplateLibrary("Template library %s not found, tried %s" % (library_name, ','.join(tried_modules)))
78     return lib
79 
80-def add_to_builtins(module_name):
81-    builtins.append(get_library(module_name))
82+def add_to_builtins(module, library_name):
83+    builtins.append(import_library(module, library_name))
84 
85-add_to_builtins('django.template.defaulttags')
86-add_to_builtins('django.template.defaultfilters')
87+add_to_builtins('django.templatetags', 'defaulttags')
88+add_to_builtins('django.templatetags', 'defaultfilters')
89diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
90deleted file mode 100644
91index cef3143..0000000
92--- a/django/template/defaultfilters.py
93+++ /dev/null
94@@ -1,851 +0,0 @@
95-"""Default variable filters."""
96-
97-import re
98-import random as random_module
99-try:
100-    from functools import wraps
101-except ImportError:
102-    from django.utils.functional import wraps  # Python 2.3, 2.4 fallback.
103-
104-from django.template import Variable, Library
105-from django.conf import settings
106-from django.utils.translation import ugettext, ungettext
107-from django.utils.encoding import force_unicode, iri_to_uri
108-from django.utils.safestring import mark_safe, SafeData
109-
110-register = Library()
111-
112-#######################
113-# STRING DECORATOR    #
114-#######################
115-
116-def stringfilter(func):
117-    """
118-    Decorator for filters which should only receive unicode objects. The object
119-    passed as the first positional argument will be converted to a unicode
120-    object.
121-    """
122-    def _dec(*args, **kwargs):
123-        if args:
124-            args = list(args)
125-            args[0] = force_unicode(args[0])
126-            if isinstance(args[0], SafeData) and getattr(func, 'is_safe', False):
127-                return mark_safe(func(*args, **kwargs))
128-        return func(*args, **kwargs)
129-
130-    # Include a reference to the real function (used to check original
131-    # arguments by the template parser).
132-    _dec._decorated_function = getattr(func, '_decorated_function', func)
133-    for attr in ('is_safe', 'needs_autoescape'):
134-        if hasattr(func, attr):
135-            setattr(_dec, attr, getattr(func, attr))
136-    return wraps(func)(_dec)
137-
138-###################
139-# STRINGS         #
140-###################
141-
142-
143-def addslashes(value):
144-    """
145-    Adds slashes before quotes. Useful for escaping strings in CSV, for
146-    example. Less useful for escaping JavaScript; use the ``escapejs``
147-    filter instead.
148-    """
149-    return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
150-addslashes.is_safe = True
151-addslashes = stringfilter(addslashes)
152-
153-def capfirst(value):
154-    """Capitalizes the first character of the value."""
155-    return value and value[0].upper() + value[1:]
156-capfirst.is_safe=True
157-capfirst = stringfilter(capfirst)
158-
159-_js_escapes = (
160-    ('\\', '\\\\'),
161-    ('"', '\\"'),
162-    ("'", "\\'"),
163-    ('\n', '\\n'),
164-    ('\r', '\\r'),
165-    ('\b', '\\b'),
166-    ('\f', '\\f'),
167-    ('\t', '\\t'),
168-    ('\v', '\\v'),
169-    ('</', '<\\/'),
170-)
171-def escapejs(value):
172-    """Backslash-escapes characters for use in JavaScript strings."""
173-    for bad, good in _js_escapes:
174-        value = value.replace(bad, good)
175-    return value
176-escapejs = stringfilter(escapejs)
177-
178-def fix_ampersands(value):
179-    """Replaces ampersands with ``&amp;`` entities."""
180-    from django.utils.html import fix_ampersands
181-    return fix_ampersands(value)
182-fix_ampersands.is_safe=True
183-fix_ampersands = stringfilter(fix_ampersands)
184-
185-def floatformat(text, arg=-1):
186-    """
187-    Displays a float to a specified number of decimal places.
188-
189-    If called without an argument, it displays the floating point number with
190-    one decimal place -- but only if there's a decimal place to be displayed:
191-
192-    * num1 = 34.23234
193-    * num2 = 34.00000
194-    * num3 = 34.26000
195-    * {{ num1|floatformat }} displays "34.2"
196-    * {{ num2|floatformat }} displays "34"
197-    * {{ num3|floatformat }} displays "34.3"
198-
199-    If arg is positive, it will always display exactly arg number of decimal
200-    places:
201-
202-    * {{ num1|floatformat:3 }} displays "34.232"
203-    * {{ num2|floatformat:3 }} displays "34.000"
204-    * {{ num3|floatformat:3 }} displays "34.260"
205-
206-    If arg is negative, it will display arg number of decimal places -- but
207-    only if there are places to be displayed:
208-
209-    * {{ num1|floatformat:"-3" }} displays "34.232"
210-    * {{ num2|floatformat:"-3" }} displays "34"
211-    * {{ num3|floatformat:"-3" }} displays "34.260"
212-    """
213-    try:
214-        f = float(text)
215-    except (ValueError, TypeError):
216-        return u''
217-    try:
218-        d = int(arg)
219-    except ValueError:
220-        return force_unicode(f)
221-    try:
222-        m = f - int(f)
223-    except OverflowError:
224-        return force_unicode(f)
225-    if not m and d < 0:
226-        return mark_safe(u'%d' % int(f))
227-    else:
228-        formatstr = u'%%.%df' % abs(d)
229-        return mark_safe(formatstr % f)
230-floatformat.is_safe = True
231-
232-def iriencode(value):
233-    """Escapes an IRI value for use in a URL."""
234-    return force_unicode(iri_to_uri(value))
235-iriencode.is_safe = True
236-iriencode = stringfilter(iriencode)
237-
238-def linenumbers(value, autoescape=None):
239-    """Displays text with line numbers."""
240-    from django.utils.html import escape
241-    lines = value.split(u'\n')
242-    # Find the maximum width of the line count, for use with zero padding
243-    # string format command
244-    width = unicode(len(unicode(len(lines))))
245-    if not autoescape or isinstance(value, SafeData):
246-        for i, line in enumerate(lines):
247-            lines[i] = (u"%0" + width  + u"d. %s") % (i + 1, line)
248-    else:
249-        for i, line in enumerate(lines):
250-            lines[i] = (u"%0" + width  + u"d. %s") % (i + 1, escape(line))
251-    return mark_safe(u'\n'.join(lines))
252-linenumbers.is_safe = True
253-linenumbers.needs_autoescape = True
254-linenumbers = stringfilter(linenumbers)
255-
256-def lower(value):
257-    """Converts a string into all lowercase."""
258-    return value.lower()
259-lower.is_safe = True
260-lower = stringfilter(lower)
261-
262-def make_list(value):
263-    """
264-    Returns the value turned into a list.
265-
266-    For an integer, it's a list of digits.
267-    For a string, it's a list of characters.
268-    """
269-    return list(value)
270-make_list.is_safe = False
271-make_list = stringfilter(make_list)
272-
273-def slugify(value):
274-    """
275-    Normalizes string, converts to lowercase, removes non-alpha characters,
276-    and converts spaces to hyphens.
277-    """
278-    import unicodedata
279-    value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
280-    value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
281-    return mark_safe(re.sub('[-\s]+', '-', value))
282-slugify.is_safe = True
283-slugify = stringfilter(slugify)
284-
285-def stringformat(value, arg):
286-    """
287-    Formats the variable according to the arg, a string formatting specifier.
288-
289-    This specifier uses Python string formating syntax, with the exception that
290-    the leading "%" is dropped.
291-
292-    See http://docs.python.org/lib/typesseq-strings.html for documentation
293-    of Python string formatting
294-    """
295-    try:
296-        return (u"%" + unicode(arg)) % value
297-    except (ValueError, TypeError):
298-        return u""
299-stringformat.is_safe = True
300-
301-def title(value):
302-    """Converts a string into titlecase."""
303-    return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
304-title.is_safe = True
305-title = stringfilter(title)
306-
307-def truncatewords(value, arg):
308-    """
309-    Truncates a string after a certain number of words.
310-
311-    Argument: Number of words to truncate after.
312-    """
313-    from django.utils.text import truncate_words
314-    try:
315-        length = int(arg)
316-    except ValueError: # Invalid literal for int().
317-        return value # Fail silently.
318-    return truncate_words(value, length)
319-truncatewords.is_safe = True
320-truncatewords = stringfilter(truncatewords)
321-
322-def truncatewords_html(value, arg):
323-    """
324-    Truncates HTML after a certain number of words.
325-
326-    Argument: Number of words to truncate after.
327-    """
328-    from django.utils.text import truncate_html_words
329-    try:
330-        length = int(arg)
331-    except ValueError: # invalid literal for int()
332-        return value # Fail silently.
333-    return truncate_html_words(value, length)
334-truncatewords_html.is_safe = True
335-truncatewords_html = stringfilter(truncatewords_html)
336-
337-def upper(value):
338-    """Converts a string into all uppercase."""
339-    return value.upper()
340-upper.is_safe = False
341-upper = stringfilter(upper)
342-
343-def urlencode(value):
344-    """Escapes a value for use in a URL."""
345-    from django.utils.http import urlquote
346-    return urlquote(value)
347-urlencode.is_safe = False
348-urlencode = stringfilter(urlencode)
349-
350-def urlize(value, autoescape=None):
351-    """Converts URLs in plain text into clickable links."""
352-    from django.utils.html import urlize
353-    return mark_safe(urlize(value, nofollow=True, autoescape=autoescape))
354-urlize.is_safe=True
355-urlize.needs_autoescape = True
356-urlize = stringfilter(urlize)
357-
358-def urlizetrunc(value, limit, autoescape=None):
359-    """
360-    Converts URLs into clickable links, truncating URLs to the given character
361-    limit, and adding 'rel=nofollow' attribute to discourage spamming.
362-
363-    Argument: Length to truncate URLs to.
364-    """
365-    from django.utils.html import urlize
366-    return mark_safe(urlize(value, trim_url_limit=int(limit), nofollow=True,
367-                            autoescape=autoescape))
368-urlizetrunc.is_safe = True
369-urlizetrunc.needs_autoescape = True
370-urlizetrunc = stringfilter(urlizetrunc)
371-
372-def wordcount(value):
373-    """Returns the number of words."""
374-    return len(value.split())
375-wordcount.is_safe = False
376-wordcount = stringfilter(wordcount)
377-
378-def wordwrap(value, arg):
379-    """
380-    Wraps words at specified line length.
381-
382-    Argument: number of characters to wrap the text at.
383-    """
384-    from django.utils.text import wrap
385-    return wrap(value, int(arg))
386-wordwrap.is_safe = True
387-wordwrap = stringfilter(wordwrap)
388-
389-def ljust(value, arg):
390-    """
391-    Left-aligns the value in a field of a given width.
392-
393-    Argument: field size.
394-    """
395-    return value.ljust(int(arg))
396-ljust.is_safe = True
397-ljust = stringfilter(ljust)
398-
399-def rjust(value, arg):
400-    """
401-    Right-aligns the value in a field of a given width.
402-
403-    Argument: field size.
404-    """
405-    return value.rjust(int(arg))
406-rjust.is_safe = True
407-rjust = stringfilter(rjust)
408-
409-def center(value, arg):
410-    """Centers the value in a field of a given width."""
411-    return value.center(int(arg))
412-center.is_safe = True
413-center = stringfilter(center)
414-
415-def cut(value, arg):
416-    """
417-    Removes all values of arg from the given string.
418-    """
419-    safe = isinstance(value, SafeData)
420-    value = value.replace(arg, u'')
421-    if safe and arg != ';':
422-        return mark_safe(value)
423-    return value
424-cut = stringfilter(cut)
425-
426-###################
427-# HTML STRINGS    #
428-###################
429-
430-def escape(value):
431-    """
432-    Marks the value as a string that should not be auto-escaped.
433-    """
434-    from django.utils.safestring import mark_for_escaping
435-    return mark_for_escaping(value)
436-escape.is_safe = True
437-escape = stringfilter(escape)
438-
439-def force_escape(value):
440-    """
441-    Escapes a string's HTML. This returns a new string containing the escaped
442-    characters (as opposed to "escape", which marks the content for later
443-    possible escaping).
444-    """
445-    from django.utils.html import escape
446-    return mark_safe(escape(value))
447-force_escape = stringfilter(force_escape)
448-force_escape.is_safe = True
449-
450-def linebreaks(value, autoescape=None):
451-    """
452-    Replaces line breaks in plain text with appropriate HTML; a single
453-    newline becomes an HTML line break (``<br />``) and a new line
454-    followed by a blank line becomes a paragraph break (``</p>``).
455-    """
456-    from django.utils.html import linebreaks
457-    autoescape = autoescape and not isinstance(value, SafeData)
458-    return mark_safe(linebreaks(value, autoescape))
459-linebreaks.is_safe = True
460-linebreaks.needs_autoescape = True
461-linebreaks = stringfilter(linebreaks)
462-
463-def linebreaksbr(value, autoescape=None):
464-    """
465-    Converts all newlines in a piece of plain text to HTML line breaks
466-    (``<br />``).
467-    """
468-    if autoescape and not isinstance(value, SafeData):
469-        from django.utils.html import escape
470-        value = escape(value)
471-    return mark_safe(value.replace('\n', '<br />'))
472-linebreaksbr.is_safe = True
473-linebreaksbr.needs_autoescape = True
474-linebreaksbr = stringfilter(linebreaksbr)
475-
476-def safe(value):
477-    """
478-    Marks the value as a string that should not be auto-escaped.
479-    """
480-    from django.utils.safestring import mark_safe
481-    return mark_safe(value)
482-safe.is_safe = True
483-safe = stringfilter(safe)
484-
485-def removetags(value, tags):
486-    """Removes a space separated list of [X]HTML tags from the output."""
487-    tags = [re.escape(tag) for tag in tags.split()]
488-    tags_re = u'(%s)' % u'|'.join(tags)
489-    starttag_re = re.compile(ur'<%s(/?>|(\s+[^>]*>))' % tags_re, re.U)
490-    endtag_re = re.compile(u'</%s>' % tags_re)
491-    value = starttag_re.sub(u'', value)
492-    value = endtag_re.sub(u'', value)
493-    return value
494-removetags.is_safe = True
495-removetags = stringfilter(removetags)
496-
497-def striptags(value):
498-    """Strips all [X]HTML tags."""
499-    from django.utils.html import strip_tags
500-    return strip_tags(value)
501-striptags.is_safe = True
502-striptags = stringfilter(striptags)
503-
504-###################
505-# LISTS           #
506-###################
507-
508-def dictsort(value, arg):
509-    """
510-    Takes a list of dicts, returns that list sorted by the property given in
511-    the argument.
512-    """
513-    var_resolve = Variable(arg).resolve
514-    decorated = [(var_resolve(item), item) for item in value]
515-    decorated.sort()
516-    return [item[1] for item in decorated]
517-dictsort.is_safe = False
518-
519-def dictsortreversed(value, arg):
520-    """
521-    Takes a list of dicts, returns that list sorted in reverse order by the
522-    property given in the argument.
523-    """
524-    var_resolve = Variable(arg).resolve
525-    decorated = [(var_resolve(item), item) for item in value]
526-    decorated.sort()
527-    decorated.reverse()
528-    return [item[1] for item in decorated]
529-dictsortreversed.is_safe = False
530-
531-def first(value):
532-    """Returns the first item in a list."""
533-    try:
534-        return value[0]
535-    except IndexError:
536-        return u''
537-first.is_safe = False
538-
539-def join(value, arg):
540-    """Joins a list with a string, like Python's ``str.join(list)``."""
541-    try:
542-        data = arg.join(map(force_unicode, value))
543-    except AttributeError: # fail silently but nicely
544-        return value
545-    safe_args = reduce(lambda lhs, rhs: lhs and isinstance(rhs, SafeData),
546-            value, True)
547-    if safe_args:
548-        return mark_safe(data)
549-    else:
550-        return data
551-join.is_safe = True
552-
553-def last(value):
554-    "Returns the last item in a list"
555-    try:
556-        return value[-1]
557-    except IndexError:
558-        return u''
559-last.is_safe = True
560-
561-def length(value):
562-    """Returns the length of the value - useful for lists."""
563-    return len(value)
564-length.is_safe = True
565-
566-def length_is(value, arg):
567-    """Returns a boolean of whether the value's length is the argument."""
568-    return len(value) == int(arg)
569-length_is.is_safe = True
570-
571-def random(value):
572-    """Returns a random item from the list."""
573-    return random_module.choice(value)
574-random.is_safe = True
575-
576-def slice_(value, arg):
577-    """
578-    Returns a slice of the list.
579-
580-    Uses the same syntax as Python's list slicing; see
581-    http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice
582-    for an introduction.
583-    """
584-    try:
585-        bits = []
586-        for x in arg.split(u':'):
587-            if len(x) == 0:
588-                bits.append(None)
589-            else:
590-                bits.append(int(x))
591-        return value[slice(*bits)]
592-
593-    except (ValueError, TypeError):
594-        return value # Fail silently.
595-slice_.is_safe = True
596-
597-def unordered_list(value, autoescape=None):
598-    """
599-    Recursively takes a self-nested list and returns an HTML unordered list --
600-    WITHOUT opening and closing <ul> tags.
601-
602-    The list is assumed to be in the proper format. For example, if ``var``
603-    contains: ``['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']]``,
604-    then ``{{ var|unordered_list }}`` would return::
605-
606-        <li>States
607-        <ul>
608-                <li>Kansas
609-                <ul>
610-                        <li>Lawrence</li>
611-                        <li>Topeka</li>
612-                </ul>
613-                </li>
614-                <li>Illinois</li>
615-        </ul>
616-        </li>
617-    """
618-    if autoescape:
619-        from django.utils.html import conditional_escape
620-        escaper = conditional_escape
621-    else:
622-        escaper = lambda x: x
623-    def convert_old_style_list(list_):
624-        """
625-        Converts old style lists to the new easier to understand format.
626-
627-        The old list format looked like:
628-            ['Item 1', [['Item 1.1', []], ['Item 1.2', []]]
629-
630-        And it is converted to:
631-            ['Item 1', ['Item 1.1', 'Item 1.2]]
632-        """
633-        if not isinstance(list_, (tuple, list)) or len(list_) != 2:
634-            return list_, False
635-        first_item, second_item = list_
636-        if second_item == []:
637-            return [first_item], True
638-        old_style_list = True
639-        new_second_item = []
640-        for sublist in second_item:
641-            item, old_style_list = convert_old_style_list(sublist)
642-            if not old_style_list:
643-                break
644-            new_second_item.extend(item)
645-        if old_style_list:
646-            second_item = new_second_item
647-        return [first_item, second_item], old_style_list
648-    def _helper(list_, tabs=1):
649-        indent = u'\t' * tabs
650-        output = []
651-
652-        list_length = len(list_)
653-        i = 0
654-        while i < list_length:
655-            title = list_[i]
656-            sublist = ''
657-            sublist_item = None
658-            if isinstance(title, (list, tuple)):
659-                sublist_item = title
660-                title = ''
661-            elif i < list_length - 1:
662-                next_item = list_[i+1]
663-                if next_item and isinstance(next_item, (list, tuple)):
664-                    # The next item is a sub-list.
665-                    sublist_item = next_item
666-                    # We've processed the next item now too.
667-                    i += 1
668-            if sublist_item:
669-                sublist = _helper(sublist_item, tabs+1)
670-                sublist = '\n%s<ul>\n%s\n%s</ul>\n%s' % (indent, sublist,
671-                                                         indent, indent)
672-            output.append('%s<li>%s%s</li>' % (indent,
673-                    escaper(force_unicode(title)), sublist))
674-            i += 1
675-        return '\n'.join(output)
676-    value, converted = convert_old_style_list(value)
677-    return mark_safe(_helper(value))
678-unordered_list.is_safe = True
679-unordered_list.needs_autoescape = True
680-
681-###################
682-# INTEGERS        #
683-###################
684-
685-def add(value, arg):
686-    """Adds the arg to the value."""
687-    return int(value) + int(arg)
688-add.is_safe = False
689-
690-def get_digit(value, arg):
691-    """
692-    Given a whole number, returns the requested digit of it, where 1 is the
693-    right-most digit, 2 is the second-right-most digit, etc. Returns the
694-    original value for invalid input (if input or argument is not an integer,
695-    or if argument is less than 1). Otherwise, output is always an integer.
696-    """
697-    try:
698-        arg = int(arg)
699-        value = int(value)
700-    except ValueError:
701-        return value # Fail silently for an invalid argument
702-    if arg < 1:
703-        return value
704-    try:
705-        return int(str(value)[-arg])
706-    except IndexError:
707-        return 0
708-get_digit.is_safe = False
709-
710-###################
711-# DATES           #
712-###################
713-
714-def date(value, arg=None):
715-    """Formats a date according to the given format."""
716-    from django.utils.dateformat import format
717-    if not value:
718-        return u''
719-    if arg is None:
720-        arg = settings.DATE_FORMAT
721-    return format(value, arg)
722-date.is_safe = False
723-
724-def time(value, arg=None):
725-    """Formats a time according to the given format."""
726-    from django.utils.dateformat import time_format
727-    if value in (None, u''):
728-        return u''
729-    if arg is None:
730-        arg = settings.TIME_FORMAT
731-    return time_format(value, arg)
732-time.is_safe = False
733-
734-def timesince(value, arg=None):
735-    """Formats a date as the time since that date (i.e. "4 days, 6 hours")."""
736-    from django.utils.timesince import timesince
737-    if not value:
738-        return u''
739-    if arg:
740-        return timesince(arg, value)
741-    return timesince(value)
742-timesince.is_safe = False
743-
744-def timeuntil(value, arg=None):
745-    """Formats a date as the time until that date (i.e. "4 days, 6 hours")."""
746-    from django.utils.timesince import timesince
747-    from datetime import datetime
748-    if not value:
749-        return u''
750-    if arg:
751-        return timesince(arg, value)
752-    return timesince(datetime.now(), value)
753-timeuntil.is_safe = False
754-
755-###################
756-# LOGIC           #
757-###################
758-
759-def default(value, arg):
760-    """If value is unavailable, use given default."""
761-    return value or arg
762-default.is_safe = False
763-
764-def default_if_none(value, arg):
765-    """If value is None, use given default."""
766-    if value is None:
767-        return arg
768-    return value
769-default_if_none.is_safe = False
770-
771-def divisibleby(value, arg):
772-    """Returns True if the value is devisible by the argument."""
773-    return int(value) % int(arg) == 0
774-divisibleby.is_safe = False
775-
776-def yesno(value, arg=None):
777-    """
778-    Given a string mapping values for true, false and (optionally) None,
779-    returns one of those strings accoding to the value:
780-
781-    ==========  ======================  ==================================
782-    Value       Argument                Outputs
783-    ==========  ======================  ==================================
784-    ``True``    ``"yeah,no,maybe"``     ``yeah``
785-    ``False``   ``"yeah,no,maybe"``     ``no``
786-    ``None``    ``"yeah,no,maybe"``     ``maybe``
787-    ``None``    ``"yeah,no"``           ``"no"`` (converts None to False
788-                                        if no mapping for None is given.
789-    ==========  ======================  ==================================
790-    """
791-    if arg is None:
792-        arg = ugettext('yes,no,maybe')
793-    bits = arg.split(u',')
794-    if len(bits) < 2:
795-        return value # Invalid arg.
796-    try:
797-        yes, no, maybe = bits
798-    except ValueError:
799-        # Unpack list of wrong size (no "maybe" value provided).
800-        yes, no, maybe = bits[0], bits[1], bits[1]
801-    if value is None:
802-        return maybe
803-    if value:
804-        return yes
805-    return no
806-yesno.is_safe = False
807-
808-###################
809-# MISC            #
810-###################
811-
812-def filesizeformat(bytes):
813-    """
814-    Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB,
815-    102 bytes, etc).
816-    """
817-    try:
818-        bytes = float(bytes)
819-    except TypeError:
820-        return u"0 bytes"
821-
822-    if bytes < 1024:
823-        return ungettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes}
824-    if bytes < 1024 * 1024:
825-        return ugettext("%.1f KB") % (bytes / 1024)
826-    if bytes < 1024 * 1024 * 1024:
827-        return ugettext("%.1f MB") % (bytes / (1024 * 1024))
828-    return ugettext("%.1f GB") % (bytes / (1024 * 1024 * 1024))
829-filesizeformat.is_safe = True
830-
831-def pluralize(value, arg=u's'):
832-    """
833-    Returns a plural suffix if the value is not 1. By default, 's' is used as
834-    the suffix:
835-
836-    * If value is 0, vote{{ value|pluralize }} displays "0 votes".
837-    * If value is 1, vote{{ value|pluralize }} displays "1 vote".
838-    * If value is 2, vote{{ value|pluralize }} displays "2 votes".
839-
840-    If an argument is provided, that string is used instead:
841-
842-    * If value is 0, class{{ value|pluralize:"es" }} displays "0 classes".
843-    * If value is 1, class{{ value|pluralize:"es" }} displays "1 class".
844-    * If value is 2, class{{ value|pluralize:"es" }} displays "2 classes".
845-
846-    If the provided argument contains a comma, the text before the comma is
847-    used for the singular case and the text after the comma is used for the
848-    plural case:
849-
850-    * If value is 0, cand{{ value|pluralize:"y,ies" }} displays "0 candies".
851-    * If value is 1, cand{{ value|pluralize:"y,ies" }} displays "1 candy".
852-    * If value is 2, cand{{ value|pluralize:"y,ies" }} displays "2 candies".
853-    """
854-    if not u',' in arg:
855-        arg = u',' + arg
856-    bits = arg.split(u',')
857-    if len(bits) > 2:
858-        return u''
859-    singular_suffix, plural_suffix = bits[:2]
860-
861-    try:
862-        if int(value) != 1:
863-            return plural_suffix
864-    except ValueError: # Invalid string that's not a number.
865-        pass
866-    except TypeError: # Value isn't a string or a number; maybe it's a list?
867-        try:
868-            if len(value) != 1:
869-                return plural_suffix
870-        except TypeError: # len() of unsized object.
871-            pass
872-    return singular_suffix
873-pluralize.is_safe = False
874-
875-def phone2numeric(value):
876-    """Takes a phone number and converts it in to its numerical equivalent."""
877-    from django.utils.text import phone2numeric
878-    return phone2numeric(value)
879-phone2numeric.is_safe = True
880-
881-def pprint(value):
882-    """A wrapper around pprint.pprint -- for debugging, really."""
883-    from pprint import pformat
884-    try:
885-        return pformat(value)
886-    except Exception, e:
887-        return u"Error in formatting: %s" % force_unicode(e, errors="replace")
888-pprint.is_safe = True
889-
890-# Syntax: register.filter(name of filter, callback)
891-register.filter(add)
892-register.filter(addslashes)
893-register.filter(capfirst)
894-register.filter(center)
895-register.filter(cut)
896-register.filter(date)
897-register.filter(default)
898-register.filter(default_if_none)
899-register.filter(dictsort)
900-register.filter(dictsortreversed)
901-register.filter(divisibleby)
902-register.filter(escape)
903-register.filter(escapejs)
904-register.filter(filesizeformat)
905-register.filter(first)
906-register.filter(fix_ampersands)
907-register.filter(floatformat)
908-register.filter(force_escape)
909-register.filter(get_digit)
910-register.filter(iriencode)
911-register.filter(join)
912-register.filter(last)
913-register.filter(length)
914-register.filter(length_is)
915-register.filter(linebreaks)
916-register.filter(linebreaksbr)
917-register.filter(linenumbers)
918-register.filter(ljust)
919-register.filter(lower)
920-register.filter(make_list)
921-register.filter(phone2numeric)
922-register.filter(pluralize)
923-register.filter(pprint)
924-register.filter(removetags)
925-register.filter(random)
926-register.filter(rjust)
927-register.filter(safe)
928-register.filter('slice', slice_)
929-register.filter(slugify)
930-register.filter(stringformat)
931-register.filter(striptags)
932-register.filter(time)
933-register.filter(timesince)
934-register.filter(timeuntil)
935-register.filter(title)
936-register.filter(truncatewords)
937-register.filter(truncatewords_html)
938-register.filter(unordered_list)
939-register.filter(upper)
940-register.filter(urlencode)
941-register.filter(urlize)
942-register.filter(urlizetrunc)
943-register.filter(wordcount)
944-register.filter(wordwrap)
945-register.filter(yesno)
946diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
947deleted file mode 100644
948index 01c43ee..0000000
949--- a/django/template/defaulttags.py
950+++ /dev/null
951@@ -1,1105 +0,0 @@
952-"""Default tags used by the template system, available to all templates."""
953-
954-import sys
955-import re
956-from itertools import cycle as itertools_cycle
957-try:
958-    reversed
959-except NameError:
960-    from django.utils.itercompat import reversed     # Python 2.3 fallback
961-
962-from django.template import Node, NodeList, Template, Context, Variable
963-from 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
964-from django.template import get_library, Library, InvalidTemplateLibrary
965-from django.conf import settings
966-from django.utils.encoding import smart_str, smart_unicode
967-from django.utils.itercompat import groupby
968-from django.utils.safestring import mark_safe
969-
970-register = Library()
971-
972-class AutoEscapeControlNode(Node):
973-    """Implements the actions of the autoescape tag."""
974-    def __init__(self, setting, nodelist):
975-        self.setting, self.nodelist = setting, nodelist
976-
977-    def render(self, context):
978-        old_setting = context.autoescape
979-        context.autoescape = self.setting
980-        output = self.nodelist.render(context)
981-        context.autoescape = old_setting
982-        if self.setting:
983-            return mark_safe(output)
984-        else:
985-            return output
986-
987-class CommentNode(Node):
988-    def render(self, context):
989-        return ''
990-
991-class CycleNode(Node):
992-    def __init__(self, cyclevars, variable_name=None):
993-        self.cycle_iter = itertools_cycle([Variable(v) for v in cyclevars])
994-        self.variable_name = variable_name
995-
996-    def render(self, context):
997-        value = self.cycle_iter.next().resolve(context)
998-        if self.variable_name:
999-            context[self.variable_name] = value
1000-        return value
1001-
1002-class DebugNode(Node):
1003-    def render(self, context):
1004-        from pprint import pformat
1005-        output = [pformat(val) for val in context]
1006-        output.append('\n\n')
1007-        output.append(pformat(sys.modules))
1008-        return ''.join(output)
1009-
1010-class FilterNode(Node):
1011-    def __init__(self, filter_expr, nodelist):
1012-        self.filter_expr, self.nodelist = filter_expr, nodelist
1013-
1014-    def render(self, context):
1015-        output = self.nodelist.render(context)
1016-        # Apply filters.
1017-        context.update({'var': output})
1018-        filtered = self.filter_expr.resolve(context)
1019-        context.pop()
1020-        return filtered
1021-
1022-class FirstOfNode(Node):
1023-    def __init__(self, vars):
1024-        self.vars = map(Variable, vars)
1025-
1026-    def render(self, context):
1027-        for var in self.vars:
1028-            try:
1029-                value = var.resolve(context)
1030-            except VariableDoesNotExist:
1031-                continue
1032-            if value:
1033-                return smart_unicode(value)
1034-        return u''
1035-
1036-class ForNode(Node):
1037-    def __init__(self, loopvars, sequence, is_reversed, nodelist_loop):
1038-        self.loopvars, self.sequence = loopvars, sequence
1039-        self.is_reversed = is_reversed
1040-        self.nodelist_loop = nodelist_loop
1041-
1042-    def __repr__(self):
1043-        reversed_text = self.is_reversed and ' reversed' or ''
1044-        return "<For Node: for %s in %s, tail_len: %d%s>" % \
1045-            (', '.join(self.loopvars), self.sequence, len(self.nodelist_loop),
1046-             reversed_text)
1047-
1048-    def __iter__(self):
1049-        for node in self.nodelist_loop:
1050-            yield node
1051-
1052-    def get_nodes_by_type(self, nodetype):
1053-        nodes = []
1054-        if isinstance(self, nodetype):
1055-            nodes.append(self)
1056-        nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype))
1057-        return nodes
1058-
1059-    def render(self, context):
1060-        nodelist = NodeList()
1061-        if 'forloop' in context:
1062-            parentloop = context['forloop']
1063-        else:
1064-            parentloop = {}
1065-        context.push()
1066-        try:
1067-            values = self.sequence.resolve(context, True)
1068-        except VariableDoesNotExist:
1069-            values = []
1070-        if values is None:
1071-            values = []
1072-        if not hasattr(values, '__len__'):
1073-            values = list(values)
1074-        len_values = len(values)
1075-        if self.is_reversed:
1076-            values = reversed(values)
1077-        unpack = len(self.loopvars) > 1
1078-        # Create a forloop value in the context.  We'll update counters on each
1079-        # iteration just below.
1080-        loop_dict = context['forloop'] = {'parentloop': parentloop}
1081-        for i, item in enumerate(values):
1082-            # Shortcuts for current loop iteration number.
1083-            loop_dict['counter0'] = i
1084-            loop_dict['counter'] = i+1
1085-            # Reverse counter iteration numbers.
1086-            loop_dict['revcounter'] = len_values - i
1087-            loop_dict['revcounter0'] = len_values - i - 1
1088-            # Boolean values designating first and last times through loop.
1089-            loop_dict['first'] = (i == 0)
1090-            loop_dict['last'] = (i == len_values - 1)
1091-
1092-            if unpack:
1093-                # If there are multiple loop variables, unpack the item into
1094-                # them.
1095-                context.update(dict(zip(self.loopvars, item)))
1096-            else:
1097-                context[self.loopvars[0]] = item
1098-            for node in self.nodelist_loop:
1099-                nodelist.append(node.render(context))
1100-            if unpack:
1101-                # The loop variables were pushed on to the context so pop them
1102-                # off again. This is necessary because the tag lets the length
1103-                # of loopvars differ to the length of each set of items and we
1104-                # don't want to leave any vars from the previous loop on the
1105-                # context.
1106-                context.pop()
1107-        context.pop()
1108-        return nodelist.render(context)
1109-
1110-class IfChangedNode(Node):
1111-    def __init__(self, nodelist, *varlist):
1112-        self.nodelist = nodelist
1113-        self._last_seen = None
1114-        self._varlist = map(Variable, varlist)
1115-        self._id = str(id(self))
1116-
1117-    def render(self, context):
1118-        if 'forloop' in context and self._id not in context['forloop']:
1119-            self._last_seen = None
1120-            context['forloop'][self._id] = 1
1121-        try:
1122-            if self._varlist:
1123-                # Consider multiple parameters.  This automatically behaves
1124-                # like an OR evaluation of the multiple variables.
1125-                compare_to = [var.resolve(context) for var in self._varlist]
1126-            else:
1127-                compare_to = self.nodelist.render(context)
1128-        except VariableDoesNotExist:
1129-            compare_to = None
1130-
1131-        if  compare_to != self._last_seen:
1132-            firstloop = (self._last_seen == None)
1133-            self._last_seen = compare_to
1134-            context.push()
1135-            context['ifchanged'] = {'firstloop': firstloop}
1136-            content = self.nodelist.render(context)
1137-            context.pop()
1138-            return content
1139-        else:
1140-            return ''
1141-
1142-class IfEqualNode(Node):
1143-    def __init__(self, var1, var2, nodelist_true, nodelist_false, negate):
1144-        self.var1, self.var2 = Variable(var1), Variable(var2)
1145-        self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
1146-        self.negate = negate
1147-
1148-    def __repr__(self):
1149-        return "<IfEqualNode>"
1150-
1151-    def render(self, context):
1152-        try:
1153-            val1 = self.var1.resolve(context)
1154-        except VariableDoesNotExist:
1155-            val1 = None
1156-        try:
1157-            val2 = self.var2.resolve(context)
1158-        except VariableDoesNotExist:
1159-            val2 = None
1160-        if (self.negate and val1 != val2) or (not self.negate and val1 == val2):
1161-            return self.nodelist_true.render(context)
1162-        return self.nodelist_false.render(context)
1163-
1164-class IfNode(Node):
1165-    def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type):
1166-        self.bool_exprs = bool_exprs
1167-        self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
1168-        self.link_type = link_type
1169-
1170-    def __repr__(self):
1171-        return "<If node>"
1172-
1173-    def __iter__(self):
1174-        for node in self.nodelist_true:
1175-            yield node
1176-        for node in self.nodelist_false:
1177-            yield node
1178-
1179-    def get_nodes_by_type(self, nodetype):
1180-        nodes = []
1181-        if isinstance(self, nodetype):
1182-            nodes.append(self)
1183-        nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))
1184-        nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
1185-        return nodes
1186-
1187-    def render(self, context):
1188-        if self.link_type == IfNode.LinkTypes.or_:
1189-            for ifnot, bool_expr in self.bool_exprs:
1190-                try:
1191-                    value = bool_expr.resolve(context, True)
1192-                except VariableDoesNotExist:
1193-                    value = None
1194-                if (value and not ifnot) or (ifnot and not value):
1195-                    return self.nodelist_true.render(context)
1196-            return self.nodelist_false.render(context)
1197-        else:
1198-            for ifnot, bool_expr in self.bool_exprs:
1199-                try:
1200-                    value = bool_expr.resolve(context, True)
1201-                except VariableDoesNotExist:
1202-                    value = None
1203-                if not ((value and not ifnot) or (ifnot and not value)):
1204-                    return self.nodelist_false.render(context)
1205-            return self.nodelist_true.render(context)
1206-
1207-    class LinkTypes:
1208-        and_ = 0,
1209-        or_ = 1
1210-
1211-class RegroupNode(Node):
1212-    def __init__(self, target, expression, var_name):
1213-        self.target, self.expression = target, expression
1214-        self.var_name = var_name
1215-
1216-    def render(self, context):
1217-        obj_list = self.target.resolve(context, True)
1218-        if obj_list == None:
1219-            # target variable wasn't found in context; fail silently.
1220-            context[self.var_name] = []
1221-            return ''
1222-        # List of dictionaries in the format:
1223-        # {'grouper': 'key', 'list': [list of contents]}.
1224-        context[self.var_name] = [
1225-            {'grouper': key, 'list': list(val)}
1226-            for key, val in
1227-            groupby(obj_list, lambda v, f=self.expression.resolve: f(v, True))
1228-        ]
1229-        return ''
1230-
1231-def include_is_allowed(filepath):
1232-    for root in settings.ALLOWED_INCLUDE_ROOTS:
1233-        if filepath.startswith(root):
1234-            return True
1235-    return False
1236-
1237-class SsiNode(Node):
1238-    def __init__(self, filepath, parsed):
1239-        self.filepath, self.parsed = filepath, parsed
1240-
1241-    def render(self, context):
1242-        if not include_is_allowed(self.filepath):
1243-            if settings.DEBUG:
1244-                return "[Didn't have permission to include file]"
1245-            else:
1246-                return '' # Fail silently for invalid includes.
1247-        try:
1248-            fp = open(self.filepath, 'r')
1249-            output = fp.read()
1250-            fp.close()
1251-        except IOError:
1252-            output = ''
1253-        if self.parsed:
1254-            try:
1255-                t = Template(output, name=self.filepath)
1256-                return t.render(context)
1257-            except TemplateSyntaxError, e:
1258-                if settings.DEBUG:
1259-                    return "[Included template had syntax error: %s]" % e
1260-                else:
1261-                    return '' # Fail silently for invalid included templates.
1262-        return output
1263-
1264-class LoadNode(Node):
1265-    def render(self, context):
1266-        return ''
1267-
1268-class NowNode(Node):
1269-    def __init__(self, format_string):
1270-        self.format_string = format_string
1271-
1272-    def render(self, context):
1273-        from datetime import datetime
1274-        from django.utils.dateformat import DateFormat
1275-        df = DateFormat(datetime.now())
1276-        return df.format(self.format_string)
1277-
1278-class SpacelessNode(Node):
1279-    def __init__(self, nodelist):
1280-        self.nodelist = nodelist
1281-
1282-    def render(self, context):
1283-        from django.utils.html import strip_spaces_between_tags
1284-        return strip_spaces_between_tags(self.nodelist.render(context).strip())
1285-
1286-class TemplateTagNode(Node):
1287-    mapping = {'openblock': BLOCK_TAG_START,
1288-               'closeblock': BLOCK_TAG_END,
1289-               'openvariable': VARIABLE_TAG_START,
1290-               'closevariable': VARIABLE_TAG_END,
1291-               'openbrace': SINGLE_BRACE_START,
1292-               'closebrace': SINGLE_BRACE_END,
1293-               'opencomment': COMMENT_TAG_START,
1294-               'closecomment': COMMENT_TAG_END,
1295-               }
1296-
1297-    def __init__(self, tagtype):
1298-        self.tagtype = tagtype
1299-
1300-    def render(self, context):
1301-        return self.mapping.get(self.tagtype, '')
1302-
1303-class URLNode(Node):
1304-    def __init__(self, view_name, args, kwargs):
1305-        self.view_name = view_name
1306-        self.args = args
1307-        self.kwargs = kwargs
1308-
1309-    def render(self, context):
1310-        from django.core.urlresolvers import reverse, NoReverseMatch
1311-        args = [arg.resolve(context) for arg in self.args]
1312-        kwargs = dict([(smart_str(k,'ascii'), v.resolve(context))
1313-                       for k, v in self.kwargs.items()])
1314-        try:
1315-            return reverse(self.view_name, args=args, kwargs=kwargs)
1316-        except NoReverseMatch:
1317-            try:
1318-                project_name = settings.SETTINGS_MODULE.split('.')[0]
1319-                return reverse(project_name + '.' + self.view_name,
1320-                               args=args, kwargs=kwargs)
1321-            except NoReverseMatch:
1322-                return ''
1323-
1324-class WidthRatioNode(Node):
1325-    def __init__(self, val_expr, max_expr, max_width):
1326-        self.val_expr = val_expr
1327-        self.max_expr = max_expr
1328-        self.max_width = max_width
1329-
1330-    def render(self, context):
1331-        try:
1332-            value = self.val_expr.resolve(context)
1333-            maxvalue = self.max_expr.resolve(context)
1334-        except VariableDoesNotExist:
1335-            return ''
1336-        try:
1337-            value = float(value)
1338-            maxvalue = float(maxvalue)
1339-            ratio = (value / maxvalue) * int(self.max_width)
1340-        except (ValueError, ZeroDivisionError):
1341-            return ''
1342-        return str(int(round(ratio)))
1343-
1344-class WithNode(Node):
1345-    def __init__(self, var, name, nodelist):
1346-        self.var = var
1347-        self.name = name
1348-        self.nodelist = nodelist
1349-
1350-    def __repr__(self):
1351-        return "<WithNode>"
1352-
1353-    def render(self, context):
1354-        val = self.var.resolve(context)
1355-        context.push()
1356-        context[self.name] = val
1357-        output = self.nodelist.render(context)
1358-        context.pop()
1359-        return output
1360-
1361-#@register.tag
1362-def autoescape(parser, token):
1363-    """
1364-    Force autoescape behaviour for this block.
1365-    """
1366-    args = token.contents.split()
1367-    if len(args) != 2:
1368-        raise TemplateSyntaxError("'Autoescape' tag requires exactly one argument.")
1369-    arg = args[1]
1370-    if arg not in (u'on', u'off'):
1371-        raise TemplateSyntaxError("'Autoescape' argument should be 'on' or 'off'")
1372-    nodelist = parser.parse(('endautoescape',))
1373-    parser.delete_first_token()
1374-    return AutoEscapeControlNode((arg == 'on'), nodelist)
1375-autoescape = register.tag(autoescape)
1376-
1377-#@register.tag
1378-def comment(parser, token):
1379-    """
1380-    Ignores everything between ``{% comment %}`` and ``{% endcomment %}``.
1381-    """
1382-    parser.skip_past('endcomment')
1383-    return CommentNode()
1384-comment = register.tag(comment)
1385-
1386-#@register.tag
1387-def cycle(parser, token):
1388-    """
1389-    Cycles among the given strings each time this tag is encountered.
1390-
1391-    Within a loop, cycles among the given strings each time through
1392-    the loop::
1393-
1394-        {% for o in some_list %}
1395-            <tr class="{% cycle 'row1' 'row2' %}">
1396-                ...
1397-            </tr>
1398-        {% endfor %}
1399-
1400-    Outside of a loop, give the values a unique name the first time you call
1401-    it, then use that name each sucessive time through::
1402-
1403-            <tr class="{% cycle 'row1' 'row2' 'row3' as rowcolors %}">...</tr>
1404-            <tr class="{% cycle rowcolors %}">...</tr>
1405-            <tr class="{% cycle rowcolors %}">...</tr>
1406-
1407-    You can use any number of values, separated by spaces. Commas can also
1408-    be used to separate values; if a comma is used, the cycle values are
1409-    interpreted as literal strings.
1410-    """
1411-
1412-    # Note: This returns the exact same node on each {% cycle name %} call;
1413-    # that is, the node object returned from {% cycle a b c as name %} and the
1414-    # one returned from {% cycle name %} are the exact same object. This
1415-    # shouldn't cause problems (heh), but if it does, now you know.
1416-    #
1417-    # Ugly hack warning: This stuffs the named template dict into parser so
1418-    # that names are only unique within each template (as opposed to using
1419-    # a global variable, which would make cycle names have to be unique across
1420-    # *all* templates.
1421-
1422-    args = token.split_contents()
1423-
1424-    if len(args) < 2:
1425-        raise TemplateSyntaxError("'cycle' tag requires at least two arguments")
1426-
1427-    if ',' in args[1]:
1428-        # Backwards compatibility: {% cycle a,b %} or {% cycle a,b as foo %}
1429-        # case.
1430-        args[1:2] = ['"%s"' % arg for arg in args[1].split(",")]
1431-
1432-    if len(args) == 2:
1433-        # {% cycle foo %} case.
1434-        name = args[1]
1435-        if not hasattr(parser, '_namedCycleNodes'):
1436-            raise TemplateSyntaxError("No named cycles in template. '%s' is not defined" % name)
1437-        if not name in parser._namedCycleNodes:
1438-            raise TemplateSyntaxError("Named cycle '%s' does not exist" % name)
1439-        return parser._namedCycleNodes[name]
1440-
1441-    if len(args) > 4 and args[-2] == 'as':
1442-        name = args[-1]
1443-        node = CycleNode(args[1:-2], name)
1444-        if not hasattr(parser, '_namedCycleNodes'):
1445-            parser._namedCycleNodes = {}
1446-        parser._namedCycleNodes[name] = node
1447-    else:
1448-        node = CycleNode(args[1:])
1449-    return node
1450-cycle = register.tag(cycle)
1451-
1452-def debug(parser, token):
1453-    """
1454-    Outputs a whole load of debugging information, including the current
1455-    context and imported modules.
1456-
1457-    Sample usage::
1458-
1459-        <pre>
1460-            {% debug %}
1461-        </pre>
1462-    """
1463-    return DebugNode()
1464-debug = register.tag(debug)
1465-
1466-#@register.tag(name="filter")
1467-def do_filter(parser, token):
1468-    """
1469-    Filters the contents of the block through variable filters.
1470-
1471-    Filters can also be piped through each other, and they can have
1472-    arguments -- just like in variable syntax.
1473-
1474-    Sample usage::
1475-
1476-        {% filter force_escape|lower %}
1477-            This text will be HTML-escaped, and will appear in lowercase.
1478-        {% endfilter %}
1479-    """
1480-    _, rest = token.contents.split(None, 1)
1481-    filter_expr = parser.compile_filter("var|%s" % (rest))
1482-    for func, unused in filter_expr.filters:
1483-        if getattr(func, '_decorated_function', func).__name__ in ('escape', 'safe'):
1484-            raise TemplateSyntaxError('"filter %s" is not permitted.  Use the "autoescape" tag instead.' % func.__name__)
1485-    nodelist = parser.parse(('endfilter',))
1486-    parser.delete_first_token()
1487-    return FilterNode(filter_expr, nodelist)
1488-do_filter = register.tag("filter", do_filter)
1489-
1490-#@register.tag
1491-def firstof(parser, token):
1492-    """
1493-    Outputs the first variable passed that is not False.
1494-
1495-    Outputs nothing if all the passed variables are False.
1496-
1497-    Sample usage::
1498-
1499-        {% firstof var1 var2 var3 %}
1500-
1501-    This is equivalent to::
1502-
1503-        {% if var1 %}
1504-            {{ var1 }}
1505-        {% else %}{% if var2 %}
1506-            {{ var2 }}
1507-        {% else %}{% if var3 %}
1508-            {{ var3 }}
1509-        {% endif %}{% endif %}{% endif %}
1510-
1511-    but obviously much cleaner!
1512-
1513-    You can also use a literal string as a fallback value in case all
1514-    passed variables are False::
1515-
1516-        {% firstof var1 var2 var3 "fallback value" %}
1517-
1518-    """
1519-    bits = token.split_contents()[1:]
1520-    if len(bits) < 1:
1521-        raise TemplateSyntaxError("'firstof' statement requires at least one"
1522-                                  " argument")
1523-    return FirstOfNode(bits)
1524-firstof = register.tag(firstof)
1525-
1526-#@register.tag(name="for")
1527-def do_for(parser, token):
1528-    """
1529-    Loops over each item in an array.
1530-
1531-    For example, to display a list of athletes given ``athlete_list``::
1532-
1533-        <ul>
1534-        {% for athlete in athlete_list %}
1535-            <li>{{ athlete.name }}</li>
1536-        {% endfor %}
1537-        </ul>
1538-
1539-    You can loop over a list in reverse by using
1540-    ``{% for obj in list reversed %}``.
1541-
1542-    You can also unpack multiple values from a two-dimensional array::
1543-
1544-        {% for key,value in dict.items %}
1545-            {{ key }}: {{ value }}
1546-        {% endfor %}
1547-
1548-    The for loop sets a number of variables available within the loop:
1549-
1550-        ==========================  ================================================
1551-        Variable                    Description
1552-        ==========================  ================================================
1553-        ``forloop.counter``         The current iteration of the loop (1-indexed)
1554-        ``forloop.counter0``        The current iteration of the loop (0-indexed)
1555-        ``forloop.revcounter``      The number of iterations from the end of the
1556-                                    loop (1-indexed)
1557-        ``forloop.revcounter0``     The number of iterations from the end of the
1558-                                    loop (0-indexed)
1559-        ``forloop.first``           True if this is the first time through the loop
1560-        ``forloop.last``            True if this is the last time through the loop
1561-        ``forloop.parentloop``      For nested loops, this is the loop "above" the
1562-                                    current one
1563-        ==========================  ================================================
1564-
1565-    """
1566-    bits = token.contents.split()
1567-    if len(bits) < 4:
1568-        raise TemplateSyntaxError("'for' statements should have at least four"
1569-                                  " words: %s" % token.contents)
1570-
1571-    is_reversed = bits[-1] == 'reversed'
1572-    in_index = is_reversed and -3 or -2
1573-    if bits[in_index] != 'in':
1574-        raise TemplateSyntaxError("'for' statements should use the format"
1575-                                  " 'for x in y': %s" % token.contents)
1576-
1577-    loopvars = re.sub(r' *, *', ',', ' '.join(bits[1:in_index])).split(',')
1578-    for var in loopvars:
1579-        if not var or ' ' in var:
1580-            raise TemplateSyntaxError("'for' tag received an invalid argument:"
1581-                                      " %s" % token.contents)
1582-
1583-    sequence = parser.compile_filter(bits[in_index+1])
1584-    nodelist_loop = parser.parse(('endfor',))
1585-    parser.delete_first_token()
1586-    return ForNode(loopvars, sequence, is_reversed, nodelist_loop)
1587-do_for = register.tag("for", do_for)
1588-
1589-def do_ifequal(parser, token, negate):
1590-    bits = list(token.split_contents())
1591-    if len(bits) != 3:
1592-        raise TemplateSyntaxError, "%r takes two arguments" % bits[0]
1593-    end_tag = 'end' + bits[0]
1594-    nodelist_true = parser.parse(('else', end_tag))
1595-    token = parser.next_token()
1596-    if token.contents == 'else':
1597-        nodelist_false = parser.parse((end_tag,))
1598-        parser.delete_first_token()
1599-    else:
1600-        nodelist_false = NodeList()
1601-    return IfEqualNode(bits[1], bits[2], nodelist_true, nodelist_false, negate)
1602-
1603-#@register.tag
1604-def ifequal(parser, token):
1605-    """
1606-    Outputs the contents of the block if the two arguments equal each other.
1607-
1608-    Examples::
1609-
1610-        {% ifequal user.id comment.user_id %}
1611-            ...
1612-        {% endifequal %}
1613-
1614-        {% ifnotequal user.id comment.user_id %}
1615-            ...
1616-        {% else %}
1617-            ...
1618-        {% endifnotequal %}
1619-    """
1620-    return do_ifequal(parser, token, False)
1621-ifequal = register.tag(ifequal)
1622-
1623-#@register.tag
1624-def ifnotequal(parser, token):
1625-    """
1626-    Outputs the contents of the block if the two arguments are not equal.
1627-    See ifequal.
1628-    """
1629-    return do_ifequal(parser, token, True)
1630-ifnotequal = register.tag(ifnotequal)
1631-
1632-#@register.tag(name="if")
1633-def do_if(parser, token):
1634-    """
1635-    The ``{% if %}`` tag evaluates a variable, and if that variable is "true"
1636-    (i.e., exists, is not empty, and is not a false boolean value), the
1637-    contents of the block are output:
1638-
1639-    ::
1640-
1641-        {% if athlete_list %}
1642-            Number of athletes: {{ athlete_list|count }}
1643-        {% else %}
1644-            No athletes.
1645-        {% endif %}
1646-
1647-    In the above, if ``athlete_list`` is not empty, the number of athletes will
1648-    be displayed by the ``{{ athlete_list|count }}`` variable.
1649-
1650-    As you can see, the ``if`` tag can take an option ``{% else %}`` clause
1651-    that will be displayed if the test fails.
1652-
1653-    ``if`` tags may use ``or``, ``and`` or ``not`` to test a number of
1654-    variables or to negate a given variable::
1655-
1656-        {% if not athlete_list %}
1657-            There are no athletes.
1658-        {% endif %}
1659-
1660-        {% if athlete_list or coach_list %}
1661-            There are some athletes or some coaches.
1662-        {% endif %}
1663-
1664-        {% if athlete_list and coach_list %}
1665-            Both atheletes and coaches are available.
1666-        {% endif %}
1667-
1668-        {% if not athlete_list or coach_list %}
1669-            There are no athletes, or there are some coaches.
1670-        {% endif %}
1671-
1672-        {% if athlete_list and not coach_list %}
1673-            There are some athletes and absolutely no coaches.
1674-        {% endif %}
1675-
1676-    ``if`` tags do not allow ``and`` and ``or`` clauses with the same tag,
1677-    because the order of logic would be ambigous. For example, this is
1678-    invalid::
1679-
1680-        {% if athlete_list and coach_list or cheerleader_list %}
1681-
1682-    If you need to combine ``and`` and ``or`` to do advanced logic, just use
1683-    nested if tags. For example::
1684-
1685-        {% if athlete_list %}
1686-            {% if coach_list or cheerleader_list %}
1687-                We have athletes, and either coaches or cheerleaders!
1688-            {% endif %}
1689-        {% endif %}
1690-    """
1691-    bits = token.contents.split()
1692-    del bits[0]
1693-    if not bits:
1694-        raise TemplateSyntaxError("'if' statement requires at least one argument")
1695-    # Bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d']
1696-    bitstr = ' '.join(bits)
1697-    boolpairs = bitstr.split(' and ')
1698-    boolvars = []
1699-    if len(boolpairs) == 1:
1700-        link_type = IfNode.LinkTypes.or_
1701-        boolpairs = bitstr.split(' or ')
1702-    else:
1703-        link_type = IfNode.LinkTypes.and_
1704-        if ' or ' in bitstr:
1705-            raise TemplateSyntaxError, "'if' tags can't mix 'and' and 'or'"
1706-    for boolpair in boolpairs:
1707-        if ' ' in boolpair:
1708-            try:
1709-                not_, boolvar = boolpair.split()
1710-            except ValueError:
1711-                raise TemplateSyntaxError, "'if' statement improperly formatted"
1712-            if not_ != 'not':
1713-                raise TemplateSyntaxError, "Expected 'not' in if statement"
1714-            boolvars.append((True, parser.compile_filter(boolvar)))
1715-        else:
1716-            boolvars.append((False, parser.compile_filter(boolpair)))
1717-    nodelist_true = parser.parse(('else', 'endif'))
1718-    token = parser.next_token()
1719-    if token.contents == 'else':
1720-        nodelist_false = parser.parse(('endif',))
1721-        parser.delete_first_token()
1722-    else:
1723-        nodelist_false = NodeList()
1724-    return IfNode(boolvars, nodelist_true, nodelist_false, link_type)
1725-do_if = register.tag("if", do_if)
1726-
1727-#@register.tag
1728-def ifchanged(parser, token):
1729-    """
1730-    Checks if a value has changed from the last iteration of a loop.
1731-
1732-    The 'ifchanged' block tag is used within a loop. It has two possible uses.
1733-
1734-    1. Checks its own rendered contents against its previous state and only
1735-       displays the content if it has changed. For example, this displays a
1736-       list of days, only displaying the month if it changes::
1737-
1738-            <h1>Archive for {{ year }}</h1>
1739-
1740-            {% for date in days %}
1741-                {% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %}
1742-                <a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a>
1743-            {% endfor %}
1744-
1745-    2. If given a variable, check whether that variable has changed.
1746-       For example, the following shows the date every time it changes, but
1747-       only shows the hour if both the hour and the date have changed::
1748-
1749-            {% for date in days %}
1750-                {% ifchanged date.date %} {{ date.date }} {% endifchanged %}
1751-                {% ifchanged date.hour date.date %}
1752-                    {{ date.hour }}
1753-                {% endifchanged %}
1754-            {% endfor %}
1755-    """
1756-    bits = token.contents.split()
1757-    nodelist = parser.parse(('endifchanged',))
1758-    parser.delete_first_token()
1759-    return IfChangedNode(nodelist, *bits[1:])
1760-ifchanged = register.tag(ifchanged)
1761-
1762-#@register.tag
1763-def ssi(parser, token):
1764-    """
1765-    Outputs the contents of a given file into the page.
1766-
1767-    Like a simple "include" tag, the ``ssi`` tag includes the contents
1768-    of another file -- which must be specified using an absolute path --
1769-    in the current page::
1770-
1771-        {% ssi /home/html/ljworld.com/includes/right_generic.html %}
1772-
1773-    If the optional "parsed" parameter is given, the contents of the included
1774-    file are evaluated as template code, with the current context::
1775-
1776-        {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %}
1777-    """
1778-    bits = token.contents.split()
1779-    parsed = False
1780-    if len(bits) not in (2, 3):
1781-        raise TemplateSyntaxError("'ssi' tag takes one argument: the path to"
1782-                                  " the file to be included")
1783-    if len(bits) == 3:
1784-        if bits[2] == 'parsed':
1785-            parsed = True
1786-        else:
1787-            raise TemplateSyntaxError("Second (optional) argument to %s tag"
1788-                                      " must be 'parsed'" % bits[0])
1789-    return SsiNode(bits[1], parsed)
1790-ssi = register.tag(ssi)
1791-
1792-#@register.tag
1793-def load(parser, token):
1794-    """
1795-    Loads a custom template tag set.
1796-
1797-    For example, to load the template tags in
1798-    ``django/templatetags/news/photos.py``::
1799-
1800-        {% load news.photos %}
1801-    """
1802-    bits = token.contents.split()
1803-    for taglib in bits[1:]:
1804-        # add the library to the parser
1805-        try:
1806-            lib = get_library("django.templatetags.%s" % taglib)
1807-            parser.add_library(lib)
1808-        except InvalidTemplateLibrary, e:
1809-            raise TemplateSyntaxError("'%s' is not a valid tag library: %s" %
1810-                                      (taglib, e))
1811-    return LoadNode()
1812-load = register.tag(load)
1813-
1814-#@register.tag
1815-def now(parser, token):
1816-    """
1817-    Displays the date, formatted according to the given string.
1818-
1819-    Uses the same format as PHP's ``date()`` function; see http://php.net/date
1820-    for all the possible values.
1821-
1822-    Sample usage::
1823-
1824-        It is {% now "jS F Y H:i" %}
1825-    """
1826-    bits = token.contents.split('"')
1827-    if len(bits) != 3:
1828-        raise TemplateSyntaxError, "'now' statement takes one argument"
1829-    format_string = bits[1]
1830-    return NowNode(format_string)
1831-now = register.tag(now)
1832-
1833-#@register.tag
1834-def regroup(parser, token):
1835-    """
1836-    Regroups a list of alike objects by a common attribute.
1837-
1838-    This complex tag is best illustrated by use of an example:  say that
1839-    ``people`` is a list of ``Person`` objects that have ``first_name``,
1840-    ``last_name``, and ``gender`` attributes, and you'd like to display a list
1841-    that looks like:
1842-
1843-        * Male:
1844-            * George Bush
1845-            * Bill Clinton
1846-        * Female:
1847-            * Margaret Thatcher
1848-            * Colendeeza Rice
1849-        * Unknown:
1850-            * Pat Smith
1851-
1852-    The following snippet of template code would accomplish this dubious task::
1853-
1854-        {% regroup people by gender as grouped %}
1855-        <ul>
1856-        {% for group in grouped %}
1857-            <li>{{ group.grouper }}
1858-            <ul>
1859-                {% for item in group.list %}
1860-                <li>{{ item }}</li>
1861-                {% endfor %}
1862-            </ul>
1863-        {% endfor %}
1864-        </ul>
1865-
1866-    As you can see, ``{% regroup %}`` populates a variable with a list of
1867-    objects with ``grouper`` and ``list`` attributes.  ``grouper`` contains the
1868-    item that was grouped by; ``list`` contains the list of objects that share
1869-    that ``grouper``.  In this case, ``grouper`` would be ``Male``, ``Female``
1870-    and ``Unknown``, and ``list`` is the list of people with those genders.
1871-
1872-    Note that `{% regroup %}`` does not work when the list to be grouped is not
1873-    sorted by the key you are grouping by!  This means that if your list of
1874-    people was not sorted by gender, you'd need to make sure it is sorted
1875-    before using it, i.e.::
1876-
1877-        {% regroup people|dictsort:"gender" by gender as grouped %}
1878-
1879-    """
1880-    firstbits = token.contents.split(None, 3)
1881-    if len(firstbits) != 4:
1882-        raise TemplateSyntaxError, "'regroup' tag takes five arguments"
1883-    target = parser.compile_filter(firstbits[1])
1884-    if firstbits[2] != 'by':
1885-        raise TemplateSyntaxError("second argument to 'regroup' tag must be 'by'")
1886-    lastbits_reversed = firstbits[3][::-1].split(None, 2)
1887-    if lastbits_reversed[1][::-1] != 'as':
1888-        raise TemplateSyntaxError("next-to-last argument to 'regroup' tag must"
1889-                                  " be 'as'")
1890-
1891-    expression = parser.compile_filter(lastbits_reversed[2][::-1])
1892-
1893-    var_name = lastbits_reversed[0][::-1]
1894-    return RegroupNode(target, expression, var_name)
1895-regroup = register.tag(regroup)
1896-
1897-def spaceless(parser, token):
1898-    """
1899-    Removes whitespace between HTML tags, including tab and newline characters.
1900-
1901-    Example usage::
1902-
1903-        {% spaceless %}
1904-            <p>
1905-                <a href="foo/">Foo</a>
1906-            </p>
1907-        {% endspaceless %}
1908-
1909-    This example would return this HTML::
1910-
1911-        <p><a href="foo/">Foo</a></p>
1912-
1913-    Only space between *tags* is normalized -- not space between tags and text.
1914-    In this example, the space around ``Hello`` won't be stripped::
1915-
1916-        {% spaceless %}
1917-            <strong>
1918-                Hello
1919-            </strong>
1920-        {% endspaceless %}
1921-    """
1922-    nodelist = parser.parse(('endspaceless',))
1923-    parser.delete_first_token()
1924-    return SpacelessNode(nodelist)
1925-spaceless = register.tag(spaceless)
1926-
1927-#@register.tag
1928-def templatetag(parser, token):
1929-    """
1930-    Outputs one of the bits used to compose template tags.
1931-
1932-    Since the template system has no concept of "escaping", to display one of
1933-    the bits used in template tags, you must use the ``{% templatetag %}`` tag.
1934-
1935-    The argument tells which template bit to output:
1936-
1937-        ==================  =======
1938-        Argument            Outputs
1939-        ==================  =======
1940-        ``openblock``       ``{%``
1941-        ``closeblock``      ``%}``
1942-        ``openvariable``    ``{{``
1943-        ``closevariable``   ``}}``
1944-        ``openbrace``       ``{``
1945-        ``closebrace``      ``}``
1946-        ``opencomment``     ``{#``
1947-        ``closecomment``    ``#}``
1948-        ==================  =======
1949-    """
1950-    bits = token.contents.split()
1951-    if len(bits) != 2:
1952-        raise TemplateSyntaxError, "'templatetag' statement takes one argument"
1953-    tag = bits[1]
1954-    if tag not in TemplateTagNode.mapping:
1955-        raise TemplateSyntaxError("Invalid templatetag argument: '%s'."
1956-                                  " Must be one of: %s" %
1957-                                  (tag, TemplateTagNode.mapping.keys()))
1958-    return TemplateTagNode(tag)
1959-templatetag = register.tag(templatetag)
1960-
1961-def url(parser, token):
1962-    """
1963-    Returns an absolute URL matching given view with its parameters.
1964-
1965-    This is a way to define links that aren't tied to a particular URL
1966-    configuration::
1967-
1968-        {% url path.to.some_view arg1,arg2,name1=value1 %}
1969-
1970-    The first argument is a path to a view. It can be an absolute python path
1971-    or just ``app_name.view_name`` without the project name if the view is
1972-    located inside the project.  Other arguments are comma-separated values
1973-    that will be filled in place of positional and keyword arguments in the
1974-    URL. All arguments for the URL should be present.
1975-
1976-    For example if you have a view ``app_name.client`` taking client's id and
1977-    the corresponding line in a URLconf looks like this::
1978-
1979-        ('^client/(\d+)/$', 'app_name.client')
1980-
1981-    and this app's URLconf is included into the project's URLconf under some
1982-    path::
1983-
1984-        ('^clients/', include('project_name.app_name.urls'))
1985-
1986-    then in a template you can create a link for a certain client like this::
1987-
1988-        {% url app_name.client client.id %}
1989-
1990-    The URL will look like ``/clients/client/123/``.
1991-    """
1992-    bits = token.contents.split(' ', 2)
1993-    if len(bits) < 2:
1994-        raise TemplateSyntaxError("'%s' takes at least one argument"
1995-                                  " (path to a view)" % bits[0])
1996-    args = []
1997-    kwargs = {}
1998-    if len(bits) > 2:
1999-        for arg in bits[2].split(','):
2000-            if '=' in arg:
2001-                k, v = arg.split('=', 1)
2002-                k = k.strip()
2003-                kwargs[k] = parser.compile_filter(v)
2004-            else:
2005-                args.append(parser.compile_filter(arg))
2006-    return URLNode(bits[1], args, kwargs)
2007-url = register.tag(url)
2008-
2009-#@register.tag
2010-def widthratio(parser, token):
2011-    """
2012-    For creating bar charts and such, this tag calculates the ratio of a given
2013-    value to a maximum value, and then applies that ratio to a constant.
2014-
2015-    For example::
2016-
2017-        <img src='bar.gif' height='10' width='{% widthratio this_value max_value 100 %}' />
2018-
2019-    Above, if ``this_value`` is 175 and ``max_value`` is 200, the the image in
2020-    the above example will be 88 pixels wide (because 175/200 = .875;
2021-    .875 * 100 = 87.5 which is rounded up to 88).
2022-    """
2023-    bits = token.contents.split()
2024-    if len(bits) != 4:
2025-        raise TemplateSyntaxError("widthratio takes three arguments")
2026-    tag, this_value_expr, max_value_expr, max_width = bits
2027-    try:
2028-        max_width = int(max_width)
2029-    except ValueError:
2030-        raise TemplateSyntaxError("widthratio final argument must be an integer")
2031-    return WidthRatioNode(parser.compile_filter(this_value_expr),
2032-                          parser.compile_filter(max_value_expr), max_width)
2033-widthratio = register.tag(widthratio)
2034-
2035-#@register.tag
2036-def do_with(parser, token):
2037-    """
2038-    Adds a value to the context (inside of this block) for caching and easy
2039-    access.
2040-
2041-    For example::
2042-
2043-        {% with person.some_sql_method as total %}
2044-            {{ total }} object{{ total|pluralize }}
2045-        {% endwith %}
2046-    """
2047-    bits = list(token.split_contents())
2048-    if len(bits) != 4 or bits[2] != "as":
2049-        raise TemplateSyntaxError("%r expected format is 'value as name'" %
2050-                                  bits[0])
2051-    var = parser.compile_filter(bits[1])
2052-    name = bits[3]
2053-    nodelist = parser.parse(('endwith',))
2054-    parser.delete_first_token()
2055-    return WithNode(var, name, nodelist)
2056-do_with = register.tag('with', do_with)
2057diff --git a/django/template/loader.py b/django/template/loader.py
2058index 1d7d945..8f0d55a 100644
2059--- a/django/template/loader.py
2060+++ b/django/template/loader.py
2061@@ -116,4 +116,4 @@ def select_template(template_name_list):
2062     # If we get here, none of the templates could be loaded
2063     raise TemplateDoesNotExist, ', '.join(template_name_list)
2064 
2065-add_to_builtins('django.template.loader_tags')
2066+add_to_builtins('django.template', 'loader_tags')
2067diff --git a/django/templatetags/__init__.py b/django/templatetags/__init__.py
2068index 9204535..ef550c6 100644
2069--- a/django/templatetags/__init__.py
2070+++ b/django/templatetags/__init__.py
2071@@ -1,7 +1,19 @@
2072+import imp
2073 from django.conf import settings
2074 
2075-for a in settings.INSTALLED_APPS:
2076-    try:
2077-        __path__.extend(__import__(a + '.templatetags', {}, {}, ['']).__path__)
2078-    except ImportError:
2079-        pass
2080+templatetags_modules= []
2081+
2082+def get_templatetags_modules():
2083+    if not templatetags_modules:
2084+        """ Populate list once per thread. """
2085+        for app_module in ['django'] + list(settings.INSTALLED_APPS):
2086+            try:
2087+                components = app_module.split('.')
2088+                mod = __import__(app_module)
2089+                for comp in components[1:]:
2090+                    mod = getattr(mod, comp)
2091+                imp.find_module('templatetags', mod.__path__)
2092+                templatetags_modules.append('%s.templatetags' % app_module)
2093+            except ImportError, AttributeError:
2094+                pass
2095+    return templatetags_modules
2096diff --git a/django/templatetags/defaultfilters.py b/django/templatetags/defaultfilters.py
2097new file mode 100644
2098index 0000000..cef3143
2099--- /dev/null
2100+++ b/django/templatetags/defaultfilters.py
2101@@ -0,0 +1,851 @@
2102+"""Default variable filters."""
2103+
2104+import re
2105+import random as random_module
2106+try:
2107+    from functools import wraps
2108+except ImportError:
2109+    from django.utils.functional import wraps  # Python 2.3, 2.4 fallback.
2110+
2111+from django.template import Variable, Library
2112+from django.conf import settings
2113+from django.utils.translation import ugettext, ungettext
2114+from django.utils.encoding import force_unicode, iri_to_uri
2115+from django.utils.safestring import mark_safe, SafeData
2116+
2117+register = Library()
2118+
2119+#######################
2120+# STRING DECORATOR    #
2121+#######################
2122+
2123+def stringfilter(func):
2124+    """
2125+    Decorator for filters which should only receive unicode objects. The object
2126+    passed as the first positional argument will be converted to a unicode
2127+    object.
2128+    """
2129+    def _dec(*args, **kwargs):
2130+        if args:
2131+            args = list(args)
2132+            args[0] = force_unicode(args[0])
2133+            if isinstance(args[0], SafeData) and getattr(func, 'is_safe', False):
2134+                return mark_safe(func(*args, **kwargs))
2135+        return func(*args, **kwargs)
2136+
2137+    # Include a reference to the real function (used to check original
2138+    # arguments by the template parser).
2139+    _dec._decorated_function = getattr(func, '_decorated_function', func)
2140+    for attr in ('is_safe', 'needs_autoescape'):
2141+        if hasattr(func, attr):
2142+            setattr(_dec, attr, getattr(func, attr))
2143+    return wraps(func)(_dec)
2144+
2145+###################
2146+# STRINGS         #
2147+###################
2148+
2149+
2150+def addslashes(value):
2151+    """
2152+    Adds slashes before quotes. Useful for escaping strings in CSV, for
2153+    example. Less useful for escaping JavaScript; use the ``escapejs``
2154+    filter instead.
2155+    """
2156+    return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
2157+addslashes.is_safe = True
2158+addslashes = stringfilter(addslashes)
2159+
2160+def capfirst(value):
2161+    """Capitalizes the first character of the value."""
2162+    return value and value[0].upper() + value[1:]
2163+capfirst.is_safe=True
2164+capfirst = stringfilter(capfirst)
2165+
2166+_js_escapes = (
2167+    ('\\', '\\\\'),
2168+    ('"', '\\"'),
2169+    ("'", "\\'"),
2170+    ('\n', '\\n'),
2171+    ('\r', '\\r'),
2172+    ('\b', '\\b'),
2173+    ('\f', '\\f'),
2174+    ('\t', '\\t'),
2175+    ('\v', '\\v'),
2176+    ('</', '<\\/'),
2177+)
2178+def escapejs(value):
2179+    """Backslash-escapes characters for use in JavaScript strings."""
2180+    for bad, good in _js_escapes:
2181+        value = value.replace(bad, good)
2182+    return value
2183+escapejs = stringfilter(escapejs)
2184+
2185+def fix_ampersands(value):
2186+    """Replaces ampersands with ``&amp;`` entities."""
2187+    from django.utils.html import fix_ampersands
2188+    return fix_ampersands(value)
2189+fix_ampersands.is_safe=True
2190+fix_ampersands = stringfilter(fix_ampersands)
2191+
2192+def floatformat(text, arg=-1):
2193+    """
2194+    Displays a float to a specified number of decimal places.
2195+
2196+    If called without an argument, it displays the floating point number with
2197+    one decimal place -- but only if there's a decimal place to be displayed:
2198+
2199+    * num1 = 34.23234
2200+    * num2 = 34.00000
2201+    * num3 = 34.26000
2202+    * {{ num1|floatformat }} displays "34.2"
2203+    * {{ num2|floatformat }} displays "34"
2204+    * {{ num3|floatformat }} displays "34.3"
2205+
2206+    If arg is positive, it will always display exactly arg number of decimal
2207+    places:
2208+
2209+    * {{ num1|floatformat:3 }} displays "34.232"
2210+    * {{ num2|floatformat:3 }} displays "34.000"
2211+    * {{ num3|floatformat:3 }} displays "34.260"
2212+
2213+    If arg is negative, it will display arg number of decimal places -- but
2214+    only if there are places to be displayed:
2215+
2216+    * {{ num1|floatformat:"-3" }} displays "34.232"
2217+    * {{ num2|floatformat:"-3" }} displays "34"
2218+    * {{ num3|floatformat:"-3" }} displays "34.260"
2219+    """
2220+    try:
2221+        f = float(text)
2222+    except (ValueError, TypeError):
2223+        return u''
2224+    try:
2225+        d = int(arg)
2226+    except ValueError:
2227+        return force_unicode(f)
2228+    try:
2229+        m = f - int(f)
2230+    except OverflowError:
2231+        return force_unicode(f)
2232+    if not m and d < 0:
2233+        return mark_safe(u'%d' % int(f))
2234+    else:
2235+        formatstr = u'%%.%df' % abs(d)
2236+        return mark_safe(formatstr % f)
2237+floatformat.is_safe = True
2238+
2239+def iriencode(value):
2240+    """Escapes an IRI value for use in a URL."""
2241+    return force_unicode(iri_to_uri(value))
2242+iriencode.is_safe = True
2243+iriencode = stringfilter(iriencode)
2244+
2245+def linenumbers(value, autoescape=None):
2246+    """Displays text with line numbers."""
2247+    from django.utils.html import escape
2248+    lines = value.split(u'\n')
2249+    # Find the maximum width of the line count, for use with zero padding
2250+    # string format command
2251+    width = unicode(len(unicode(len(lines))))
2252+    if not autoescape or isinstance(value, SafeData):
2253+        for i, line in enumerate(lines):
2254+            lines[i] = (u"%0" + width  + u"d. %s") % (i + 1, line)
2255+    else:
2256+        for i, line in enumerate(lines):
2257+            lines[i] = (u"%0" + width  + u"d. %s") % (i + 1, escape(line))
2258+    return mark_safe(u'\n'.join(lines))
2259+linenumbers.is_safe = True
2260+linenumbers.needs_autoescape = True
2261+linenumbers = stringfilter(linenumbers)
2262+
2263+def lower(value):
2264+    """Converts a string into all lowercase."""
2265+    return value.lower()
2266+lower.is_safe = True
2267+lower = stringfilter(lower)
2268+
2269+def make_list(value):
2270+    """
2271+    Returns the value turned into a list.
2272+
2273+    For an integer, it's a list of digits.
2274+    For a string, it's a list of characters.
2275+    """
2276+    return list(value)
2277+make_list.is_safe = False
2278+make_list = stringfilter(make_list)
2279+
2280+def slugify(value):
2281+    """
2282+    Normalizes string, converts to lowercase, removes non-alpha characters,
2283+    and converts spaces to hyphens.
2284+    """
2285+    import unicodedata
2286+    value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
2287+    value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
2288+    return mark_safe(re.sub('[-\s]+', '-', value))
2289+slugify.is_safe = True
2290+slugify = stringfilter(slugify)
2291+
2292+def stringformat(value, arg):
2293+    """
2294+    Formats the variable according to the arg, a string formatting specifier.
2295+
2296+    This specifier uses Python string formating syntax, with the exception that
2297+    the leading "%" is dropped.
2298+
2299+    See http://docs.python.org/lib/typesseq-strings.html for documentation
2300+    of Python string formatting
2301+    """
2302+    try:
2303+        return (u"%" + unicode(arg)) % value
2304+    except (ValueError, TypeError):
2305+        return u""
2306+stringformat.is_safe = True
2307+
2308+def title(value):
2309+    """Converts a string into titlecase."""
2310+    return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
2311+title.is_safe = True
2312+title = stringfilter(title)
2313+
2314+def truncatewords(value, arg):
2315+    """
2316+    Truncates a string after a certain number of words.
2317+
2318+    Argument: Number of words to truncate after.
2319+    """
2320+    from django.utils.text import truncate_words
2321+    try:
2322+        length = int(arg)
2323+    except ValueError: # Invalid literal for int().
2324+        return value # Fail silently.
2325+    return truncate_words(value, length)
2326+truncatewords.is_safe = True
2327+truncatewords = stringfilter(truncatewords)
2328+
2329+def truncatewords_html(value, arg):
2330+    """
2331+    Truncates HTML after a certain number of words.
2332+
2333+    Argument: Number of words to truncate after.
2334+    """
2335+    from django.utils.text import truncate_html_words
2336+    try:
2337+        length = int(arg)
2338+    except ValueError: # invalid literal for int()
2339+        return value # Fail silently.
2340+    return truncate_html_words(value, length)
2341+truncatewords_html.is_safe = True
2342+truncatewords_html = stringfilter(truncatewords_html)
2343+
2344+def upper(value):
2345+    """Converts a string into all uppercase."""
2346+    return value.upper()
2347+upper.is_safe = False
2348+upper = stringfilter(upper)
2349+
2350+def urlencode(value):
2351+    """Escapes a value for use in a URL."""
2352+    from django.utils.http import urlquote
2353+    return urlquote(value)
2354+urlencode.is_safe = False
2355+urlencode = stringfilter(urlencode)
2356+
2357+def urlize(value, autoescape=None):
2358+    """Converts URLs in plain text into clickable links."""
2359+    from django.utils.html import urlize
2360+    return mark_safe(urlize(value, nofollow=True, autoescape=autoescape))
2361+urlize.is_safe=True
2362+urlize.needs_autoescape = True
2363+urlize = stringfilter(urlize)
2364+
2365+def urlizetrunc(value, limit, autoescape=None):
2366+    """
2367+    Converts URLs into clickable links, truncating URLs to the given character
2368+    limit, and adding 'rel=nofollow' attribute to discourage spamming.
2369+
2370+    Argument: Length to truncate URLs to.
2371+    """
2372+    from django.utils.html import urlize
2373+    return mark_safe(urlize(value, trim_url_limit=int(limit), nofollow=True,
2374+                            autoescape=autoescape))
2375+urlizetrunc.is_safe = True
2376+urlizetrunc.needs_autoescape = True
2377+urlizetrunc = stringfilter(urlizetrunc)
2378+
2379+def wordcount(value):
2380+    """Returns the number of words."""
2381+    return len(value.split())
2382+wordcount.is_safe = False
2383+wordcount = stringfilter(wordcount)
2384+
2385+def wordwrap(value, arg):
2386+    """
2387+    Wraps words at specified line length.
2388+
2389+    Argument: number of characters to wrap the text at.
2390+    """
2391+    from django.utils.text import wrap
2392+    return wrap(value, int(arg))
2393+wordwrap.is_safe = True
2394+wordwrap = stringfilter(wordwrap)
2395+
2396+def ljust(value, arg):
2397+    """
2398+    Left-aligns the value in a field of a given width.
2399+
2400+    Argument: field size.
2401+    """
2402+    return value.ljust(int(arg))
2403+ljust.is_safe = True
2404+ljust = stringfilter(ljust)
2405+
2406+def rjust(value, arg):
2407+    """
2408+    Right-aligns the value in a field of a given width.
2409+
2410+    Argument: field size.
2411+    """
2412+    return value.rjust(int(arg))
2413+rjust.is_safe = True
2414+rjust = stringfilter(rjust)
2415+
2416+def center(value, arg):
2417+    """Centers the value in a field of a given width."""
2418+    return value.center(int(arg))
2419+center.is_safe = True
2420+center = stringfilter(center)
2421+
2422+def cut(value, arg):
2423+    """
2424+    Removes all values of arg from the given string.
2425+    """
2426+    safe = isinstance(value, SafeData)
2427+    value = value.replace(arg, u'')
2428+    if safe and arg != ';':
2429+        return mark_safe(value)
2430+    return value
2431+cut = stringfilter(cut)
2432+
2433+###################
2434+# HTML STRINGS    #
2435+###################
2436+
2437+def escape(value):
2438+    """
2439+    Marks the value as a string that should not be auto-escaped.
2440+    """
2441+    from django.utils.safestring import mark_for_escaping
2442+    return mark_for_escaping(value)
2443+escape.is_safe = True
2444+escape = stringfilter(escape)
2445+
2446+def force_escape(value):
2447+    """
2448+    Escapes a string's HTML. This returns a new string containing the escaped
2449+    characters (as opposed to "escape", which marks the content for later
2450+    possible escaping).
2451+    """
2452+    from django.utils.html import escape
2453+    return mark_safe(escape(value))
2454+force_escape = stringfilter(force_escape)
2455+force_escape.is_safe = True
2456+
2457+def linebreaks(value, autoescape=None):
2458+    """
2459+    Replaces line breaks in plain text with appropriate HTML; a single
2460+    newline becomes an HTML line break (``<br />``) and a new line
2461+    followed by a blank line becomes a paragraph break (``</p>``).
2462+    """
2463+    from django.utils.html import linebreaks
2464+    autoescape = autoescape and not isinstance(value, SafeData)
2465+    return mark_safe(linebreaks(value, autoescape))
2466+linebreaks.is_safe = True
2467+linebreaks.needs_autoescape = True
2468+linebreaks = stringfilter(linebreaks)
2469+
2470+def linebreaksbr(value, autoescape=None):
2471+    """
2472+    Converts all newlines in a piece of plain text to HTML line breaks
2473+    (``<br />``).
2474+    """
2475+    if autoescape and not isinstance(value, SafeData):
2476+        from django.utils.html import escape
2477+        value = escape(value)
2478+    return mark_safe(value.replace('\n', '<br />'))
2479+linebreaksbr.is_safe = True
2480+linebreaksbr.needs_autoescape = True
2481+linebreaksbr = stringfilter(linebreaksbr)
2482+
2483+def safe(value):
2484+    """
2485+    Marks the value as a string that should not be auto-escaped.
2486+    """
2487+    from django.utils.safestring import mark_safe
2488+    return mark_safe(value)
2489+safe.is_safe = True
2490+safe = stringfilter(safe)
2491+
2492+def removetags(value, tags):
2493+    """Removes a space separated list of [X]HTML tags from the output."""
2494+    tags = [re.escape(tag) for tag in tags.split()]
2495+    tags_re = u'(%s)' % u'|'.join(tags)
2496+    starttag_re = re.compile(ur'<%s(/?>|(\s+[^>]*>))' % tags_re, re.U)
2497+    endtag_re = re.compile(u'</%s>' % tags_re)
2498+    value = starttag_re.sub(u'', value)
2499+    value = endtag_re.sub(u'', value)
2500+    return value
2501+removetags.is_safe = True
2502+removetags = stringfilter(removetags)
2503+
2504+def striptags(value):
2505+    """Strips all [X]HTML tags."""
2506+    from django.utils.html import strip_tags
2507+    return strip_tags(value)
2508+striptags.is_safe = True
2509+striptags = stringfilter(striptags)
2510+
2511+###################
2512+# LISTS           #
2513+###################
2514+
2515+def dictsort(value, arg):
2516+    """
2517+    Takes a list of dicts, returns that list sorted by the property given in
2518+    the argument.
2519+    """
2520+    var_resolve = Variable(arg).resolve
2521+    decorated = [(var_resolve(item), item) for item in value]
2522+    decorated.sort()
2523+    return [item[1] for item in decorated]
2524+dictsort.is_safe = False
2525+
2526+def dictsortreversed(value, arg):
2527+    """
2528+    Takes a list of dicts, returns that list sorted in reverse order by the
2529+    property given in the argument.
2530+    """
2531+    var_resolve = Variable(arg).resolve
2532+    decorated = [(var_resolve(item), item) for item in value]
2533+    decorated.sort()
2534+    decorated.reverse()
2535+    return [item[1] for item in decorated]
2536+dictsortreversed.is_safe = False
2537+
2538+def first(value):
2539+    """Returns the first item in a list."""
2540+    try:
2541+        return value[0]
2542+    except IndexError:
2543+        return u''
2544+first.is_safe = False
2545+
2546+def join(value, arg):
2547+    """Joins a list with a string, like Python's ``str.join(list)``."""
2548+    try:
2549+        data = arg.join(map(force_unicode, value))
2550+    except AttributeError: # fail silently but nicely
2551+        return value
2552+    safe_args = reduce(lambda lhs, rhs: lhs and isinstance(rhs, SafeData),
2553+            value, True)
2554+    if safe_args:
2555+        return mark_safe(data)
2556+    else:
2557+        return data
2558+join.is_safe = True
2559+
2560+def last(value):
2561+    "Returns the last item in a list"
2562+    try:
2563+        return value[-1]
2564+    except IndexError:
2565+        return u''
2566+last.is_safe = True
2567+
2568+def length(value):
2569+    """Returns the length of the value - useful for lists."""
2570+    return len(value)
2571+length.is_safe = True
2572+
2573+def length_is(value, arg):
2574+    """Returns a boolean of whether the value's length is the argument."""
2575+    return len(value) == int(arg)
2576+length_is.is_safe = True
2577+
2578+def random(value):
2579+    """Returns a random item from the list."""
2580+    return random_module.choice(value)
2581+random.is_safe = True
2582+
2583+def slice_(value, arg):
2584+    """
2585+    Returns a slice of the list.
2586+
2587+    Uses the same syntax as Python's list slicing; see
2588+    http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice
2589+    for an introduction.
2590+    """
2591+    try:
2592+        bits = []
2593+        for x in arg.split(u':'):
2594+            if len(x) == 0:
2595+                bits.append(None)
2596+            else:
2597+                bits.append(int(x))
2598+        return value[slice(*bits)]
2599+
2600+    except (ValueError, TypeError):
2601+        return value # Fail silently.
2602+slice_.is_safe = True
2603+
2604+def unordered_list(value, autoescape=None):
2605+    """
2606+    Recursively takes a self-nested list and returns an HTML unordered list --
2607+    WITHOUT opening and closing <ul> tags.
2608+
2609+    The list is assumed to be in the proper format. For example, if ``var``
2610+    contains: ``['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']]``,
2611+    then ``{{ var|unordered_list }}`` would return::
2612+
2613+        <li>States
2614+        <ul>
2615+                <li>Kansas
2616+                <ul>
2617+                        <li>Lawrence</li>
2618+                        <li>Topeka</li>
2619+                </ul>
2620+                </li>
2621+                <li>Illinois</li>
2622+        </ul>
2623+        </li>
2624+    """
2625+    if autoescape:
2626+        from django.utils.html import conditional_escape
2627+        escaper = conditional_escape
2628+    else:
2629+        escaper = lambda x: x
2630+    def convert_old_style_list(list_):
2631+        """
2632+        Converts old style lists to the new easier to understand format.
2633+
2634+        The old list format looked like:
2635+            ['Item 1', [['Item 1.1', []], ['Item 1.2', []]]
2636+
2637+        And it is converted to:
2638+            ['Item 1', ['Item 1.1', 'Item 1.2]]
2639+        """
2640+        if not isinstance(list_, (tuple, list)) or len(list_) != 2:
2641+            return list_, False
2642+        first_item, second_item = list_
2643+        if second_item == []:
2644+            return [first_item], True
2645+        old_style_list = True
2646+        new_second_item = []
2647+        for sublist in second_item:
2648+            item, old_style_list = convert_old_style_list(sublist)
2649+            if not old_style_list:
2650+                break
2651+            new_second_item.extend(item)
2652+        if old_style_list:
2653+            second_item = new_second_item
2654+        return [first_item, second_item], old_style_list
2655+    def _helper(list_, tabs=1):
2656+        indent = u'\t' * tabs
2657+        output = []
2658+
2659+        list_length = len(list_)
2660+        i = 0
2661+        while i < list_length:
2662+            title = list_[i]
2663+            sublist = ''
2664+            sublist_item = None
2665+            if isinstance(title, (list, tuple)):
2666+                sublist_item = title
2667+                title = ''
2668+            elif i < list_length - 1:
2669+                next_item = list_[i+1]
2670+                if next_item and isinstance(next_item, (list, tuple)):
2671+                    # The next item is a sub-list.
2672+                    sublist_item = next_item
2673+                    # We've processed the next item now too.
2674+                    i += 1
2675+            if sublist_item:
2676+                sublist = _helper(sublist_item, tabs+1)
2677+                sublist = '\n%s<ul>\n%s\n%s</ul>\n%s' % (indent, sublist,
2678+                                                         indent, indent)
2679+            output.append('%s<li>%s%s</li>' % (indent,
2680+                    escaper(force_unicode(title)), sublist))
2681+            i += 1
2682+        return '\n'.join(output)
2683+    value, converted = convert_old_style_list(value)
2684+    return mark_safe(_helper(value))
2685+unordered_list.is_safe = True
2686+unordered_list.needs_autoescape = True
2687+
2688+###################
2689+# INTEGERS        #
2690+###################
2691+
2692+def add(value, arg):
2693+    """Adds the arg to the value."""
2694+    return int(value) + int(arg)
2695+add.is_safe = False
2696+
2697+def get_digit(value, arg):
2698+    """
2699+    Given a whole number, returns the requested digit of it, where 1 is the
2700+    right-most digit, 2 is the second-right-most digit, etc. Returns the
2701+    original value for invalid input (if input or argument is not an integer,
2702+    or if argument is less than 1). Otherwise, output is always an integer.
2703+    """
2704+    try:
2705+        arg = int(arg)
2706+        value = int(value)
2707+    except ValueError:
2708+        return value # Fail silently for an invalid argument
2709+    if arg < 1:
2710+        return value
2711+    try:
2712+        return int(str(value)[-arg])
2713+    except IndexError:
2714+        return 0
2715+get_digit.is_safe = False
2716+
2717+###################
2718+# DATES           #
2719+###################
2720+
2721+def date(value, arg=None):
2722+    """Formats a date according to the given format."""
2723+    from django.utils.dateformat import format
2724+    if not value:
2725+        return u''
2726+    if arg is None:
2727+        arg = settings.DATE_FORMAT
2728+    return format(value, arg)
2729+date.is_safe = False
2730+
2731+def time(value, arg=None):
2732+    """Formats a time according to the given format."""
2733+    from django.utils.dateformat import time_format
2734+    if value in (None, u''):
2735+        return u''
2736+    if arg is None:
2737+        arg = settings.TIME_FORMAT
2738+    return time_format(value, arg)
2739+time.is_safe = False
2740+
2741+def timesince(value, arg=None):
2742+    """Formats a date as the time since that date (i.e. "4 days, 6 hours")."""
2743+    from django.utils.timesince import timesince
2744+    if not value:
2745+        return u''
2746+    if arg:
2747+        return timesince(arg, value)
2748+    return timesince(value)
2749+timesince.is_safe = False
2750+
2751+def timeuntil(value, arg=None):
2752+    """Formats a date as the time until that date (i.e. "4 days, 6 hours")."""
2753+    from django.utils.timesince import timesince
2754+    from datetime import datetime
2755+    if not value:
2756+        return u''
2757+    if arg:
2758+        return timesince(arg, value)
2759+    return timesince(datetime.now(), value)
2760+timeuntil.is_safe = False
2761+
2762+###################
2763+# LOGIC           #
2764+###################
2765+
2766+def default(value, arg):
2767+    """If value is unavailable, use given default."""
2768+    return value or arg
2769+default.is_safe = False
2770+
2771+def default_if_none(value, arg):
2772+    """If value is None, use given default."""
2773+    if value is None:
2774+        return arg
2775+    return value
2776+default_if_none.is_safe = False
2777+
2778+def divisibleby(value, arg):
2779+    """Returns True if the value is devisible by the argument."""
2780+    return int(value) % int(arg) == 0
2781+divisibleby.is_safe = False
2782+
2783+def yesno(value, arg=None):
2784+    """
2785+    Given a string mapping values for true, false and (optionally) None,
2786+    returns one of those strings accoding to the value:
2787+
2788+    ==========  ======================  ==================================
2789+    Value       Argument                Outputs
2790+    ==========  ======================  ==================================
2791+    ``True``    ``"yeah,no,maybe"``     ``yeah``
2792+    ``False``   ``"yeah,no,maybe"``     ``no``
2793+    ``None``    ``"yeah,no,maybe"``     ``maybe``
2794+    ``None``    ``"yeah,no"``           ``"no"`` (converts None to False
2795+                                        if no mapping for None is given.
2796+    ==========  ======================  ==================================
2797+    """
2798+    if arg is None:
2799+        arg = ugettext('yes,no,maybe')
2800+    bits = arg.split(u',')
2801+    if len(bits) < 2:
2802+        return value # Invalid arg.
2803+    try:
2804+        yes, no, maybe = bits
2805+    except ValueError:
2806+        # Unpack list of wrong size (no "maybe" value provided).
2807+        yes, no, maybe = bits[0], bits[1], bits[1]
2808+    if value is None:
2809+        return maybe
2810+    if value:
2811+        return yes
2812+    return no
2813+yesno.is_safe = False
2814+
2815+###################
2816+# MISC            #
2817+###################
2818+
2819+def filesizeformat(bytes):
2820+    """
2821+    Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB,
2822+    102 bytes, etc).
2823+    """
2824+    try:
2825+        bytes = float(bytes)
2826+    except TypeError:
2827+        return u"0 bytes"
2828+
2829+    if bytes < 1024:
2830+        return ungettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes}
2831+    if bytes < 1024 * 1024:
2832+        return ugettext("%.1f KB") % (bytes / 1024)
2833+    if bytes < 1024 * 1024 * 1024:
2834+        return ugettext("%.1f MB") % (bytes / (1024 * 1024))
2835+    return ugettext("%.1f GB") % (bytes / (1024 * 1024 * 1024))
2836+filesizeformat.is_safe = True
2837+
2838+def pluralize(value, arg=u's'):
2839+    """
2840+    Returns a plural suffix if the value is not 1. By default, 's' is used as
2841+    the suffix:
2842+
2843+    * If value is 0, vote{{ value|pluralize }} displays "0 votes".
2844+    * If value is 1, vote{{ value|pluralize }} displays "1 vote".
2845+    * If value is 2, vote{{ value|pluralize }} displays "2 votes".
2846+
2847+    If an argument is provided, that string is used instead:
2848+
2849+    * If value is 0, class{{ value|pluralize:"es" }} displays "0 classes".
2850+    * If value is 1, class{{ value|pluralize:"es" }} displays "1 class".
2851+    * If value is 2, class{{ value|pluralize:"es" }} displays "2 classes".
2852+
2853+    If the provided argument contains a comma, the text before the comma is
2854+    used for the singular case and the text after the comma is used for the
2855+    plural case:
2856+
2857+    * If value is 0, cand{{ value|pluralize:"y,ies" }} displays "0 candies".
2858+    * If value is 1, cand{{ value|pluralize:"y,ies" }} displays "1 candy".
2859+    * If value is 2, cand{{ value|pluralize:"y,ies" }} displays "2 candies".
2860+    """
2861+    if not u',' in arg:
2862+        arg = u',' + arg
2863+    bits = arg.split(u',')
2864+    if len(bits) > 2:
2865+        return u''
2866+    singular_suffix, plural_suffix = bits[:2]
2867+
2868+    try:
2869+        if int(value) != 1:
2870+            return plural_suffix
2871+    except ValueError: # Invalid string that's not a number.
2872+        pass
2873+    except TypeError: # Value isn't a string or a number; maybe it's a list?
2874+        try:
2875+            if len(value) != 1:
2876+                return plural_suffix
2877+        except TypeError: # len() of unsized object.
2878+            pass
2879+    return singular_suffix
2880+pluralize.is_safe = False
2881+
2882+def phone2numeric(value):
2883+    """Takes a phone number and converts it in to its numerical equivalent."""
2884+    from django.utils.text import phone2numeric
2885+    return phone2numeric(value)
2886+phone2numeric.is_safe = True
2887+
2888+def pprint(value):
2889+    """A wrapper around pprint.pprint -- for debugging, really."""
2890+    from pprint import pformat
2891+    try:
2892+        return pformat(value)
2893+    except Exception, e:
2894+        return u"Error in formatting: %s" % force_unicode(e, errors="replace")
2895+pprint.is_safe = True
2896+
2897+# Syntax: register.filter(name of filter, callback)
2898+register.filter(add)
2899+register.filter(addslashes)
2900+register.filter(capfirst)
2901+register.filter(center)
2902+register.filter(cut)
2903+register.filter(date)
2904+register.filter(default)
2905+register.filter(default_if_none)
2906+register.filter(dictsort)
2907+register.filter(dictsortreversed)
2908+register.filter(divisibleby)
2909+register.filter(escape)
2910+register.filter(escapejs)
2911+register.filter(filesizeformat)
2912+register.filter(first)
2913+register.filter(fix_ampersands)
2914+register.filter(floatformat)
2915+register.filter(force_escape)
2916+register.filter(get_digit)
2917+register.filter(iriencode)
2918+register.filter(join)
2919+register.filter(last)
2920+register.filter(length)
2921+register.filter(length_is)
2922+register.filter(linebreaks)
2923+register.filter(linebreaksbr)
2924+register.filter(linenumbers)
2925+register.filter(ljust)
2926+register.filter(lower)
2927+register.filter(make_list)
2928+register.filter(phone2numeric)
2929+register.filter(pluralize)
2930+register.filter(pprint)
2931+register.filter(removetags)
2932+register.filter(random)
2933+register.filter(rjust)
2934+register.filter(safe)
2935+register.filter('slice', slice_)
2936+register.filter(slugify)
2937+register.filter(stringformat)
2938+register.filter(striptags)
2939+register.filter(time)
2940+register.filter(timesince)
2941+register.filter(timeuntil)
2942+register.filter(title)
2943+register.filter(truncatewords)
2944+register.filter(truncatewords_html)
2945+register.filter(unordered_list)
2946+register.filter(upper)
2947+register.filter(urlencode)
2948+register.filter(urlize)
2949+register.filter(urlizetrunc)
2950+register.filter(wordcount)
2951+register.filter(wordwrap)
2952+register.filter(yesno)
2953diff --git a/django/templatetags/defaulttags.py b/django/templatetags/defaulttags.py
2954new file mode 100644
2955index 0000000..535457a
2956--- /dev/null
2957+++ b/django/templatetags/defaulttags.py
2958@@ -0,0 +1,1105 @@
2959+"""Default tags used by the template system, available to all templates."""
2960+
2961+import sys
2962+import re
2963+from itertools import cycle as itertools_cycle
2964+try:
2965+    reversed
2966+except NameError:
2967+    from django.utils.itercompat import reversed     # Python 2.3 fallback
2968+
2969+from django.template import Node, NodeList, Template, Context, Variable
2970+from 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
2971+from django.template import get_library, Library, InvalidTemplateLibrary
2972+from django.conf import settings
2973+from django.utils.encoding import smart_str, smart_unicode
2974+from django.utils.itercompat import groupby
2975+from django.utils.safestring import mark_safe
2976+
2977+register = Library()
2978+
2979+class AutoEscapeControlNode(Node):
2980+    """Implements the actions of the autoescape tag."""
2981+    def __init__(self, setting, nodelist):
2982+        self.setting, self.nodelist = setting, nodelist
2983+
2984+    def render(self, context):
2985+        old_setting = context.autoescape
2986+        context.autoescape = self.setting
2987+        output = self.nodelist.render(context)
2988+        context.autoescape = old_setting
2989+        if self.setting:
2990+            return mark_safe(output)
2991+        else:
2992+            return output
2993+
2994+class CommentNode(Node):
2995+    def render(self, context):
2996+        return ''
2997+
2998+class CycleNode(Node):
2999+    def __init__(self, cyclevars, variable_name=None):
3000+        self.cycle_iter = itertools_cycle([Variable(v) for v in cyclevars])
3001+        self.variable_name = variable_name
3002+
3003+    def render(self, context):
3004+        value = self.cycle_iter.next().resolve(context)
3005+        if self.variable_name:
3006+            context[self.variable_name] = value
3007+        return value
3008+
3009+class DebugNode(Node):
3010+    def render(self, context):
3011+        from pprint import pformat
3012+        output = [pformat(val) for val in context]
3013+        output.append('\n\n')
3014+        output.append(pformat(sys.modules))
3015+        return ''.join(output)
3016+
3017+class FilterNode(Node):
3018+    def __init__(self, filter_expr, nodelist):
3019+        self.filter_expr, self.nodelist = filter_expr, nodelist
3020+
3021+    def render(self, context):
3022+        output = self.nodelist.render(context)
3023+        # Apply filters.
3024+        context.update({'var': output})
3025+        filtered = self.filter_expr.resolve(context)
3026+        context.pop()
3027+        return filtered
3028+
3029+class FirstOfNode(Node):
3030+    def __init__(self, vars):
3031+        self.vars = map(Variable, vars)
3032+
3033+    def render(self, context):
3034+        for var in self.vars:
3035+            try:
3036+                value = var.resolve(context)
3037+            except VariableDoesNotExist:
3038+                continue
3039+            if value:
3040+                return smart_unicode(value)
3041+        return u''
3042+
3043+class ForNode(Node):
3044+    def __init__(self, loopvars, sequence, is_reversed, nodelist_loop):
3045+        self.loopvars, self.sequence = loopvars, sequence
3046+        self.is_reversed = is_reversed
3047+        self.nodelist_loop = nodelist_loop
3048+
3049+    def __repr__(self):
3050+        reversed_text = self.is_reversed and ' reversed' or ''
3051+        return "<For Node: for %s in %s, tail_len: %d%s>" % \
3052+            (', '.join(self.loopvars), self.sequence, len(self.nodelist_loop),
3053+             reversed_text)
3054+
3055+    def __iter__(self):
3056+        for node in self.nodelist_loop:
3057+            yield node
3058+
3059+    def get_nodes_by_type(self, nodetype):
3060+        nodes = []
3061+        if isinstance(self, nodetype):
3062+            nodes.append(self)
3063+        nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype))
3064+        return nodes
3065+
3066+    def render(self, context):
3067+        nodelist = NodeList()
3068+        if 'forloop' in context:
3069+            parentloop = context['forloop']
3070+        else:
3071+            parentloop = {}
3072+        context.push()
3073+        try:
3074+            values = self.sequence.resolve(context, True)
3075+        except VariableDoesNotExist:
3076+            values = []
3077+        if values is None:
3078+            values = []
3079+        if not hasattr(values, '__len__'):
3080+            values = list(values)
3081+        len_values = len(values)
3082+        if self.is_reversed:
3083+            values = reversed(values)
3084+        unpack = len(self.loopvars) > 1
3085+        # Create a forloop value in the context.  We'll update counters on each
3086+        # iteration just below.
3087+        loop_dict = context['forloop'] = {'parentloop': parentloop}
3088+        for i, item in enumerate(values):
3089+            # Shortcuts for current loop iteration number.
3090+            loop_dict['counter0'] = i
3091+            loop_dict['counter'] = i+1
3092+            # Reverse counter iteration numbers.
3093+            loop_dict['revcounter'] = len_values - i
3094+            loop_dict['revcounter0'] = len_values - i - 1
3095+            # Boolean values designating first and last times through loop.
3096+            loop_dict['first'] = (i == 0)
3097+            loop_dict['last'] = (i == len_values - 1)
3098+
3099+            if unpack:
3100+                # If there are multiple loop variables, unpack the item into
3101+                # them.
3102+                context.update(dict(zip(self.loopvars, item)))
3103+            else:
3104+                context[self.loopvars[0]] = item
3105+            for node in self.nodelist_loop:
3106+                nodelist.append(node.render(context))
3107+            if unpack:
3108+                # The loop variables were pushed on to the context so pop them
3109+                # off again. This is necessary because the tag lets the length
3110+                # of loopvars differ to the length of each set of items and we
3111+                # don't want to leave any vars from the previous loop on the
3112+                # context.
3113+                context.pop()
3114+        context.pop()
3115+        return nodelist.render(context)
3116+
3117+class IfChangedNode(Node):
3118+    def __init__(self, nodelist, *varlist):
3119+        self.nodelist = nodelist
3120+        self._last_seen = None
3121+        self._varlist = map(Variable, varlist)
3122+        self._id = str(id(self))
3123+
3124+    def render(self, context):
3125+        if 'forloop' in context and self._id not in context['forloop']:
3126+            self._last_seen = None
3127+            context['forloop'][self._id] = 1
3128+        try:
3129+            if self._varlist:
3130+                # Consider multiple parameters.  This automatically behaves
3131+                # like an OR evaluation of the multiple variables.
3132+                compare_to = [var.resolve(context) for var in self._varlist]
3133+            else:
3134+                compare_to = self.nodelist.render(context)
3135+        except VariableDoesNotExist:
3136+            compare_to = None
3137+
3138+        if  compare_to != self._last_seen:
3139+            firstloop = (self._last_seen == None)
3140+            self._last_seen = compare_to
3141+            context.push()
3142+            context['ifchanged'] = {'firstloop': firstloop}
3143+            content = self.nodelist.render(context)
3144+            context.pop()
3145+            return content
3146+        else:
3147+            return ''
3148+
3149+class IfEqualNode(Node):
3150+    def __init__(self, var1, var2, nodelist_true, nodelist_false, negate):
3151+        self.var1, self.var2 = Variable(var1), Variable(var2)
3152+        self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
3153+        self.negate = negate
3154+
3155+    def __repr__(self):
3156+        return "<IfEqualNode>"
3157+
3158+    def render(self, context):
3159+        try:
3160+            val1 = self.var1.resolve(context)
3161+        except VariableDoesNotExist:
3162+            val1 = None
3163+        try:
3164+            val2 = self.var2.resolve(context)
3165+        except VariableDoesNotExist:
3166+            val2 = None
3167+        if (self.negate and val1 != val2) or (not self.negate and val1 == val2):
3168+            return self.nodelist_true.render(context)
3169+        return self.nodelist_false.render(context)
3170+
3171+class IfNode(Node):
3172+    def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type):
3173+        self.bool_exprs = bool_exprs
3174+        self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
3175+        self.link_type = link_type
3176+
3177+    def __repr__(self):
3178+        return "<If node>"
3179+
3180+    def __iter__(self):
3181+        for node in self.nodelist_true:
3182+            yield node
3183+        for node in self.nodelist_false:
3184+            yield node
3185+
3186+    def get_nodes_by_type(self, nodetype):
3187+        nodes = []
3188+        if isinstance(self, nodetype):
3189+            nodes.append(self)
3190+        nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))
3191+        nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
3192+        return nodes
3193+
3194+    def render(self, context):
3195+        if self.link_type == IfNode.LinkTypes.or_:
3196+            for ifnot, bool_expr in self.bool_exprs:
3197+                try:
3198+                    value = bool_expr.resolve(context, True)
3199+                except VariableDoesNotExist:
3200+                    value = None
3201+                if (value and not ifnot) or (ifnot and not value):
3202+                    return self.nodelist_true.render(context)
3203+            return self.nodelist_false.render(context)
3204+        else:
3205+            for ifnot, bool_expr in self.bool_exprs:
3206+                try:
3207+                    value = bool_expr.resolve(context, True)
3208+                except VariableDoesNotExist:
3209+                    value = None
3210+                if not ((value and not ifnot) or (ifnot and not value)):
3211+                    return self.nodelist_false.render(context)
3212+            return self.nodelist_true.render(context)
3213+
3214+    class LinkTypes:
3215+        and_ = 0,
3216+        or_ = 1
3217+
3218+class RegroupNode(Node):
3219+    def __init__(self, target, expression, var_name):
3220+        self.target, self.expression = target, expression
3221+        self.var_name = var_name
3222+
3223+    def render(self, context):
3224+        obj_list = self.target.resolve(context, True)
3225+        if obj_list == None:
3226+            # target variable wasn't found in context; fail silently.
3227+            context[self.var_name] = []
3228+            return ''
3229+        # List of dictionaries in the format:
3230+        # {'grouper': 'key', 'list': [list of contents]}.
3231+        context[self.var_name] = [
3232+            {'grouper': key, 'list': list(val)}
3233+            for key, val in
3234+            groupby(obj_list, lambda v, f=self.expression.resolve: f(v, True))
3235+        ]
3236+        return ''
3237+
3238+def include_is_allowed(filepath):
3239+    for root in settings.ALLOWED_INCLUDE_ROOTS:
3240+        if filepath.startswith(root):
3241+            return True
3242+    return False
3243+
3244+class SsiNode(Node):
3245+    def __init__(self, filepath, parsed):
3246+        self.filepath, self.parsed = filepath, parsed
3247+
3248+    def render(self, context):
3249+        if not include_is_allowed(self.filepath):
3250+            if settings.DEBUG:
3251+                return "[Didn't have permission to include file]"
3252+            else:
3253+                return '' # Fail silently for invalid includes.
3254+        try:
3255+            fp = open(self.filepath, 'r')
3256+            output = fp.read()
3257+            fp.close()
3258+        except IOError:
3259+            output = ''
3260+        if self.parsed:
3261+            try:
3262+                t = Template(output, name=self.filepath)
3263+                return t.render(context)
3264+            except TemplateSyntaxError, e:
3265+                if settings.DEBUG:
3266+                    return "[Included template had syntax error: %s]" % e
3267+                else:
3268+                    return '' # Fail silently for invalid included templates.
3269+        return output
3270+
3271+class LoadNode(Node):
3272+    def render(self, context):
3273+        return ''
3274+
3275+class NowNode(Node):
3276+    def __init__(self, format_string):
3277+        self.format_string = format_string
3278+
3279+    def render(self, context):
3280+        from datetime import datetime
3281+        from django.utils.dateformat import DateFormat
3282+        df = DateFormat(datetime.now())
3283+        return df.format(self.format_string)
3284+
3285+class SpacelessNode(Node):
3286+    def __init__(self, nodelist):
3287+        self.nodelist = nodelist
3288+
3289+    def render(self, context):
3290+        from django.utils.html import strip_spaces_between_tags
3291+        return strip_spaces_between_tags(self.nodelist.render(context).strip())
3292+
3293+class TemplateTagNode(Node):
3294+    mapping = {'openblock': BLOCK_TAG_START,
3295+               'closeblock': BLOCK_TAG_END,
3296+               'openvariable': VARIABLE_TAG_START,
3297+               'closevariable': VARIABLE_TAG_END,
3298+               'openbrace': SINGLE_BRACE_START,
3299+               'closebrace': SINGLE_BRACE_END,
3300+               'opencomment': COMMENT_TAG_START,
3301+               'closecomment': COMMENT_TAG_END,
3302+               }
3303+
3304+    def __init__(self, tagtype):
3305+        self.tagtype = tagtype
3306+
3307+    def render(self, context):
3308+        return self.mapping.get(self.tagtype, '')
3309+
3310+class URLNode(Node):
3311+    def __init__(self, view_name, args, kwargs):
3312+        self.view_name = view_name
3313+        self.args = args
3314+        self.kwargs = kwargs
3315+
3316+    def render(self, context):
3317+        from django.core.urlresolvers import reverse, NoReverseMatch
3318+        args = [arg.resolve(context) for arg in self.args]
3319+        kwargs = dict([(smart_str(k,'ascii'), v.resolve(context))
3320+                       for k, v in self.kwargs.items()])
3321+        try:
3322+            return reverse(self.view_name, args=args, kwargs=kwargs)
3323+        except NoReverseMatch:
3324+            try:
3325+                project_name = settings.SETTINGS_MODULE.split('.')[0]
3326+                return reverse(project_name + '.' + self.view_name,
3327+                               args=args, kwargs=kwargs)
3328+            except NoReverseMatch:
3329+                return ''
3330+
3331+class WidthRatioNode(Node):
3332+    def __init__(self, val_expr, max_expr, max_width):
3333+        self.val_expr = val_expr
3334+        self.max_expr = max_expr
3335+        self.max_width = max_width
3336+
3337+    def render(self, context):
3338+        try:
3339+            value = self.val_expr.resolve(context)
3340+            maxvalue = self.max_expr.resolve(context)
3341+        except VariableDoesNotExist:
3342+            return ''
3343+        try:
3344+            value = float(value)
3345+            maxvalue = float(maxvalue)
3346+            ratio = (value / maxvalue) * int(self.max_width)
3347+        except (ValueError, ZeroDivisionError):
3348+            return ''
3349+        return str(int(round(ratio)))
3350+
3351+class WithNode(Node):
3352+    def __init__(self, var, name, nodelist):
3353+        self.var = var
3354+        self.name = name
3355+        self.nodelist = nodelist
3356+
3357+    def __repr__(self):
3358+        return "<WithNode>"
3359+
3360+    def render(self, context):
3361+        val = self.var.resolve(context)
3362+        context.push()
3363+        context[self.name] = val
3364+        output = self.nodelist.render(context)
3365+        context.pop()
3366+        return output
3367+
3368+#@register.tag
3369+def autoescape(parser, token):
3370+    """
3371+    Force autoescape behaviour for this block.
3372+    """
3373+    args = token.contents.split()
3374+    if len(args) != 2:
3375+        raise TemplateSyntaxError("'Autoescape' tag requires exactly one argument.")
3376+    arg = args[1]
3377+    if arg not in (u'on', u'off'):
3378+        raise TemplateSyntaxError("'Autoescape' argument should be 'on' or 'off'")
3379+    nodelist = parser.parse(('endautoescape',))
3380+    parser.delete_first_token()
3381+    return AutoEscapeControlNode((arg == 'on'), nodelist)
3382+autoescape = register.tag(autoescape)
3383+
3384+#@register.tag
3385+def comment(parser, token):
3386+    """
3387+    Ignores everything between ``{% comment %}`` and ``{% endcomment %}``.
3388+    """
3389+    parser.skip_past('endcomment')
3390+    return CommentNode()
3391+comment = register.tag(comment)
3392+
3393+#@register.tag
3394+def cycle(parser, token):
3395+    """
3396+    Cycles among the given strings each time this tag is encountered.
3397+
3398+    Within a loop, cycles among the given strings each time through
3399+    the loop::
3400+
3401+        {% for o in some_list %}
3402+            <tr class="{% cycle 'row1' 'row2' %}">
3403+                ...
3404+            </tr>
3405+        {% endfor %}
3406+
3407+    Outside of a loop, give the values a unique name the first time you call
3408+    it, then use that name each sucessive time through::
3409+
3410+            <tr class="{% cycle 'row1' 'row2' 'row3' as rowcolors %}">...</tr>
3411+            <tr class="{% cycle rowcolors %}">...</tr>
3412+            <tr class="{% cycle rowcolors %}">...</tr>
3413+
3414+    You can use any number of values, separated by spaces. Commas can also
3415+    be used to separate values; if a comma is used, the cycle values are
3416+    interpreted as literal strings.
3417+    """
3418+
3419+    # Note: This returns the exact same node on each {% cycle name %} call;
3420+    # that is, the node object returned from {% cycle a b c as name %} and the
3421+    # one returned from {% cycle name %} are the exact same object. This
3422+    # shouldn't cause problems (heh), but if it does, now you know.
3423+    #
3424+    # Ugly hack warning: This stuffs the named template dict into parser so
3425+    # that names are only unique within each template (as opposed to using
3426+    # a global variable, which would make cycle names have to be unique across
3427+    # *all* templates.
3428+
3429+    args = token.split_contents()
3430+
3431+    if len(args) < 2:
3432+        raise TemplateSyntaxError("'cycle' tag requires at least two arguments")
3433+
3434+    if ',' in args[1]:
3435+        # Backwards compatibility: {% cycle a,b %} or {% cycle a,b as foo %}
3436+        # case.
3437+        args[1:2] = ['"%s"' % arg for arg in args[1].split(",")]
3438+
3439+    if len(args) == 2:
3440+        # {% cycle foo %} case.
3441+        name = args[1]
3442+        if not hasattr(parser, '_namedCycleNodes'):
3443+            raise TemplateSyntaxError("No named cycles in template. '%s' is not defined" % name)
3444+        if not name in parser._namedCycleNodes:
3445+            raise TemplateSyntaxError("Named cycle '%s' does not exist" % name)
3446+        return parser._namedCycleNodes[name]
3447+
3448+    if len(args) > 4 and args[-2] == 'as':
3449+        name = args[-1]
3450+        node = CycleNode(args[1:-2], name)
3451+        if not hasattr(parser, '_namedCycleNodes'):
3452+            parser._namedCycleNodes = {}
3453+        parser._namedCycleNodes[name] = node
3454+    else:
3455+        node = CycleNode(args[1:])
3456+    return node
3457+cycle = register.tag(cycle)
3458+
3459+def debug(parser, token):
3460+    """
3461+    Outputs a whole load of debugging information, including the current
3462+    context and imported modules.
3463+
3464+    Sample usage::
3465+
3466+        <pre>
3467+            {% debug %}
3468+        </pre>
3469+    """
3470+    return DebugNode()
3471+debug = register.tag(debug)
3472+
3473+#@register.tag(name="filter")
3474+def do_filter(parser, token):
3475+    """
3476+    Filters the contents of the block through variable filters.
3477+
3478+    Filters can also be piped through each other, and they can have
3479+    arguments -- just like in variable syntax.
3480+
3481+    Sample usage::
3482+
3483+        {% filter force_escape|lower %}
3484+            This text will be HTML-escaped, and will appear in lowercase.
3485+        {% endfilter %}
3486+    """
3487+    _, rest = token.contents.split(None, 1)
3488+    filter_expr = parser.compile_filter("var|%s" % (rest))
3489+    for func, unused in filter_expr.filters:
3490+        if getattr(func, '_decorated_function', func).__name__ in ('escape', 'safe'):
3491+            raise TemplateSyntaxError('"filter %s" is not permitted.  Use the "autoescape" tag instead.' % func.__name__)
3492+    nodelist = parser.parse(('endfilter',))
3493+    parser.delete_first_token()
3494+    return FilterNode(filter_expr, nodelist)
3495+do_filter = register.tag("filter", do_filter)
3496+
3497+#@register.tag
3498+def firstof(parser, token):
3499+    """
3500+    Outputs the first variable passed that is not False.
3501+
3502+    Outputs nothing if all the passed variables are False.
3503+
3504+    Sample usage::
3505+
3506+        {% firstof var1 var2 var3 %}
3507+
3508+    This is equivalent to::
3509+
3510+        {% if var1 %}
3511+            {{ var1 }}
3512+        {% else %}{% if var2 %}
3513+            {{ var2 }}
3514+        {% else %}{% if var3 %}
3515+            {{ var3 }}
3516+        {% endif %}{% endif %}{% endif %}
3517+
3518+    but obviously much cleaner!
3519+
3520+    You can also use a literal string as a fallback value in case all
3521+    passed variables are False::
3522+
3523+        {% firstof var1 var2 var3 "fallback value" %}
3524+
3525+    """
3526+    bits = token.split_contents()[1:]
3527+    if len(bits) < 1:
3528+        raise TemplateSyntaxError("'firstof' statement requires at least one"
3529+                                  " argument")
3530+    return FirstOfNode(bits)
3531+firstof = register.tag(firstof)
3532+
3533+#@register.tag(name="for")
3534+def do_for(parser, token):
3535+    """
3536+    Loops over each item in an array.
3537+
3538+    For example, to display a list of athletes given ``athlete_list``::
3539+
3540+        <ul>
3541+        {% for athlete in athlete_list %}
3542+            <li>{{ athlete.name }}</li>
3543+        {% endfor %}
3544+        </ul>
3545+
3546+    You can loop over a list in reverse by using
3547+    ``{% for obj in list reversed %}``.
3548+
3549+    You can also unpack multiple values from a two-dimensional array::
3550+
3551+        {% for key,value in dict.items %}
3552+            {{ key }}: {{ value }}
3553+        {% endfor %}
3554+
3555+    The for loop sets a number of variables available within the loop:
3556+
3557+        ==========================  ================================================
3558+        Variable                    Description
3559+        ==========================  ================================================
3560+        ``forloop.counter``         The current iteration of the loop (1-indexed)
3561+        ``forloop.counter0``        The current iteration of the loop (0-indexed)
3562+        ``forloop.revcounter``      The number of iterations from the end of the
3563+                                    loop (1-indexed)
3564+        ``forloop.revcounter0``     The number of iterations from the end of the
3565+                                    loop (0-indexed)
3566+        ``forloop.first``           True if this is the first time through the loop
3567+        ``forloop.last``            True if this is the last time through the loop
3568+        ``forloop.parentloop``      For nested loops, this is the loop "above" the
3569+                                    current one
3570+        ==========================  ================================================
3571+
3572+    """
3573+    bits = token.contents.split()
3574+    if len(bits) < 4:
3575+        raise TemplateSyntaxError("'for' statements should have at least four"
3576+                                  " words: %s" % token.contents)
3577+
3578+    is_reversed = bits[-1] == 'reversed'
3579+    in_index = is_reversed and -3 or -2
3580+    if bits[in_index] != 'in':
3581+        raise TemplateSyntaxError("'for' statements should use the format"
3582+                                  " 'for x in y': %s" % token.contents)
3583+
3584+    loopvars = re.sub(r' *, *', ',', ' '.join(bits[1:in_index])).split(',')
3585+    for var in loopvars:
3586+        if not var or ' ' in var:
3587+            raise TemplateSyntaxError("'for' tag received an invalid argument:"
3588+                                      " %s" % token.contents)
3589+
3590+    sequence = parser.compile_filter(bits[in_index+1])
3591+    nodelist_loop = parser.parse(('endfor',))
3592+    parser.delete_first_token()
3593+    return ForNode(loopvars, sequence, is_reversed, nodelist_loop)
3594+do_for = register.tag("for", do_for)
3595+
3596+def do_ifequal(parser, token, negate):
3597+    bits = list(token.split_contents())
3598+    if len(bits) != 3:
3599+        raise TemplateSyntaxError, "%r takes two arguments" % bits[0]
3600+    end_tag = 'end' + bits[0]
3601+    nodelist_true = parser.parse(('else', end_tag))
3602+    token = parser.next_token()
3603+    if token.contents == 'else':
3604+        nodelist_false = parser.parse((end_tag,))
3605+        parser.delete_first_token()
3606+    else:
3607+        nodelist_false = NodeList()
3608+    return IfEqualNode(bits[1], bits[2], nodelist_true, nodelist_false, negate)
3609+
3610+#@register.tag
3611+def ifequal(parser, token):
3612+    """
3613+    Outputs the contents of the block if the two arguments equal each other.
3614+
3615+    Examples::
3616+
3617+        {% ifequal user.id comment.user_id %}
3618+            ...
3619+        {% endifequal %}
3620+
3621+        {% ifnotequal user.id comment.user_id %}
3622+            ...
3623+        {% else %}
3624+            ...
3625+        {% endifnotequal %}
3626+    """
3627+    return do_ifequal(parser, token, False)
3628+ifequal = register.tag(ifequal)
3629+
3630+#@register.tag
3631+def ifnotequal(parser, token):
3632+    """
3633+    Outputs the contents of the block if the two arguments are not equal.
3634+    See ifequal.
3635+    """
3636+    return do_ifequal(parser, token, True)
3637+ifnotequal = register.tag(ifnotequal)
3638+
3639+#@register.tag(name="if")
3640+def do_if(parser, token):
3641+    """
3642+    The ``{% if %}`` tag evaluates a variable, and if that variable is "true"
3643+    (i.e., exists, is not empty, and is not a false boolean value), the
3644+    contents of the block are output:
3645+
3646+    ::
3647+
3648+        {% if athlete_list %}
3649+            Number of athletes: {{ athlete_list|count }}
3650+        {% else %}
3651+            No athletes.
3652+        {% endif %}
3653+
3654+    In the above, if ``athlete_list`` is not empty, the number of athletes will
3655+    be displayed by the ``{{ athlete_list|count }}`` variable.
3656+
3657+    As you can see, the ``if`` tag can take an option ``{% else %}`` clause
3658+    that will be displayed if the test fails.
3659+
3660+    ``if`` tags may use ``or``, ``and`` or ``not`` to test a number of
3661+    variables or to negate a given variable::
3662+
3663+        {% if not athlete_list %}
3664+            There are no athletes.
3665+        {% endif %}
3666+
3667+        {% if athlete_list or coach_list %}
3668+            There are some athletes or some coaches.
3669+        {% endif %}
3670+
3671+        {% if athlete_list and coach_list %}
3672+            Both atheletes and coaches are available.
3673+        {% endif %}
3674+
3675+        {% if not athlete_list or coach_list %}
3676+            There are no athletes, or there are some coaches.
3677+        {% endif %}
3678+
3679+        {% if athlete_list and not coach_list %}
3680+            There are some athletes and absolutely no coaches.
3681+        {% endif %}
3682+
3683+    ``if`` tags do not allow ``and`` and ``or`` clauses with the same tag,
3684+    because the order of logic would be ambigous. For example, this is
3685+    invalid::
3686+
3687+        {% if athlete_list and coach_list or cheerleader_list %}
3688+
3689+    If you need to combine ``and`` and ``or`` to do advanced logic, just use
3690+    nested if tags. For example::
3691+
3692+        {% if athlete_list %}
3693+            {% if coach_list or cheerleader_list %}
3694+                We have athletes, and either coaches or cheerleaders!
3695+            {% endif %}
3696+        {% endif %}
3697+    """
3698+    bits = token.contents.split()
3699+    del bits[0]
3700+    if not bits:
3701+        raise TemplateSyntaxError("'if' statement requires at least one argument")
3702+    # Bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d']
3703+    bitstr = ' '.join(bits)
3704+    boolpairs = bitstr.split(' and ')
3705+    boolvars = []
3706+    if len(boolpairs) == 1:
3707+        link_type = IfNode.LinkTypes.or_
3708+        boolpairs = bitstr.split(' or ')
3709+    else:
3710+        link_type = IfNode.LinkTypes.and_
3711+        if ' or ' in bitstr:
3712+            raise TemplateSyntaxError, "'if' tags can't mix 'and' and 'or'"
3713+    for boolpair in boolpairs:
3714+        if ' ' in boolpair:
3715+            try:
3716+                not_, boolvar = boolpair.split()
3717+            except ValueError:
3718+                raise TemplateSyntaxError, "'if' statement improperly formatted"
3719+            if not_ != 'not':
3720+                raise TemplateSyntaxError, "Expected 'not' in if statement"
3721+            boolvars.append((True, parser.compile_filter(boolvar)))
3722+        else:
3723+            boolvars.append((False, parser.compile_filter(boolpair)))
3724+    nodelist_true = parser.parse(('else', 'endif'))
3725+    token = parser.next_token()
3726+    if token.contents == 'else':
3727+        nodelist_false = parser.parse(('endif',))
3728+        parser.delete_first_token()
3729+    else:
3730+        nodelist_false = NodeList()
3731+    return IfNode(boolvars, nodelist_true, nodelist_false, link_type)
3732+do_if = register.tag("if", do_if)
3733+
3734+#@register.tag
3735+def ifchanged(parser, token):
3736+    """
3737+    Checks if a value has changed from the last iteration of a loop.
3738+
3739+    The 'ifchanged' block tag is used within a loop. It has two possible uses.
3740+
3741+    1. Checks its own rendered contents against its previous state and only
3742+       displays the content if it has changed. For example, this displays a
3743+       list of days, only displaying the month if it changes::
3744+
3745+            <h1>Archive for {{ year }}</h1>
3746+
3747+            {% for date in days %}
3748+                {% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %}
3749+                <a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a>
3750+            {% endfor %}
3751+
3752+    2. If given a variable, check whether that variable has changed.
3753+       For example, the following shows the date every time it changes, but
3754+       only shows the hour if both the hour and the date have changed::
3755+
3756+            {% for date in days %}
3757+                {% ifchanged date.date %} {{ date.date }} {% endifchanged %}
3758+                {% ifchanged date.hour date.date %}
3759+                    {{ date.hour }}
3760+                {% endifchanged %}
3761+            {% endfor %}
3762+    """
3763+    bits = token.contents.split()
3764+    nodelist = parser.parse(('endifchanged',))
3765+    parser.delete_first_token()
3766+    return IfChangedNode(nodelist, *bits[1:])
3767+ifchanged = register.tag(ifchanged)
3768+
3769+#@register.tag
3770+def ssi(parser, token):
3771+    """
3772+    Outputs the contents of a given file into the page.
3773+
3774+    Like a simple "include" tag, the ``ssi`` tag includes the contents
3775+    of another file -- which must be specified using an absolute path --
3776+    in the current page::
3777+
3778+        {% ssi /home/html/ljworld.com/includes/right_generic.html %}
3779+
3780+    If the optional "parsed" parameter is given, the contents of the included
3781+    file are evaluated as template code, with the current context::
3782+
3783+        {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %}
3784+    """
3785+    bits = token.contents.split()
3786+    parsed = False
3787+    if len(bits) not in (2, 3):
3788+        raise TemplateSyntaxError("'ssi' tag takes one argument: the path to"
3789+                                  " the file to be included")
3790+    if len(bits) == 3:
3791+        if bits[2] == 'parsed':
3792+            parsed = True
3793+        else:
3794+            raise TemplateSyntaxError("Second (optional) argument to %s tag"
3795+                                      " must be 'parsed'" % bits[0])
3796+    return SsiNode(bits[1], parsed)
3797+ssi = register.tag(ssi)
3798+
3799+#@register.tag
3800+def load(parser, token):
3801+    """
3802+    Loads a custom template tag set.
3803+
3804+    For example, to load the template tags in
3805+    ``django/templatetags/news/photos.py``::
3806+
3807+        {% load news.photos %}
3808+    """
3809+    bits = token.contents.split()
3810+    for taglib in bits[1:]:
3811+        # add the library to the parser
3812+        try:
3813+            lib = get_library(taglib)
3814+            parser.add_library(lib)
3815+        except InvalidTemplateLibrary, e:
3816+            raise TemplateSyntaxError("'%s' is not a valid tag library: %s" %
3817+                                      (taglib, e))
3818+    return LoadNode()
3819+load = register.tag(load)
3820+
3821+#@register.tag
3822+def now(parser, token):
3823+    """
3824+    Displays the date, formatted according to the given string.
3825+
3826+    Uses the same format as PHP's ``date()`` function; see http://php.net/date
3827+    for all the possible values.
3828+
3829+    Sample usage::
3830+
3831+        It is {% now "jS F Y H:i" %}
3832+    """
3833+    bits = token.contents.split('"')
3834+    if len(bits) != 3:
3835+        raise TemplateSyntaxError, "'now' statement takes one argument"
3836+    format_string = bits[1]
3837+    return NowNode(format_string)
3838+now = register.tag(now)
3839+
3840+#@register.tag
3841+def regroup(parser, token):
3842+    """
3843+    Regroups a list of alike objects by a common attribute.
3844+
3845+    This complex tag is best illustrated by use of an example:  say that
3846+    ``people`` is a list of ``Person`` objects that have ``first_name``,
3847+    ``last_name``, and ``gender`` attributes, and you'd like to display a list
3848+    that looks like:
3849+
3850+        * Male:
3851+            * George Bush
3852+            * Bill Clinton
3853+        * Female:
3854+            * Margaret Thatcher
3855+            * Colendeeza Rice
3856+        * Unknown:
3857+            * Pat Smith
3858+
3859+    The following snippet of template code would accomplish this dubious task::
3860+
3861+        {% regroup people by gender as grouped %}
3862+        <ul>
3863+        {% for group in grouped %}
3864+            <li>{{ group.grouper }}
3865+            <ul>
3866+                {% for item in group.list %}
3867+                <li>{{ item }}</li>
3868+                {% endfor %}
3869+            </ul>
3870+        {% endfor %}
3871+        </ul>
3872+
3873+    As you can see, ``{% regroup %}`` populates a variable with a list of
3874+    objects with ``grouper`` and ``list`` attributes.  ``grouper`` contains the
3875+    item that was grouped by; ``list`` contains the list of objects that share
3876+    that ``grouper``.  In this case, ``grouper`` would be ``Male``, ``Female``
3877+    and ``Unknown``, and ``list`` is the list of people with those genders.
3878+
3879+    Note that `{% regroup %}`` does not work when the list to be grouped is not
3880+    sorted by the key you are grouping by!  This means that if your list of
3881+    people was not sorted by gender, you'd need to make sure it is sorted
3882+    before using it, i.e.::
3883+
3884+        {% regroup people|dictsort:"gender" by gender as grouped %}
3885+
3886+    """
3887+    firstbits = token.contents.split(None, 3)
3888+    if len(firstbits) != 4:
3889+        raise TemplateSyntaxError, "'regroup' tag takes five arguments"
3890+    target = parser.compile_filter(firstbits[1])
3891+    if firstbits[2] != 'by':
3892+        raise TemplateSyntaxError("second argument to 'regroup' tag must be 'by'")
3893+    lastbits_reversed = firstbits[3][::-1].split(None, 2)
3894+    if lastbits_reversed[1][::-1] != 'as':
3895+        raise TemplateSyntaxError("next-to-last argument to 'regroup' tag must"
3896+                                  " be 'as'")
3897+
3898+    expression = parser.compile_filter(lastbits_reversed[2][::-1])
3899+
3900+    var_name = lastbits_reversed[0][::-1]
3901+    return RegroupNode(target, expression, var_name)
3902+regroup = register.tag(regroup)
3903+
3904+def spaceless(parser, token):
3905+    """
3906+    Removes whitespace between HTML tags, including tab and newline characters.
3907+
3908+    Example usage::
3909+
3910+        {% spaceless %}
3911+            <p>
3912+                <a href="foo/">Foo</a>
3913+            </p>
3914+        {% endspaceless %}
3915+
3916+    This example would return this HTML::
3917+
3918+        <p><a href="foo/">Foo</a></p>
3919+
3920+    Only space between *tags* is normalized -- not space between tags and text.
3921+    In this example, the space around ``Hello`` won't be stripped::
3922+
3923+        {% spaceless %}
3924+            <strong>
3925+                Hello
3926+            </strong>
3927+        {% endspaceless %}
3928+    """
3929+    nodelist = parser.parse(('endspaceless',))
3930+    parser.delete_first_token()
3931+    return SpacelessNode(nodelist)
3932+spaceless = register.tag(spaceless)
3933+
3934+#@register.tag
3935+def templatetag(parser, token):
3936+    """
3937+    Outputs one of the bits used to compose template tags.
3938+
3939+    Since the template system has no concept of "escaping", to display one of
3940+    the bits used in template tags, you must use the ``{% templatetag %}`` tag.
3941+
3942+    The argument tells which template bit to output:
3943+
3944+        ==================  =======
3945+        Argument            Outputs
3946+        ==================  =======
3947+        ``openblock``       ``{%``
3948+        ``closeblock``      ``%}``
3949+        ``openvariable``    ``{{``
3950+        ``closevariable``   ``}}``
3951+        ``openbrace``       ``{``
3952+        ``closebrace``      ``}``
3953+        ``opencomment``     ``{#``
3954+        ``closecomment``    ``#}``
3955+        ==================  =======
3956+    """
3957+    bits = token.contents.split()
3958+    if len(bits) != 2:
3959+        raise TemplateSyntaxError, "'templatetag' statement takes one argument"
3960+    tag = bits[1]
3961+    if tag not in TemplateTagNode.mapping:
3962+        raise TemplateSyntaxError("Invalid templatetag argument: '%s'."
3963+                                  " Must be one of: %s" %
3964+                                  (tag, TemplateTagNode.mapping.keys()))
3965+    return TemplateTagNode(tag)
3966+templatetag = register.tag(templatetag)
3967+
3968+def url(parser, token):
3969+    """
3970+    Returns an absolute URL matching given view with its parameters.
3971+
3972+    This is a way to define links that aren't tied to a particular URL
3973+    configuration::
3974+
3975+        {% url path.to.some_view arg1,arg2,name1=value1 %}
3976+
3977+    The first argument is a path to a view. It can be an absolute python path
3978+    or just ``app_name.view_name`` without the project name if the view is
3979+    located inside the project.  Other arguments are comma-separated values
3980+    that will be filled in place of positional and keyword arguments in the
3981+    URL. All arguments for the URL should be present.
3982+
3983+    For example if you have a view ``app_name.client`` taking client's id and
3984+    the corresponding line in a URLconf looks like this::
3985+
3986+        ('^client/(\d+)/$', 'app_name.client')
3987+
3988+    and this app's URLconf is included into the project's URLconf under some
3989+    path::
3990+
3991+        ('^clients/', include('project_name.app_name.urls'))
3992+
3993+    then in a template you can create a link for a certain client like this::
3994+
3995+        {% url app_name.client client.id %}
3996+
3997+    The URL will look like ``/clients/client/123/``.
3998+    """
3999+    bits = token.contents.split(' ', 2)
4000+    if len(bits) < 2:
4001+        raise TemplateSyntaxError("'%s' takes at least one argument"
4002+                                  " (path to a view)" % bits[0])
4003+    args = []
4004+    kwargs = {}
4005+    if len(bits) > 2:
4006+        for arg in bits[2].split(','):
4007+            if '=' in arg:
4008+                k, v = arg.split('=', 1)
4009+                k = k.strip()
4010+                kwargs[k] = parser.compile_filter(v)
4011+            else:
4012+                args.append(parser.compile_filter(arg))
4013+    return URLNode(bits[1], args, kwargs)
4014+url = register.tag(url)
4015+
4016+#@register.tag
4017+def widthratio(parser, token):
4018+    """
4019+    For creating bar charts and such, this tag calculates the ratio of a given
4020+    value to a maximum value, and then applies that ratio to a constant.
4021+
4022+    For example::
4023+
4024+        <img src='bar.gif' height='10' width='{% widthratio this_value max_value 100 %}' />
4025+
4026+    Above, if ``this_value`` is 175 and ``max_value`` is 200, the the image in
4027+    the above example will be 88 pixels wide (because 175/200 = .875;
4028+    .875 * 100 = 87.5 which is rounded up to 88).
4029+    """
4030+    bits = token.contents.split()
4031+    if len(bits) != 4:
4032+        raise TemplateSyntaxError("widthratio takes three arguments")
4033+    tag, this_value_expr, max_value_expr, max_width = bits
4034+    try:
4035+        max_width = int(max_width)
4036+    except ValueError:
4037+        raise TemplateSyntaxError("widthratio final argument must be an integer")
4038+    return WidthRatioNode(parser.compile_filter(this_value_expr),
4039+                          parser.compile_filter(max_value_expr), max_width)
4040+widthratio = register.tag(widthratio)
4041+
4042+#@register.tag
4043+def do_with(parser, token):
4044+    """
4045+    Adds a value to the context (inside of this block) for caching and easy
4046+    access.
4047+
4048+    For example::
4049+
4050+        {% with person.some_sql_method as total %}
4051+            {{ total }} object{{ total|pluralize }}
4052+        {% endwith %}
4053+    """
4054+    bits = list(token.split_contents())
4055+    if len(bits) != 4 or bits[2] != "as":
4056+        raise TemplateSyntaxError("%r expected format is 'value as name'" %
4057+                                  bits[0])
4058+    var = parser.compile_filter(bits[1])
4059+    name = bits[3]
4060+    nodelist = parser.parse(('endwith',))
4061+    parser.delete_first_token()
4062+    return WithNode(var, name, nodelist)
4063+do_with = register.tag('with', do_with)
4064diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
4065index 186b8aa..ac0912d 100644
4066--- a/tests/regressiontests/templates/tests.py
4067+++ b/tests/regressiontests/templates/tests.py
4068@@ -51,7 +51,7 @@ def do_echo(parser, token):
4069 
4070 register.tag("echo", do_echo)
4071 
4072-template.libraries['django.templatetags.testtags'] = register
4073+template.libraries['testtags'] = register
4074 
4075 #####################################
4076 # Helper objects for template tests #