Fixed #17135 -- Made it possible to use decorators (like stringfilter) on template filter functions in combination with auto-escaping. Refs #16726.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17056 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Aymeric Augustin 2011-10-30 07:32:21 +00:00
parent bebbc9e4a5
commit d17bc72880
8 changed files with 154 additions and 178 deletions

View File

@ -11,7 +11,7 @@ from django.utils.tzinfo import LocalTimezone
register = template.Library() register = template.Library()
@register.filter @register.filter(is_safe=True)
def ordinal(value): def ordinal(value):
""" """
Converts an integer to its ordinal as a string. 1 is '1st', 2 is '2nd', Converts an integer to its ordinal as a string. 1 is '1st', 2 is '2nd',
@ -25,9 +25,8 @@ def ordinal(value):
if value % 100 in (11, 12, 13): # special case if value % 100 in (11, 12, 13): # special case
return u"%d%s" % (value, suffixes[0]) return u"%d%s" % (value, suffixes[0])
return u"%d%s" % (value, suffixes[value % 10]) return u"%d%s" % (value, suffixes[value % 10])
ordinal.is_safe = True
@register.filter @register.filter(is_safe=True)
def intcomma(value, use_l10n=True): def intcomma(value, use_l10n=True):
""" """
Converts an integer to a string containing commas every three digits. Converts an integer to a string containing commas every three digits.
@ -47,7 +46,6 @@ def intcomma(value, use_l10n=True):
return new return new
else: else:
return intcomma(new, use_l10n) return intcomma(new, use_l10n)
intcomma.is_safe = True
# A tuple of standard large number to their converters # A tuple of standard large number to their converters
intword_converters = ( intword_converters = (
@ -97,7 +95,7 @@ intword_converters = (
)), )),
) )
@register.filter @register.filter(is_safe=False)
def intword(value): def intword(value):
""" """
Converts a large integer to a friendly text representation. Works best Converts a large integer to a friendly text representation. Works best
@ -129,9 +127,8 @@ def intword(value):
new_value = value / float(large_number) new_value = value / float(large_number)
return _check_for_i18n(new_value, *converters(new_value)) return _check_for_i18n(new_value, *converters(new_value))
return value return value
intword.is_safe = False
@register.filter @register.filter(is_safe=True)
def apnumber(value): def apnumber(value):
""" """
For numbers 1-9, returns the number spelled out. Otherwise, returns the For numbers 1-9, returns the number spelled out. Otherwise, returns the
@ -144,7 +141,6 @@ def apnumber(value):
if not 0 < value < 10: if not 0 < value < 10:
return value return value
return (_('one'), _('two'), _('three'), _('four'), _('five'), _('six'), _('seven'), _('eight'), _('nine'))[value-1] return (_('one'), _('two'), _('three'), _('four'), _('five'), _('six'), _('seven'), _('eight'), _('nine'))[value-1]
apnumber.is_safe = True
@register.filter @register.filter
def naturalday(value, arg=None): def naturalday(value, arg=None):

View File

@ -18,7 +18,7 @@ from django.utils.safestring import mark_safe
register = template.Library() register = template.Library()
@register.filter @register.filter(is_safe=True)
def textile(value): def textile(value):
try: try:
import textile import textile
@ -28,9 +28,8 @@ def textile(value):
return force_unicode(value) return force_unicode(value)
else: else:
return mark_safe(force_unicode(textile.textile(smart_str(value), encoding='utf-8', output='utf-8'))) return mark_safe(force_unicode(textile.textile(smart_str(value), encoding='utf-8', output='utf-8')))
textile.is_safe = True
@register.filter @register.filter(is_safe=True)
def markdown(value, arg=''): def markdown(value, arg=''):
""" """
Runs Markdown over a given value, optionally using various Runs Markdown over a given value, optionally using various
@ -73,9 +72,8 @@ def markdown(value, arg=''):
return mark_safe(markdown.markdown(force_unicode(value), extensions, safe_mode=safe_mode)) return mark_safe(markdown.markdown(force_unicode(value), extensions, safe_mode=safe_mode))
else: else:
return mark_safe(force_unicode(markdown.markdown(smart_str(value)))) return mark_safe(force_unicode(markdown.markdown(smart_str(value))))
markdown.is_safe = True
@register.filter @register.filter(is_safe=True)
def restructuredtext(value): def restructuredtext(value):
try: try:
from docutils.core import publish_parts from docutils.core import publish_parts
@ -87,5 +85,3 @@ def restructuredtext(value):
docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {}) docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {})
parts = publish_parts(source=smart_str(value), writer_name="html4css1", settings_overrides=docutils_settings) parts = publish_parts(source=smart_str(value), writer_name="html4css1", settings_overrides=docutils_settings)
return mark_safe(force_unicode(parts["fragment"])) return mark_safe(force_unicode(parts["fragment"]))
restructuredtext.is_safe = True

View File

