Fixed #343: filters that take strings now handle non-strings correctly. Thanks to Boffbowsh for the original patch, and to SmileyChris for the updated patch and tests.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@4558 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jacob Kaplan-Moss 2007-02-23 18:02:51 +00:00
parent 1266766da4
commit 36512d5d73
4 changed files with 129 additions and 13 deletions

View File

@ -580,6 +580,8 @@ class FilterExpression(object):
def args_check(name, func, provided): def args_check(name, func, provided):
provided = list(provided) provided = list(provided)
plen = len(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) args, varargs, varkw, defaults = getargspec(func)
# First argument is filter input. # First argument is filter input.
args.pop(0) args.pop(0)

View File

@ -8,6 +8,42 @@ import random as random_module
register = Library() 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 # # STRINGS #
################### ###################
@ -16,16 +52,18 @@ register = Library()
def addslashes(value): def addslashes(value):
"Adds slashes - useful for passing strings to JavaScript, for example." "Adds slashes - useful for passing strings to JavaScript, for example."
return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
addslashes = stringfilter(addslashes)
def capfirst(value): def capfirst(value):
"Capitalizes the first character of the value" "Capitalizes the first character of the value"
value = str(value)
return value and value[0].upper() + value[1:] return value and value[0].upper() + value[1:]
capfirst = stringfilter(capfirst)
def fix_ampersands(value): def fix_ampersands(value):
"Replaces ampersands with ``&`` entities" "Replaces ampersands with ``&`` entities"
from django.utils.html import fix_ampersands from django.utils.html import fix_ampersands
return fix_ampersands(value) return fix_ampersands(value)
fix_ampersands = stringfilter(fix_ampersands)
def floatformat(text, arg=-1): def floatformat(text, arg=-1):
""" """
@ -52,7 +90,7 @@ def floatformat(text, arg=-1):
try: try:
d = int(arg) d = int(arg)
except ValueError: except ValueError:
return str(f) return smart_string(f)
m = f - int(f) m = f - int(f)
if not m and d < 0: if not m and d < 0:
return '%d' % int(f) return '%d' % int(f)
@ -69,22 +107,26 @@ def linenumbers(value):
for i, line in enumerate(lines): for i, line in enumerate(lines):
lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line)) lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line))
return '\n'.join(lines) return '\n'.join(lines)
linenumbers = stringfilter(linenumbers)
def lower(value): def lower(value):
"Converts a string into all lowercase" "Converts a string into all lowercase"
return value.lower() return value.lower()
lower = stringfilter(lower)
def make_list(value): def make_list(value):
""" """
Returns the value turned into a list. For an integer, it's a list of 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. 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): def slugify(value):
"Converts to lowercase, removes non-alpha chars and converts spaces to hyphens" "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens"
value = re.sub('[^\w\s-]', '', value).strip().lower() value = re.sub('[^\w\s-]', '', value).strip().lower()
return re.sub('[-\s]+', '-', value) return re.sub('[-\s]+', '-', value)
slugify = stringfilter(slugify)
def stringformat(value, arg): def stringformat(value, arg):
""" """
@ -96,13 +138,14 @@ def stringformat(value, arg):
of Python string formatting of Python string formatting
""" """
try: try:
return ("%" + arg) % value return ("%" + str(arg)) % value
except (ValueError, TypeError): except (ValueError, TypeError):
return "" return ""
def title(value): def title(value):
"Converts a string into titlecase" "Converts a string into titlecase"
return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
title = stringfilter(title)
def truncatewords(value, arg): def truncatewords(value, arg):
""" """
@ -118,6 +161,7 @@ def truncatewords(value, arg):
if not isinstance(value, basestring): if not isinstance(value, basestring):
value = str(value) value = str(value)
return truncate_words(value, length) return truncate_words(value, length)
truncatewords = stringfilter(truncatewords)
def truncatewords_html(value, arg): def truncatewords_html(value, arg):
""" """
@ -133,10 +177,12 @@ def truncatewords_html(value, arg):
if not isinstance(value, basestring): if not isinstance(value, basestring):
value = str(value) value = str(value)
return truncate_html_words(value, length) return truncate_html_words(value, length)
truncatewords_html = stringfilter(truncatewords_html)
def upper(value): def upper(value):
"Converts a string into all uppercase" "Converts a string into all uppercase"
return value.upper() return value.upper()
upper = stringfilter(upper)
def urlencode(value): def urlencode(value):
"Escapes a value for use in a URL" "Escapes a value for use in a URL"
@ -144,11 +190,13 @@ def urlencode(value):
if not isinstance(value, basestring): if not isinstance(value, basestring):
value = str(value) value = str(value)
return urllib.quote(value) return urllib.quote(value)
urlencode = stringfilter(urlencode)
def urlize(value): def urlize(value):
"Converts URLs in plain text into clickable links" "Converts URLs in plain text into clickable links"
from django.utils.html import urlize from django.utils.html import urlize
return urlize(value, nofollow=True) return urlize(value, nofollow=True)
urlize = stringfilter(urlize)
def urlizetrunc(value, limit): def urlizetrunc(value, limit):
""" """
@ -159,10 +207,12 @@ def urlizetrunc(value, limit):
""" """
from django.utils.html import urlize from django.utils.html import urlize
return urlize(value, trim_url_limit=int(limit), nofollow=True) return urlize(value, trim_url_limit=int(limit), nofollow=True)
urlizetrunc = stringfilter(urlizetrunc)
def wordcount(value): def wordcount(value):
"Returns the number of words" "Returns the number of words"
return len(value.split()) return len(value.split())
wordcount = stringfilter(wordcount)
def wordwrap(value, arg): def wordwrap(value, arg):
""" """
@ -171,7 +221,8 @@ def wordwrap(value, arg):
Argument: number of characters to wrap the text at. Argument: number of characters to wrap the text at.
""" """
from django.utils.text import wrap from django.utils.text import wrap
return wrap(str(value), int(arg)) return wrap(value, int(arg))
wordwrap = stringfilter(wordwrap)
def ljust(value, arg): def ljust(value, arg):
""" """
@ -179,7 +230,8 @@ def ljust(value, arg):
Argument: field size Argument: field size
""" """
return str(value).ljust(int(arg)) return value.ljust(int(arg))
ljust = stringfilter(ljust)
def rjust(value, arg): def rjust(value, arg):
""" """
@ -187,15 +239,18 @@ def rjust(value, arg):
Argument: field size Argument: field size
""" """
return str(value).rjust(int(arg)) return value.rjust(int(arg))
rjust = stringfilter(rjust)
def center(value, arg): def center(value, arg):
"Centers the value in a field of a given width" "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): def cut(value, arg):
"Removes all values of arg from the given string" "Removes all values of arg from the given string"
return value.replace(arg, '') return value.replace(arg, '')
cut = stringfilter(cut)
################### ###################
# HTML STRINGS # # HTML STRINGS #
@ -205,15 +260,18 @@ def escape(value):
"Escapes a string's HTML" "Escapes a string's HTML"
from django.utils.html import escape from django.utils.html import escape
return escape(value) return escape(value)
escape = stringfilter(escape)
def linebreaks(value): def linebreaks(value):
"Converts newlines into <p> and <br />s" "Converts newlines into <p> and <br />s"
from django.utils.html import linebreaks from django.utils.html import linebreaks
return linebreaks(value) return linebreaks(value)
linebreaks = stringfilter(linebreaks)
def linebreaksbr(value): def linebreaksbr(value):
"Converts newlines into <br />s" "Converts newlines into <br />s"
return value.replace('\n', '<br />') return value.replace('\n', '<br />')
linebreaksbr = stringfilter(linebreaksbr)
def removetags(value, tags): def removetags(value, tags):
"Removes a space separated list of [X]HTML tags from the output" "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 = starttag_re.sub('', value)
value = endtag_re.sub('', value) value = endtag_re.sub('', value)
return value return value
removetags = stringfilter(removetags)
def striptags(value): def striptags(value):
"Strips all [X]HTML tags" "Strips all [X]HTML tags"
from django.utils.html import strip_tags from django.utils.html import strip_tags
if not isinstance(value, basestring):
value = str(value)
return strip_tags(value) return strip_tags(value)
striptags = stringfilter(striptags)
################### ###################
# LISTS # # LISTS #
@ -265,7 +323,7 @@ def first(value):
def join(value, arg): def join(value, arg):
"Joins a list with a string, like Python's ``str.join(list)``" "Joins a list with a string, like Python's ``str.join(list)``"
try: try:
return arg.join(map(str, value)) return arg.join(map(smart_string, value))
except AttributeError: # fail silently but nicely except AttributeError: # fail silently but nicely
return value return value

View File

@ -654,6 +654,16 @@ decorator instead::
If you leave off the ``name`` argument, as in the second example above, Django If you leave off the ``name`` argument, as in the second example above, Django
will use the function's name as the filter name. 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 Writing custom template tags
---------------------------- ----------------------------

View File

@ -388,7 +388,53 @@ False
>>> phone2numeric('0800 flowers') >>> phone2numeric('0800 flowers')
'0800 3569377' '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)
'<p>123</p>'
>>> linebreaksbr(123)
'123'
>>> removetags(123, 'a')
'123'
>>> striptags(123)
'123'
""" """