comparison 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
comparison
equal deleted inserted replaced
2402:f905dfe69fcc 2403:dec31114c402
29 import time 29 import time
30 import re 30 import re
31 from babel import support 31 from babel import support
32 from babel import Locale 32 from babel import Locale
33 from babel.core import UnknownLocaleError 33 from babel.core import UnknownLocaleError
34 from babel import dates
34 try: 35 try:
35 import sat_templates 36 import sat_templates
36 except ImportError: 37 except ImportError:
37 raise exceptions.MissingModule(u'sat_templates module is not available, please install it or check your path to use template engine') 38 raise exceptions.MissingModule(u'sat_templates module is not available, please install it or check your path to use template engine')
38 else: 39 else:
42 import jinja2 43 import jinja2
43 except: 44 except:
44 raise exceptions.MissingModule(u'Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2') 45 raise exceptions.MissingModule(u'Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2')
45 46
46 from jinja2 import Markup as safe 47 from jinja2 import Markup as safe
48 from jinja2 import is_undefined
47 49
48 HTML_EXT = ('html', 'xhtml') 50 HTML_EXT = ('html', 'xhtml')
49 DEFAULT_LOCALE = u'en' 51 DEFAULT_LOCALE = u'en_GB'
50 RE_ATTR_ESCAPE = re.compile(r'[^a-z_-]') 52 RE_ATTR_ESCAPE = re.compile(r'[^a-z_-]')
51 # TODO: handle external path (an additional search path for templates should be settable by user 53 # TODO: handle external path (an additional search path for templates should be settable by user
52 # TODO: handle absolute URL (should be used for trusted use cases) only (e.g. jp) for security reason 54 # TODO: handle absolute URL (should be used for trusted use cases) only (e.g. jp) for security reason
53 55
54 56
198 # we want to have access to SàT constants in templates 200 # we want to have access to SàT constants in templates
199 self.env.globals[u'C'] = C 201 self.env.globals[u'C'] = C
200 # custom filters 202 # custom filters
201 self.env.filters['next_gidx'] = self._next_gidx 203 self.env.filters['next_gidx'] = self._next_gidx
202 self.env.filters['cur_gidx'] = self._cur_gidx 204 self.env.filters['cur_gidx'] = self._cur_gidx
203 self.env.filters['date_days'] = self._date_days 205 self.env.filters['date_fmt'] = self._date_fmt
204 self.env.filters['xmlui_class'] = self._xmlui_class 206 self.env.filters['xmlui_class'] = self._xmlui_class
205 self.env.filters['attr_escape'] = self.attr_escape 207 self.env.filters['attr_escape'] = self.attr_escape
206 self.env.filters['item_filter'] = self._item_filter 208 self.env.filters['item_filter'] = self._item_filter
207 self.env.filters['adv_format'] = self._adv_format 209 self.env.filters['adv_format'] = self._adv_format
210 # custom tests
211 self.env.tests['in_the_past'] = self._in_the_past
208 212
209 def installTranslations(self): 213 def installTranslations(self):
210 i18n_dir = os.path.join(self.base_dir, 'i18n') 214 i18n_dir = os.path.join(self.base_dir, 'i18n')
211 self.translations = {} 215 self.translations = {}
212 for lang_dir in os.listdir(i18n_dir): 216 for lang_dir in os.listdir(i18n_dir):
224 else: 228 else:
225 log.info(_(u'loaded {lang} templates translations').format(lang=lang_dir)) 229 log.info(_(u'loaded {lang} templates translations').format(lang=lang_dir))
226 self.env.install_null_translations(True) 230 self.env.install_null_translations(True)
227 231
228 def setLocale(self, locale_str): 232 def setLocale(self, locale_str):
233 """set current locale
234
235 change current translation locale and self self._locale and self._locale_str
236 """
229 if locale_str == self._locale_str: 237 if locale_str == self._locale_str:
230 return 238 return
239 if locale_str == 'en':
240 # we default to GB English when it's not specified
241 # one of the main reason is to avoid the nonsense U.S. short date format
242 locale_str = 'en_GB'
231 try: 243 try:
232 locale = Locale.parse(locale_str) 244 locale = Locale.parse(locale_str)
233 except ValueError as e: 245 except ValueError as e:
234 log.warning(_(u"invalid locale value: {msg}").format(msg=e)) 246 log.warning(_(u"invalid locale value: {msg}").format(msg=e))
235 locale_str = self._locale_str = DEFAULT_LOCALE 247 locale_str = self._locale_str = DEFAULT_LOCALE
322 if css_path is not None: 334 if css_path is not None:
323 css_files.append(css_path) 335 css_files.append(css_path)
324 336
325 return css_files 337 return css_files
326 338
339
340 ## custom filters ##
341
327 @jinja2.contextfilter 342 @jinja2.contextfilter
328 def _next_gidx(self, ctx, value): 343 def _next_gidx(self, ctx, value):
329 """Use next current global index as suffix""" 344 """Use next current global index as suffix"""
330 next_ = ctx['gidx'].next(value) 345 next_ = ctx['gidx'].next(value)
331 return value if next_ == 0 else u"{}_{}".format(value, next_) 346 return value if next_ == 0 else u"{}_{}".format(value, next_)
334 def _cur_gidx(self, ctx, value): 349 def _cur_gidx(self, ctx, value):
335 """Use current current global index as suffix""" 350 """Use current current global index as suffix"""
336 current = ctx['gidx'].current(value) 351 current = ctx['gidx'].current(value)
337 return value if not current else u"{}_{}".format(value, current) 352 return value if not current else u"{}_{}".format(value, current)
338 353
339 def _date_days(self, timestamp): 354 def _date_fmt(self, timestamp, fmt='short', date_only=False, auto_limit=None, auto_old_fmt=None):
340 return int(time.time() - int(timestamp))/(3600*24) 355 """format date according to locale
356
357 @param timestamp(basestring, int): unix time
358 @param fmt(str): one of:
359 - short: e.g. u'31/12/17'
360 - medium: e.g. u'Apr 1, 2007'
361 - long: e.g. u'April 1, 2007'
362 - full: e.g. u'Sunday, April 1, 2007'
363 - relative: format in relative time
364 e.g.: 3 hours
365 note that this format is not precise
366 - iso: ISO 8601 format
367 e.g.: u'2007-04-01T19:53:23Z'
368 - auto_limit (int, None): limit in days before using auto_old_fmt
369 None: use default(7 days)
370 - auto_old_fmt(unicode, None): format to use when date is olded than limit
371 None: use default(short)
372 or a free value which is passed to babel.dates.format_datetime
373 @param date_only(bool): if True, only display date (not datetime)
374
375 """
376 if is_undefined(fmt):
377 fmt = u'short'
378
379 if (auto_limit is not None or auto_old_fmt is not None) and fmt != 'auto':
380 raise ValueError(u'auto argument can only be used with auto fmt')
381 if fmt == 'auto':
382 days_delta = (time.time() - int(timestamp)) / 3600
383 if days_delta > (auto_limit or 7):
384 fmt = auto_old_fmt or 'short'
385 else:
386 fmt = 'relative'
387
388 if fmt == 'relative':
389 delta = int(timestamp) - time.time()
390 return dates.format_timedelta(delta, granularity="minute", add_direction=True, locale=self._locale_str)
391 elif fmt in ('short', 'long'):
392 formatter = dates.format_date if date_only else dates.format_datetime
393 return formatter(int(timestamp), format=fmt, locale=self._locale_str)
394 elif fmt == 'iso':
395 if date_only:
396 fmt = 'yyyy-MM-dd'
397 else:
398 fmt = "yyyy-MM-ddTHH:mm:ss'Z'"
399 return dates.format_datetime(int(timestamp), format=fmt)
400 else:
401 return dates.format_datetime(int(timestamp), format=fmt, locale=self._locale_str)
341 402
342 def attr_escape(self, text): 403 def attr_escape(self, text):
343 """escape a text to a value usable as an attribute 404 """escape a text to a value usable as an attribute
344 405
345 remove spaces, and put in lower case 406 remove spaces, and put in lower case
407 if template is None: 468 if template is None:
408 return value 469 return value
409 # jinja use string when no special char is used, so we have to convert to unicode 470 # jinja use string when no special char is used, so we have to convert to unicode
410 return unicode(template).format(value=value, **kwargs) 471 return unicode(template).format(value=value, **kwargs)
411 472
473 ## custom tests ##
474
475 def _in_the_past(self, timestamp):
476 """check if a date is in the past
477
478 @param timestamp(unicode, int): unix time
479 @return (bool): True if date is in the past
480 """
481 return time.time() > int(timestamp)
482
412 def render(self, template, theme=None, locale=DEFAULT_LOCALE, root_path=u'', media_path=u'', css_files=None, css_inline=False, **kwargs): 483 def render(self, template, theme=None, locale=DEFAULT_LOCALE, root_path=u'', media_path=u'', css_files=None, css_inline=False, **kwargs):
413 """render a template 484 """render a template
414 485
415 @param template(unicode): template to render (e.g. blog/articles.html) 486 @param template(unicode): template to render (e.g. blog/articles.html)
416 @param theme(unicode): template theme 487 @param theme(unicode): template theme