@ -1057,30 +1057,43 @@ class Library(object):
self.tags[getattr(func, "_decorated_function", func).__name__] = func self.tags[getattr(func, "_decorated_function", func).__name__] = func
return func return func
def filter(self, name=None, filter_func=None): def filter(self, name=None, filter_func=None, **flags):
if name is None and filter_func is None: if name is None and filter_func is None:
# @register.filter() # @register.filter()
return self.filter_function def dec(func):
elif filter_func is None: return self.filter_function(func, **flags)
return dec
elif name is not None and filter_func is None:
if callable(name): if callable(name):
# @register.filter # @register.filter
return self.filter_function(name) return self.filter_function(name, **flags)
else: else:
# @register.filter('somename') or @register.filter(name='somename') # @register.filter('somename') or @register.filter(name='somename')
def dec(func): def dec(func):
return self.filter(name, func) return self.filter(name, func, **flags)
return dec return dec
elif name is not None and filter_func is not None: elif name is not None and filter_func is not None:
# register.filter('somename', somefunc) # register.filter('somename', somefunc)
self.filters[name] = filter_func self.filters[name] = filter_func
for attr in ('is_safe', 'needs_autoescape'):
if attr in flags:
value = flags[attr]
# set the flag on the filter for FilterExpression.resolve
setattr(filter_func, attr, value)
# set the flag on the innermost decorated function
# for decorators that need it e.g. stringfilter
if hasattr(filter_func, "_decorated_function"):
setattr(filter_func._decorated_function, attr, value)
return filter_func return filter_func
else: else:
raise InvalidTemplateLibrary("Unsupported arguments to " raise InvalidTemplateLibrary("Unsupported arguments to "
"Library.filter: (%r, %r)", (name, filter_func)) "Library.filter: (%r, %r)", (name, filter_func))
def filter_function(self, func): def filter_function(self, func, **flags):
self.filters[getattr(func, "_decorated_function", func).__name__] = func name = getattr(func, "_decorated_function", func).__name__
return func return self.filter(name, func, **flags)
def simple_tag(self, func=None, takes_context=None, name=None): def simple_tag(self, func=None, takes_context=None, name=None):
def dec(func): def dec(func):

View File

