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' """