"""Default variable filters.""" import random as random_module import re from contextlib import suppress from decimal import ROUND_HALF_UP, Context, Decimal, InvalidOperation from functools import wraps from operator import itemgetter from pprint import pformat from urllib.parse import quote from django.utils import formats from django.utils.dateformat import format, time_format from django.utils.encoding import iri_to_uri from django.utils.html import ( avoid_wrapping, conditional_escape, escape, escapejs, linebreaks, strip_tags, urlize as _urlize, ) from django.utils.safestring import SafeData, mark_safe from django.utils.text import ( Truncator, normalize_newlines, phone2numeric, slugify as _slugify, wrap, ) from django.utils.timesince import timesince, timeuntil from django.utils.translation import gettext, ngettext from .base import Variable, VariableDoesNotExist from .library import Library register = Library() ####################### # STRING DECORATOR # ####################### 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] = str(args[0]) 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, and to bear the 'is_safe' attribute # when multiple decorators are applied). _dec._decorated_function = getattr(func, '_decorated_function', func) return wraps(func)(_dec) ################### # STRINGS # ################### @register.filter(is_safe=True) @stringfilter def addslashes(value): """ Add 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("'", "\\'") @register.filter(is_safe=True) @stringfilter def capfirst(value): """Capitalize the first character of the value.""" return value and value[0].upper() + value[1:] @register.filter("escapejs") @stringfilter def escapejs_filter(value): """Hex encode characters for use in JavaScript strings.""" return escapejs(value) @register.filter(is_safe=True) def floatformat(text, arg=-1): """ Display a float to a specified number of decimal places. If called without an argument, display 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, 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, 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, display the string representation of that value. """ try: input_val = repr(text) d = Decimal(input_val) except InvalidOperation: try: d = Decimal(str(float(text))) except (ValueError, InvalidOperation, TypeError): return '' 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('%d' % (int(d)), 0)) if p == 0: exp = Decimal(1) else: exp = Decimal('1.0') / (Decimal(10) ** abs(p)) # Set the precision high enough to avoid an exception (#15789). tupl = d.as_tuple() units = len(tupl[1]) units += -tupl[2] if m else tupl[2] prec = abs(p) + units + 1 # 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, Context(prec=prec)).as_tuple() digits = [str(digit) for digit in reversed(digits)] while len(digits) <= abs(exponent): digits.append('0') digits.insert(-exponent, '.') if sign: digits.append('-') number = ''.join(reversed(digits)) return mark_safe(formats.number_format(number, abs(p))) @register.filter(is_safe=True) @stringfilter def iriencode(value): """Escape an IRI value for use in a URL.""" return iri_to_uri(value) @register.filter(is_safe=True, needs_autoescape=True) @stringfilter def linenumbers(value, autoescape=True): """Display text with line numbers.""" lines = value.split('\n') # Find the maximum width of the line count, for use with zero padding # string format command width = str(len(str(len(lines)))) if not autoescape or isinstance(value, SafeData): for i, line in enumerate(lines): lines[i] = ("%0" + width + "d. %s") % (i + 1, line) else: for i, line in enumerate(lines): lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line)) return mark_safe('\n'.join(lines)) @register.filter(is_safe=True) @stringfilter def lower(value): """Convert a string into all lowercase.""" return value.lower() @register.filter(is_safe=False) @stringfilter def make_list(value): """ Return 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) @register.filter(is_safe=True) @stringfilter def slugify(value): """ Convert to ASCII. Convert spaces to hyphens. Remove characters that aren't alphanumerics, underscores, or hyphens. Convert to lowercase. Also strip leading and trailing whitespace. """ return _slugify(value) @register.filter(is_safe=True) def stringformat(value, arg): """ Format 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 https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting for documentation of Python string formatting. """ try: return ("%" + str(arg)) % value except (ValueError, TypeError): return "" @register.filter(is_safe=True) @stringfilter def title(value): """Convert a string into titlecase.""" t = re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) return re.sub(r"\d([A-Z])", lambda m: m.group(0).lower(), t) @register.filter(is_safe=True) @stringfilter def truncatechars(value, arg): """Truncate a string after `arg` number of characters.""" try: length = int(arg) except ValueError: # Invalid literal for int(). return value # Fail silently. return Truncator(value).chars(length) @register.filter(is_safe=True) @stringfilter def truncatechars_html(value, arg): """ Truncate HTML after `arg` number of chars. Preserve newlines in the HTML. """ try: length = int(arg) except ValueError: # invalid literal for int() return value # Fail silently. return Truncator(value).chars(length, html=True) @register.filter(is_safe=True) @stringfilter def truncatewords(value, arg): """ Truncate a string after `arg` number of words. Remove newlines within the string. """ try: length = int(arg) except ValueError: # Invalid literal for int(). return value # Fail silently. return Truncator(value).words(length, truncate=' ...') @register.filter(is_safe=True) @stringfilter def truncatewords_html(value, arg): """ Truncate HTML after `arg` number of words. Preserve newlines in the HTML. """ try: length = int(arg) except ValueError: # invalid literal for int() return value # Fail silently. return Truncator(value).words(length, html=True, truncate=' ...') @register.filter(is_safe=False) @stringfilter def upper(value): """Convert a string into all uppercase.""" return value.upper() @register.filter(is_safe=False) @stringfilter def urlencode(value, safe=None): """ Escape a value for use in a URL. The ``safe`` parameter determines the characters which should not be escaped by Python's quote() function. If not provided, use the default safe characters (but an empty string can be provided when *all* characters should be escaped). """ kwargs = {} if safe is not None: kwargs['safe'] = safe return quote(value, **kwargs) @register.filter(is_safe=True, needs_autoescape=True) @stringfilter def urlize(value, autoescape=True): """Convert URLs in plain text into clickable links.""" return mark_safe(_urlize(value, nofollow=True, autoescape=autoescape)) @register.filter(is_safe=True, needs_autoescape=True) @stringfilter def urlizetrunc(value, limit, autoescape=True): """ Convert 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(value, trim_url_limit=int(limit), nofollow=True, autoescape=autoescape)) @register.filter(is_safe=False) @stringfilter def wordcount(value): """Return the number of words.""" return len(value.split()) @register.filter(is_safe=True) @stringfilter def wordwrap(value, arg): """Wrap words at `arg` line length.""" return wrap(value, int(arg)) @register.filter(is_safe=True) @stringfilter def ljust(value, arg): """Left-align the value in a field of a given width.""" return value.ljust(int(arg)) @register.filter(is_safe=True) @stringfilter def rjust(value, arg): """Right-align the value in a field of a given width.""" return value.rjust(int(arg)) @register.filter(is_safe=True) @stringfilter def center(value, arg): """Center the value in a field of a given width.""" return value.center(int(arg)) @register.filter @stringfilter def cut(value, arg): """Remove all values of arg from the given string.""" safe = isinstance(value, SafeData) value = value.replace(arg, '') if safe and arg != ';': return mark_safe(value) return value ################### # HTML STRINGS # ################### @register.filter("escape", is_safe=True) @stringfilter def escape_filter(value): """Mark the value as a string that should be auto-escaped.""" return conditional_escape(value) @register.filter(is_safe=True) @stringfilter def force_escape(value): """ Escape a string's HTML. Return a new string containing the escaped characters (as opposed to "escape", which marks the content for later possible escaping). """ return escape(value) @register.filter("linebreaks", is_safe=True, needs_autoescape=True) @stringfilter def linebreaks_filter(value, autoescape=True): """ Replace 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)) @register.filter(is_safe=True, needs_autoescape=True) @stringfilter def linebreaksbr(value, autoescape=True): """ Convert 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', '
')) @register.filter(is_safe=True) @stringfilter def safe(value): """Mark the value as a string that should not be auto-escaped.""" return mark_safe(value) @register.filter(is_safe=True) def safeseq(value): """ A "safe" filter for sequences. Mark each element in the sequence, individually, as safe, after converting them to strings. Return a list with the results. """ return [mark_safe(str(obj)) for obj in value] @register.filter(is_safe=True) @stringfilter def striptags(value): """Strip all [X]HTML tags.""" return strip_tags(value) ################### # LISTS # ################### def _property_resolver(arg): """ When arg is convertible to float, behave like operator.itemgetter(arg) Otherwise, behave like Variable(arg).resolve >>> _property_resolver(1)('abc') 'b' >>> _property_resolver('1')('abc') Traceback (most recent call last): ... TypeError: string indices must be integers >>> class Foo: ... a = 42 ... b = 3.14 ... c = 'Hey!' >>> _property_resolver('b')(Foo()) 3.14 """ try: float(arg) except ValueError: return Variable(arg).resolve else: return itemgetter(arg) @register.filter(is_safe=False) def dictsort(value, arg): """ Given a list of dicts, return that list sorted by the property given in the argument. """ try: return sorted(value, key=_property_resolver(arg)) except (TypeError, VariableDoesNotExist): return '' @register.filter(is_safe=False) def dictsortreversed(value, arg): """ Given a list of dicts, return that list sorted in reverse order by the property given in the argument. """ try: return sorted(value, key=_property_resolver(arg), reverse=True) except (TypeError, VariableDoesNotExist): return '' @register.filter(is_safe=False) def first(value): """Return the first item in a list.""" try: return value[0] except IndexError: return '' @register.filter(is_safe=True, needs_autoescape=True) def join(value, arg, autoescape=True): """Join a list with a string, like Python's ``str.join(list)``.""" 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) @register.filter(is_safe=True) def last(value): """Return the last item in a list.""" try: return value[-1] except IndexError: return '' @register.filter(is_safe=False) def length(value): """Return the length of the value - useful for lists.""" try: return len(value) except (ValueError, TypeError): return 0 @register.filter(is_safe=False) def length_is(value, arg): """Return a boolean of whether the value's length is the argument.""" try: return len(value) == int(arg) except (ValueError, TypeError): return '' @register.filter(is_safe=True) def random(value): """Return a random item from the list.""" return random_module.choice(value) @register.filter("slice", is_safe=True) def slice_filter(value, arg): """ Return a slice of the list using the same syntax as Python's list slicing. """ try: bits = [] for x in arg.split(':'): if len(x) == 0: bits.append(None) else: bits.append(int(x)) return value[slice(*bits)] except (ValueError, TypeError): return value # Fail silently. @register.filter(is_safe=True, needs_autoescape=True) def unordered_list(value, autoescape=True): """ Recursively take a self-nested list and return an HTML unordered list -- WITHOUT opening and closing