Mercurial > libervia-backend
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 |