diff --git a/django/template/__init__.py b/django/template/__init__.py index d01d73e15e..6fd8a03250 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -580,6 +580,8 @@ class FilterExpression(object): def args_check(name, func, provided): provided = list(provided) plen = len(provided) + # Check to see if a decorator is providing the real function. + func = getattr(func, '_decorated_function', func) args, varargs, varkw, defaults = getargspec(func) # First argument is filter input. args.pop(0) diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index a95a5dff6f..9c72201984 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -8,6 +8,42 @@ import random as random_module register = Library() +####################### +# STRING DECORATOR # +####################### + +def smart_string(obj): + # FUTURE: Unicode strings should probably be normalized to a specific + # encoding and non-unicode strings should be converted to unicode too. +# if isinstance(obj, unicode): +# obj = obj.encode(settings.DEFAULT_CHARSET) +# else: +# obj = unicode(obj, settings.DEFAULT_CHARSET) + # FUTURE: Replace dumb string logic below with cool unicode logic above. + if not isinstance(obj, basestring): + obj = str(obj) + return obj + +def stringfilter(func): + """ + Decorator for filters which should only receive strings. The object passed + as the first positional argument will be converted to a string. + """ + def _dec(*args, **kwargs): + if args: + args = list(args) + args[0] = smart_string(args[0]) + return func(*args, **kwargs) + + # Make sure the internal name is the original function name because this + # is the internal name of the filter if passed directly to Library().filter + _dec.__name__ = func.__name__ + + # Include a reference to the real function (used to check original + # arguments by the template parser). + _dec._decorated_function = getattr(func, '_decorated_function', func) + return _dec + ################### # STRINGS # ################### @@ -16,16 +52,18 @@ register = Library() def addslashes(value): "Adds slashes - useful for passing strings to JavaScript, for example." return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") +addslashes = stringfilter(addslashes) def capfirst(value): "Capitalizes the first character of the value" - value = str(value) return value and value[0].upper() + value[1:] - +capfirst = stringfilter(capfirst) + def fix_ampersands(value): "Replaces ampersands with ``&`` entities" from django.utils.html import fix_ampersands return fix_ampersands(value) +fix_ampersands = stringfilter(fix_ampersands) def floatformat(text, arg=-1): """ @@ -52,7 +90,7 @@ def floatformat(text, arg=-1): try: d = int(arg) except ValueError: - return str(f) + return smart_string(f) m = f - int(f) if not m and d < 0: return '%d' % int(f) @@ -69,22 +107,26 @@ def linenumbers(value): for i, line in enumerate(lines): lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line)) return '\n'.join(lines) +linenumbers = stringfilter(linenumbers) def lower(value): "Converts a string into all lowercase" return value.lower() +lower = stringfilter(lower) def make_list(value): """ Returns the value turned into a list. For an integer, it's a list of digits. For a string, it's a list of characters. """ - return list(str(value)) + return list(value) +make_list = stringfilter(make_list) def slugify(value): "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens" value = re.sub('[^\w\s-]', '', value).strip().lower() return re.sub('[-\s]+', '-', value) +slugify = stringfilter(slugify) def stringformat(value, arg): """ @@ -96,13 +138,14 @@ def stringformat(value, arg): of Python string formatting """ try: - return ("%" + arg) % value + return ("%" + str(arg)) % value except (ValueError, TypeError): return "" def title(value): "Converts a string into titlecase" return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) +title = stringfilter(title) def truncatewords(value, arg): """ @@ -118,6 +161,7 @@ def truncatewords(value, arg): if not isinstance(value, basestring): value = str(value) return truncate_words(value, length) +truncatewords = stringfilter(truncatewords) def truncatewords_html(value, arg): """ @@ -133,10 +177,12 @@ def truncatewords_html(value, arg): if not isinstance(value, basestring): value = str(value) return truncate_html_words(value, length) +truncatewords_html = stringfilter(truncatewords_html) def upper(value): "Converts a string into all uppercase" return value.upper() +upper = stringfilter(upper) def urlencode(value): "Escapes a value for use in a URL" @@ -144,11 +190,13 @@ def urlencode(value): if not isinstance(value, basestring): value = str(value) return urllib.quote(value) +urlencode = stringfilter(urlencode) def urlize(value): "Converts URLs in plain text into clickable links" from django.utils.html import urlize return urlize(value, nofollow=True) +urlize = stringfilter(urlize) def urlizetrunc(value, limit): """ @@ -159,10 +207,12 @@ def urlizetrunc(value, limit): """ from django.utils.html import urlize return urlize(value, trim_url_limit=int(limit), nofollow=True) +urlizetrunc = stringfilter(urlizetrunc) def wordcount(value): "Returns the number of words" return len(value.split()) +wordcount = stringfilter(wordcount) def wordwrap(value, arg): """ @@ -171,7 +221,8 @@ def wordwrap(value, arg): Argument: number of characters to wrap the text at. """ from django.utils.text import wrap - return wrap(str(value), int(arg)) + return wrap(value, int(arg)) +wordwrap = stringfilter(wordwrap) def ljust(value, arg): """ @@ -179,7 +230,8 @@ def ljust(value, arg): Argument: field size """ - return str(value).ljust(int(arg)) + return value.ljust(int(arg)) +ljust = stringfilter(ljust) def rjust(value, arg): """ @@ -187,15 +239,18 @@ def rjust(value, arg): Argument: field size """ - return str(value).rjust(int(arg)) + return value.rjust(int(arg)) +rjust = stringfilter(rjust) def center(value, arg): "Centers the value in a field of a given width" - return str(value).center(int(arg)) + return value.center(int(arg)) +center = stringfilter(center) def cut(value, arg): "Removes all values of arg from the given string" return value.replace(arg, '') +cut = stringfilter(cut) ################### # HTML STRINGS # @@ -205,15 +260,18 @@ def escape(value): "Escapes a string's HTML" from django.utils.html import escape return escape(value) +escape = stringfilter(escape) def linebreaks(value): "Converts newlines into
and
s"
from django.utils.html import linebreaks
return linebreaks(value)
+linebreaks = stringfilter(linebreaks)
def linebreaksbr(value):
"Converts newlines into
s"
return value.replace('\n', '
')
+linebreaksbr = stringfilter(linebreaksbr)
def removetags(value, tags):
"Removes a space separated list of [X]HTML tags from the output"
@@ -224,13 +282,13 @@ def removetags(value, tags):
value = starttag_re.sub('', value)
value = endtag_re.sub('', value)
return value
+removetags = stringfilter(removetags)
def striptags(value):
"Strips all [X]HTML tags"
from django.utils.html import strip_tags
- if not isinstance(value, basestring):
- value = str(value)
return strip_tags(value)
+striptags = stringfilter(striptags)
###################
# LISTS #
@@ -265,7 +323,7 @@ def first(value):
def join(value, arg):
"Joins a list with a string, like Python's ``str.join(list)``"
try:
- return arg.join(map(str, value))
+ return arg.join(map(smart_string, value))
except AttributeError: # fail silently but nicely
return value
diff --git a/docs/templates_python.txt b/docs/templates_python.txt
index a6b565ed5c..b6bfe67da2 100644
--- a/docs/templates_python.txt
+++ b/docs/templates_python.txt
@@ -654,6 +654,16 @@ decorator instead::
If you leave off the ``name`` argument, as in the second example above, Django
will use the function's name as the filter name.
+Template filters which expect strings
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+If you are writing a template filter which only expects a string as the first
+argument, you should use the included decorator ``to_str`` which will convert
+an object to it's string value before being passed to your function::
+
+ def lower(value):
+ return value.lower()
+ lower = template.to_str(lower)
+
Writing custom template tags
----------------------------
diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py
index b4ec9a0b03..c850806052 100644
--- a/tests/regressiontests/defaultfilters/tests.py
+++ b/tests/regressiontests/defaultfilters/tests.py
@@ -388,7 +388,53 @@ False
>>> phone2numeric('0800 flowers')
'0800 3569377'
-
+# Filters shouldn't break if passed non-strings
+>>> addslashes(123)
+'123'
+>>> linenumbers(123)
+'1. 123'
+>>> lower(123)
+'123'
+>>> make_list(123)
+['1', '2', '3']
+>>> slugify(123)
+'123'
+>>> title(123)
+'123'
+>>> truncatewords(123, 2)
+'123'
+>>> upper(123)
+'123'
+>>> urlencode(123)
+'123'
+>>> urlize(123)
+'123'
+>>> urlizetrunc(123, 1)
+'123'
+>>> wordcount(123)
+1
+>>> wordwrap(123, 2)
+'123'
+>>> ljust('123', 4)
+'123 '
+>>> rjust('123', 4)
+' 123'
+>>> center('123', 5)
+' 123 '
+>>> center('123', 6)
+' 123 '
+>>> cut(123, '2')
+'13'
+>>> escape(123)
+'123'
+>>> linebreaks(123)
+'
123
' +>>> linebreaksbr(123) +'123' +>>> removetags(123, 'a') +'123' +>>> striptags(123) +'123' """