"""
Add a few specific filters to Jinja2.
"""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
import re
from functools import wraps
import datetime
from calendar import timegm
import dateutil.parser
from flask import Flask
from flask.ext import babel
from jinja2 import Markup, escape, evalcontextfilter
from pytz import utc
from babel.dates import DateTimePattern, format_timedelta, parse_pattern
import bleach
from werkzeug.routing import BuildError
from ..core.util import local_dt, utc_dt, slugify
from .util import url_for
[docs]def autoescape(filter_func):
"""
Decorator to autoescape result from filters.
"""
@evalcontextfilter
@wraps(filter_func)
def _autoescape(eval_ctx, *args, **kwargs):
result = filter_func(*args, **kwargs)
if eval_ctx.autoescape:
result = Markup(result)
return result
return _autoescape
@autoescape
[docs]def nl2br(value):
"""
Replace newlines with <br />.
"""
result = escape(value).replace(u'\n', Markup(u'<br />\n'))
return result
_PARAGRAPH_RE = re.compile(r'(?:\r\n|\r|\n){2,}')
@autoescape
[docs]def paragraphs(value):
"""
Blank lines delimitates paragraphs.
"""
result = u'\n\n'.join(
(u'<p>{}</p>'.format(p.strip().replace('\n', Markup('<br />\n')))
for p in _PARAGRAPH_RE.split(escape(value))))
return result
[docs]def labelize(s):
return u" ".join([ w.capitalize() for w in s.split(u"_") ])
[docs]def filesize(d):
if not isinstance(d, int):
d = int(d)
if d < 1000:
s = "%d B" % d
elif d < 1e4:
s = "%.1f kB" % (d / 1e3)
elif d < 1e6:
s = "%.0f kB" % (d / 1e3)
elif d < 1e7:
s = "%.1f MB" % (d / 1e6)
elif d < 1e9:
s = "%.0f MB" % (d / 1e6)
elif d < 1e10:
s = "%.1f GB" % (d / 1e9)
else:
s = "%.0f GB" % (d / 1e9)
return Markup(s)
[docs]def roughsize(size, above=20, mod=10):
"""
6 -> '6'
15 -> '15'
134 -> '130+'
"""
if size < above:
return unicode(size)
return u'{:d}+'.format(size - size % mod)
[docs]def datetimeparse(s):
"""
Parse a string date time to a datetime object.
Suitable for dates serialized with .isoformat()
:return: None, or an aware datetime instance, tz=UTC.
"""
try:
dt = dateutil.parser.parse(s)
except:
return None
return utc_dt(dt)
[docs]def age(dt, now=None, add_direction=True, date_threshold=None):
"""
:param dt: :class:`datetime<datetime.datetime>` instance to format
:param now: :class:`datetime<datetime.datetime>` instance to compare to `dt`
:param add_direction: if `True`, will add "in" or "ago" (example for `en`
locale) to time difference `dt - now`, i.e "in 9 min." or " 9min. ago"
:param date_threshold: above threshold, will use a formated date instead of
elapsed time indication. Supported values: "day".
"""
# Fail silently for now XXX
if not dt:
return ""
if not now:
now = datetime.datetime.utcnow()
locale = babel.get_locale()
dt = utc_dt(dt)
now = utc_dt(now)
delta = dt - now
if date_threshold is not None:
dy, dw, dd = dt_cal = dt.isocalendar()
ny, nw, nd = now_cal =now.isocalendar()
if dt_cal != now_cal:
# not same day
remove_year = dy != dy
date_fmt = locale.date_formats['long'].pattern
time_fmt = locale.time_formats['short'].pattern
fmt = locale.datetime_formats['medium']
if remove_year:
date_fmt.replace('y', '').strip()
date_fmt.replace('y', '')
# remove leading or trailing spaces, comma, etc...
date_fmt = re.sub(u'^[^A-Za-z]*|[^A-Za-z]*$', u'', date_fmt)
fmt = fmt.format(time_fmt, date_fmt)
return babel.format_datetime(dt, format=fmt)
# don't use (flask.ext.)babel.format_timedelta: as of Flask-Babel 0.9 it
# doesn't support "threshold" arg.
return format_timedelta(delta,
locale=locale,
granularity='minute',
threshold=0.9,
add_direction=add_direction)
[docs]def date_age(dt, now=None):
# Fail silently for now XXX
if not dt:
return ""
formatted_date = babel.format_datetime(dt, format='yyyy-MM-dd HH:mm')
return u"{} ({})".format(formatted_date, age(dt, now))
[docs]def date(value, format="EE, d MMMM y"):
if isinstance(value, datetime.date):
return babel.format_date(value, format)
else:
return babel.format_date(local_dt(value), format)
[docs]def babel2datepicker(pattern):
"""
Converts date format from babel
(http://babel.pocoo.org/docs/dates/#date-fields)) to a format understood by
bootstrap-datepicker.
"""
if not isinstance(pattern, DateTimePattern):
pattern = parse_pattern(pattern)
map_fmt = {
# days
'd': 'dd',
'dd': 'dd',
'EEE': 'D',
'EEEE': 'DD',
'EEEEE': 'D', # narrow name => short name
# months
'M': 'mm',
'MM': 'mm',
'MMM': 'M',
'MMMM': 'MM',
# years
'y': 'yyyy',
'yy': 'yyyy',
'yyy': 'yyyy',
'yyyy': 'yyyy',
# time picker format
# hours
'h': '%I',
'hh': '%I',
'H': '%H',
'HH': '%H',
# minutes,
'm': '%M',
'mm': '%M',
# seconds
's': '%S',
'ss': '%S',
# am/pm
'a': '%p',
}
return pattern.format % map_fmt
# Doesn't work yet. TZ issues.
[docs]def to_timestamp(dt):
utc_datetime = dt.astimezone(utc)
return timegm(utc_datetime.timetuple()) + utc_datetime.microsecond / 1e6
[docs]def abbrev(s, max_size):
if len(s) <= max_size:
return s
else:
h = max_size // 2 - 1
return s[0:h] + "..." + s[-h:]
@autoescape
[docs]def linkify(s):
return Markup(bleach.linkify(s))
[docs]def obj_to_url(obj):
"""
Find url for obj using :func:`url_for`, return empty string is not found.
:func:`url_for` is also provided in jinja context, the filtering version is
forgiving when `obj` has no default view set.
"""
try:
return url_for(obj)
except BuildError:
return u''
[docs]def init_filters(env):
if isinstance(env, Flask):
# old api for init_filters: we used to pass flask application
env = env.jinja_env
env.filters['nl2br'] = nl2br
env.filters['paragraphs'] = paragraphs
env.filters['date_age'] = date_age
env.filters['datetimeparse'] = datetimeparse
env.filters['age'] = age
env.filters['date'] = date
env.filters['babel2datepicker'] = babel2datepicker
env.filters['to_timestamp'] = to_timestamp
env.filters['url_for'] = obj_to_url
env.filters['abbrev'] = abbrev
env.filters['filesize'] = filesize
env.filters['roughsize'] = roughsize
env.filters['labelize'] = labelize
env.filters['linkify'] = linkify
env.filters['toslug'] = slugify