From d17bc72880caea967ee1f4332941269837b68a2a Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 30 Oct 2011 07:32:21 +0000 Subject: [PATCH] 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 --- .../contrib/humanize/templatetags/humanize.py | 12 +- django/contrib/markup/templatetags/markup.py | 10 +- django/template/base.py | 29 ++- django/template/defaultfilters.py | 186 +++++++----------- django/templatetags/l10n.py | 6 +- docs/howto/custom-template-tags.txt | 79 +++++--- docs/internals/deprecation.txt | 3 + .../templates/templatetags/custom.py | 7 +- 8 files changed, 154 insertions(+), 178 deletions(-) diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py index 7b83d9237e..4ed07125c0 100644 --- a/django/contrib/humanize/templatetags/humanize.py +++ b/django/contrib/humanize/templatetags/humanize.py @@ -11,7 +11,7 @@ from django.utils.tzinfo import LocalTimezone register = template.Library() -@register.filter +@register.filter(is_safe=True) def ordinal(value): """ 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 return u"%d%s" % (value, suffixes[0]) 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): """ Converts an integer to a string containing commas every three digits. @@ -47,7 +46,6 @@ def intcomma(value, use_l10n=True): return new else: return intcomma(new, use_l10n) -intcomma.is_safe = True # A tuple of standard large number to their converters intword_converters = ( @@ -97,7 +95,7 @@ intword_converters = ( )), ) -@register.filter +@register.filter(is_safe=False) def intword(value): """ Converts a large integer to a friendly text representation. Works best @@ -129,9 +127,8 @@ def intword(value): new_value = value / float(large_number) return _check_for_i18n(new_value, *converters(new_value)) return value -intword.is_safe = False -@register.filter +@register.filter(is_safe=True) def apnumber(value): """ For numbers 1-9, returns the number spelled out. Otherwise, returns the @@ -144,7 +141,6 @@ def apnumber(value): if not 0 < value < 10: return value return (_('one'), _('two'), _('three'), _('four'), _('five'), _('six'), _('seven'), _('eight'), _('nine'))[value-1] -apnumber.is_safe = True @register.filter def naturalday(value, arg=None): diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py index 74b5b9efcc..968277284a 100644 --- a/django/contrib/markup/templatetags/markup.py +++ b/django/contrib/markup/templatetags/markup.py @@ -18,7 +18,7 @@ from django.utils.safestring import mark_safe register = template.Library() -@register.filter +@register.filter(is_safe=True) def textile(value): try: import textile @@ -28,9 +28,8 @@ def textile(value): return force_unicode(value) else: 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=''): """ 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)) else: return mark_safe(force_unicode(markdown.markdown(smart_str(value)))) -markdown.is_safe = True -@register.filter +@register.filter(is_safe=True) def restructuredtext(value): try: from docutils.core import publish_parts @@ -87,5 +85,3 @@ def restructuredtext(value): docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {}) parts = publish_parts(source=smart_str(value), writer_name="html4css1", settings_overrides=docutils_settings) return mark_safe(force_unicode(parts["fragment"])) -restructuredtext.is_safe = True - diff --git a/django/template/base.py b/django/template/base.py index 0fafb2d182..238e2db9ef 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -1057,30 +1057,43 @@ class Library(object): self.tags[getattr(func, "_decorated_function", func).__name__] = 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: # @register.filter() - return self.filter_function - elif filter_func is None: + def dec(func): + return self.filter_function(func, **flags) + return dec + + elif name is not None and filter_func is None: if callable(name): # @register.filter - return self.filter_function(name) + return self.filter_function(name, **flags) else: # @register.filter('somename') or @register.filter(name='somename') def dec(func): - return self.filter(name, func) + return self.filter(name, func, **flags) return dec + elif name is not None and filter_func is not None: # register.filter('somename', somefunc) 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 else: raise InvalidTemplateLibrary("Unsupported arguments to " "Library.filter: (%r, %r)", (name, filter_func)) - def filter_function(self, func): - self.filters[getattr(func, "_decorated_function", func).__name__] = func - return func + def filter_function(self, func, **flags): + name = getattr(func, "_decorated_function", func).__name__ + return self.filter(name, func, **flags) def simple_tag(self, func=None, takes_context=None, name=None): def dec(func): diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 5301c7cc86..7484c7bf28 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -37,23 +37,33 @@ def stringfilter(func): if args: args = list(args) 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 func(*args, **kwargs) # 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) + for attr in ('is_safe', 'needs_autoescape'): 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)) + return wraps(func)(_dec) + ################### # STRINGS # ################### -@register.filter +@register.filter(is_safe=True) @stringfilter def addslashes(value): """ @@ -62,14 +72,12 @@ def addslashes(value): filter instead. """ return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") -addslashes.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def capfirst(value): """Capitalizes the first character of the value.""" return value and value[0].upper() + value[1:] -capfirst.is_safe = True @register.filter("escapejs") @stringfilter @@ -77,12 +85,11 @@ def escapejs_filter(value): """Hex encodes characters for use in JavaScript strings.""" return escapejs(value) -@register.filter("fix_ampersands") +@register.filter("fix_ampersands", is_safe=True) @stringfilter def fix_ampersands_filter(value): """Replaces ampersands with ``&`` entities.""" return fix_ampersands(value) -fix_ampersands_filter.is_safe = True # Values for testing floatformat input against infinity and NaN representations, # 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) special_floats = [str(pos_inf), str(neg_inf), str(nan)] -@register.filter +@register.filter(is_safe=True) def floatformat(text, arg=-1): """ 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))) except InvalidOperation: return input_val -floatformat.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def iriencode(value): """Escapes an IRI value for use in a URL.""" return force_unicode(iri_to_uri(value)) -iriencode.is_safe = True -@register.filter +@register.filter(is_safe=True, needs_autoescape=True) @stringfilter def linenumbers(value, autoescape=None): """Displays text with line numbers.""" @@ -196,17 +201,14 @@ def linenumbers(value, autoescape=None): for i, line in enumerate(lines): lines[i] = (u"%0" + width + u"d. %s") % (i + 1, escape(line)) return mark_safe(u'\n'.join(lines)) -linenumbers.is_safe = True -linenumbers.needs_autoescape = True -@register.filter +@register.filter(is_safe=True) @stringfilter def lower(value): """Converts a string into all lowercase.""" return value.lower() -lower.is_safe = True -@register.filter +@register.filter(is_safe=False) @stringfilter def make_list(value): """ @@ -216,9 +218,8 @@ def make_list(value): For a string, it's a list of characters. """ return list(value) -make_list.is_safe = False -@register.filter +@register.filter(is_safe=True) @stringfilter def slugify(value): """ @@ -228,9 +229,8 @@ def slugify(value): value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') value = unicode(re.sub('[^\w\s-]', '', value).strip().lower()) return mark_safe(re.sub('[-\s]+', '-', value)) -slugify.is_safe = True -@register.filter +@register.filter(is_safe=True) def stringformat(value, arg): """ Formats the variable according to the arg, a string formatting specifier. @@ -245,17 +245,15 @@ def stringformat(value, arg): return (u"%" + unicode(arg)) % value except (ValueError, TypeError): return u"" -stringformat.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def title(value): """Converts a string into titlecase.""" 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) -title.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def truncatechars(value, arg): """ @@ -268,9 +266,8 @@ def truncatechars(value, arg): except ValueError: # Invalid literal for int(). return value # Fail silently. return Truncator(value).chars(length) -truncatechars.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def truncatewords(value, arg): """ @@ -285,9 +282,8 @@ def truncatewords(value, arg): except ValueError: # Invalid literal for int(). return value # Fail silently. return Truncator(value).words(length, truncate=' ...') -truncatewords.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def truncatewords_html(value, arg): """ @@ -302,16 +298,14 @@ def truncatewords_html(value, arg): except ValueError: # invalid literal for int() return value # Fail silently. return Truncator(value).words(length, html=True, truncate=' ...') -truncatewords_html.is_safe = True -@register.filter +@register.filter(is_safe=False) @stringfilter def upper(value): """Converts a string into all uppercase.""" return value.upper() -upper.is_safe = False -@register.filter +@register.filter(is_safe=False) @stringfilter def urlencode(value, safe=None): """ @@ -326,17 +320,14 @@ def urlencode(value, safe=None): if safe is not None: kwargs['safe'] = safe return urlquote(value, **kwargs) -urlencode.is_safe = False -@register.filter +@register.filter(is_safe=True, needs_autoescape=True) @stringfilter def urlize(value, autoescape=None): """Converts URLs in plain text into clickable links.""" 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 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, autoescape=autoescape)) -urlizetrunc.is_safe = True -urlizetrunc.needs_autoescape = True -@register.filter +@register.filter(is_safe=False) @stringfilter def wordcount(value): """Returns the number of words.""" return len(value.split()) -wordcount.is_safe = False -@register.filter +@register.filter(is_safe=True) @stringfilter def wordwrap(value, arg): """ @@ -366,9 +354,8 @@ def wordwrap(value, arg): Argument: number of characters to wrap the text at. """ return wrap(value, int(arg)) -wordwrap.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def ljust(value, arg): """ @@ -377,9 +364,8 @@ def ljust(value, arg): Argument: field size. """ return value.ljust(int(arg)) -ljust.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def rjust(value, arg): """ @@ -388,14 +374,12 @@ def rjust(value, arg): Argument: field size. """ return value.rjust(int(arg)) -rjust.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def center(value, arg): """Centers the value in a field of a given width.""" return value.center(int(arg)) -center.is_safe = True @register.filter @stringfilter @@ -413,16 +397,15 @@ def cut(value, arg): # HTML STRINGS # ################### -@register.filter("escape") +@register.filter("escape", is_safe=True) @stringfilter def escape_filter(value): """ Marks the value as a string that should not be auto-escaped. """ return mark_for_escaping(value) -escape_filter.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def force_escape(value): """ @@ -431,9 +414,8 @@ def force_escape(value): possible escaping). """ return mark_safe(escape(value)) -force_escape.is_safe = True -@register.filter("linebreaks") +@register.filter("linebreaks", is_safe=True, needs_autoescape=True) @stringfilter def linebreaks_filter(value, autoescape=None): """ @@ -443,10 +425,8 @@ def linebreaks_filter(value, autoescape=None): """ autoescape = autoescape and not isinstance(value, SafeData) 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 def linebreaksbr(value, autoescape=None): """ @@ -458,19 +438,16 @@ def linebreaksbr(value, autoescape=None): if autoescape: value = escape(value) return mark_safe(value.replace('\n', '
')) -linebreaksbr.is_safe = True -linebreaksbr.needs_autoescape = True -@register.filter +@register.filter(is_safe=True) @stringfilter def safe(value): """ Marks the value as a string that should not be auto-escaped. """ return mark_safe(value) -safe.is_safe = True -@register.filter +@register.filter(is_safe=True) def safeseq(value): """ A "safe" filter for sequences. Marks each element in the sequence, @@ -478,9 +455,8 @@ def safeseq(value): with the results. """ return [mark_safe(force_unicode(obj)) for obj in value] -safeseq.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def removetags(value, tags): """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 = endtag_re.sub(u'', value) return value -removetags.is_safe = True -@register.filter +@register.filter(is_safe=True) @stringfilter def striptags(value): """Strips all [X]HTML tags.""" return strip_tags(value) -striptags.is_safe = True ################### # LISTS # ################### -@register.filter +@register.filter(is_safe=False) def dictsort(value, arg): """ Takes a list of dicts, returns that list sorted by the property given in the argument. """ return sorted(value, key=Variable(arg).resolve) -dictsort.is_safe = False -@register.filter +@register.filter(is_safe=False) def dictsortreversed(value, arg): """ Takes a list of dicts, returns that list sorted in reverse order by the property given in the argument. """ return sorted(value, key=Variable(arg).resolve, reverse=True) -dictsortreversed.is_safe = False -@register.filter +@register.filter(is_safe=False) def first(value): """Returns the first item in a list.""" try: return value[0] except IndexError: return u'' -first.is_safe = False -@register.filter +@register.filter(is_safe=True, needs_autoescape=True) def join(value, arg, autoescape=None): """ 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 return value return mark_safe(data) -join.is_safe = True -join.needs_autoescape = True -@register.filter +@register.filter(is_safe=True) def last(value): "Returns the last item in a list" try: return value[-1] except IndexError: return u'' -last.is_safe = True -@register.filter +@register.filter(is_safe=True) def length(value): """Returns the length of the value - useful for lists.""" try: return len(value) except (ValueError, TypeError): return '' -length.is_safe = True -@register.filter +@register.filter(is_safe=False) def length_is(value, arg): """Returns a boolean of whether the value's length is the argument.""" try: return len(value) == int(arg) except (ValueError, TypeError): return '' -length_is.is_safe = False -@register.filter +@register.filter(is_safe=True) def random(value): """Returns a random item from the list.""" return random_module.choice(value) -random.is_safe = True -@register.filter("slice") +@register.filter("slice", is_safe=True) def slice_filter(value, arg): """ Returns a slice of the list. @@ -600,9 +565,8 @@ def slice_filter(value, arg): except (ValueError, TypeError): 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): """ 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) value, converted = convert_old_style_list(value) return mark_safe(_helper(value)) -unordered_list.is_safe = True -unordered_list.needs_autoescape = True ################### # INTEGERS # ################### -@register.filter +@register.filter(is_safe=False) def add(value, arg): """Adds the arg to the value.""" try: @@ -705,9 +667,8 @@ def add(value, arg): return value + arg except Exception: return '' -add.is_safe = False -@register.filter +@register.filter(is_safe=False) def get_digit(value, arg): """ 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]) except IndexError: return 0 -get_digit.is_safe = False ################### # DATES # ################### -@register.filter +@register.filter(is_safe=False) def date(value, arg=None): """Formats a date according to the given format.""" if not value: @@ -746,9 +706,8 @@ def date(value, arg=None): return format(value, arg) except AttributeError: return '' -date.is_safe = False -@register.filter +@register.filter(is_safe=False) def time(value, arg=None): """Formats a time according to the given format.""" if value in (None, u''): @@ -762,9 +721,8 @@ def time(value, arg=None): return time_format(value, arg) except AttributeError: return '' -time.is_safe = False -@register.filter("timesince") +@register.filter("timesince", is_safe=False) def timesince_filter(value, arg=None): """Formats a date as the time since that date (i.e. "4 days, 6 hours").""" if not value: @@ -775,9 +733,8 @@ def timesince_filter(value, arg=None): return timesince(value) except (ValueError, TypeError): return u'' -timesince_filter.is_safe = False -@register.filter("timeuntil") +@register.filter("timeuntil", is_safe=False) def timeuntil_filter(value, arg=None): """Formats a date as the time until that date (i.e. "4 days, 6 hours").""" if not value: @@ -786,33 +743,29 @@ def timeuntil_filter(value, arg=None): return timeuntil(value, arg) except (ValueError, TypeError): return u'' -timeuntil_filter.is_safe = False ################### # LOGIC # ################### -@register.filter +@register.filter(is_safe=False) def default(value, arg): """If value is unavailable, use given default.""" return value or arg -default.is_safe = False -@register.filter +@register.filter(is_safe=False) def default_if_none(value, arg): """If value is None, use given default.""" if value is None: return arg return value -default_if_none.is_safe = False -@register.filter +@register.filter(is_safe=False) def divisibleby(value, arg): """Returns True if the value is devisible by the argument.""" return int(value) % int(arg) == 0 -divisibleby.is_safe = False -@register.filter +@register.filter(is_safe=False) def yesno(value, arg=None): """ Given a string mapping values for true, false and (optionally) None, @@ -843,13 +796,12 @@ def yesno(value, arg=None): if value: return yes return no -yesno.is_safe = False ################### # MISC # ################### -@register.filter +@register.filter(is_safe=True) def filesizeformat(bytes): """ 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: 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)) -filesizeformat.is_safe = True -@register.filter +@register.filter(is_safe=False) def pluralize(value, arg=u's'): """ 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. pass return singular_suffix -pluralize.is_safe = False -@register.filter("phone2numeric") +@register.filter("phone2numeric", is_safe=True) def phone2numeric_filter(value): """Takes a phone number and converts it in to its numerical equivalent.""" return phone2numeric(value) -phone2numeric_filter.is_safe = True -@register.filter +@register.filter(is_safe=True) def pprint(value): """A wrapper around pprint.pprint -- for debugging, really.""" try: return pformat(value) except Exception, e: return u"Error in formatting: %s" % force_unicode(e, errors="replace") -pprint.is_safe = True diff --git a/django/templatetags/l10n.py b/django/templatetags/l10n.py index 32651d2cb8..1eac1de071 100644 --- a/django/templatetags/l10n.py +++ b/django/templatetags/l10n.py @@ -5,23 +5,21 @@ from django.utils.encoding import force_unicode register = Library() -@register.filter +@register.filter(is_safe=False) def localize(value): """ Forces a value to be rendered as a localized value, regardless of the value of ``settings.USE_L10N``. """ return force_unicode(formats.localize(value, use_l10n=True)) -localize.is_safe = False -@register.filter +@register.filter(is_safe=False) def unlocalize(value): """ Forces a value to be rendered as a non-localized value, regardless of the value of ``settings.USE_L10N``. """ return force_unicode(value) -unlocalize.is_safe = False class LocalizeNode(Node): def __init__(self, nodelist, use_l10n): diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 581d69c631..6b2355e8d0 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -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 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 +` below. + 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()`` methods). +.. _filters-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 (``<``, ``>``, ``'``, ``"`` or ``&``) into the result that were not already present. In 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 - your filter function and set it to ``True``, like so: + handling for you. All you need to do is set the ``is_safe`` flag to ``True`` + when you register your filter function, like so: .. code-block:: python - @register.filter + @register.filter(is_safe=True) def myfilter(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 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 - @register.filter + @register.filter(is_safe=True) def add_xx(value): return '%sxx' % value - add_xx.is_safe = True 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 as "safe". - By default, ``is_safe`` defaults to ``False``, and you can omit it from - any filters where it isn't required. + By default, ``is_safe`` is ``False``, and you can omit it from any filters + where it isn't required. Be careful when deciding if your filter really does leave safe strings 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 order to make things easier for your template authors. - In order for your filter to know the current auto-escaping state, set - the ``needs_autoescape`` attribute to ``True`` on your function. (If you - don't specify this attribute, it defaults to ``False``). This attribute - tells Django that your filter function wants to be passed an extra - keyword argument, called ``autoescape``, that is ``True`` if - auto-escaping is in effect and ``False`` otherwise. + In order for your filter to know the current auto-escaping state, set the + ``needs_autoescape`` flag to ``True`` when you register your filter function. + (If you don't specify this flag, it defaults to ``False``). This flag tells + Django that your filter function wants to be passed an extra keyword + argument, called ``autoescape``, that is ``True`` if auto-escaping is in + effect and ``False`` otherwise. For example, let's write a filter that emphasizes the first character of 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.safestring import mark_safe + @register.filter(needs_autoescape=True) def initial_letter_filter(text, autoescape=None): first, other = text[0], text[1:] if autoescape: @@ -302,27 +307,45 @@ Template filter code falls into one of two situations: esc = lambda x: x result = '%s%s' % (esc(first), esc(other)) return mark_safe(result) - initial_letter_filter.needs_autoescape = True - The ``needs_autoescape`` attribute on the filter function and the - ``autoescape`` keyword argument mean that our function will know whether - automatic escaping is in effect when the filter is called. We use - ``autoescape`` to decide whether the input data needs to be passed - through ``django.utils.html.conditional_escape`` or not. (In the latter - case, we just use the identity function as the "escape" function.) The - ``conditional_escape()`` function is like ``escape()`` except it only - escapes input that is **not** a ``SafeData`` instance. If a ``SafeData`` - instance is passed to ``conditional_escape()``, the data is returned - unchanged. + The ``needs_autoescape`` flag and the ``autoescape`` keyword argument mean + that our function will know whether automatic escaping is in effect when the + filter is called. We use ``autoescape`` to decide whether the input data + needs to be passed through ``django.utils.html.conditional_escape`` or not. + (In the latter case, we just use the identity function as the "escape" + function.) The ``conditional_escape()`` function is like ``escape()`` except + it only escapes input that is **not** a ``SafeData`` instance. If a + ``SafeData`` instance is passed to ``conditional_escape()``, the data is + returned unchanged. 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 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 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 ---------------------------- diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index f50b0423e5..603f20dbab 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -251,6 +251,9 @@ these changes. :mod:`django.core.management`. This also means that the old (pre-1.4) 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 --- diff --git a/tests/regressiontests/templates/templatetags/custom.py b/tests/regressiontests/templates/templatetags/custom.py index 86206617b8..7f788311c6 100644 --- a/tests/regressiontests/templates/templatetags/custom.py +++ b/tests/regressiontests/templates/templatetags/custom.py @@ -6,11 +6,10 @@ from django.template.loader import get_template register = template.Library() +@register.filter +@stringfilter def trim(value, num): return value[:num] -trim = stringfilter(trim) - -register.filter(trim) @register.simple_tag def no_params(): @@ -303,4 +302,4 @@ assignment_unlimited_args_kwargs.anything = "Expected assignment_unlimited_args_ def assignment_tag_without_context_parameter(arg): """Expected assignment_tag_without_context_parameter __doc__""" return "Expected result" -assignment_tag_without_context_parameter.anything = "Expected assignment_tag_without_context_parameter __dict__" \ No newline at end of file +assignment_tag_without_context_parameter.anything = "Expected assignment_tag_without_context_parameter __dict__"