"""Default variable filters.""" import re import random as random_module import unicodedata from decimal import Decimal, InvalidOperation, ROUND_HALF_UP from functools import wraps from pprint import pformat from django.template.base import Variable, Library from django.conf import settings from django.utils import formats from django.utils.dateformat import format, time_format from django.utils.encoding import force_unicode, iri_to_uri from django.utils.html import (conditional_escape, escapejs, fix_ampersands, escape, urlize as urlize_impl, linebreaks, strip_tags) from django.utils.http import urlquote from django.utils.text import Truncator, wrap, phone2numeric from django.utils.safestring import mark_safe, SafeData, mark_for_escaping from django.utils.timesince import timesince, timeuntil from django.utils.translation import ugettext, ungettext from django.utils.text import normalize_newlines register = Library() ####################### # STRING DECORATOR # ####################### def stringfilter(func): """ Decorator for filters which should only receive unicode objects. The object passed as the first positional argument will be converted to a unicode object. """ def _dec(*args, **kwargs): if args: args = list(args) args[0] = force_unicode(args[0]) if isinstance(args[0], SafeData) and getattr(func, '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). _dec._decorated_function = getattr(func, '_decorated_function', func) for attr in ('is_safe', 'needs_autoescape'): if hasattr(func, attr): setattr(_dec, attr, getattr(func, attr)) return wraps(func)(_dec) ################### # STRINGS # ################### def addslashes(value): """ Adds slashes before quotes. Useful for escaping strings in CSV, for example. Less useful for escaping JavaScript; use the ``escapejs`` filter instead. """ return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") addslashes.is_safe = True addslashes = stringfilter(addslashes) def capfirst(value): """Capitalizes the first character of the value.""" return value and value[0].upper() + value[1:] capfirst.is_safe=True capfirst = stringfilter(capfirst) @register.filter("escapejs") @stringfilter def escapejs_filter(value): """Hex encodes characters for use in JavaScript strings.""" return escapejs(value) @register.filter("fix_ampersands") @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 # ones) are not recognized by Decimal but we want to return them unchanged vs. # returning an empty string as we do for completley invalid input. Note these # need to be built up from values that are not inf/nan, since inf/nan values do # not reload properly from .pyc files on Windows prior to some level of Python 2.5 # (see Python Issue757815 and Issue1080440). pos_inf = 1e200 * 1e200 neg_inf = -1e200 * 1e200 nan = (1e200 * 1e200) / (1e200 * 1e200) special_floats = [str(pos_inf), str(neg_inf), str(nan)] def floatformat(text, arg=-1): """ Displays a float to a specified number of decimal places. If called without an argument, it displays the floating point number with one decimal place -- but only if there's a decimal place to be displayed: * num1 = 34.23234 * num2 = 34.00000 * num3 = 34.26000 * {{ num1|floatformat }} displays "34.2" * {{ num2|floatformat }} displays "34" * {{ num3|floatformat }} displays "34.3" If arg is positive, it will always display exactly arg number of decimal places: * {{ num1|floatformat:3 }} displays "34.232" * {{ num2|floatformat:3 }} displays "34.000" * {{ num3|floatformat:3 }} displays "34.260" If arg is negative, it will display arg number of decimal places -- but only if there are places to be displayed: * {{ num1|floatformat:"-3" }} displays "34.232" * {{ num2|floatformat:"-3" }} displays "34" * {{ num3|floatformat:"-3" }} displays "34.260" If the input float is infinity or NaN, the (platform-dependent) string representation of that value will be displayed. """ try: input_val = force_unicode(text) d = Decimal(input_val) except UnicodeEncodeError: return u'' except InvalidOperation: if input_val in special_floats: return input_val try: d = Decimal(force_unicode(float(text))) except (ValueError, InvalidOperation, TypeError, UnicodeEncodeError): return u'' try: p = int(arg) except ValueError: return input_val try: m = int(d) - d except (ValueError, OverflowError, InvalidOperation): return input_val if not m and p < 0: return mark_safe(formats.number_format(u'%d' % (int(d)), 0)) if p == 0: exp = Decimal(1) else: exp = Decimal(u'1.0') / (Decimal(10) ** abs(p)) try: # Avoid conversion to scientific notation by accessing `sign`, `digits` # and `exponent` from `Decimal.as_tuple()` directly. sign, digits, exponent = d.quantize(exp, ROUND_HALF_UP).as_tuple() digits = [unicode(digit) for digit in reversed(digits)] while len(digits) <= abs(exponent): digits.append(u'0') digits.insert(-exponent, u'.') if sign: digits.append(u'-') number = u''.join(reversed(digits)) return mark_safe(formats.number_format(number, abs(p))) except InvalidOperation: return input_val floatformat.is_safe = True def iriencode(value): """Escapes an IRI value for use in a URL.""" return force_unicode(iri_to_uri(value)) iriencode.is_safe = True iriencode = stringfilter(iriencode) def linenumbers(value, autoescape=None): """Displays text with line numbers.""" lines = value.split(u'\n') # Find the maximum width of the line count, for use with zero padding # string format command width = unicode(len(unicode(len(lines)))) if not autoescape or isinstance(value, SafeData): for i, line in enumerate(lines): lines[i] = (u"%0" + width + u"d. %s") % (i + 1, line) else: 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 linenumbers = stringfilter(linenumbers) def lower(value): """Converts a string into all lowercase.""" return value.lower() lower.is_safe = True 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(value) make_list.is_safe = False make_list = stringfilter(make_list) def slugify(value): """ Normalizes string, converts to lowercase, removes non-alpha characters, and converts spaces to hyphens. """ 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 slugify = stringfilter(slugify) def stringformat(value, arg): """ Formats the variable according to the arg, a string formatting specifier. This specifier uses Python string formating syntax, with the exception that the leading "%" is dropped. See http://docs.python.org/lib/typesseq-strings.html for documentation of Python string formatting """ try: return (u"%" + unicode(arg)) % value except (ValueError, TypeError): return u"" stringformat.is_safe = True 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 title = stringfilter(title) def truncatechars(value, arg): """ Truncates a string after a certain number of characters. Argument: Number of characters to truncate after. """ try: length = int(arg) except ValueError: # Invalid literal for int(). return value # Fail silently. return Truncator(value).chars(length) truncatechars.is_safe = True truncatechars = stringfilter(truncatechars) def truncatewords(value, arg): """ Truncates a string after a certain number of words. Argument: Number of words to truncate after. Newlines within the string are removed. """ try: length = int(arg) except ValueError: # Invalid literal for int(). return value # Fail silently. return Truncator(value).words(length, truncate=' ...') truncatewords.is_safe = True truncatewords = stringfilter(truncatewords) def truncatewords_html(value, arg): """ Truncates HTML after a certain number of words. Argument: Number of words to truncate after. Newlines in the HTML are preserved. """ try: length = int(arg) except ValueError: # invalid literal for int() return value # Fail silently. return Truncator(value).words(length, html=True, truncate=' ...') truncatewords_html.is_safe = True truncatewords_html = stringfilter(truncatewords_html) def upper(value): """Converts a string into all uppercase.""" return value.upper() upper.is_safe = False upper = stringfilter(upper) def urlencode(value, safe=None): """ Escapes a value for use in a URL. Takes an optional ``safe`` parameter used to determine the characters which should not be escaped by Django's ``urlquote`` method. If not provided, the default safe characters will be used (but an empty string can be provided when *all* characters should be escaped). """ kwargs = {} if safe is not None: kwargs['safe'] = safe return urlquote(value, **kwargs) urlencode.is_safe = False urlencode = stringfilter(urlencode) @register.filter @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 def urlizetrunc(value, limit, autoescape=None): """ Converts URLs into clickable links, truncating URLs to the given character limit, and adding 'rel=nofollow' attribute to discourage spamming. Argument: Length to truncate URLs to. """ return mark_safe(urlize_impl(value, trim_url_limit=int(limit), nofollow=True, autoescape=autoescape)) urlizetrunc.is_safe = True urlizetrunc.needs_autoescape = True urlizetrunc = stringfilter(urlizetrunc) def wordcount(value): """Returns the number of words.""" return len(value.split()) wordcount.is_safe = False wordcount = stringfilter(wordcount) def wordwrap(value, arg): """ Wraps words at specified line length. Argument: number of characters to wrap the text at. """ return wrap(value, int(arg)) wordwrap.is_safe = True wordwrap = stringfilter(wordwrap) def ljust(value, arg): """ Left-aligns the value in a field of a given width. Argument: field size. """ return value.ljust(int(arg)) ljust.is_safe = True ljust = stringfilter(ljust) def rjust(value, arg): """ Right-aligns the value in a field of a given width. Argument: field size. """ return value.rjust(int(arg)) rjust.is_safe = True rjust = stringfilter(rjust) def center(value, arg): """Centers the value in a field of a given width.""" return value.center(int(arg)) center.is_safe = True center = stringfilter(center) def cut(value, arg): """ Removes all values of arg from the given string. """ safe = isinstance(value, SafeData) value = value.replace(arg, u'') if safe and arg != ';': return mark_safe(value) return value cut = stringfilter(cut) ################### # HTML STRINGS # ################### @register.filter("escape") @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 def force_escape(value): """ Escapes a string's HTML. This returns a new string containing the escaped characters (as opposed to "escape", which marks the content for later possible escaping). """ return mark_safe(escape(value)) force_escape = stringfilter(force_escape) force_escape.is_safe = True @register.filter("linebreaks") @stringfilter def linebreaks_filter(value, autoescape=None): """ Replaces line breaks in plain text with appropriate HTML; a single newline becomes an HTML line break (``
``) and a new line followed by a blank line becomes a paragraph break (``

``). """ autoescape = autoescape and not isinstance(value, SafeData) return mark_safe(linebreaks(value, autoescape)) linebreaks_filter.is_safe = True linebreaks_filter.needs_autoescape = True linebreaks = stringfilter(linebreaks) def linebreaksbr(value, autoescape=None): """ Converts all newlines in a piece of plain text to HTML line breaks (``
``). """ autoescape = autoescape and not isinstance(value, SafeData) value = normalize_newlines(value) if autoescape: value = escape(value) return mark_safe(value.replace('\n', '
')) linebreaksbr.is_safe = True linebreaksbr.needs_autoescape = True linebreaksbr = stringfilter(linebreaksbr) def safe(value): """ Marks the value as a string that should not be auto-escaped. """ return mark_safe(value) safe.is_safe = True safe = stringfilter(safe) def safeseq(value): """ A "safe" filter for sequences. Marks each element in the sequence, individually, as safe, after converting them to unicode. Returns a list with the results. """ return [mark_safe(force_unicode(obj)) for obj in value] safeseq.is_safe = True def removetags(value, tags): """Removes a space separated list of [X]HTML tags from the output.""" tags = [re.escape(tag) for tag in tags.split()] tags_re = u'(%s)' % u'|'.join(tags) starttag_re = re.compile(ur'<%s(/?>|(\s+[^>]*>))' % tags_re, re.U) endtag_re = re.compile(u'' % tags_re) value = starttag_re.sub(u'', value) value = endtag_re.sub(u'', value) return value removetags.is_safe = True removetags = stringfilter(removetags) def striptags(value): """Strips all [X]HTML tags.""" return strip_tags(value) striptags.is_safe = True striptags = stringfilter(striptags) ################### # LISTS # ################### 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 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 def first(value): """Returns the first item in a list.""" try: return value[0] except IndexError: return u'' first.is_safe = False def join(value, arg, autoescape=None): """ Joins a list with a string, like Python's ``str.join(list)``. """ value = map(force_unicode, value) if autoescape: value = [conditional_escape(v) for v in value] try: data = conditional_escape(arg).join(value) except AttributeError: # fail silently but nicely return value return mark_safe(data) join.is_safe = True join.needs_autoescape = True def last(value): "Returns the last item in a list" try: return value[-1] except IndexError: return u'' last.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 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 def random(value): """Returns a random item from the list.""" return random_module.choice(value) random.is_safe = True def slice_(value, arg): """ Returns a slice of the list. Uses the same syntax as Python's list slicing; see http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice for an introduction. """ try: bits = [] for x in arg.split(u':'): if len(x) == 0: bits.append(None) else: bits.append(int(x)) return value[slice(*bits)] except (ValueError, TypeError): return value # Fail silently. slice_.is_safe = True def unordered_list(value, autoescape=None): """ Recursively takes a self-nested list and returns an HTML unordered list -- WITHOUT opening and closing