abilian.web.filters — Abilian 0.1 documentation

# Source code for abilian.web.filters

    """
    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

    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
    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
    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

    def labelize(s):
      return u" ".join([ w.capitalize() for w in s.split(u"_") ])

    def filesize(d):
      if not isinstance(d, int):
        d = int(d)

      if d < 1000:
        s = "%d&nbsp;B" % d

      elif d < 1e4:
        s = "%.1f&nbsp;kB" % (d / 1e3)
      elif d < 1e6:
        s = "%.0f&nbsp;kB" % (d / 1e3)

      elif d < 1e7:
        s = "%.1f&nbsp;MB" % (d / 1e6)
      elif d < 1e9:
        s = "%.0f&nbsp;MB" % (d / 1e6)

      elif d < 1e10:
        s = "%.1f&nbsp;GB" % (d / 1e9)

      else:
        s = "%.0f&nbsp;GB" % (d / 1e9)

      return Markup(s)

    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)

    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)

    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)

    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))

    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)

    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.
    def to_timestamp(dt):
      utc_datetime = dt.astimezone(utc)
      return timegm(utc_datetime.timetuple()) + utc_datetime.microsecond / 1e6

    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
    def linkify(s):
      return Markup(bleach.linkify(s))

    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''

    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