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