diff src/tools/common/template.py @ 2403:dec31114c402

template: improved date formatter: the date filter is now named "date_fmt" and can be use to specify several useful formats (see docstring), using the current locale. The "relative" format return the whole human readable relative date. The new in_the_past test allow to check if a date is passed.
author Goffi <goffi@goffi.org>
date Fri, 27 Oct 2017 18:20:10 +0200
parents f905dfe69fcc
children 8b37a62336c3
line wrap: on
line diff
--- a/src/tools/common/template.py	Fri Oct 27 18:17:35 2017 +0200
+++ b/src/tools/common/template.py	Fri Oct 27 18:20:10 2017 +0200
@@ -31,6 +31,7 @@
 from babel import support
 from babel import Locale
 from babel.core import UnknownLocaleError
+from babel import dates
 try:
     import sat_templates
 except ImportError:
@@ -44,9 +45,10 @@
     raise exceptions.MissingModule(u'Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2')
 
 from jinja2 import Markup as safe
+from jinja2 import is_undefined
 
 HTML_EXT = ('html', 'xhtml')
-DEFAULT_LOCALE = u'en'
+DEFAULT_LOCALE = u'en_GB'
 RE_ATTR_ESCAPE = re.compile(r'[^a-z_-]')
 # TODO: handle external path (an additional search path for templates should be settable by user
 # TODO: handle absolute URL (should be used for trusted use cases) only (e.g. jp) for security reason
@@ -200,11 +202,13 @@
         # custom filters
         self.env.filters['next_gidx'] = self._next_gidx
         self.env.filters['cur_gidx'] = self._cur_gidx
-        self.env.filters['date_days'] = self._date_days
+        self.env.filters['date_fmt'] = self._date_fmt
         self.env.filters['xmlui_class'] = self._xmlui_class
         self.env.filters['attr_escape'] = self.attr_escape
         self.env.filters['item_filter'] = self._item_filter
         self.env.filters['adv_format'] = self._adv_format
+        # custom tests
+        self.env.tests['in_the_past'] = self._in_the_past
 
     def installTranslations(self):
         i18n_dir = os.path.join(self.base_dir, 'i18n')
@@ -226,8 +230,16 @@
         self.env.install_null_translations(True)
 
     def setLocale(self, locale_str):
+        """set current locale
+
+        change current translation locale and self self._locale and self._locale_str
+        """
         if locale_str == self._locale_str:
             return
+        if locale_str == 'en':
+            # we default to GB English when it's not specified
+            # one of the main reason is to avoid the nonsense U.S. short date format
+            locale_str = 'en_GB'
         try:
             locale = Locale.parse(locale_str)
         except ValueError as e:
@@ -324,6 +336,9 @@
 
         return css_files
 
+
+    ## custom filters ##
+
     @jinja2.contextfilter
     def _next_gidx(self, ctx, value):
         """Use next current global index as suffix"""
@@ -336,8 +351,54 @@
         current = ctx['gidx'].current(value)
         return value if not current else u"{}_{}".format(value, current)
 
-    def _date_days(self, timestamp):
-        return int(time.time() - int(timestamp))/(3600*24)
+    def _date_fmt(self, timestamp, fmt='short', date_only=False, auto_limit=None, auto_old_fmt=None):
+        """format date according to locale
+
+        @param timestamp(basestring, int): unix time
+        @param fmt(str): one of:
+            - short: e.g. u'31/12/17'
+            - medium: e.g. u'Apr 1, 2007'
+            - long: e.g. u'April 1, 2007'
+            - full: e.g. u'Sunday, April 1, 2007'
+            - relative: format in relative time
+                e.g.: 3 hours
+                note that this format is not precise
+            - iso: ISO 8601 format
+                e.g.: u'2007-04-01T19:53:23Z'
+            - auto_limit (int, None): limit in days before using auto_old_fmt
+                None: use default(7 days)
+            - auto_old_fmt(unicode, None): format to use when date is olded than limit
+                None: use default(short)
+            or a free value which is passed to babel.dates.format_datetime
+        @param date_only(bool): if True, only display date (not datetime)
+
+        """
+        if is_undefined(fmt):
+            fmt = u'short'
+
+        if (auto_limit is not None or auto_old_fmt is not None) and fmt != 'auto':
+            raise ValueError(u'auto argument can only be used with auto fmt')
+        if fmt == 'auto':
+            days_delta = (time.time() - int(timestamp)) / 3600
+            if days_delta > (auto_limit or 7):
+                fmt = auto_old_fmt or 'short'
+            else:
+                fmt = 'relative'
+
+        if fmt == 'relative':
+            delta = int(timestamp) - time.time()
+            return dates.format_timedelta(delta, granularity="minute", add_direction=True, locale=self._locale_str)
+        elif fmt in ('short', 'long'):
+            formatter = dates.format_date if date_only else dates.format_datetime
+            return formatter(int(timestamp), format=fmt, locale=self._locale_str)
+        elif fmt == 'iso':
+            if date_only:
+                fmt = 'yyyy-MM-dd'
+            else:
+                fmt = "yyyy-MM-ddTHH:mm:ss'Z'"
+            return dates.format_datetime(int(timestamp), format=fmt)
+        else:
+            return dates.format_datetime(int(timestamp), format=fmt, locale=self._locale_str)
 
     def attr_escape(self, text):
         """escape a text to a value usable as an attribute
@@ -409,6 +470,16 @@
         # jinja use string when no special char is used, so we have to convert to unicode
         return unicode(template).format(value=value, **kwargs)
 
+    ## custom tests ##
+
+    def _in_the_past(self, timestamp):
+        """check if a date is in the past
+
+        @param timestamp(unicode, int): unix time
+        @return (bool): True if date is in the past
+        """
+        return time.time() > int(timestamp)
+
     def render(self, template, theme=None, locale=DEFAULT_LOCALE, root_path=u'', media_path=u'', css_files=None, css_inline=False, **kwargs):
         """render a template