mirror of
https://github.com/janeczku/calibre-web
synced 2024-11-08 07:10:39 +00:00
988 lines
29 KiB
Python
988 lines
29 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
"""
|
||
|
jinja2.filters
|
||
|
~~~~~~~~~~~~~~
|
||
|
|
||
|
Bundled jinja filters.
|
||
|
|
||
|
:copyright: (c) 2010 by the Jinja Team.
|
||
|
:license: BSD, see LICENSE for more details.
|
||
|
"""
|
||
|
import re
|
||
|
import math
|
||
|
|
||
|
from random import choice
|
||
|
from operator import itemgetter
|
||
|
from itertools import groupby
|
||
|
from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode, \
|
||
|
unicode_urlencode
|
||
|
from jinja2.runtime import Undefined
|
||
|
from jinja2.exceptions import FilterArgumentError
|
||
|
from jinja2._compat import next, imap, string_types, text_type, iteritems
|
||
|
|
||
|
|
||
|
_word_re = re.compile(r'\w+(?u)')
|
||
|
|
||
|
|
||
|
def contextfilter(f):
|
||
|
"""Decorator for marking context dependent filters. The current
|
||
|
:class:`Context` will be passed as first argument.
|
||
|
"""
|
||
|
f.contextfilter = True
|
||
|
return f
|
||
|
|
||
|
|
||
|
def evalcontextfilter(f):
|
||
|
"""Decorator for marking eval-context dependent filters. An eval
|
||
|
context object is passed as first argument. For more information
|
||
|
about the eval context, see :ref:`eval-context`.
|
||
|
|
||
|
.. versionadded:: 2.4
|
||
|
"""
|
||
|
f.evalcontextfilter = True
|
||
|
return f
|
||
|
|
||
|
|
||
|
def environmentfilter(f):
|
||
|
"""Decorator for marking evironment dependent filters. The current
|
||
|
:class:`Environment` is passed to the filter as first argument.
|
||
|
"""
|
||
|
f.environmentfilter = True
|
||
|
return f
|
||
|
|
||
|
|
||
|
def make_attrgetter(environment, attribute):
|
||
|
"""Returns a callable that looks up the given attribute from a
|
||
|
passed object with the rules of the environment. Dots are allowed
|
||
|
to access attributes of attributes. Integer parts in paths are
|
||
|
looked up as integers.
|
||
|
"""
|
||
|
if not isinstance(attribute, string_types) \
|
||
|
or ('.' not in attribute and not attribute.isdigit()):
|
||
|
return lambda x: environment.getitem(x, attribute)
|
||
|
attribute = attribute.split('.')
|
||
|
def attrgetter(item):
|
||
|
for part in attribute:
|
||
|
if part.isdigit():
|
||
|
part = int(part)
|
||
|
item = environment.getitem(item, part)
|
||
|
return item
|
||
|
return attrgetter
|
||
|
|
||
|
|
||
|
def do_forceescape(value):
|
||
|
"""Enforce HTML escaping. This will probably double escape variables."""
|
||
|
if hasattr(value, '__html__'):
|
||
|
value = value.__html__()
|
||
|
return escape(text_type(value))
|
||
|
|
||
|
|
||
|
def do_urlencode(value):
|
||
|
"""Escape strings for use in URLs (uses UTF-8 encoding). It accepts both
|
||
|
dictionaries and regular strings as well as pairwise iterables.
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
itemiter = None
|
||
|
if isinstance(value, dict):
|
||
|
itemiter = iteritems(value)
|
||
|
elif not isinstance(value, string_types):
|
||
|
try:
|
||
|
itemiter = iter(value)
|
||
|
except TypeError:
|
||
|
pass
|
||
|
if itemiter is None:
|
||
|
return unicode_urlencode(value)
|
||
|
return u'&'.join(unicode_urlencode(k) + '=' +
|
||
|
unicode_urlencode(v) for k, v in itemiter)
|
||
|
|
||
|
|
||
|
@evalcontextfilter
|
||
|
def do_replace(eval_ctx, s, old, new, count=None):
|
||
|
"""Return a copy of the value with all occurrences of a substring
|
||
|
replaced with a new one. The first argument is the substring
|
||
|
that should be replaced, the second is the replacement string.
|
||
|
If the optional third argument ``count`` is given, only the first
|
||
|
``count`` occurrences are replaced:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ "Hello World"|replace("Hello", "Goodbye") }}
|
||
|
-> Goodbye World
|
||
|
|
||
|
{{ "aaaaargh"|replace("a", "d'oh, ", 2) }}
|
||
|
-> d'oh, d'oh, aaargh
|
||
|
"""
|
||
|
if count is None:
|
||
|
count = -1
|
||
|
if not eval_ctx.autoescape:
|
||
|
return text_type(s).replace(text_type(old), text_type(new), count)
|
||
|
if hasattr(old, '__html__') or hasattr(new, '__html__') and \
|
||
|
not hasattr(s, '__html__'):
|
||
|
s = escape(s)
|
||
|
else:
|
||
|
s = soft_unicode(s)
|
||
|
return s.replace(soft_unicode(old), soft_unicode(new), count)
|
||
|
|
||
|
|
||
|
def do_upper(s):
|
||
|
"""Convert a value to uppercase."""
|
||
|
return soft_unicode(s).upper()
|
||
|
|
||
|
|
||
|
def do_lower(s):
|
||
|
"""Convert a value to lowercase."""
|
||
|
return soft_unicode(s).lower()
|
||
|
|
||
|
|
||
|
@evalcontextfilter
|
||
|
def do_xmlattr(_eval_ctx, d, autospace=True):
|
||
|
"""Create an SGML/XML attribute string based on the items in a dict.
|
||
|
All values that are neither `none` nor `undefined` are automatically
|
||
|
escaped:
|
||
|
|
||
|
.. sourcecode:: html+jinja
|
||
|
|
||
|
<ul{{ {'class': 'my_list', 'missing': none,
|
||
|
'id': 'list-%d'|format(variable)}|xmlattr }}>
|
||
|
...
|
||
|
</ul>
|
||
|
|
||
|
Results in something like this:
|
||
|
|
||
|
.. sourcecode:: html
|
||
|
|
||
|
<ul class="my_list" id="list-42">
|
||
|
...
|
||
|
</ul>
|
||
|
|
||
|
As you can see it automatically prepends a space in front of the item
|
||
|
if the filter returned something unless the second parameter is false.
|
||
|
"""
|
||
|
rv = u' '.join(
|
||
|
u'%s="%s"' % (escape(key), escape(value))
|
||
|
for key, value in iteritems(d)
|
||
|
if value is not None and not isinstance(value, Undefined)
|
||
|
)
|
||
|
if autospace and rv:
|
||
|
rv = u' ' + rv
|
||
|
if _eval_ctx.autoescape:
|
||
|
rv = Markup(rv)
|
||
|
return rv
|
||
|
|
||
|
|
||
|
def do_capitalize(s):
|
||
|
"""Capitalize a value. The first character will be uppercase, all others
|
||
|
lowercase.
|
||
|
"""
|
||
|
return soft_unicode(s).capitalize()
|
||
|
|
||
|
|
||
|
def do_title(s):
|
||
|
"""Return a titlecased version of the value. I.e. words will start with
|
||
|
uppercase letters, all remaining characters are lowercase.
|
||
|
"""
|
||
|
rv = []
|
||
|
for item in re.compile(r'([-\s]+)(?u)').split(s):
|
||
|
if not item:
|
||
|
continue
|
||
|
rv.append(item[0].upper() + item[1:])
|
||
|
return ''.join(rv)
|
||
|
|
||
|
|
||
|
def do_dictsort(value, case_sensitive=False, by='key'):
|
||
|
"""Sort a dict and yield (key, value) pairs. Because python dicts are
|
||
|
unsorted you may want to use this function to order them by either
|
||
|
key or value:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{% for item in mydict|dictsort %}
|
||
|
sort the dict by key, case insensitive
|
||
|
|
||
|
{% for item in mydict|dictsort(true) %}
|
||
|
sort the dict by key, case sensitive
|
||
|
|
||
|
{% for item in mydict|dictsort(false, 'value') %}
|
||
|
sort the dict by key, case insensitive, sorted
|
||
|
normally and ordered by value.
|
||
|
"""
|
||
|
if by == 'key':
|
||
|
pos = 0
|
||
|
elif by == 'value':
|
||
|
pos = 1
|
||
|
else:
|
||
|
raise FilterArgumentError('You can only sort by either '
|
||
|
'"key" or "value"')
|
||
|
def sort_func(item):
|
||
|
value = item[pos]
|
||
|
if isinstance(value, string_types) and not case_sensitive:
|
||
|
value = value.lower()
|
||
|
return value
|
||
|
|
||
|
return sorted(value.items(), key=sort_func)
|
||
|
|
||
|
|
||
|
@environmentfilter
|
||
|
def do_sort(environment, value, reverse=False, case_sensitive=False,
|
||
|
attribute=None):
|
||
|
"""Sort an iterable. Per default it sorts ascending, if you pass it
|
||
|
true as first argument it will reverse the sorting.
|
||
|
|
||
|
If the iterable is made of strings the third parameter can be used to
|
||
|
control the case sensitiveness of the comparison which is disabled by
|
||
|
default.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{% for item in iterable|sort %}
|
||
|
...
|
||
|
{% endfor %}
|
||
|
|
||
|
It is also possible to sort by an attribute (for example to sort
|
||
|
by the date of an object) by specifying the `attribute` parameter:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{% for item in iterable|sort(attribute='date') %}
|
||
|
...
|
||
|
{% endfor %}
|
||
|
|
||
|
.. versionchanged:: 2.6
|
||
|
The `attribute` parameter was added.
|
||
|
"""
|
||
|
if not case_sensitive:
|
||
|
def sort_func(item):
|
||
|
if isinstance(item, string_types):
|
||
|
item = item.lower()
|
||
|
return item
|
||
|
else:
|
||
|
sort_func = None
|
||
|
if attribute is not None:
|
||
|
getter = make_attrgetter(environment, attribute)
|
||
|
def sort_func(item, processor=sort_func or (lambda x: x)):
|
||
|
return processor(getter(item))
|
||
|
return sorted(value, key=sort_func, reverse=reverse)
|
||
|
|
||
|
|
||
|
def do_default(value, default_value=u'', boolean=False):
|
||
|
"""If the value is undefined it will return the passed default value,
|
||
|
otherwise the value of the variable:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ my_variable|default('my_variable is not defined') }}
|
||
|
|
||
|
This will output the value of ``my_variable`` if the variable was
|
||
|
defined, otherwise ``'my_variable is not defined'``. If you want
|
||
|
to use default with variables that evaluate to false you have to
|
||
|
set the second parameter to `true`:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ ''|default('the string was empty', true) }}
|
||
|
"""
|
||
|
if isinstance(value, Undefined) or (boolean and not value):
|
||
|
return default_value
|
||
|
return value
|
||
|
|
||
|
|
||
|
@evalcontextfilter
|
||
|
def do_join(eval_ctx, value, d=u'', attribute=None):
|
||
|
"""Return a string which is the concatenation of the strings in the
|
||
|
sequence. The separator between elements is an empty string per
|
||
|
default, you can define it with the optional parameter:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ [1, 2, 3]|join('|') }}
|
||
|
-> 1|2|3
|
||
|
|
||
|
{{ [1, 2, 3]|join }}
|
||
|
-> 123
|
||
|
|
||
|
It is also possible to join certain attributes of an object:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ users|join(', ', attribute='username') }}
|
||
|
|
||
|
.. versionadded:: 2.6
|
||
|
The `attribute` parameter was added.
|
||
|
"""
|
||
|
if attribute is not None:
|
||
|
value = imap(make_attrgetter(eval_ctx.environment, attribute), value)
|
||
|
|
||
|
# no automatic escaping? joining is a lot eaiser then
|
||
|
if not eval_ctx.autoescape:
|
||
|
return text_type(d).join(imap(text_type, value))
|
||
|
|
||
|
# if the delimiter doesn't have an html representation we check
|
||
|
# if any of the items has. If yes we do a coercion to Markup
|
||
|
if not hasattr(d, '__html__'):
|
||
|
value = list(value)
|
||
|
do_escape = False
|
||
|
for idx, item in enumerate(value):
|
||
|
if hasattr(item, '__html__'):
|
||
|
do_escape = True
|
||
|
else:
|
||
|
value[idx] = text_type(item)
|
||
|
if do_escape:
|
||
|
d = escape(d)
|
||
|
else:
|
||
|
d = text_type(d)
|
||
|
return d.join(value)
|
||
|
|
||
|
# no html involved, to normal joining
|
||
|
return soft_unicode(d).join(imap(soft_unicode, value))
|
||
|
|
||
|
|
||
|
def do_center(value, width=80):
|
||
|
"""Centers the value in a field of a given width."""
|
||
|
return text_type(value).center(width)
|
||
|
|
||
|
|
||
|
@environmentfilter
|
||
|
def do_first(environment, seq):
|
||
|
"""Return the first item of a sequence."""
|
||
|
try:
|
||
|
return next(iter(seq))
|
||
|
except StopIteration:
|
||
|
return environment.undefined('No first item, sequence was empty.')
|
||
|
|
||
|
|
||
|
@environmentfilter
|
||
|
def do_last(environment, seq):
|
||
|
"""Return the last item of a sequence."""
|
||
|
try:
|
||
|
return next(iter(reversed(seq)))
|
||
|
except StopIteration:
|
||
|
return environment.undefined('No last item, sequence was empty.')
|
||
|
|
||
|
|
||
|
@environmentfilter
|
||
|
def do_random(environment, seq):
|
||
|
"""Return a random item from the sequence."""
|
||
|
try:
|
||
|
return choice(seq)
|
||
|
except IndexError:
|
||
|
return environment.undefined('No random item, sequence was empty.')
|
||
|
|
||
|
|
||
|
def do_filesizeformat(value, binary=False):
|
||
|
"""Format the value like a 'human-readable' file size (i.e. 13 kB,
|
||
|
4.1 MB, 102 Bytes, etc). Per default decimal prefixes are used (Mega,
|
||
|
Giga, etc.), if the second parameter is set to `True` the binary
|
||
|
prefixes are used (Mebi, Gibi).
|
||
|
"""
|
||
|
bytes = float(value)
|
||
|
base = binary and 1024 or 1000
|
||
|
prefixes = [
|
||
|
(binary and 'KiB' or 'kB'),
|
||
|
(binary and 'MiB' or 'MB'),
|
||
|
(binary and 'GiB' or 'GB'),
|
||
|
(binary and 'TiB' or 'TB'),
|
||
|
(binary and 'PiB' or 'PB'),
|
||
|
(binary and 'EiB' or 'EB'),
|
||
|
(binary and 'ZiB' or 'ZB'),
|
||
|
(binary and 'YiB' or 'YB')
|
||
|
]
|
||
|
if bytes == 1:
|
||
|
return '1 Byte'
|
||
|
elif bytes < base:
|
||
|
return '%d Bytes' % bytes
|
||
|
else:
|
||
|
for i, prefix in enumerate(prefixes):
|
||
|
unit = base ** (i + 2)
|
||
|
if bytes < unit:
|
||
|
return '%.1f %s' % ((base * bytes / unit), prefix)
|
||
|
return '%.1f %s' % ((base * bytes / unit), prefix)
|
||
|
|
||
|
|
||
|
def do_pprint(value, verbose=False):
|
||
|
"""Pretty print a variable. Useful for debugging.
|
||
|
|
||
|
With Jinja 1.2 onwards you can pass it a parameter. If this parameter
|
||
|
is truthy the output will be more verbose (this requires `pretty`)
|
||
|
"""
|
||
|
return pformat(value, verbose=verbose)
|
||
|
|
||
|
|
||
|
@evalcontextfilter
|
||
|
def do_urlize(eval_ctx, value, trim_url_limit=None, nofollow=False):
|
||
|
"""Converts URLs in plain text into clickable links.
|
||
|
|
||
|
If you pass the filter an additional integer it will shorten the urls
|
||
|
to that number. Also a third argument exists that makes the urls
|
||
|
"nofollow":
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ mytext|urlize(40, true) }}
|
||
|
links are shortened to 40 chars and defined with rel="nofollow"
|
||
|
"""
|
||
|
rv = urlize(value, trim_url_limit, nofollow)
|
||
|
if eval_ctx.autoescape:
|
||
|
rv = Markup(rv)
|
||
|
return rv
|
||
|
|
||
|
|
||
|
def do_indent(s, width=4, indentfirst=False):
|
||
|
"""Return a copy of the passed string, each line indented by
|
||
|
4 spaces. The first line is not indented. If you want to
|
||
|
change the number of spaces or indent the first line too
|
||
|
you can pass additional parameters to the filter:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ mytext|indent(2, true) }}
|
||
|
indent by two spaces and indent the first line too.
|
||
|
"""
|
||
|
indention = u' ' * width
|
||
|
rv = (u'\n' + indention).join(s.splitlines())
|
||
|
if indentfirst:
|
||
|
rv = indention + rv
|
||
|
return rv
|
||
|
|
||
|
|
||
|
def do_truncate(s, length=255, killwords=False, end='...'):
|
||
|
"""Return a truncated copy of the string. The length is specified
|
||
|
with the first parameter which defaults to ``255``. If the second
|
||
|
parameter is ``true`` the filter will cut the text at length. Otherwise
|
||
|
it will discard the last word. If the text was in fact
|
||
|
truncated it will append an ellipsis sign (``"..."``). If you want a
|
||
|
different ellipsis sign than ``"..."`` you can specify it using the
|
||
|
third parameter.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ "foo bar"|truncate(5) }}
|
||
|
-> "foo ..."
|
||
|
{{ "foo bar"|truncate(5, True) }}
|
||
|
-> "foo b..."
|
||
|
"""
|
||
|
if len(s) <= length:
|
||
|
return s
|
||
|
elif killwords:
|
||
|
return s[:length] + end
|
||
|
words = s.split(' ')
|
||
|
result = []
|
||
|
m = 0
|
||
|
for word in words:
|
||
|
m += len(word) + 1
|
||
|
if m > length:
|
||
|
break
|
||
|
result.append(word)
|
||
|
result.append(end)
|
||
|
return u' '.join(result)
|
||
|
|
||
|
@environmentfilter
|
||
|
def do_wordwrap(environment, s, width=79, break_long_words=True,
|
||
|
wrapstring=None):
|
||
|
"""
|
||
|
Return a copy of the string passed to the filter wrapped after
|
||
|
``79`` characters. You can override this default using the first
|
||
|
parameter. If you set the second parameter to `false` Jinja will not
|
||
|
split words apart if they are longer than `width`. By default, the newlines
|
||
|
will be the default newlines for the environment, but this can be changed
|
||
|
using the wrapstring keyword argument.
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
Added support for the `wrapstring` parameter.
|
||
|
"""
|
||
|
if not wrapstring:
|
||
|
wrapstring = environment.newline_sequence
|
||
|
import textwrap
|
||
|
return wrapstring.join(textwrap.wrap(s, width=width, expand_tabs=False,
|
||
|
replace_whitespace=False,
|
||
|
break_long_words=break_long_words))
|
||
|
|
||
|
|
||
|
def do_wordcount(s):
|
||
|
"""Count the words in that string."""
|
||
|
return len(_word_re.findall(s))
|
||
|
|
||
|
|
||
|
def do_int(value, default=0):
|
||
|
"""Convert the value into an integer. If the
|
||
|
conversion doesn't work it will return ``0``. You can
|
||
|
override this default using the first parameter.
|
||
|
"""
|
||
|
try:
|
||
|
return int(value)
|
||
|
except (TypeError, ValueError):
|
||
|
# this quirk is necessary so that "42.23"|int gives 42.
|
||
|
try:
|
||
|
return int(float(value))
|
||
|
except (TypeError, ValueError):
|
||
|
return default
|
||
|
|
||
|
|
||
|
def do_float(value, default=0.0):
|
||
|
"""Convert the value into a floating point number. If the
|
||
|
conversion doesn't work it will return ``0.0``. You can
|
||
|
override this default using the first parameter.
|
||
|
"""
|
||
|
try:
|
||
|
return float(value)
|
||
|
except (TypeError, ValueError):
|
||
|
return default
|
||
|
|
||
|
|
||
|
def do_format(value, *args, **kwargs):
|
||
|
"""
|
||
|
Apply python string formatting on an object:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ "%s - %s"|format("Hello?", "Foo!") }}
|
||
|
-> Hello? - Foo!
|
||
|
"""
|
||
|
if args and kwargs:
|
||
|
raise FilterArgumentError('can\'t handle positional and keyword '
|
||
|
'arguments at the same time')
|
||
|
return soft_unicode(value) % (kwargs or args)
|
||
|
|
||
|
|
||
|
def do_trim(value):
|
||
|
"""Strip leading and trailing whitespace."""
|
||
|
return soft_unicode(value).strip()
|
||
|
|
||
|
|
||
|
def do_striptags(value):
|
||
|
"""Strip SGML/XML tags and replace adjacent whitespace by one space.
|
||
|
"""
|
||
|
if hasattr(value, '__html__'):
|
||
|
value = value.__html__()
|
||
|
return Markup(text_type(value)).striptags()
|
||
|
|
||
|
|
||
|
def do_slice(value, slices, fill_with=None):
|
||
|
"""Slice an iterator and return a list of lists containing
|
||
|
those items. Useful if you want to create a div containing
|
||
|
three ul tags that represent columns:
|
||
|
|
||
|
.. sourcecode:: html+jinja
|
||
|
|
||
|
<div class="columwrapper">
|
||
|
{%- for column in items|slice(3) %}
|
||
|
<ul class="column-{{ loop.index }}">
|
||
|
{%- for item in column %}
|
||
|
<li>{{ item }}</li>
|
||
|
{%- endfor %}
|
||
|
</ul>
|
||
|
{%- endfor %}
|
||
|
</div>
|
||
|
|
||
|
If you pass it a second argument it's used to fill missing
|
||
|
values on the last iteration.
|
||
|
"""
|
||
|
seq = list(value)
|
||
|
length = len(seq)
|
||
|
items_per_slice = length // slices
|
||
|
slices_with_extra = length % slices
|
||
|
offset = 0
|
||
|
for slice_number in range(slices):
|
||
|
start = offset + slice_number * items_per_slice
|
||
|
if slice_number < slices_with_extra:
|
||
|
offset += 1
|
||
|
end = offset + (slice_number + 1) * items_per_slice
|
||
|
tmp = seq[start:end]
|
||
|
if fill_with is not None and slice_number >= slices_with_extra:
|
||
|
tmp.append(fill_with)
|
||
|
yield tmp
|
||
|
|
||
|
|
||
|
def do_batch(value, linecount, fill_with=None):
|
||
|
"""
|
||
|
A filter that batches items. It works pretty much like `slice`
|
||
|
just the other way round. It returns a list of lists with the
|
||
|
given number of items. If you provide a second parameter this
|
||
|
is used to fill up missing items. See this example:
|
||
|
|
||
|
.. sourcecode:: html+jinja
|
||
|
|
||
|
<table>
|
||
|
{%- for row in items|batch(3, ' ') %}
|
||
|
<tr>
|
||
|
{%- for column in row %}
|
||
|
<td>{{ column }}</td>
|
||
|
{%- endfor %}
|
||
|
</tr>
|
||
|
{%- endfor %}
|
||
|
</table>
|
||
|
"""
|
||
|
result = []
|
||
|
tmp = []
|
||
|
for item in value:
|
||
|
if len(tmp) == linecount:
|
||
|
yield tmp
|
||
|
tmp = []
|
||
|
tmp.append(item)
|
||
|
if tmp:
|
||
|
if fill_with is not None and len(tmp) < linecount:
|
||
|
tmp += [fill_with] * (linecount - len(tmp))
|
||
|
yield tmp
|
||
|
|
||
|
|
||
|
def do_round(value, precision=0, method='common'):
|
||
|
"""Round the number to a given precision. The first
|
||
|
parameter specifies the precision (default is ``0``), the
|
||
|
second the rounding method:
|
||
|
|
||
|
- ``'common'`` rounds either up or down
|
||
|
- ``'ceil'`` always rounds up
|
||
|
- ``'floor'`` always rounds down
|
||
|
|
||
|
If you don't specify a method ``'common'`` is used.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ 42.55|round }}
|
||
|
-> 43.0
|
||
|
{{ 42.55|round(1, 'floor') }}
|
||
|
-> 42.5
|
||
|
|
||
|
Note that even if rounded to 0 precision, a float is returned. If
|
||
|
you need a real integer, pipe it through `int`:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ 42.55|round|int }}
|
||
|
-> 43
|
||
|
"""
|
||
|
if not method in ('common', 'ceil', 'floor'):
|
||
|
raise FilterArgumentError('method must be common, ceil or floor')
|
||
|
if method == 'common':
|
||
|
return round(value, precision)
|
||
|
func = getattr(math, method)
|
||
|
return func(value * (10 ** precision)) / (10 ** precision)
|
||
|
|
||
|
|
||
|
@environmentfilter
|
||
|
def do_groupby(environment, value, attribute):
|
||
|
"""Group a sequence of objects by a common attribute.
|
||
|
|
||
|
If you for example have a list of dicts or objects that represent persons
|
||
|
with `gender`, `first_name` and `last_name` attributes and you want to
|
||
|
group all users by genders you can do something like the following
|
||
|
snippet:
|
||
|
|
||
|
.. sourcecode:: html+jinja
|
||
|
|
||
|
<ul>
|
||
|
{% for group in persons|groupby('gender') %}
|
||
|
<li>{{ group.grouper }}<ul>
|
||
|
{% for person in group.list %}
|
||
|
<li>{{ person.first_name }} {{ person.last_name }}</li>
|
||
|
{% endfor %}</ul></li>
|
||
|
{% endfor %}
|
||
|
</ul>
|
||
|
|
||
|
Additionally it's possible to use tuple unpacking for the grouper and
|
||
|
list:
|
||
|
|
||
|
.. sourcecode:: html+jinja
|
||
|
|
||
|
<ul>
|
||
|
{% for grouper, list in persons|groupby('gender') %}
|
||
|
...
|
||
|
{% endfor %}
|
||
|
</ul>
|
||
|
|
||
|
As you can see the item we're grouping by is stored in the `grouper`
|
||
|
attribute and the `list` contains all the objects that have this grouper
|
||
|
in common.
|
||
|
|
||
|
.. versionchanged:: 2.6
|
||
|
It's now possible to use dotted notation to group by the child
|
||
|
attribute of another attribute.
|
||
|
"""
|
||
|
expr = make_attrgetter(environment, attribute)
|
||
|
return sorted(map(_GroupTuple, groupby(sorted(value, key=expr), expr)))
|
||
|
|
||
|
|
||
|
class _GroupTuple(tuple):
|
||
|
__slots__ = ()
|
||
|
grouper = property(itemgetter(0))
|
||
|
list = property(itemgetter(1))
|
||
|
|
||
|
def __new__(cls, xxx_todo_changeme):
|
||
|
(key, value) = xxx_todo_changeme
|
||
|
return tuple.__new__(cls, (key, list(value)))
|
||
|
|
||
|
|
||
|
@environmentfilter
|
||
|
def do_sum(environment, iterable, attribute=None, start=0):
|
||
|
"""Returns the sum of a sequence of numbers plus the value of parameter
|
||
|
'start' (which defaults to 0). When the sequence is empty it returns
|
||
|
start.
|
||
|
|
||
|
It is also possible to sum up only certain attributes:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
Total: {{ items|sum(attribute='price') }}
|
||
|
|
||
|
.. versionchanged:: 2.6
|
||
|
The `attribute` parameter was added to allow suming up over
|
||
|
attributes. Also the `start` parameter was moved on to the right.
|
||
|
"""
|
||
|
if attribute is not None:
|
||
|
iterable = imap(make_attrgetter(environment, attribute), iterable)
|
||
|
return sum(iterable, start)
|
||
|
|
||
|
|
||
|
def do_list(value):
|
||
|
"""Convert the value into a list. If it was a string the returned list
|
||
|
will be a list of characters.
|
||
|
"""
|
||
|
return list(value)
|
||
|
|
||
|
|
||
|
def do_mark_safe(value):
|
||
|
"""Mark the value as safe which means that in an environment with automatic
|
||
|
escaping enabled this variable will not be escaped.
|
||
|
"""
|
||
|
return Markup(value)
|
||
|
|
||
|
|
||
|
def do_mark_unsafe(value):
|
||
|
"""Mark a value as unsafe. This is the reverse operation for :func:`safe`."""
|
||
|
return text_type(value)
|
||
|
|
||
|
|
||
|
def do_reverse(value):
|
||
|
"""Reverse the object or return an iterator the iterates over it the other
|
||
|
way round.
|
||
|
"""
|
||
|
if isinstance(value, string_types):
|
||
|
return value[::-1]
|
||
|
try:
|
||
|
return reversed(value)
|
||
|
except TypeError:
|
||
|
try:
|
||
|
rv = list(value)
|
||
|
rv.reverse()
|
||
|
return rv
|
||
|
except TypeError:
|
||
|
raise FilterArgumentError('argument must be iterable')
|
||
|
|
||
|
|
||
|
@environmentfilter
|
||
|
def do_attr(environment, obj, name):
|
||
|
"""Get an attribute of an object. ``foo|attr("bar")`` works like
|
||
|
``foo["bar"]`` just that always an attribute is returned and items are not
|
||
|
looked up.
|
||
|
|
||
|
See :ref:`Notes on subscriptions <notes-on-subscriptions>` for more details.
|
||
|
"""
|
||
|
try:
|
||
|
name = str(name)
|
||
|
except UnicodeError:
|
||
|
pass
|
||
|
else:
|
||
|
try:
|
||
|
value = getattr(obj, name)
|
||
|
except AttributeError:
|
||
|
pass
|
||
|
else:
|
||
|
if environment.sandboxed and not \
|
||
|
environment.is_safe_attribute(obj, name, value):
|
||
|
return environment.unsafe_undefined(obj, name)
|
||
|
return value
|
||
|
return environment.undefined(obj=obj, name=name)
|
||
|
|
||
|
|
||
|
@contextfilter
|
||
|
def do_map(*args, **kwargs):
|
||
|
"""Applies a filter on a sequence of objects or looks up an attribute.
|
||
|
This is useful when dealing with lists of objects but you are really
|
||
|
only interested in a certain value of it.
|
||
|
|
||
|
The basic usage is mapping on an attribute. Imagine you have a list
|
||
|
of users but you are only interested in a list of usernames:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
Users on this page: {{ users|map(attribute='username')|join(', ') }}
|
||
|
|
||
|
Alternatively you can let it invoke a filter by passing the name of the
|
||
|
filter and the arguments afterwards. A good example would be applying a
|
||
|
text conversion filter on a sequence:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
Users on this page: {{ titles|map('lower')|join(', ') }}
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
context = args[0]
|
||
|
seq = args[1]
|
||
|
|
||
|
if len(args) == 2 and 'attribute' in kwargs:
|
||
|
attribute = kwargs.pop('attribute')
|
||
|
if kwargs:
|
||
|
raise FilterArgumentError('Unexpected keyword argument %r' %
|
||
|
next(iter(kwargs)))
|
||
|
func = make_attrgetter(context.environment, attribute)
|
||
|
else:
|
||
|
try:
|
||
|
name = args[2]
|
||
|
args = args[3:]
|
||
|
except LookupError:
|
||
|
raise FilterArgumentError('map requires a filter argument')
|
||
|
func = lambda item: context.environment.call_filter(
|
||
|
name, item, args, kwargs, context=context)
|
||
|
|
||
|
if seq:
|
||
|
for item in seq:
|
||
|
yield func(item)
|
||
|
|
||
|
|
||
|
@contextfilter
|
||
|
def do_select(*args, **kwargs):
|
||
|
"""Filters a sequence of objects by appying a test to either the object
|
||
|
or the attribute and only selecting the ones with the test succeeding.
|
||
|
|
||
|
Example usage:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ numbers|select("odd") }}
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
return _select_or_reject(args, kwargs, lambda x: x, False)
|
||
|
|
||
|
|
||
|
@contextfilter
|
||
|
def do_reject(*args, **kwargs):
|
||
|
"""Filters a sequence of objects by appying a test to either the object
|
||
|
or the attribute and rejecting the ones with the test succeeding.
|
||
|
|
||
|
Example usage:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ numbers|reject("odd") }}
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
return _select_or_reject(args, kwargs, lambda x: not x, False)
|
||
|
|
||
|
|
||
|
@contextfilter
|
||
|
def do_selectattr(*args, **kwargs):
|
||
|
"""Filters a sequence of objects by appying a test to either the object
|
||
|
or the attribute and only selecting the ones with the test succeeding.
|
||
|
|
||
|
Example usage:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ users|selectattr("is_active") }}
|
||
|
{{ users|selectattr("email", "none") }}
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
return _select_or_reject(args, kwargs, lambda x: x, True)
|
||
|
|
||
|
|
||
|
@contextfilter
|
||
|
def do_rejectattr(*args, **kwargs):
|
||
|
"""Filters a sequence of objects by appying a test to either the object
|
||
|
or the attribute and rejecting the ones with the test succeeding.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ users|rejectattr("is_active") }}
|
||
|
{{ users|rejectattr("email", "none") }}
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
return _select_or_reject(args, kwargs, lambda x: not x, True)
|
||
|
|
||
|
|
||
|
def _select_or_reject(args, kwargs, modfunc, lookup_attr):
|
||
|
context = args[0]
|
||
|
seq = args[1]
|
||
|
if lookup_attr:
|
||
|
try:
|
||
|
attr = args[2]
|
||
|
except LookupError:
|
||
|
raise FilterArgumentError('Missing parameter for attribute name')
|
||
|
transfunc = make_attrgetter(context.environment, attr)
|
||
|
off = 1
|
||
|
else:
|
||
|
off = 0
|
||
|
transfunc = lambda x: x
|
||
|
|
||
|
try:
|
||
|
name = args[2 + off]
|
||
|
args = args[3 + off:]
|
||
|
func = lambda item: context.environment.call_test(
|
||
|
name, item, args, kwargs)
|
||
|
except LookupError:
|
||
|
func = bool
|
||
|
|
||
|
if seq:
|
||
|
for item in seq:
|
||
|
if modfunc(func(transfunc(item))):
|
||
|
yield item
|
||
|
|
||
|
|
||
|
FILTERS = {
|
||
|
'attr': do_attr,
|
||
|
'replace': do_replace,
|
||
|
'upper': do_upper,
|
||
|
'lower': do_lower,
|
||
|
'escape': escape,
|
||
|
'e': escape,
|
||
|
'forceescape': do_forceescape,
|
||
|
'capitalize': do_capitalize,
|
||
|
'title': do_title,
|
||
|
'default': do_default,
|
||
|
'd': do_default,
|
||
|
'join': do_join,
|
||
|
'count': len,
|
||
|
'dictsort': do_dictsort,
|
||
|
'sort': do_sort,
|
||
|
'length': len,
|
||
|
'reverse': do_reverse,
|
||
|
'center': do_center,
|
||
|
'indent': do_indent,
|
||
|
'title': do_title,
|
||
|
'capitalize': do_capitalize,
|
||
|
'first': do_first,
|
||
|
'last': do_last,
|
||
|
'map': do_map,
|
||
|
'random': do_random,
|
||
|
'reject': do_reject,
|
||
|
'rejectattr': do_rejectattr,
|
||
|
'filesizeformat': do_filesizeformat,
|
||
|
'pprint': do_pprint,
|
||
|
'truncate': do_truncate,
|
||
|
'wordwrap': do_wordwrap,
|
||
|
'wordcount': do_wordcount,
|
||
|
'int': do_int,
|
||
|
'float': do_float,
|
||
|
'string': soft_unicode,
|
||
|
'list': do_list,
|
||
|
'urlize': do_urlize,
|
||
|
'format': do_format,
|
||
|
'trim': do_trim,
|
||
|
'striptags': do_striptags,
|
||
|
'select': do_select,
|
||
|
'selectattr': do_selectattr,
|
||
|
'slice': do_slice,
|
||
|
'batch': do_batch,
|
||
|
'sum': do_sum,
|
||
|
'abs': abs,
|
||
|
'round': do_round,
|
||
|
'groupby': do_groupby,
|
||
|
'safe': do_mark_safe,
|
||
|
'xmlattr': do_xmlattr,
|
||
|
'urlencode': do_urlencode
|
||
|
}
|