comparison sat/tools/common/template.py @ 3028:ab2696e34d29

Python 3 port: /!\ this is a huge commit /!\ starting from this commit, SàT is needs Python 3.6+ /!\ SàT maybe be instable or some feature may not work anymore, this will improve with time This patch port backend, bridge and frontends to Python 3. Roughly this has been done this way: - 2to3 tools has been applied (with python 3.7) - all references to python2 have been replaced with python3 (notably shebangs) - fixed files not handled by 2to3 (notably the shell script) - several manual fixes - fixed issues reported by Python 3 that where not handled in Python 2 - replaced "async" with "async_" when needed (it's a reserved word from Python 3.7) - replaced zope's "implements" with @implementer decorator - temporary hack to handle data pickled in database, as str or bytes may be returned, to be checked later - fixed hash comparison for password - removed some code which is not needed anymore with Python 3 - deactivated some code which needs to be checked (notably certificate validation) - tested with jp, fixed reported issues until some basic commands worked - ported Primitivus (after porting dependencies like urwid satext) - more manual fixes
author Goffi <goffi@goffi.org>
date Tue, 13 Aug 2019 19:08:41 +0200
parents d8857e913309
children 5f3068915686
comparison
equal deleted inserted replaced
3027:ff5bcb12ae60 3028:ab2696e34d29
1 #!/usr/bin/env python2 1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 3
4 # SAT: a jabber client 4 # SAT: a jabber client
5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) 5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org)
6 6
39 39
40 try: 40 try:
41 import sat_templates 41 import sat_templates
42 except ImportError: 42 except ImportError:
43 raise exceptions.MissingModule( 43 raise exceptions.MissingModule(
44 u"sat_templates module is not available, please install it or check your path to " 44 "sat_templates module is not available, please install it or check your path to "
45 u"use template engine" 45 "use template engine"
46 ) 46 )
47 else: 47 else:
48 sat_templates # to avoid pyflakes warning 48 sat_templates # to avoid pyflakes warning
49 49
50 try: 50 try:
51 import jinja2 51 import jinja2
52 except: 52 except:
53 raise exceptions.MissingModule( 53 raise exceptions.MissingModule(
54 u"Missing module jinja2, please install it from http://jinja.pocoo.org or with " 54 "Missing module jinja2, please install it from http://jinja.pocoo.org or with "
55 u"pip install jinja2" 55 "pip install jinja2"
56 ) 56 )
57 57
58 from jinja2 import Markup as safe 58 from jinja2 import Markup as safe
59 from jinja2 import is_undefined 59 from jinja2 import is_undefined
60 from jinja2 import utils 60 from jinja2 import utils
65 65
66 log = getLogger(__name__) 66 log = getLogger(__name__)
67 67
68 HTML_EXT = ("html", "xhtml") 68 HTML_EXT = ("html", "xhtml")
69 RE_ATTR_ESCAPE = re.compile(r"[^a-z_-]") 69 RE_ATTR_ESCAPE = re.compile(r"[^a-z_-]")
70 SITE_RESERVED_NAMES = (u"sat",) 70 SITE_RESERVED_NAMES = ("sat",)
71 TPL_RESERVED_CHARS = ur"()/." 71 TPL_RESERVED_CHARS = r"()/."
72 RE_TPL_RESERVED_CHARS = re.compile(u"[" + TPL_RESERVED_CHARS + u"]") 72 RE_TPL_RESERVED_CHARS = re.compile("[" + TPL_RESERVED_CHARS + "]")
73 73
74 TemplateData = namedtuple("TemplateData", ['site', 'theme', 'path']) 74 TemplateData = namedtuple("TemplateData", ['site', 'theme', 'path'])
75 75
76 76
77 class TemplateLoader(jinja2.BaseLoader): 77 class TemplateLoader(jinja2.BaseLoader):
83 @param trusted(bool): if True, absolue template paths will be allowed 83 @param trusted(bool): if True, absolue template paths will be allowed
84 be careful when using this option and sure that you can trust the template, 84 be careful when using this option and sure that you can trust the template,
85 as this allow the template to open any file on the system that the 85 as this allow the template to open any file on the system that the
86 launching user can access. 86 launching user can access.
87 """ 87 """
88 if not sites_paths or not u"" in sites_paths: 88 if not sites_paths or not "" in sites_paths:
89 raise exceptions.InternalError(u"Invalid sites_paths") 89 raise exceptions.InternalError("Invalid sites_paths")
90 super(jinja2.BaseLoader, self).__init__() 90 super(jinja2.BaseLoader, self).__init__()
91 self.sites_paths = sites_paths 91 self.sites_paths = sites_paths
92 self.trusted = trusted 92 self.trusted = trusted
93 93
94 @staticmethod 94 @staticmethod
106 site, theme and template_path. 106 site, theme and template_path.
107 if site is empty, SàT Templates are used 107 if site is empty, SàT Templates are used
108 site and theme can be both None if absolute path is used 108 site and theme can be both None if absolute path is used
109 Relative path is the path from theme root dir e.g. blog/articles.html 109 Relative path is the path from theme root dir e.g. blog/articles.html
110 """ 110 """
111 if template.startswith(u"("): 111 if template.startswith("("):
112 # site and/or theme are specified 112 # site and/or theme are specified
113 try: 113 try:
114 theme_end = template.index(u")") 114 theme_end = template.index(")")
115 except IndexError: 115 except IndexError:
116 raise ValueError(u"incorrect site/theme in template") 116 raise ValueError("incorrect site/theme in template")
117 theme_data = template[1:theme_end] 117 theme_data = template[1:theme_end]
118 theme_splitted = theme_data.split(u'/') 118 theme_splitted = theme_data.split('/')
119 if len(theme_splitted) == 1: 119 if len(theme_splitted) == 1:
120 site, theme = u"", theme_splitted[0] 120 site, theme = "", theme_splitted[0]
121 elif len(theme_splitted) == 2: 121 elif len(theme_splitted) == 2:
122 site, theme = theme_splitted 122 site, theme = theme_splitted
123 else: 123 else:
124 raise ValueError(u"incorrect site/theme in template") 124 raise ValueError("incorrect site/theme in template")
125 template_path = template[theme_end+1:] 125 template_path = template[theme_end+1:]
126 if not template_path or template_path.startswith(u"/"): 126 if not template_path or template_path.startswith("/"):
127 raise ValueError(u"incorrect template path") 127 raise ValueError("incorrect template path")
128 elif template.startswith(u"/"): 128 elif template.startswith("/"):
129 # this is an absolute path, so we have no site and no theme 129 # this is an absolute path, so we have no site and no theme
130 site = None 130 site = None
131 theme = None 131 theme = None
132 template_path = template 132 template_path = template
133 else: 133 else:
134 # a default template 134 # a default template
135 site = u"" 135 site = ""
136 theme = C.TEMPLATE_THEME_DEFAULT 136 theme = C.TEMPLATE_THEME_DEFAULT
137 template_path = template 137 template_path = template
138 138
139 if site is not None: 139 if site is not None:
140 site = site.strip() 140 site = site.strip()
141 if not site: 141 if not site:
142 site = u"" 142 site = ""
143 elif site in SITE_RESERVED_NAMES: 143 elif site in SITE_RESERVED_NAMES:
144 raise ValueError(_(u"{site} can't be used as site name, " 144 raise ValueError(_("{site} can't be used as site name, "
145 u"it's reserved.").format(site=site)) 145 "it's reserved.").format(site=site))
146 146
147 if theme is not None: 147 if theme is not None:
148 theme = theme.strip() 148 theme = theme.strip()
149 if not theme: 149 if not theme:
150 theme = C.TEMPLATE_THEME_DEFAULT 150 theme = C.TEMPLATE_THEME_DEFAULT
151 if RE_TPL_RESERVED_CHARS.search(theme): 151 if RE_TPL_RESERVED_CHARS.search(theme):
152 raise ValueError(_(u"{theme} contain forbidden char. Following chars " 152 raise ValueError(_("{theme} contain forbidden char. Following chars "
153 u"are forbidden: {reserved}").format( 153 "are forbidden: {reserved}").format(
154 theme=theme, reserved=TPL_RESERVED_CHARS)) 154 theme=theme, reserved=TPL_RESERVED_CHARS))
155 155
156 return TemplateData(site, theme, template_path) 156 return TemplateData(site, theme, template_path)
157 157
158 @staticmethod 158 @staticmethod
167 sites_and_themes = [[site, theme]] 167 sites_and_themes = [[site, theme]]
168 if theme != C.TEMPLATE_THEME_DEFAULT: 168 if theme != C.TEMPLATE_THEME_DEFAULT:
169 sites_and_themes.append([site, C.TEMPLATE_THEME_DEFAULT]) 169 sites_and_themes.append([site, C.TEMPLATE_THEME_DEFAULT])
170 if site: 170 if site:
171 # the site is not the default one, so we add default at the end 171 # the site is not the default one, so we add default at the end
172 sites_and_themes.append([u'', C.TEMPLATE_THEME_DEFAULT]) 172 sites_and_themes.append(['', C.TEMPLATE_THEME_DEFAULT])
173 return sites_and_themes 173 return sites_and_themes
174 174
175 def _get_template_f(self, site, theme, path_elts): 175 def _get_template_f(self, site, theme, path_elts):
176 """Look for template and return opened file if found 176 """Look for template and return opened file if found
177 177
183 - opened template, or None if not found 183 - opened template, or None if not found
184 - absolute file path, or None if not found 184 - absolute file path, or None if not found
185 """ 185 """
186 if site is None: 186 if site is None:
187 raise exceptions.InternalError( 187 raise exceptions.InternalError(
188 u"_get_template_f must not be used with absolute path") 188 "_get_template_f must not be used with absolute path")
189 for site, theme in self.getSitesAndThemes(site, theme): 189 for site, theme in self.getSitesAndThemes(site, theme):
190 try: 190 try:
191 base_path = self.sites_paths[site] 191 base_path = self.sites_paths[site]
192 except KeyError: 192 except KeyError:
193 log.warning(_(u"Unregistered site requested: {site}").format( 193 log.warning(_("Unregistered site requested: {site}").format(
194 site=site)) 194 site=site))
195 filepath = os.path.join(base_path, C.TEMPLATE_TPL_DIR, theme, *path_elts) 195 filepath = os.path.join(base_path, C.TEMPLATE_TPL_DIR, theme, *path_elts)
196 f = utils.open_if_exists(filepath) 196 f = utils.open_if_exists(filepath, 'r')
197 if f is not None: 197 if f is not None:
198 return f, filepath 198 return f, filepath
199 return None, None 199 return None, None
200 200
201 def get_source(self, environment, template): 201 def get_source(self, environment, template):
209 site, theme, template_path = self.parse_template(template) 209 site, theme, template_path = self.parse_template(template)
210 210
211 if site is None: 211 if site is None:
212 # we have an abolute template 212 # we have an abolute template
213 if theme is not None: 213 if theme is not None:
214 raise exceptions.InternalError(u"We can't have a theme with absolute " 214 raise exceptions.InternalError("We can't have a theme with absolute "
215 u"template.") 215 "template.")
216 if not self.trusted: 216 if not self.trusted:
217 log.error(_(u"Absolute template used while unsecure is disabled, hack " 217 log.error(_("Absolute template used while unsecure is disabled, hack "
218 u"attempt? Template: {template}").format(template=template)) 218 "attempt? Template: {template}").format(template=template))
219 raise exceptions.PermissionError(u"absolute template is not allowed") 219 raise exceptions.PermissionError("absolute template is not allowed")
220 filepath = template_path 220 filepath = template_path
221 f = utils.open_if_exists(filepath) 221 f = utils.open_if_exists(filepath, 'r')
222 else: 222 else:
223 # relative path, we have to deal with site and theme 223 # relative path, we have to deal with site and theme
224 assert theme and template_path 224 assert theme and template_path
225 path_elts = split_template_path(template_path) 225 path_elts = split_template_path(template_path)
226 # if we have non default site, we check it first, else we only check default 226 # if we have non default site, we check it first, else we only check default
227 f, filepath = self._get_template_f(site, theme, path_elts) 227 f, filepath = self._get_template_f(site, theme, path_elts)
228 228
229 if f is None: 229 if f is None:
230 if (site is not None and path_elts[0] == u"error" 230 if (site is not None and path_elts[0] == "error"
231 and os.path.splitext(template_path)[1][1:] in HTML_EXT): 231 and os.path.splitext(template_path)[1][1:] in HTML_EXT):
232 # if an HTML error is requested but doesn't exist, we try again 232 # if an HTML error is requested but doesn't exist, we try again
233 # with base error. 233 # with base error.
234 f, filepath = self._get_template_f( 234 f, filepath = self._get_template_f(
235 site, theme, ("error", "base.html")) 235 site, theme, ("error", "base.html"))
236 if f is None: 236 if f is None:
237 raise exceptions.InternalError(u"error/base.html should exist") 237 raise exceptions.InternalError("error/base.html should exist")
238 else: 238 else:
239 raise TemplateNotFound(template) 239 raise TemplateNotFound(template)
240 240
241 try: 241 try:
242 contents = f.read().decode('utf-8') 242 contents = f.read()
243 finally: 243 finally:
244 f.close() 244 f.close()
245 245
246 mtime = os.path.getmtime(filepath) 246 mtime = os.path.getmtime(filepath)
247 247
283 Must be used before base.html is extended, as <script> are generated there. 283 Must be used before base.html is extended, as <script> are generated there.
284 If called several time with the same library, it will be imported once. 284 If called several time with the same library, it will be imported once.
285 @param library_name(unicode): name of the library to import 285 @param library_name(unicode): name of the library to import
286 @param loading: 286 @param loading:
287 """ 287 """
288 if attribute not in (u"defer", u"async", u""): 288 if attribute not in ("defer", "async", ""):
289 raise exceptions.DataError( 289 raise exceptions.DataError(
290 _(u'Invalid attribute, please use one of "defer", "async" or ""') 290 _('Invalid attribute, please use one of "defer", "async" or ""')
291 ) 291 )
292 if not library_name.endswith(u".js"): 292 if not library_name.endswith(".js"):
293 library_name = library_name + u".js" 293 library_name = library_name + ".js"
294 if (library_name, attribute) not in self.scripts: 294 if (library_name, attribute) not in self.scripts:
295 self.scripts.append((library_name, attribute)) 295 self.scripts.append((library_name, attribute))
296 return u"" 296 return ""
297 297
298 def generate_scripts(self): 298 def generate_scripts(self):
299 """Generate the <script> elements 299 """Generate the <script> elements
300 300
301 @return (unicode): <scripts> HTML tags 301 @return (unicode): <scripts> HTML tags
302 """ 302 """
303 scripts = [] 303 scripts = []
304 tpl = u"<script src={src} {attribute}></script>" 304 tpl = "<script src={src} {attribute}></script>"
305 for library, attribute in self.scripts: 305 for library, attribute in self.scripts:
306 library_path = self.renderer.getStaticPath(self.template_data, library) 306 library_path = self.renderer.getStaticPath(self.template_data, library)
307 if library_path is None: 307 if library_path is None:
308 log.warning(_(u"Can't find {libary} javascript library").format( 308 log.warning(_("Can't find {libary} javascript library").format(
309 library=library)) 309 library=library))
310 continue 310 continue
311 path = self.renderer.getFrontURL(library_path) 311 path = self.renderer.getFrontURL(library_path)
312 scripts.append(tpl.format(src=quoteattr(path), attribute=attribute)) 312 scripts.append(tpl.format(src=quoteattr(path), attribute=attribute))
313 return safe(u"\n".join(scripts)) 313 return safe("\n".join(scripts))
314 314
315 315
316 class Environment(jinja2.Environment): 316 class Environment(jinja2.Environment):
317 317
318 def get_template(self, name, parent=None, globals=None): 318 def get_template(self, name, parent=None, globals=None):
319 if name[0] not in (u'/', u'('): 319 if name[0] not in ('/', '('):
320 # if name is not an absolute path or a full template name (this happen on 320 # if name is not an absolute path or a full template name (this happen on
321 # extend or import during rendering), we convert it to a full template name. 321 # extend or import during rendering), we convert it to a full template name.
322 # This is needed to handle cache correctly when a base template is overriden. 322 # This is needed to handle cache correctly when a base template is overriden.
323 # Without that, we could not distinguish something like base/base.html if 323 # Without that, we could not distinguish something like base/base.html if
324 # it's launched from some_site/some_theme or from [default]/default 324 # it's launched from some_site/some_theme or from [default]/default
325 name = u"({site}/{theme}){template}".format( 325 name = "({site}/{theme}){template}".format(
326 site=self._template_data.site, 326 site=self._template_data.site,
327 theme=self._template_data.theme, 327 theme=self._template_data.theme,
328 template=name) 328 template=name)
329 329
330 return super(Environment, self).get_template(name, parent, globals) 330 return super(Environment, self).get_template(name, parent, globals)
346 @param private(bool): if True, also load sites from sites_path_private_dict 346 @param private(bool): if True, also load sites from sites_path_private_dict
347 """ 347 """
348 self.host = host 348 self.host = host
349 self.trusted = trusted 349 self.trusted = trusted
350 self.sites_paths = { 350 self.sites_paths = {
351 u"": os.path.dirname(sat_templates.__file__), 351 "": os.path.dirname(sat_templates.__file__),
352 } 352 }
353 conf = config.parseMainConf() 353 conf = config.parseMainConf()
354 public_sites = config.getConfig(conf, None, u"sites_path_public_dict", {}) 354 public_sites = config.getConfig(conf, None, "sites_path_public_dict", {})
355 sites_data = [public_sites] 355 sites_data = [public_sites]
356 if private: 356 if private:
357 private_sites = config.getConfig(conf, None, u"sites_path_private_dict", {}) 357 private_sites = config.getConfig(conf, None, "sites_path_private_dict", {})
358 sites_data.append(private_sites) 358 sites_data.append(private_sites)
359 for sites in sites_data: 359 for sites in sites_data:
360 normalised = {} 360 normalised = {}
361 for name, path in sites.iteritems(): 361 for name, path in sites.items():
362 if RE_TPL_RESERVED_CHARS.search(name): 362 if RE_TPL_RESERVED_CHARS.search(name):
363 log.warning(_(u"Can't add \"{name}\" site, it contains forbidden " 363 log.warning(_("Can't add \"{name}\" site, it contains forbidden "
364 u"characters. Forbidden characters are {forbidden}.") 364 "characters. Forbidden characters are {forbidden}.")
365 .format(name=name, forbidden=TPL_RESERVED_CHARS)) 365 .format(name=name, forbidden=TPL_RESERVED_CHARS))
366 continue 366 continue
367 path = os.path.expanduser(os.path.normpath(path)) 367 path = os.path.expanduser(os.path.normpath(path))
368 if not path or not path.startswith(u"/"): 368 if not path or not path.startswith("/"):
369 log.warning(_(u"Can't add \"{name}\" site, it should map to an " 369 log.warning(_("Can't add \"{name}\" site, it should map to an "
370 u"absolute path").format(name=name)) 370 "absolute path").format(name=name))
371 continue 371 continue
372 normalised[name] = path 372 normalised[name] = path
373 self.sites_paths.update(normalised) 373 self.sites_paths.update(normalised)
374 374
375 self.env = Environment( 375 self.env = Environment(
383 self._locale_str = C.DEFAULT_LOCALE 383 self._locale_str = C.DEFAULT_LOCALE
384 self._locale = Locale.parse(self._locale_str) 384 self._locale = Locale.parse(self._locale_str)
385 self.installTranslations() 385 self.installTranslations()
386 386
387 # we want to have access to SàT constants in templates 387 # we want to have access to SàT constants in templates
388 self.env.globals[u"C"] = C 388 self.env.globals["C"] = C
389 389
390 # custom filters 390 # custom filters
391 self.env.filters[u"next_gidx"] = self._next_gidx 391 self.env.filters["next_gidx"] = self._next_gidx
392 self.env.filters[u"cur_gidx"] = self._cur_gidx 392 self.env.filters["cur_gidx"] = self._cur_gidx
393 self.env.filters[u"date_fmt"] = self._date_fmt 393 self.env.filters["date_fmt"] = self._date_fmt
394 self.env.filters[u"xmlui_class"] = self._xmlui_class 394 self.env.filters["xmlui_class"] = self._xmlui_class
395 self.env.filters[u"attr_escape"] = self.attr_escape 395 self.env.filters["attr_escape"] = self.attr_escape
396 self.env.filters[u"item_filter"] = self._item_filter 396 self.env.filters["item_filter"] = self._item_filter
397 self.env.filters[u"adv_format"] = self._adv_format 397 self.env.filters["adv_format"] = self._adv_format
398 self.env.filters[u"dict_ext"] = self._dict_ext 398 self.env.filters["dict_ext"] = self._dict_ext
399 self.env.filters[u"highlight"] = self.highlight 399 self.env.filters["highlight"] = self.highlight
400 self.env.filters[u"front_url"] = (self._front_url if front_url_filter is None 400 self.env.filters["front_url"] = (self._front_url if front_url_filter is None
401 else front_url_filter) 401 else front_url_filter)
402 # custom tests 402 # custom tests
403 self.env.tests[u"in_the_past"] = self._in_the_past 403 self.env.tests["in_the_past"] = self._in_the_past
404 self.icons_path = os.path.join(host.media_dir, u"fonts/fontello/svg") 404 self.icons_path = os.path.join(host.media_dir, "fonts/fontello/svg")
405 405
406 # policies 406 # policies
407 self.env.policies[u"ext.i18n.trimmed"] = True 407 self.env.policies["ext.i18n.trimmed"] = True
408 408
409 def getFrontURL(self, template_data, path=None): 409 def getFrontURL(self, template_data, path=None):
410 """Give front URL (i.e. URL seen by end-user) of a path 410 """Give front URL (i.e. URL seen by end-user) of a path
411 411
412 @param template_data[TemplateData]: data of current template 412 @param template_data[TemplateData]: data of current template
413 @param path(unicode, None): relative path of file to get, 413 @param path(unicode, None): relative path of file to get,
414 if set, will remplate template_data.path 414 if set, will remplate template_data.path
415 """ 415 """
416 return self.env.filters[u"front_url"]({u"template_data": template_data}, 416 return self.env.filters["front_url"]({"template_data": template_data},
417 path or template_data.path) 417 path or template_data.path)
418 418
419 def installTranslations(self): 419 def installTranslations(self):
420 # TODO: support multi translation 420 # TODO: support multi translation
421 # for now, only translations in sat_templates are handled 421 # for now, only translations in sat_templates are handled
422 self.translations = {} 422 self.translations = {}
423 for site_key, site_path in self.sites_paths.iteritems(): 423 for site_key, site_path in self.sites_paths.items():
424 site_prefix = u"[{}] ".format(site_key) if site_key else u'' 424 site_prefix = "[{}] ".format(site_key) if site_key else ''
425 i18n_dir = os.path.join(site_path, "i18n") 425 i18n_dir = os.path.join(site_path, "i18n")
426 for lang_dir in os.listdir(i18n_dir): 426 for lang_dir in os.listdir(i18n_dir):
427 lang_path = os.path.join(i18n_dir, lang_dir) 427 lang_path = os.path.join(i18n_dir, lang_dir)
428 if not os.path.isdir(lang_path): 428 if not os.path.isdir(lang_path):
429 continue 429 continue
437 self.translations[locale] = support.Translations(f, "sat") 437 self.translations[locale] = support.Translations(f, "sat")
438 else: 438 else:
439 translations.merge(support.Translations(f, "sat")) 439 translations.merge(support.Translations(f, "sat"))
440 except EnvironmentError: 440 except EnvironmentError:
441 log.error( 441 log.error(
442 _(u"Can't find template translation at {path}").format( 442 _("Can't find template translation at {path}").format(
443 path=po_path)) 443 path=po_path))
444 except UnknownLocaleError as e: 444 except UnknownLocaleError as e:
445 log.error(_(u"{site}Invalid locale name: {msg}").format( 445 log.error(_("{site}Invalid locale name: {msg}").format(
446 site=site_prefix, msg=e)) 446 site=site_prefix, msg=e))
447 else: 447 else:
448 log.info(_(u"{site}loaded {lang} templates translations").format( 448 log.info(_("{site}loaded {lang} templates translations").format(
449 site = site_prefix, 449 site = site_prefix,
450 lang=lang_dir)) 450 lang=lang_dir))
451 451
452 default_locale = Locale.parse(self._locale_str) 452 default_locale = Locale.parse(self._locale_str)
453 if default_locale not in self.translations: 453 if default_locale not in self.translations:
456 self.translations[default_locale] = None 456 self.translations[default_locale] = None
457 457
458 self.env.install_null_translations(True) 458 self.env.install_null_translations(True)
459 # we generate a tuple of locales ordered by display name that templates can access 459 # we generate a tuple of locales ordered by display name that templates can access
460 # through the "locales" variable 460 # through the "locales" variable
461 self.locales = tuple(sorted(self.translations.keys(), 461 self.locales = tuple(sorted(list(self.translations.keys()),
462 key=lambda l: l.language_name.lower())) 462 key=lambda l: l.language_name.lower()))
463 463
464 464
465 def setLocale(self, locale_str): 465 def setLocale(self, locale_str):
466 """set current locale 466 """set current locale
474 # one of the main reason is to avoid the nonsense U.S. short date format 474 # one of the main reason is to avoid the nonsense U.S. short date format
475 locale_str = "en_GB" 475 locale_str = "en_GB"
476 try: 476 try:
477 locale = Locale.parse(locale_str) 477 locale = Locale.parse(locale_str)
478 except ValueError as e: 478 except ValueError as e:
479 log.warning(_(u"invalid locale value: {msg}").format(msg=e)) 479 log.warning(_("invalid locale value: {msg}").format(msg=e))
480 locale_str = self._locale_str = C.DEFAULT_LOCALE 480 locale_str = self._locale_str = C.DEFAULT_LOCALE
481 locale = Locale.parse(locale_str) 481 locale = Locale.parse(locale_str)
482 482
483 locale_str = unicode(locale) 483 locale_str = str(locale)
484 if locale_str != C.DEFAULT_LOCALE: 484 if locale_str != C.DEFAULT_LOCALE:
485 try: 485 try:
486 translations = self.translations[locale] 486 translations = self.translations[locale]
487 except KeyError: 487 except KeyError:
488 log.warning(_(u"Can't find locale {locale}".format(locale=locale))) 488 log.warning(_("Can't find locale {locale}".format(locale=locale)))
489 locale_str = C.DEFAULT_LOCALE 489 locale_str = C.DEFAULT_LOCALE
490 locale = Locale.parse(self._locale_str) 490 locale = Locale.parse(self._locale_str)
491 else: 491 else:
492 self.env.install_gettext_translations(translations, True) 492 self.env.install_gettext_translations(translations, True)
493 log.debug(_(u"Switched to {lang}").format(lang=locale.english_name)) 493 log.debug(_("Switched to {lang}").format(lang=locale.english_name))
494 494
495 if locale_str == C.DEFAULT_LOCALE: 495 if locale_str == C.DEFAULT_LOCALE:
496 self.env.install_null_translations(True) 496 self.env.install_null_translations(True)
497 497
498 self._locale = locale 498 self._locale = locale
507 """ 507 """
508 # FIXME: check use in jp, and include site 508 # FIXME: check use in jp, and include site
509 site, theme, __ = self.env.loader.parse_template(template) 509 site, theme, __ = self.env.loader.parse_template(template)
510 if site is None: 510 if site is None:
511 # absolute template 511 # absolute template
512 return u"", os.path.dirname(template) 512 return "", os.path.dirname(template)
513 try: 513 try:
514 site_root_dir = self.sites_paths[site] 514 site_root_dir = self.sites_paths[site]
515 except KeyError: 515 except KeyError:
516 raise exceptions.NotFound 516 raise exceptions.NotFound
517 return theme, os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, theme) 517 return theme, os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, theme)
531 None if not found. 531 None if not found.
532 """ 532 """
533 if template_data.site is None: 533 if template_data.site is None:
534 # we have and absolue path 534 # we have and absolue path
535 if (not template_data.theme is None 535 if (not template_data.theme is None
536 or not template_data.path.startswith(u'/')): 536 or not template_data.path.startswith('/')):
537 raise exceptions.InternalError( 537 raise exceptions.InternalError(
538 u"invalid template data, was expecting absolute URL") 538 "invalid template data, was expecting absolute URL")
539 static_dir = os.path.dirname(template_data.path) 539 static_dir = os.path.dirname(template_data.path)
540 file_path = os.path.join(static_dir, filename) 540 file_path = os.path.join(static_dir, filename)
541 if os.path.exists(file_path): 541 if os.path.exists(file_path):
542 return TemplateData(site=None, theme=None, path=file_path) 542 return TemplateData(site=None, theme=None, path=file_path)
543 else: 543 else:
560 560
561 @param css_files(list): list to fill of relative path to found css file 561 @param css_files(list): list to fill of relative path to found css file
562 @param css_files_noscript(list): list to fill of relative path to found css file 562 @param css_files_noscript(list): list to fill of relative path to found css file
563 with "_noscript" suffix 563 with "_noscript" suffix
564 """ 564 """
565 name = name_root + u".css" 565 name = name_root + ".css"
566 css_path = self.getStaticPath(template_data, name) 566 css_path = self.getStaticPath(template_data, name)
567 if css_path is not None: 567 if css_path is not None:
568 css_files.append(self.getFrontURL(css_path)) 568 css_files.append(self.getFrontURL(css_path))
569 noscript_name = name_root + u"_noscript.css" 569 noscript_name = name_root + "_noscript.css"
570 noscript_path = self.getStaticPath(template_data, noscript_name) 570 noscript_path = self.getStaticPath(template_data, noscript_name)
571 if noscript_path is not None: 571 if noscript_path is not None:
572 css_files_noscript.append(self.getFrontURL(noscript_path)) 572 css_files_noscript.append(self.getFrontURL(noscript_path))
573 573
574 def getCSSFiles(self, template_data): 574 def getCSSFiles(self, template_data):
598 - front URLs of CSS files to use when scripts are not enabled 598 - front URLs of CSS files to use when scripts are not enabled
599 """ 599 """
600 # TODO: some caching would be nice 600 # TODO: some caching would be nice
601 css_files = [] 601 css_files = []
602 css_files_noscript = [] 602 css_files_noscript = []
603 path_elems = template_data.path.split(u'/') 603 path_elems = template_data.path.split('/')
604 path_elems[-1] = os.path.splitext(path_elems[-1])[0] 604 path_elems[-1] = os.path.splitext(path_elems[-1])[0]
605 605
606 css_path = self.getStaticPath(template_data, u'fonts.css') 606 css_path = self.getStaticPath(template_data, 'fonts.css')
607 if css_path is not None: 607 if css_path is not None:
608 css_files.append(self.getFrontURL(css_path)) 608 css_files.append(self.getFrontURL(css_path))
609 609
610 for name_root in (u'styles', u'styles_extra', u'highlight'): 610 for name_root in ('styles', 'styles_extra', 'highlight'):
611 self._appendCSSPaths(template_data, css_files, css_files_noscript, name_root) 611 self._appendCSSPaths(template_data, css_files, css_files_noscript, name_root)
612 612
613 for idx in xrange(len(path_elems)): 613 for idx in range(len(path_elems)):
614 name_root = u"_".join(path_elems[:idx+1]) 614 name_root = "_".join(path_elems[:idx+1])
615 self._appendCSSPaths(template_data, css_files, css_files_noscript, name_root) 615 self._appendCSSPaths(template_data, css_files, css_files_noscript, name_root)
616 616
617 return css_files, css_files_noscript 617 return css_files, css_files_noscript
618 618
619 ## custom filters ## 619 ## custom filters ##
622 def _front_url(self, ctx, relative_url): 622 def _front_url(self, ctx, relative_url):
623 """Get front URL (URL seen by end-user) from a relative URL 623 """Get front URL (URL seen by end-user) from a relative URL
624 624
625 This default method return absolute full path 625 This default method return absolute full path
626 """ 626 """
627 template_data = ctx[u'template_data'] 627 template_data = ctx['template_data']
628 if template_data.site is None: 628 if template_data.site is None:
629 assert template_data.theme is None 629 assert template_data.theme is None
630 assert template_data.path.startswith(u"/") 630 assert template_data.path.startswith("/")
631 return os.path.join(os.path.dirname(template_data.path, relative_url)) 631 return os.path.join(os.path.dirname(template_data.path, relative_url))
632 632
633 site_root_dir = self.sites_paths[template_data.site] 633 site_root_dir = self.sites_paths[template_data.site]
634 return os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, template_data.theme, 634 return os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, template_data.theme,
635 relative_url) 635 relative_url)
636 636
637 @contextfilter 637 @contextfilter
638 def _next_gidx(self, ctx, value): 638 def _next_gidx(self, ctx, value):
639 """Use next current global index as suffix""" 639 """Use next current global index as suffix"""
640 next_ = ctx["gidx"].next(value) 640 next_ = ctx["gidx"].next(value)
641 return value if next_ == 0 else u"{}_{}".format(value, next_) 641 return value if next_ == 0 else "{}_{}".format(value, next_)
642 642
643 @contextfilter 643 @contextfilter
644 def _cur_gidx(self, ctx, value): 644 def _cur_gidx(self, ctx, value):
645 """Use current current global index as suffix""" 645 """Use current current global index as suffix"""
646 current = ctx["gidx"].current(value) 646 current = ctx["gidx"].current(value)
647 return value if not current else u"{}_{}".format(value, current) 647 return value if not current else "{}_{}".format(value, current)
648 648
649 def _date_fmt(self, timestamp, fmt="short", date_only=False, auto_limit=None, 649 def _date_fmt(self, timestamp, fmt="short", date_only=False, auto_limit=None,
650 auto_old_fmt=None): 650 auto_old_fmt=None):
651 if is_undefined(fmt): 651 if is_undefined(fmt):
652 fmt = u"short" 652 fmt = "short"
653 try: 653 try:
654 return date_utils.date_fmt( 654 return date_utils.date_fmt(
655 timestamp, fmt, date_only, auto_limit, auto_old_fmt, 655 timestamp, fmt, date_only, auto_limit, auto_old_fmt,
656 locale_str = self._locale_str 656 locale_str = self._locale_str
657 ) 657 )
658 except Exception as e: 658 except Exception as e:
659 log.warning(_(u"Can't parse date: {msg}").format(msg=e)) 659 log.warning(_("Can't parse date: {msg}").format(msg=e))
660 return timestamp 660 return timestamp
661 661
662 def attr_escape(self, text): 662 def attr_escape(self, text):
663 """escape a text to a value usable as an attribute 663 """escape a text to a value usable as an attribute
664 664
665 remove spaces, and put in lower case 665 remove spaces, and put in lower case
666 """ 666 """
667 return RE_ATTR_ESCAPE.sub(u"_", text.strip().lower())[:50] 667 return RE_ATTR_ESCAPE.sub("_", text.strip().lower())[:50]
668 668
669 def _xmlui_class(self, xmlui_item, fields): 669 def _xmlui_class(self, xmlui_item, fields):
670 """return classes computed from XMLUI fields name 670 """return classes computed from XMLUI fields name
671 671
672 will return a string with a series of escaped {name}_{value} separated by spaces. 672 will return a string with a series of escaped {name}_{value} separated by spaces.
681 try: 681 try:
682 for value in xmlui_item.widgets[name].values: 682 for value in xmlui_item.widgets[name].values:
683 classes.append(escaped_name + "_" + self.attr_escape(value)) 683 classes.append(escaped_name + "_" + self.attr_escape(value))
684 except KeyError: 684 except KeyError:
685 log.debug( 685 log.debug(
686 _(u'ignoring field "{name}": it doesn\'t exists').format(name=name) 686 _('ignoring field "{name}": it doesn\'t exists').format(name=name)
687 ) 687 )
688 continue 688 continue
689 return u" ".join(classes) or None 689 return " ".join(classes) or None
690 690
691 @contextfilter 691 @contextfilter
692 def _item_filter(self, ctx, item, filters): 692 def _item_filter(self, ctx, item, filters):
693 """return item's value, filtered if suitable 693 """return item's value, filtered if suitable
694 694
707 value = item.value 707 value = item.value
708 filter_ = filters.get(item.name, None) 708 filter_ = filters.get(item.name, None)
709 if filter_ is None: 709 if filter_ is None:
710 return value 710 return value
711 elif isinstance(filter_, dict): 711 elif isinstance(filter_, dict):
712 filters_args = filter_.get(u"filters_args") 712 filters_args = filter_.get("filters_args")
713 for idx, f_name in enumerate(filter_.get(u"filters", [])): 713 for idx, f_name in enumerate(filter_.get("filters", [])):
714 kwargs = filters_args[idx] if filters_args is not None else {} 714 kwargs = filters_args[idx] if filters_args is not None else {}
715 filter_func = self.env.filters[f_name] 715 filter_func = self.env.filters[f_name]
716 try: 716 try:
717 eval_context_filter = filter_func.evalcontextfilter 717 eval_context_filter = filter_func.evalcontextfilter
718 except AttributeError: 718 except AttributeError:
720 720
721 if eval_context_filter: 721 if eval_context_filter:
722 value = filter_func(ctx.eval_ctx, value, **kwargs) 722 value = filter_func(ctx.eval_ctx, value, **kwargs)
723 else: 723 else:
724 value = filter_func(value, **kwargs) 724 value = filter_func(value, **kwargs)
725 template = filter_.get(u"template") 725 template = filter_.get("template")
726 if template: 726 if template:
727 # format will return a string, so we need to check first 727 # format will return a string, so we need to check first
728 # if the value is safe or not, and re-mark it after formatting 728 # if the value is safe or not, and re-mark it after formatting
729 is_safe = isinstance(value, safe) 729 is_safe = isinstance(value, safe)
730 value = template.format(value=value) 730 value = template.format(value=value)
743 @return (unicode): formatted value 743 @return (unicode): formatted value
744 """ 744 """
745 if template is None: 745 if template is None:
746 return value 746 return value
747 #  jinja use string when no special char is used, so we have to convert to unicode 747 #  jinja use string when no special char is used, so we have to convert to unicode
748 return unicode(template).format(value=value, **kwargs) 748 return str(template).format(value=value, **kwargs)
749 749
750 def _dict_ext(self, source_dict, extra_dict, key=None): 750 def _dict_ext(self, source_dict, extra_dict, key=None):
751 """extend source_dict with extra dict and return the result 751 """extend source_dict with extra dict and return the result
752 752
753 @param source_dict(dict): dictionary to extend 753 @param source_dict(dict): dictionary to extend
811 height="0", 811 height="0",
812 style="display: block", 812 style="display: block",
813 ) 813 )
814 defs_elt = etree.SubElement(svg_elt, "defs") 814 defs_elt = etree.SubElement(svg_elt, "defs")
815 for name in names: 815 for name in names:
816 path = os.path.join(self.icons_path, name + u".svg") 816 path = os.path.join(self.icons_path, name + ".svg")
817 icon_svg_elt = etree.parse(path).getroot() 817 icon_svg_elt = etree.parse(path).getroot()
818 # we use icon name as id, so we can retrieve them easily 818 # we use icon name as id, so we can retrieve them easily
819 icon_svg_elt.set("id", name) 819 icon_svg_elt.set("id", name)
820 if not icon_svg_elt.tag == "{http://www.w3.org/2000/svg}svg": 820 if not icon_svg_elt.tag == "{http://www.w3.org/2000/svg}svg":
821 raise exceptions.DataError(u"invalid SVG element") 821 raise exceptions.DataError("invalid SVG element")
822 defs_elt.append(icon_svg_elt) 822 defs_elt.append(icon_svg_elt)
823 return safe(etree.tostring(svg_elt, encoding="unicode")) 823 return safe(etree.tostring(svg_elt, encoding="unicode"))
824 824
825 def _icon_use(self, name, cls=""): 825 def _icon_use(self, name, cls=""):
826 return safe(u'<svg class="svg-icon{cls}" xmlns="http://www.w3.org/2000/svg" ' 826 return safe('<svg class="svg-icon{cls}" xmlns="http://www.w3.org/2000/svg" '
827 u'viewBox="0 0 100 100">\n' 827 'viewBox="0 0 100 100">\n'
828 u' <use href="#{name}"/>' 828 ' <use href="#{name}"/>'
829 u'</svg>\n'.format(name=name, cls=(" " + cls) if cls else "")) 829 '</svg>\n'.format(name=name, cls=(" " + cls) if cls else ""))
830 830
831 def render(self, template, site=None, theme=None, locale=C.DEFAULT_LOCALE, 831 def render(self, template, site=None, theme=None, locale=C.DEFAULT_LOCALE,
832 media_path=u"", css_files=None, css_inline=False, **kwargs): 832 media_path="", css_files=None, css_inline=False, **kwargs):
833 """Render a template 833 """Render a template
834 834
835 @param template(unicode): template to render (e.g. blog/articles.html) 835 @param template(unicode): template to render (e.g. blog/articles.html)
836 @param site(unicide): site name 836 @param site(unicide): site name
837 None or empty string for defaut site (i.e. SàT templates) 837 None or empty string for defaut site (i.e. SàT templates)
845 used. 845 used.
846 @param css_inline(bool): if True, CSS will be embedded in the HTML page 846 @param css_inline(bool): if True, CSS will be embedded in the HTML page
847 @param **kwargs: variable to transmit to the template 847 @param **kwargs: variable to transmit to the template
848 """ 848 """
849 if not template: 849 if not template:
850 raise ValueError(u"template can't be empty") 850 raise ValueError("template can't be empty")
851 if site is not None or theme is not None: 851 if site is not None or theme is not None:
852 # user wants to set site and/or theme, so we add it to the template path 852 # user wants to set site and/or theme, so we add it to the template path
853 if site is None: 853 if site is None:
854 site = u'' 854 site = ''
855 if theme is None: 855 if theme is None:
856 theme = C.TEMPLATE_THEME_DEFAULT 856 theme = C.TEMPLATE_THEME_DEFAULT
857 if template[0] == u"(": 857 if template[0] == "(":
858 raise ValueError( 858 raise ValueError(
859 u"you can't specify site or theme in template path and in argument " 859 "you can't specify site or theme in template path and in argument "
860 u"at the same time" 860 "at the same time"
861 ) 861 )
862 862
863 template_data = TemplateData(site, theme, template) 863 template_data = TemplateData(site, theme, template)
864 template = u"({site}/{theme}){template}".format( 864 template = "({site}/{theme}){template}".format(
865 site=site, theme=theme, template=template) 865 site=site, theme=theme, template=template)
866 else: 866 else:
867 template_data = self.env.loader.parse_template(template) 867 template_data = self.env.loader.parse_template(template)
868 868
869 # we need to save template_data in environment, to load right templates when they 869 # we need to save template_data in environment, to load right templates when they
881 kwargs["icon_defs"] = self._icon_defs 881 kwargs["icon_defs"] = self._icon_defs
882 kwargs["icon"] = self._icon_use 882 kwargs["icon"] = self._icon_use
883 883
884 if css_inline: 884 if css_inline:
885 css_contents = [] 885 css_contents = []
886 for files, suffix in ((css_files, u""), 886 for files, suffix in ((css_files, ""),
887 (css_files_noscript, u"_noscript")): 887 (css_files_noscript, "_noscript")):
888 site_root_dir = self.sites_paths[template_data.site] 888 site_root_dir = self.sites_paths[template_data.site]
889 for css_file in files: 889 for css_file in files:
890 css_file_path = os.path.join(site_root_dir, css_file) 890 css_file_path = os.path.join(site_root_dir, css_file)
891 with open(css_file_path) as f: 891 with open(css_file_path) as f:
892 css_contents.append(f.read()) 892 css_contents.append(f.read())
893 if css_contents: 893 if css_contents:
894 kwargs[u"css_content" + suffix] = u"\n".join(css_contents) 894 kwargs["css_content" + suffix] = "\n".join(css_contents)
895 895
896 scripts_handler = ScriptsHandler(self, template_data) 896 scripts_handler = ScriptsHandler(self, template_data)
897 self.setLocale(locale) 897 self.setLocale(locale)
898 898
899 # XXX: theme used in template arguments is the requested theme, which may differ 899 # XXX: theme used in template arguments is the requested theme, which may differ