@ -37,23 +37,33 @@ def stringfilter(func):
if args: if args:
args = list(args) args = list(args)
args[0] = force_unicode(args[0]) args[0] = force_unicode(args[0])
if isinstance(args[0], SafeData) and getattr(func, 'is_safe', False): if (isinstance(args[0], SafeData) and
getattr(_dec._decorated_function, 'is_safe', False)):
return mark_safe(func(*args, **kwargs)) return mark_safe(func(*args, **kwargs))
return func(*args, **kwargs) return func(*args, **kwargs)
# Include a reference to the real function (used to check original # Include a reference to the real function (used to check original
# arguments by the template parser). # arguments by the template parser, and to bear the 'is_safe' attribute
# when multiple decorators are applied).
_dec._decorated_function = getattr(func, '_decorated_function', func) _dec._decorated_function = getattr(func, '_decorated_function', func)
for attr in ('is_safe', 'needs_autoescape'): for attr in ('is_safe', 'needs_autoescape'):
if hasattr(func, attr): if hasattr(func, attr):
import warnings
warnings.warn("Setting the %s attribute of a template filter "
"function is deprecated; use @register.filter(%s=%s) "
"instead" % (attr, attr, getattr(func, attr)),
PendingDeprecationWarning)
setattr(_dec, attr, getattr(func, attr)) setattr(_dec, attr, getattr(func, attr))
return wraps(func)(_dec) return wraps(func)(_dec)
################### ###################
# STRINGS # # STRINGS #
################### ###################
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def addslashes(value): def addslashes(value):
""" """
@ -62,14 +72,12 @@ def addslashes(value):
filter instead. filter instead.
""" """
return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
addslashes.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def capfirst(value): def capfirst(value):
"""Capitalizes the first character of the value.""" """Capitalizes the first character of the value."""
return value and value[0].upper() + value[1:] return value and value[0].upper() + value[1:]
capfirst.is_safe = True
@register.filter("escapejs") @register.filter("escapejs")
@stringfilter @stringfilter
@ -77,12 +85,11 @@ def escapejs_filter(value):
"""Hex encodes characters for use in JavaScript strings.""" """Hex encodes characters for use in JavaScript strings."""
return escapejs(value) return escapejs(value)
@register.filter("fix_ampersands") @register.filter("fix_ampersands", is_safe=True)
@stringfilter @stringfilter
def fix_ampersands_filter(value): def fix_ampersands_filter(value):
"""Replaces ampersands with ``&amp;`` entities.""" """Replaces ampersands with ``&amp;`` entities."""
return fix_ampersands(value) return fix_ampersands(value)
fix_ampersands_filter.is_safe = True
# Values for testing floatformat input against infinity and NaN representations, # Values for testing floatformat input against infinity and NaN representations,
# which differ across platforms and Python versions. Some (i.e. old Windows # which differ across platforms and Python versions. Some (i.e. old Windows
@ -96,7 +103,7 @@ neg_inf = -1e200 * 1e200
nan = (1e200 * 1e200) // (1e200 * 1e200) nan = (1e200 * 1e200) // (1e200 * 1e200)
special_floats = [str(pos_inf), str(neg_inf), str(nan)] special_floats = [str(pos_inf), str(neg_inf), str(nan)]
@register.filter @register.filter(is_safe=True)
def floatformat(text, arg=-1): def floatformat(text, arg=-1):
""" """
Displays a float to a specified number of decimal places. Displays a float to a specified number of decimal places.
@ -172,16 +179,14 @@ def floatformat(text, arg=-1):
return mark_safe(formats.number_format(number, abs(p))) return mark_safe(formats.number_format(number, abs(p)))
except InvalidOperation: except InvalidOperation:
return input_val return input_val
floatformat.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def iriencode(value): def iriencode(value):
"""Escapes an IRI value for use in a URL.""" """Escapes an IRI value for use in a URL."""
return force_unicode(iri_to_uri(value)) return force_unicode(iri_to_uri(value))
iriencode.is_safe = True
@register.filter @register.filter(is_safe=True, needs_autoescape=True)
@stringfilter @stringfilter
def linenumbers(value, autoescape=None): def linenumbers(value, autoescape=None):
"""Displays text with line numbers.""" """Displays text with line numbers."""
@ -196,17 +201,14 @@ def linenumbers(value, autoescape=None):
for i, line in enumerate(lines): for i, line in enumerate(lines):
lines[i] = (u"%0" + width + u"d. %s") % (i + 1, escape(line)) lines[i] = (u"%0" + width + u"d. %s") % (i + 1, escape(line))
return mark_safe(u'\n'.join(lines)) return mark_safe(u'\n'.join(lines))
linenumbers.is_safe = True
linenumbers.needs_autoescape = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
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.is_safe = True
@register.filter @register.filter(is_safe=False)
@stringfilter @stringfilter
def make_list(value): def make_list(value):
""" """
@ -216,9 +218,8 @@ def make_list(value):
For a string, it's a list of characters. For a string, it's a list of characters.
""" """
return list(value) return list(value)
make_list.is_safe = False
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def slugify(value): def slugify(value):
""" """
@ -228,9 +229,8 @@ def slugify(value):
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
value = unicode(re.sub('[^\w\s-]', '', value).strip().lower()) value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
return mark_safe(re.sub('[-\s]+', '-', value)) return mark_safe(re.sub('[-\s]+', '-', value))
slugify.is_safe = True
@register.filter @register.filter(is_safe=True)
def stringformat(value, arg): def stringformat(value, arg):
""" """
Formats the variable according to the arg, a string formatting specifier. Formats the variable according to the arg, a string formatting specifier.
@ -245,17 +245,15 @@ def stringformat(value, arg):
return (u"%" + unicode(arg)) % value return (u"%" + unicode(arg)) % value
except (ValueError, TypeError): except (ValueError, TypeError):
return u"" return u""
stringformat.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def title(value): def title(value):
"""Converts a string into titlecase.""" """Converts a string into titlecase."""
t = re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) t = re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
return re.sub("\d([A-Z])", lambda m: m.group(0).lower(), t) return re.sub("\d([A-Z])", lambda m: m.group(0).lower(), t)
title.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def truncatechars(value, arg): def truncatechars(value, arg):
""" """
@ -268,9 +266,8 @@ def truncatechars(value, arg):
except ValueError: # Invalid literal for int(). except ValueError: # Invalid literal for int().
return value # Fail silently. return value # Fail silently.
return Truncator(value).chars(length) return Truncator(value).chars(length)
truncatechars.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def truncatewords(value, arg): def truncatewords(value, arg):
""" """
@ -285,9 +282,8 @@ def truncatewords(value, arg):
except ValueError: # Invalid literal for int(). except ValueError: # Invalid literal for int().
return value # Fail silently. return value # Fail silently.
return Truncator(value).words(length, truncate=' ...') return Truncator(value).words(length, truncate=' ...')
truncatewords.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def truncatewords_html(value, arg): def truncatewords_html(value, arg):
""" """
@ -302,16 +298,14 @@ def truncatewords_html(value, arg):
except ValueError: # invalid literal for int() except ValueError: # invalid literal for int()
return value # Fail silently. return value # Fail silently.
return Truncator(value).words(length, html=True, truncate=' ...') return Truncator(value).words(length, html=True, truncate=' ...')
truncatewords_html.is_safe = True
@register.filter @register.filter(is_safe=False)
@stringfilter @stringfilter
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.is_safe = False
@register.filter @register.filter(is_safe=False)
@stringfilter @stringfilter
def urlencode(value, safe=None): def urlencode(value, safe=None):
""" """
@ -326,17 +320,14 @@ def urlencode(value, safe=None):
if safe is not None: if safe is not None:
kwargs['safe'] = safe kwargs['safe'] = safe
return urlquote(value, **kwargs) return urlquote(value, **kwargs)
urlencode.is_safe = False
@register.filter @register.filter(is_safe=True, needs_autoescape=True)
@stringfilter @stringfilter
def urlize(value, autoescape=None): def urlize(value, autoescape=None):
"""Converts URLs in plain text into clickable links.""" """Converts URLs in plain text into clickable links."""
return mark_safe(urlize_impl(value, nofollow=True, autoescape=autoescape)) return mark_safe(urlize_impl(value, nofollow=True, autoescape=autoescape))
urlize.is_safe = True
urlize.needs_autoescape = True
@register.filter @register.filter(is_safe=True, needs_autoescape=True)
@stringfilter @stringfilter
def urlizetrunc(value, limit, autoescape=None): def urlizetrunc(value, limit, autoescape=None):
""" """
@ -347,17 +338,14 @@ def urlizetrunc(value, limit, autoescape=None):
""" """
return mark_safe(urlize_impl(value, trim_url_limit=int(limit), nofollow=True, return mark_safe(urlize_impl(value, trim_url_limit=int(limit), nofollow=True,
autoescape=autoescape)) autoescape=autoescape))
urlizetrunc.is_safe = True
urlizetrunc.needs_autoescape = True
@register.filter @register.filter(is_safe=False)
@stringfilter @stringfilter
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.is_safe = False
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def wordwrap(value, arg): def wordwrap(value, arg):
""" """
@ -366,9 +354,8 @@ def wordwrap(value, arg):
Argument: number of characters to wrap the text at. Argument: number of characters to wrap the text at.
""" """
return wrap(value, int(arg)) return wrap(value, int(arg))
wordwrap.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def ljust(value, arg): def ljust(value, arg):
""" """
@ -377,9 +364,8 @@ def ljust(value, arg):
Argument: field size. Argument: field size.
""" """
return value.ljust(int(arg)) return value.ljust(int(arg))
ljust.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def rjust(value, arg): def rjust(value, arg):
""" """
@ -388,14 +374,12 @@ def rjust(value, arg):
Argument: field size. Argument: field size.
""" """
return value.rjust(int(arg)) return value.rjust(int(arg))
rjust.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
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 value.center(int(arg)) return value.center(int(arg))
center.is_safe = True
@register.filter @register.filter
@stringfilter @stringfilter
@ -413,16 +397,15 @@ def cut(value, arg):
# HTML STRINGS # # HTML STRINGS #
################### ###################
@register.filter("escape") @register.filter("escape", is_safe=True)
@stringfilter @stringfilter
def escape_filter(value): def escape_filter(value):
""" """
Marks the value as a string that should not be auto-escaped. Marks the value as a string that should not be auto-escaped.
""" """
return mark_for_escaping(value) return mark_for_escaping(value)
escape_filter.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def force_escape(value): def force_escape(value):
""" """
@ -431,9 +414,8 @@ def force_escape(value):
possible escaping). possible escaping).
""" """
return mark_safe(escape(value)) return mark_safe(escape(value))
force_escape.is_safe = True
@register.filter("linebreaks") @register.filter("linebreaks", is_safe=True, needs_autoescape=True)
@stringfilter @stringfilter
def linebreaks_filter(value, autoescape=None): def linebreaks_filter(value, autoescape=None):
""" """
@ -443,10 +425,8 @@ def linebreaks_filter(value, autoescape=None):
""" """
autoescape = autoescape and not isinstance(value, SafeData) autoescape = autoescape and not isinstance(value, SafeData)
return mark_safe(linebreaks(value, autoescape)) return mark_safe(linebreaks(value, autoescape))
linebreaks_filter.is_safe = True
linebreaks_filter.needs_autoescape = True
@register.filter @register.filter(is_safe=True, needs_autoescape=True)
@stringfilter @stringfilter
def linebreaksbr(value, autoescape=None): def linebreaksbr(value, autoescape=None):
""" """
@ -458,19 +438,16 @@ def linebreaksbr(value, autoescape=None):
if autoescape: if autoescape:
value = escape(value) value = escape(value)
return mark_safe(value.replace('\n', '<br />')) return mark_safe(value.replace('\n', '<br />'))
linebreaksbr.is_safe = True
linebreaksbr.needs_autoescape = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def safe(value): def safe(value):
""" """
Marks the value as a string that should not be auto-escaped. Marks the value as a string that should not be auto-escaped.
""" """
return mark_safe(value) return mark_safe(value)
safe.is_safe = True
@register.filter @register.filter(is_safe=True)
def safeseq(value): def safeseq(value):
""" """
A "safe" filter for sequences. Marks each element in the sequence, A "safe" filter for sequences. Marks each element in the sequence,
@ -478,9 +455,8 @@ def safeseq(value):
with the results. with the results.
""" """
return [mark_safe(force_unicode(obj)) for obj in value] return [mark_safe(force_unicode(obj)) for obj in value]
safeseq.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
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."""
@ -491,47 +467,42 @@ def removetags(value, tags):
value = starttag_re.sub(u'', value) value = starttag_re.sub(u'', value)
value = endtag_re.sub(u'', value) value = endtag_re.sub(u'', value)
return value return value
removetags.is_safe = True
@register.filter @register.filter(is_safe=True)
@stringfilter @stringfilter
def striptags(value): def striptags(value):
"""Strips all [X]HTML tags.""" """Strips all [X]HTML tags."""
return strip_tags(value) return strip_tags(value)
striptags.is_safe = True
################### ###################
# LISTS # # LISTS #
################### ###################
@register.filter @register.filter(is_safe=False)
def dictsort(value, arg): def dictsort(value, arg):
""" """
Takes a list of dicts, returns that list sorted by the property given in Takes a list of dicts, returns that list sorted by the property given in
the argument. the argument.
""" """
return sorted(value, key=Variable(arg).resolve) return sorted(value, key=Variable(arg).resolve)
dictsort.is_safe = False
@register.filter @register.filter(is_safe=False)
def dictsortreversed(value, arg): def dictsortreversed(value, arg):
""" """
Takes a list of dicts, returns that list sorted in reverse order by the Takes a list of dicts, returns that list sorted in reverse order by the
property given in the argument. property given in the argument.
""" """
return sorted(value, key=Variable(arg).resolve, reverse=True) return sorted(value, key=Variable(arg).resolve, reverse=True)
dictsortreversed.is_safe = False
@register.filter @register.filter(is_safe=False)
def first(value): def first(value):
"""Returns the first item in a list.""" """Returns the first item in a list."""
try: try:
return value[0] return value[0]
except IndexError: except IndexError:
return u'' return u''
first.is_safe = False
@register.filter @register.filter(is_safe=True, needs_autoescape=True)
def join(value, arg, autoescape=None): def join(value, arg, autoescape=None):
""" """
Joins a list with a string, like Python's ``str.join(list)``. Joins a list with a string, like Python's ``str.join(list)``.
@ -544,43 +515,37 @@ def join(value, arg, autoescape=None):
except AttributeError: # fail silently but nicely except AttributeError: # fail silently but nicely
return value return value
return mark_safe(data) return mark_safe(data)
join.is_safe = True
join.needs_autoescape = True
@register.filter @register.filter(is_safe=True)
def last(value): def last(value):
"Returns the last item in a list" "Returns the last item in a list"
try: try:
return value[-1] return value[-1]
except IndexError: except IndexError:
return u'' return u''
last.is_safe = True
@register.filter @register.filter(is_safe=True)
def length(value): def length(value):
"""Returns the length of the value - useful for lists.""" """Returns the length of the value - useful for lists."""
try: try:
return len(value) return len(value)
except (ValueError, TypeError): except (ValueError, TypeError):
return '' return ''
length.is_safe = True
@register.filter @register.filter(is_safe=False)
def length_is(value, arg): def length_is(value, arg):
"""Returns a boolean of whether the value's length is the argument.""" """Returns a boolean of whether the value's length is the argument."""
try: try:
return len(value) == int(arg) return len(value) == int(arg)
except (ValueError, TypeError): except (ValueError, TypeError):
return '' return ''
length_is.is_safe = False
@register.filter @register.filter(is_safe=True)
def random(value): def random(value):
"""Returns a random item from the list.""" """Returns a random item from the list."""
return random_module.choice(value) return random_module.choice(value)
random.is_safe = True
@register.filter("slice") @register.filter("slice", is_safe=True)
def slice_filter(value, arg): def slice_filter(value, arg):
""" """
Returns a slice of the list. Returns a slice of the list.
@ -600,9 +565,8 @@ def slice_filter(value, arg):
except (ValueError, TypeError): except (ValueError, TypeError):
return value # Fail silently. return value # Fail silently.
slice_filter.is_safe = True
@register.filter @register.filter(is_safe=True, needs_autoescape=True)
def unordered_list(value, autoescape=None): def unordered_list(value, autoescape=None):
""" """
Recursively takes a self-nested list and returns an HTML unordered list -- Recursively takes a self-nested list and returns an HTML unordered list --
@ -688,14 +652,12 @@ def unordered_list(value, autoescape=None):
return '\n'.join(output) return '\n'.join(output)
value, converted = convert_old_style_list(value) value, converted = convert_old_style_list(value)
return mark_safe(_helper(value)) return mark_safe(_helper(value))
unordered_list.is_safe = True
unordered_list.needs_autoescape = True
################### ###################
# INTEGERS # # INTEGERS #
################### ###################
@register.filter @register.filter(is_safe=False)
def add(value, arg): def add(value, arg):
"""Adds the arg to the value.""" """Adds the arg to the value."""
try: try:
@ -705,9 +667,8 @@ def add(value, arg):
return value + arg return value + arg
except Exception: except Exception:
return '' return ''
add.is_safe = False
@register.filter @register.filter(is_safe=False)
def get_digit(value, arg): def get_digit(value, arg):
""" """
Given a whole number, returns the requested digit of it, where 1 is the Given a whole number, returns the requested digit of it, where 1 is the
@ -726,13 +687,12 @@ def get_digit(value, arg):
return int(str(value)[-arg]) return int(str(value)[-arg])
except IndexError: except IndexError:
return 0 return 0
get_digit.is_safe = False
################### ###################
# DATES # # DATES #
################### ###################
@register.filter @register.filter(is_safe=False)
def date(value, arg=None): def date(value, arg=None):
"""Formats a date according to the given format.""" """Formats a date according to the given format."""
if not value: if not value:
@ -746,9 +706,8 @@ def date(value, arg=None):
return format(value, arg) return format(value, arg)
except AttributeError: except AttributeError:
return '' return ''
date.is_safe = False
@register.filter @register.filter(is_safe=False)
def time(value, arg=None): def time(value, arg=None):
"""Formats a time according to the given format.""" """Formats a time according to the given format."""
if value in (None, u''): if value in (None, u''):
@ -762,9 +721,8 @@ def time(value, arg=None):
return time_format(value, arg) return time_format(value, arg)
except AttributeError: except AttributeError:
return '' return ''
time.is_safe = False
@register.filter("timesince") @register.filter("timesince", is_safe=False)
def timesince_filter(value, arg=None): def timesince_filter(value, arg=None):
"""Formats a date as the time since that date (i.e. "4 days, 6 hours").""" """Formats a date as the time since that date (i.e. "4 days, 6 hours")."""
if not value: if not value:
@ -775,9 +733,8 @@ def timesince_filter(value, arg=None):
return timesince(value) return timesince(value)
except (ValueError, TypeError): except (ValueError, TypeError):
return u'' return u''
timesince_filter.is_safe = False
@register.filter("timeuntil") @register.filter("timeuntil", is_safe=False)
def timeuntil_filter(value, arg=None): def timeuntil_filter(value, arg=None):
"""Formats a date as the time until that date (i.e. "4 days, 6 hours").""" """Formats a date as the time until that date (i.e. "4 days, 6 hours")."""
if not value: if not value:
@ -786,33 +743,29 @@ def timeuntil_filter(value, arg=None):
return timeuntil(value, arg) return timeuntil(value, arg)
except (ValueError, TypeError): except (ValueError, TypeError):
return u'' return u''
timeuntil_filter.is_safe = False
################### ###################
# LOGIC # # LOGIC #
################### ###################
@register.filter @register.filter(is_safe=False)
def default(value, arg): def default(value, arg):
"""If value is unavailable, use given default.""" """If value is unavailable, use given default."""
return value or arg return value or arg
default.is_safe = False
@register.filter @register.filter(is_safe=False)
def default_if_none(value, arg): def default_if_none(value, arg):
"""If value is None, use given default.""" """If value is None, use given default."""
if value is None: if value is None:
return arg return arg
return value return value
default_if_none.is_safe = False
@register.filter @register.filter(is_safe=False)
def divisibleby(value, arg): def divisibleby(value, arg):
"""Returns True if the value is devisible by the argument.""" """Returns True if the value is devisible by the argument."""
return int(value) % int(arg) == 0 return int(value) % int(arg) == 0
divisibleby.is_safe = False
@register.filter @register.filter(is_safe=False)
def yesno(value, arg=None): def yesno(value, arg=None):
""" """
Given a string mapping values for true, false and (optionally) None, Given a string mapping values for true, false and (optionally) None,
@ -843,13 +796,12 @@ def yesno(value, arg=None):
if value: if value:
return yes return yes
return no return no
yesno.is_safe = False
################### ###################
# MISC # # MISC #
################### ###################
@register.filter @register.filter(is_safe=True)
def filesizeformat(bytes): def filesizeformat(bytes):
""" """
Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB,
@ -873,9 +825,8 @@ def filesizeformat(bytes):
if bytes < 1024 * 1024 * 1024 * 1024 * 1024: if bytes < 1024 * 1024 * 1024 * 1024 * 1024:
return ugettext("%s TB") % filesize_number_format(bytes / (1024 * 1024 * 1024 * 1024)) return ugettext("%s TB") % filesize_number_format(bytes / (1024 * 1024 * 1024 * 1024))
return ugettext("%s PB") % filesize_number_format(bytes / (1024 * 1024 * 1024 * 1024 * 1024)) return ugettext("%s PB") % filesize_number_format(bytes / (1024 * 1024 * 1024 * 1024 * 1024))
filesizeformat.is_safe = True
@register.filter @register.filter(is_safe=False)
def pluralize(value, arg=u's'): def pluralize(value, arg=u's'):
""" """
Returns a plural suffix if the value is not 1. By default, 's' is used as Returns a plural suffix if the value is not 1. By default, 's' is used as
@ -918,19 +869,16 @@ def pluralize(value, arg=u's'):
except TypeError: # len() of unsized object. except TypeError: # len() of unsized object.
pass pass
return singular_suffix return singular_suffix
pluralize.is_safe = False
@register.filter("phone2numeric") @register.filter("phone2numeric", is_safe=True)
def phone2numeric_filter(value): def phone2numeric_filter(value):
"""Takes a phone number and converts it in to its numerical equivalent.""" """Takes a phone number and converts it in to its numerical equivalent."""
return phone2numeric(value) return phone2numeric(value)
phone2numeric_filter.is_safe = True
@register.filter @register.filter(is_safe=True)
def pprint(value): def pprint(value):
"""A wrapper around pprint.pprint -- for debugging, really.""" """A wrapper around pprint.pprint -- for debugging, really."""
try: try:
return pformat(value) return pformat(value)
except Exception, e: except Exception, e:
return u"Error in formatting: %s" % force_unicode(e, errors="replace") return u"Error in formatting: %s" % force_unicode(e, errors="replace")
pprint.is_safe = True

View File

@ -5,23 +5,21 @@ from django.utils.encoding import force_unicode
register = Library() register = Library()
@register.filter @register.filter(is_safe=False)
def localize(value): def localize(value):
""" """
Forces a value to be rendered as a localized value, Forces a value to be rendered as a localized value,
regardless of the value of ``settings.USE_L10N``. regardless of the value of ``settings.USE_L10N``.
""" """
return force_unicode(formats.localize(value, use_l10n=True)) return force_unicode(formats.localize(value, use_l10n=True))
localize.is_safe = False
@register.filter @register.filter(is_safe=False)
def unlocalize(value): def unlocalize(value):
""" """
Forces a value to be rendered as a non-localized value, Forces a value to be rendered as a non-localized value,
regardless of the value of ``settings.USE_L10N``. regardless of the value of ``settings.USE_L10N``.
""" """
return force_unicode(value) return force_unicode(value)
unlocalize.is_safe = False
class LocalizeNode(Node): class LocalizeNode(Node):
def __init__(self, nodelist, use_l10n): def __init__(self, nodelist, use_l10n):

View File

@ -143,6 +143,10 @@ You can use ``register.filter()`` as a 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.
Finally, ``register.filter()`` also accepts two keyword arguments, ``is_safe``
and ``needs_autoescape``, described in :ref:`filters and auto-escaping
<filters-auto-escaping>` below.
Template filters that expect strings Template filters that expect strings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -166,6 +170,8 @@ This way, you'll be able to pass, say, an integer to this filter, and it
won't cause an ``AttributeError`` (because integers don't have ``lower()`` won't cause an ``AttributeError`` (because integers don't have ``lower()``
methods). methods).
.. _filters-auto-escaping:
Filters and auto-escaping Filters and auto-escaping
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
@ -206,17 +212,16 @@ Template filter code falls into one of two situations:
1. Your filter does not introduce any HTML-unsafe characters (``<``, ``>``, 1. Your filter does not introduce any HTML-unsafe characters (``<``, ``>``,
``'``, ``"`` or ``&``) into the result that were not already present. In ``'``, ``"`` or ``&``) into the result that were not already present. In
this case, you can let Django take care of all the auto-escaping this case, you can let Django take care of all the auto-escaping
handling for you. All you need to do is put the ``is_safe`` attribute on handling for you. All you need to do is set the ``is_safe`` flag to ``True``
your filter function and set it to ``True``, like so: when you register your filter function, like so:
.. code-block:: python .. code-block:: python
@register.filter @register.filter(is_safe=True)
def myfilter(value): def myfilter(value):
return value return value
myfilter.is_safe = True
This attribute tells Django that if a "safe" string is passed into your This flag tells Django that if a "safe" string is passed into your
filter, the result will still be "safe" and if a non-safe string is filter, the result will still be "safe" and if a non-safe string is
passed in, Django will automatically escape it, if necessary. passed in, Django will automatically escape it, if necessary.
@ -236,17 +241,16 @@ Template filter code falls into one of two situations:
.. code-block:: python .. code-block:: python
@register.filter @register.filter(is_safe=True)
def add_xx(value): def add_xx(value):
return '%sxx' % value return '%sxx' % value
add_xx.is_safe = True
When this filter is used in a template where auto-escaping is enabled, When this filter is used in a template where auto-escaping is enabled,
Django will escape the output whenever the input is not already marked Django will escape the output whenever the input is not already marked
as "safe". as "safe".
By default, ``is_safe`` defaults to ``False``, and you can omit it from By default, ``is_safe`` is ``False``, and you can omit it from any filters
any filters where it isn't required. where it isn't required.
Be careful when deciding if your filter really does leave safe strings Be careful when deciding if your filter really does leave safe strings
as safe. If you're *removing* characters, you might inadvertently leave as safe. If you're *removing* characters, you might inadvertently leave
@ -279,12 +283,12 @@ Template filter code falls into one of two situations:
can operate in templates where auto-escaping is either on or off in can operate in templates where auto-escaping is either on or off in
order to make things easier for your template authors. order to make things easier for your template authors.
In order for your filter to know the current auto-escaping state, set In order for your filter to know the current auto-escaping state, set the
the ``needs_autoescape`` attribute to ``True`` on your function. (If you ``needs_autoescape`` flag to ``True`` when you register your filter function.
don't specify this attribute, it defaults to ``False``). This attribute (If you don't specify this flag, it defaults to ``False``). This flag tells
tells Django that your filter function wants to be passed an extra Django that your filter function wants to be passed an extra keyword
keyword argument, called ``autoescape``, that is ``True`` if argument, called ``autoescape``, that is ``True`` if auto-escaping is in
auto-escaping is in effect and ``False`` otherwise. effect and ``False`` otherwise.
For example, let's write a filter that emphasizes the first character of For example, let's write a filter that emphasizes the first character of
a string: a string:
@ -294,6 +298,7 @@ Template filter code falls into one of two situations:
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@register.filter(needs_autoescape=True)
def initial_letter_filter(text, autoescape=None): def initial_letter_filter(text, autoescape=None):
first, other = text[0], text[1:] first, other = text[0], text[1:]
if autoescape: if autoescape:
@ -302,27 +307,45 @@ Template filter code falls into one of two situations:
esc = lambda x: x esc = lambda x: x
result = '<strong>%s</strong>%s' % (esc(first), esc(other)) result = '<strong>%s</strong>%s' % (esc(first), esc(other))
return mark_safe(result) return mark_safe(result)
initial_letter_filter.needs_autoescape = True
The ``needs_autoescape`` attribute on the filter function and the The ``needs_autoescape`` flag and the ``autoescape`` keyword argument mean
``autoescape`` keyword argument mean that our function will know whether that our function will know whether automatic escaping is in effect when the
automatic escaping is in effect when the filter is called. We use filter is called. We use ``autoescape`` to decide whether the input data
``autoescape`` to decide whether the input data needs to be passed needs to be passed through ``django.utils.html.conditional_escape`` or not.
through ``django.utils.html.conditional_escape`` or not. (In the latter (In the latter case, we just use the identity function as the "escape"
case, we just use the identity function as the "escape" function.) The function.) The ``conditional_escape()`` function is like ``escape()`` except
``conditional_escape()`` function is like ``escape()`` except it only it only escapes input that is **not** a ``SafeData`` instance. If a
escapes input that is **not** a ``SafeData`` instance. If a ``SafeData`` ``SafeData`` instance is passed to ``conditional_escape()``, the data is
instance is passed to ``conditional_escape()``, the data is returned returned unchanged.
unchanged.
Finally, in the above example, we remember to mark the result as safe Finally, in the above example, we remember to mark the result as safe
so that our HTML is inserted directly into the template without further so that our HTML is inserted directly into the template without further
escaping. escaping.
There's no need to worry about the ``is_safe`` attribute in this case There's no need to worry about the ``is_safe`` flag in this case
(although including it wouldn't hurt anything). Whenever you manually (although including it wouldn't hurt anything). Whenever you manually
handle the auto-escaping issues and return a safe string, the handle the auto-escaping issues and return a safe string, the
``is_safe`` attribute won't change anything either way. ``is_safe`` flag won't change anything either way.
.. versionchanged:: 1.4
``is_safe`` and ``needs_autoescape`` used to be attributes of the filter
function; this syntax is deprecated.
.. code-block:: python
@register.filter
def myfilter(value):
return value
myfilter.is_safe = True
.. code-block:: python
@register.filter
def initial_letter_filter(text, autoescape=None):
# ...
return mark_safe(result)
initial_letter_filter.needs_autoescape = True
Writing custom template tags Writing custom template tags
---------------------------- ----------------------------

View File

@ -251,6 +251,9 @@ these changes.
:mod:`django.core.management`. This also means that the old (pre-1.4) :mod:`django.core.management`. This also means that the old (pre-1.4)
style of :file:`manage.py` file will no longer work. style of :file:`manage.py` file will no longer work.
* Setting the ``is_safe`` and ``needs_autoescape`` flags as attributes of
template filter functions will no longer be supported.
2.0 2.0
--- ---

View File

@ -6,11 +6,10 @@ from django.template.loader import get_template
register = template.Library() register = template.Library()
@register.filter
@stringfilter
def trim(value, num): def trim(value, num):
return value[:num] return value[:num]
trim = stringfilter(trim)
register.filter(trim)
@register.simple_tag @register.simple_tag
def no_params(): def no_params():