Source code for gaetk2.jinja_filters

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
jinja_filters - custom jinja2 filters for gaetk2.

Copyright (c) 2010, 2012, 2014, 2017, 2018 Maximillian Dornseif. MIT Licensed.
"""
from __future__ import absolute_import
from __future__ import unicode_literals

import decimal
import json
import logging
import re
import urllib
import warnings

import jinja2

from gaetk2.tools.datetools import convert_to_date
from gaetk2.tools.datetools import convert_to_datetime
from jinja2.utils import Markup


logger = logging.getLogger(__name__)


# Access Control

BLOCKTAGS = """address article aside blockquote canvas div dl
fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hgroup
hr li main nav noscript ol output p pre section table tfoot ul video""".split()
NOTAGS = """dd dt thead tfoot tbody tr td""".split()


[docs]@jinja2.contextfilter def authorize(ctx, value, permission_types, tag=None): """Display content only if the current logged in user has a specific permission. This means if all strings in `permission_types` occur in `credential.permissions`. """ if not isinstance(permission_types, list): permission_types = [permission_types] if tag is None: tag = 'span' m = re.search(r'$\s*<(%s)' % '|'.join(NOTAGS), value) if m: tag = '' else: m = re.search(r'<(%s)' % '|'.join(BLOCKTAGS), value) if m: tag = 'div' # Permissions disabled -> granted granted = ctx.get('request').get('_gaetk_disable_permissions', False) for permission in permission_types: if ctx.get('credential') and permission in ctx.get('credential').permissions: granted = True break if granted: if not tag: return value value = '<{tag} class="gaetk_restricted">{value}</{tag}>'.format( tag=tag, value=jinja2.escape(value)) else: if not ctx.get('credential'): logger.info('context has no credential!') if not tag: value = '…<!-- Berechtigung %s -->' % (', '.join(permission_types)) else: value = '<{tag} class="gaetk_restricted_denied"><!-- !Berechtigung {perm} --></{tag}>'.format( tag=tag, perm=', '.join(permission_types)) if ctx.eval_ctx.autoescape: return Markup(value) return value
[docs]@jinja2.contextfilter def onlystaff(ctx, value, tag=None): """Display content only if the current logged in user `is_staff()`. This tag generatyes HTML. If you don't wan't HTML use this construct:: {% if is_staff() %}Internal Info{% endif %} The Tag encloses content in a ``<span>`` or ``<div>`` depending on it's contents:: {{ "bla"|onlystaff }} <!-- is rendered to: --> <span class="gaetk_onlystaff">bla</span> {% filter onlystaff %} <form ...></form> {% endfilter %} <!-- is rendered to: --> <div class="gaetk_onlystaff"> <form ...></form> </div> {% filter onlystaff %} <i>test text</i> {% endfilter %} <!-- is rendered to: --> <span class="gaetk_onlystaff"><i>test text</i></span> If you not happy with how the filter chooses between ``<span>`` and ``<div>`` you can provide a tag to be used. Or you can provide empty data to avoid all markup:: {% filter onlystaff('p') %} <i>test text</i> {% endfilter %} <!-- is rendered to: --> <p class="gaetk_onlystaff">bla</p> {% filter onlystaff('') %} foo {% endfilter %} <!-- is rendered to: --> foo Automatic detection does not work perfectly within tables. Your milage may vary. If the user is not staff an empty tag is generated:: {% filter onlystaff %} supersecret {% endfilter %} <!-- is rendered to: --> <span class="gaetk_onlystaff-denied"><!-- !is_staff() --></span> {% filter onlystaff('') %} supersecret {% endfilter %} <!-- is rendered to: (nothing) --> """ if tag is None: tag = 'span' m = re.search(r'$\s*<(%s)' % '|'.join(NOTAGS), value) if m: tag = '' else: m = re.search(r'<(%s)' % '|'.join(BLOCKTAGS), value) if m: tag = 'div' granted = False if ctx.get('credential') and ctx.get('credential').staff: granted = True if granted: if not tag: return value value = '<{tag} class="gaetk_onlystaff">{value}</{tag}>'.format( tag=tag, value=jinja2.escape(value)) else: if not ctx.get('credential'): logger.info('context has no credential!') if not tag: return '' value = '<{tag} class="gaetk_onlystaff_denied"><!-- !is_staff() --></{tag}>'.format( tag=tag) return Markup(value)
# Encoding def _attrencode(value): r"""Makes a string valid as an XML attribute value. Includes the quotation marks. Eg:: {{ "jim's garage"|attrencode }} >>> '"jim\' garage"' `xmlattr <http://jinja.pocoo.org/docs/2.10/templates/#xmlattr>`_ in jinja2 is a more sophisticated version of this. """ warnings.warn('`attrencode` is deprecated, use `xmlattr`', DeprecationWarning, stacklevel=2) import xml.sax.saxutils if value is None: return '' if hasattr(value, 'unescape'): # jinja2 Markup value = value.unescape() return xml.sax.saxutils.quoteattr(value)[1:-1]
[docs]def cssencode(value): """Makes a string valid as an CSS class name. This ensured only valid characters are used and the class name starts with an character. This is enforced by prefixing `CSS` if the string does not start with an character:: <div class="{{ 5711|cssencode }} {{ 'root beer'|cssencode }}"> >>> '<div class="CSS5711 root-beer">' """ if value is None: return '' ret = re.sub('[^A-Za-z0-9-_]+', '-', unicode(value)) if ret.startswith(tuple('-0123456789')): ret = 'CSS' + ret return ret
def _to_json(value): """Convert the given Value to JSON. Very helpful to use in Javascript. Similar to `tojson <http://jinja.pocoo.org/docs/2.10/templates/#tojson>`_, but we try to be smarter about encoding of datastore properties. """ warnings.warn('`to_json` is deprecated, use `tojson`', DeprecationWarning, stacklevel=2) return json.dumps(value) # Date-Formatting
[docs]def dateformat(value, formatstring='%Y-%m-%d', nonchar=''): """Formates a date. Tries to convert the given ``value`` to a ``date`` object and then formats it according to ``formatstring``:: {{ date.today()|dateformat }} {{ "20171224"|dateformat('%Y-%W') }} """ if not value: return nonchar return Markup(convert_to_date(value).strftime(formatstring).replace('-', '&#8209;'))
[docs]def datetimeformat(value, formatstring='%Y-%m-%d %H:%M', nonchar=''): """Formates a datetime. Tries to convert the given ``value`` to a ``datetime`` object and then formats it according to ``formatstring``:: {{ datetime.now()|datetimeformat }} {{ "20171224T235959"|datetimeformat('%H:%M') }} """ if not value: return nonchar return Markup(convert_to_datetime(value).strftime(formatstring).replace('-', '&#8209;'))
def _datetime(value, formatstring='%Y-%m-%d %H:%M'): """Legacy function, to be removed.""" warnings.warn('`datetime` is deprecated, use `datetimeformat`', DeprecationWarning, stacklevel=2) return datetimeformat(value, formatstring='%Y-%m-%d %H:%M')
[docs]def tertial(value, nonchar='␀'): """Change a Date oder Datetime-Objekt into a Tertial-String. Tertials are third-years as opposed to quater years:: {{ "20170101"|tertial }} {{ "20170606"|tertial }} {{ "20171224"|tertial }} >>> "2017-A" "2017-B" "2017-C" """ from huTools.calendar.formats import tertial if not value: return nonchar return tertial(value)
# Number-Formating
[docs]def nicenum(value, spacer='\u202F', nonchar='␀'): """Format the given number with spacer as delimiter, e.g. `1 234 456`. Default spacer is NARROW NO-BREAK SPACE U+202F. Probably `style="white-space:nowrap; word-spacing:0.5em;"` would be an CSS based alternative. """ if value is None: return nonchar rev_value = ('%d' % int(value))[::-1] return spacer.join(reversed([rev_value[i:i + 3][::-1] for i in range(0, len(rev_value), 3)]))
[docs]def intword(value, nonchar='␀'): """Converts a large integer to a friendly text representation. Works best for numbers over 1 million. For example, 1000000 becomes '1.0 Mio', 1200000 becomes '1.2 Mio' and '1200000000' becomes '1200 Mio'. """ return _formatint(value, nonchar)
def _formatint(value, nonchar='␀'): """Format an Integer nicely with spacing.""" # Inspired by Django # https://github.com/django/django/blob/master/django/contrib/humanize/templatetags/humanize.py if value is None: return nonchar value = int(value) if abs(value) < 1000000: rev_value = ('%d' % int(value))[::-1] return ' '.join(reversed([rev_value[i:i + 3][::-1] for i in range(0, len(rev_value), 3)])) else: new_value = value / 1000000.0 return '{value:.1f} Mio'.format(value=new_value) return value
[docs]def eurocent(value, spacer='\u202F', decimalplaces=2, nonchar='␀'): """Format the given cents as Euro with spacer as delimiter, e.g. '1 234 456.23'. Obviously works also with US$ and other 100-based. currencies. This is like :func:nicenum. Use ``decimalplaces=0`` to cut of cents, but even better use :func:euroword. Default spacer is NARROW NO-BREAK SPACE U+202F. Probably `style="white-space:nowrap; word-spacing:0.5em;"` would be an CSS based alternative. """ if value is None: return nonchar tmp = str(int(value) / decimal.Decimal(100)) # Cent anhängen if '.' not in tmp: tmp += '.' euro_value, cent_value = tmp.split('.') cent_value = cent_value.ljust(decimalplaces, '0')[:decimalplaces] rev_value = euro_value[::-1] euro_value = spacer.join(reversed([rev_value[i:i + 3][::-1] for i in range(0, len(rev_value), 3)])) return '{}.{}'.format(euro_value, cent_value)
[docs]def euroword(value, plain=False, nonchar='␀'): """Fomat Cents as pretty Euros.""" if value is None: return nonchar return _formatint(value / 100)
[docs]def g2kg(value, spacer='\u202F', nonchar='␀'): """Wandelt meist g in kg um, aber auch in andere Einheiten.""" if value is None: return nonchar if not value: return value elif value < 100: return '{:d}{}g'.format(value, spacer) elif value < 1000 * 50: return '{:.2f}{}kg'.format(value / 1000.0, spacer) elif value < 1000 * 1000: return '{:.1f}{}kg'.format(value / 1000.0, spacer) else: return '{:.1f}{}t'.format(value / 1000.0 ** 2, spacer)
[docs]def percent(value, nonchar='␀'): """Fomat Percent and handle None.""" if value is None: return nonchar return '%.0f' % float(value)
[docs]def iban(value, spacer='\u202F', nonchar='␀'): """Format the given string like an IBAN Account Number. Default spacer is NARROW NO-BREAK SPACE U+202F. Eg:: {{ "DE77123413500000567844"|iban }} DE77 1234 1350 0000 5678 44 """ if not value: return nonchar return spacer.join([value[i:i + 4] for i in range(0, len(value), 4)])
# Text-Formatting
[docs]def markdown(value): """Renders a string as Markdown. Syntax: {{ value|markdown }} We are using `markdown2 <https://pypi.python.org/pypi/markdown2>`_ to do the rendering. """ import markdown2 return Markup(markdown2.markdown(value))
[docs]@jinja2.evalcontextfilter def nl2br(eval_ctx, value): """Newlines in <br/>-Tags konvertieren.""" paragraph_re = re.compile(r'(?:\r\n|\r|\n){2,}') result = '\n\n'.join( '<p>%s</p>' % paragraph.replace('\n', '<br>\n') for paragraph in paragraph_re.split(value)) if eval_ctx.autoescape: return Markup(result) return result
[docs]def left_justify(value, width): """Prefix the given string with spaces until it is width characters long.""" return unicode(value or '').ljust(int(width))
[docs]def right_justify(value, width): """Postfix the given string with spaces until it is width characters long.""" stripped = unicode(value or '')[0:width] return stripped.rjust(int(width))
# Boolean-Formatting (and None)
[docs]def yesno(value, answers='yes,no,maybe'): """Output a text based on Falsyness, Trueyness and ``is None``. Example:: {{ value|yesno:"yeah,nope,maybe" }}. """ bits = answers.split(',') if len(bits) == 3: vyes, vno, vmaybe = bits elif len(bits) == 2: vyes, vno, vmaybe = bits[0], bits[1], bits[1] else: return value if value is None: return vmaybe if value: return vyes return vno
[docs]def onoff(value): """Display Boolean as Font Awesome Icon Icon darstellen. We use Font Awesome `toogle-on <http://fontawesome.io/icon/toggle-on/>`_ and `toogle-of <http://fontawesome.io/icon/toggle-off/>`_ to indicate state. """ if value: return Markup('<i class="fa fa-toggle-on" aria-hidden="true" style="color:green"></i>') else: return Markup('<i class="fa fa-toggle-off" aria-hidden="true" style="color:gray"></i>')
[docs]def none(value, nonchar=''): """Converts ``None`` to ``''``. Similar to ``|default('', true)`` in jinja2 but more explicit. """ if value is None: return nonchar return value
# Datastore Protocol
[docs]def otag(obj): """Link like this: `<a href="obj.url">obj.designator</a>`.""" if not getattr(obj, 'url'): return link = obj.url designator = obj.designator style = '' klass = '' # # wir machen ein bischen intelligente Formatierung hier # # TODO: inaktiv und erledigt und storniert unterscheiden # if getattr(obj, 'erledigt', False): # style = '' # klass = 'class="cs_erledigt"' return Markup('<a href="{}" {} {}>{}</a>'.format( link, style, klass, jinja2.escape(designator)))
[docs]def datastore(entity, attr=None, value=None, text=None): """Generate HTML a-Tag to Google Datastore Query. Example:: {{ credential|datastore }} -> queries for key {{ credential|datastore('email') }} -> queries for email {{ credential|datastore('name', '') }} -> queries for credential.name == '' {{ credential|datastore(text='Search in Datastore') }} -> changes Link-Text """ if not attr: attr = '__key__' value = entity.key.urlsafe() typ = 'KEY' qtext = "SELECT * FROM {} WHERE __key__ = KEY('{}')".format(entity._get_kind(), value) else: if not value: value = getattr(entity, attr, '') typ = 'STR' qtext = "SELECT * FROM {} WHERE __key__ = '{}'".format(entity._get_kind(), value) query = { # TODO: auch INT? auch andere Vergleichsoperatoren? 'filter': '{}/{}|{}|EQ|{}/{}'.format( len(attr), attr, typ, len(value), value), 'kind': entity._get_kind()} url = 'https://console.cloud.google.com/datastore/entities/query?' + urllib.urlencode(query) if text is None: text = qtext content = '<a href="{}">{}</a>'.format(url, jinja2.escape(text)) return Markup(content)
# Misc
[docs]def plural(value, singular_str, plural_str): """Return value with singular or plural form. ``{{ l|length|plural('Items', 'Items') }}`` """ if not isinstance(value, (int, int)): return singular_str if value == 1: return singular_str return plural_str
[docs]def register_custom_filters(jinjaenv): """Register the filters to the given Jinja environment.""" jinjaenv.filters['authorize'] = authorize jinjaenv.filters['onlystaff'] = onlystaff jinjaenv.filters['attrencode'] = _attrencode jinjaenv.filters['cssencode'] = cssencode jinjaenv.filters['to_json'] = _to_json jinjaenv.filters['dateformat'] = dateformat jinjaenv.filters['datetimeformat'] = datetimeformat jinjaenv.filters['datetime'] = _datetime jinjaenv.filters['tertial'] = tertial jinjaenv.filters['nicenum'] = nicenum jinjaenv.filters['intword'] = intword jinjaenv.filters['eurocent'] = eurocent jinjaenv.filters['euroword'] = euroword jinjaenv.filters['percent'] = percent jinjaenv.filters['g2kg'] = g2kg jinjaenv.filters['iban'] = iban jinjaenv.filters['markdown'] = markdown jinjaenv.filters['nl2br'] = nl2br jinjaenv.filters['ljustify'] = left_justify jinjaenv.filters['rjustify'] = right_justify jinjaenv.filters['yesno'] = yesno jinjaenv.filters['onoff'] = onoff jinjaenv.filters['none'] = none jinjaenv.filters['otag'] = otag jinjaenv.filters['datastore'] = datastore jinjaenv.filters['plural'] = plural