Mercurial > libervia-backend
comparison libervia/backend/tools/common/template.py @ 4270:0d7bb4df2343
Reformatted code base using black.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 19 Jun 2024 18:44:57 +0200 |
parents | 81faa85c9cfa |
children | 00837fa13e5a |
comparison
equal
deleted
inserted
replaced
4269:64a85ce8be70 | 4270:0d7bb4df2343 |
---|---|
70 "Missing module jinja2, please install it from http://jinja.pocoo.org or with " | 70 "Missing module jinja2, please install it from http://jinja.pocoo.org or with " |
71 "pip install jinja2" | 71 "pip install jinja2" |
72 ) | 72 ) |
73 | 73 |
74 | 74 |
75 | |
76 HTML_EXT = ("html", "xhtml") | 75 HTML_EXT = ("html", "xhtml") |
77 RE_ATTR_ESCAPE = re.compile(r"[^a-z_-]") | 76 RE_ATTR_ESCAPE = re.compile(r"[^a-z_-]") |
78 SITE_RESERVED_NAMES = ("sat",) | 77 SITE_RESERVED_NAMES = ("sat",) |
79 TPL_RESERVED_CHARS = r"()/." | 78 TPL_RESERVED_CHARS = r"()/." |
80 RE_TPL_RESERVED_CHARS = re.compile("[" + TPL_RESERVED_CHARS + "]") | 79 RE_TPL_RESERVED_CHARS = re.compile("[" + TPL_RESERVED_CHARS + "]") |
81 BROWSER_DIR = "_browser" | 80 BROWSER_DIR = "_browser" |
82 BROWSER_META_FILE = "browser_meta.json" | 81 BROWSER_META_FILE = "browser_meta.json" |
83 | 82 |
84 TemplateData = namedtuple("TemplateData", ['site', 'theme', 'path']) | 83 TemplateData = namedtuple("TemplateData", ["site", "theme", "path"]) |
85 | 84 |
86 | 85 |
87 class TemplateLoader(jinja2.BaseLoader): | 86 class TemplateLoader(jinja2.BaseLoader): |
88 """A template loader which handle site, theme and absolute paths""" | 87 """A template loader which handle site, theme and absolute paths""" |
88 | |
89 # TODO: list_templates should be implemented | 89 # TODO: list_templates should be implemented |
90 | 90 |
91 def __init__(self, sites_paths, sites_themes, trusted=False): | 91 def __init__(self, sites_paths, sites_themes, trusted=False): |
92 """ | 92 """ |
93 @param trusted(bool): if True, absolue template paths will be allowed | 93 @param trusted(bool): if True, absolue template paths will be allowed |
124 try: | 124 try: |
125 theme_end = template.index(")") | 125 theme_end = template.index(")") |
126 except IndexError: | 126 except IndexError: |
127 raise ValueError("incorrect site/theme in template") | 127 raise ValueError("incorrect site/theme in template") |
128 theme_data = template[1:theme_end] | 128 theme_data = template[1:theme_end] |
129 theme_splitted = theme_data.split('/') | 129 theme_splitted = theme_data.split("/") |
130 if len(theme_splitted) == 1: | 130 if len(theme_splitted) == 1: |
131 site, theme = "", theme_splitted[0] | 131 site, theme = "", theme_splitted[0] |
132 elif len(theme_splitted) == 2: | 132 elif len(theme_splitted) == 2: |
133 site, theme = theme_splitted | 133 site, theme = theme_splitted |
134 else: | 134 else: |
135 raise ValueError("incorrect site/theme in template") | 135 raise ValueError("incorrect site/theme in template") |
136 template_path = template[theme_end+1:] | 136 template_path = template[theme_end + 1 :] |
137 if not template_path or template_path.startswith("/"): | 137 if not template_path or template_path.startswith("/"): |
138 raise ValueError("incorrect template path") | 138 raise ValueError("incorrect template path") |
139 elif template.startswith("/"): | 139 elif template.startswith("/"): |
140 # this is an absolute path, so we have no site and no theme | 140 # this is an absolute path, so we have no site and no theme |
141 site = None | 141 site = None |
150 if site is not None: | 150 if site is not None: |
151 site = site.strip() | 151 site = site.strip() |
152 if not site: | 152 if not site: |
153 site = "" | 153 site = "" |
154 elif site in SITE_RESERVED_NAMES: | 154 elif site in SITE_RESERVED_NAMES: |
155 raise ValueError(_("{site} can't be used as site name, " | 155 raise ValueError( |
156 "it's reserved.").format(site=site)) | 156 _("{site} can't be used as site name, " "it's reserved.").format( |
157 site=site | |
158 ) | |
159 ) | |
157 | 160 |
158 if theme is not None: | 161 if theme is not None: |
159 theme = theme.strip() | 162 theme = theme.strip() |
160 if not theme: | 163 if not theme: |
161 theme = C.TEMPLATE_THEME_DEFAULT | 164 theme = C.TEMPLATE_THEME_DEFAULT |
162 if RE_TPL_RESERVED_CHARS.search(theme): | 165 if RE_TPL_RESERVED_CHARS.search(theme): |
163 raise ValueError(_("{theme} contain forbidden char. Following chars " | 166 raise ValueError( |
164 "are forbidden: {reserved}").format( | 167 _( |
165 theme=theme, reserved=TPL_RESERVED_CHARS)) | 168 "{theme} contain forbidden char. Following chars " |
169 "are forbidden: {reserved}" | |
170 ).format(theme=theme, reserved=TPL_RESERVED_CHARS) | |
171 ) | |
166 | 172 |
167 return TemplateData(site, theme, template_path) | 173 return TemplateData(site, theme, template_path) |
168 | 174 |
169 @staticmethod | 175 @staticmethod |
170 def get_sites_and_themes( | 176 def get_sites_and_themes( |
171 site: str, | 177 site: str, |
172 theme: str, | 178 theme: str, |
173 settings: Optional[dict] = None, | 179 settings: Optional[dict] = None, |
174 ) -> List[Tuple[str, str]]: | 180 ) -> List[Tuple[str, str]]: |
175 """Get sites and themes to check for template/file | 181 """Get sites and themes to check for template/file |
176 | 182 |
177 Will add default theme and default site in search list when suitable. Settings' | 183 Will add default theme and default site in search list when suitable. Settings' |
178 `fallback` can be used to modify behaviour: themes in this list will then be used | 184 `fallback` can be used to modify behaviour: themes in this list will then be used |
179 instead of default (it can also be empty list or None, in which case no fallback | 185 instead of default (it can also be empty list or None, in which case no fallback |
206 - opened template, or None if not found | 212 - opened template, or None if not found |
207 - absolute file path, or None if not found | 213 - absolute file path, or None if not found |
208 """ | 214 """ |
209 if site is None: | 215 if site is None: |
210 raise exceptions.InternalError( | 216 raise exceptions.InternalError( |
211 "_get_template_f must not be used with absolute path") | 217 "_get_template_f must not be used with absolute path" |
212 settings = self.sites_themes[site][theme]['settings'] | 218 ) |
219 settings = self.sites_themes[site][theme]["settings"] | |
213 for site_to_check, theme_to_check in self.get_sites_and_themes( | 220 for site_to_check, theme_to_check in self.get_sites_and_themes( |
214 site, theme, settings): | 221 site, theme, settings |
222 ): | |
215 try: | 223 try: |
216 base_path = self.sites_paths[site_to_check] | 224 base_path = self.sites_paths[site_to_check] |
217 except KeyError: | 225 except KeyError: |
218 log.warning(_("Unregistered site requested: {site_to_check}").format( | 226 log.warning( |
219 site_to_check=site_to_check)) | 227 _("Unregistered site requested: {site_to_check}").format( |
228 site_to_check=site_to_check | |
229 ) | |
230 ) | |
220 filepath = os.path.join( | 231 filepath = os.path.join( |
221 base_path, | 232 base_path, C.TEMPLATE_TPL_DIR, theme_to_check, *path_elts |
222 C.TEMPLATE_TPL_DIR, | |
223 theme_to_check, | |
224 *path_elts | |
225 ) | 233 ) |
226 f = utils.open_if_exists(filepath, 'r') | 234 f = utils.open_if_exists(filepath, "r") |
227 if f is not None: | 235 if f is not None: |
228 return f, filepath | 236 return f, filepath |
229 return None, None | 237 return None, None |
230 | 238 |
231 def get_source(self, environment, template): | 239 def get_source(self, environment, template): |
239 site, theme, template_path = self.parse_template(template) | 247 site, theme, template_path = self.parse_template(template) |
240 | 248 |
241 if site is None: | 249 if site is None: |
242 # we have an abolute template | 250 # we have an abolute template |
243 if theme is not None: | 251 if theme is not None: |
244 raise exceptions.InternalError("We can't have a theme with absolute " | 252 raise exceptions.InternalError( |
245 "template.") | 253 "We can't have a theme with absolute " "template." |
254 ) | |
246 if not self.trusted: | 255 if not self.trusted: |
247 log.error(_("Absolute template used while unsecure is disabled, hack " | 256 log.error( |
248 "attempt? Template: {template}").format(template=template)) | 257 _( |
258 "Absolute template used while unsecure is disabled, hack " | |
259 "attempt? Template: {template}" | |
260 ).format(template=template) | |
261 ) | |
249 raise exceptions.PermissionError("absolute template is not allowed") | 262 raise exceptions.PermissionError("absolute template is not allowed") |
250 filepath = template_path | 263 filepath = template_path |
251 f = utils.open_if_exists(filepath, 'r') | 264 f = utils.open_if_exists(filepath, "r") |
252 else: | 265 else: |
253 # relative path, we have to deal with site and theme | 266 # relative path, we have to deal with site and theme |
254 assert theme and template_path | 267 assert theme and template_path |
255 path_elts = split_template_path(template_path) | 268 path_elts = split_template_path(template_path) |
256 # if we have non default site, we check it first, else we only check default | 269 # if we have non default site, we check it first, else we only check default |
257 f, filepath = self._get_template_f(site, theme, path_elts) | 270 f, filepath = self._get_template_f(site, theme, path_elts) |
258 | 271 |
259 if f is None: | 272 if f is None: |
260 if (site is not None and path_elts[0] == "error" | 273 if ( |
261 and os.path.splitext(template_path)[1][1:] in HTML_EXT): | 274 site is not None |
275 and path_elts[0] == "error" | |
276 and os.path.splitext(template_path)[1][1:] in HTML_EXT | |
277 ): | |
262 # if an HTML error is requested but doesn't exist, we try again | 278 # if an HTML error is requested but doesn't exist, we try again |
263 # with base error. | 279 # with base error. |
264 f, filepath = self._get_template_f( | 280 f, filepath = self._get_template_f(site, theme, ("error", "base.html")) |
265 site, theme, ("error", "base.html")) | |
266 if f is None: | 281 if f is None: |
267 raise exceptions.InternalError("error/base.html should exist") | 282 raise exceptions.InternalError("error/base.html should exist") |
268 else: | 283 else: |
269 raise TemplateNotFound(template) | 284 raise TemplateNotFound(template) |
270 | 285 |
333 scripts = [] | 348 scripts = [] |
334 tpl = "<script src={src} {attribute}></script>" | 349 tpl = "<script src={src} {attribute}></script>" |
335 for library, attribute in self.scripts: | 350 for library, attribute in self.scripts: |
336 library_path = self.renderer.get_static_path(self.template_data, library) | 351 library_path = self.renderer.get_static_path(self.template_data, library) |
337 if library_path is None: | 352 if library_path is None: |
338 log.warning(_("Can't find {libary} javascript library").format( | 353 log.warning( |
339 library=library)) | 354 _("Can't find {libary} javascript library").format(library=library) |
355 ) | |
340 continue | 356 continue |
341 path = self.renderer.get_front_url(library_path) | 357 path = self.renderer.get_front_url(library_path) |
342 scripts.append(tpl.format(src=quoteattr(path), attribute=attribute)) | 358 scripts.append(tpl.format(src=quoteattr(path), attribute=attribute)) |
343 return safe("\n".join(scripts)) | 359 return safe("\n".join(scripts)) |
344 | 360 |
345 | 361 |
346 class Environment(jinja2.Environment): | 362 class Environment(jinja2.Environment): |
347 | 363 |
348 def get_template(self, name, parent=None, globals=None): | 364 def get_template(self, name, parent=None, globals=None): |
349 if name[0] not in ('/', '('): | 365 if name[0] not in ("/", "("): |
350 # if name is not an absolute path or a full template name (this happen on | 366 # if name is not an absolute path or a full template name (this happen on |
351 # extend or import during rendering), we convert it to a full template name. | 367 # extend or import during rendering), we convert it to a full template name. |
352 # This is needed to handle cache correctly when a base template is overriden. | 368 # This is needed to handle cache correctly when a base template is overriden. |
353 # Without that, we could not distinguish something like base/base.html if | 369 # Without that, we could not distinguish something like base/base.html if |
354 # it's launched from some_site/some_theme or from [default]/default | 370 # it's launched from some_site/some_theme or from [default]/default |
355 name = "({site}/{theme}){template}".format( | 371 name = "({site}/{theme}){template}".format( |
356 site=self._template_data.site, | 372 site=self._template_data.site, |
357 theme=self._template_data.theme, | 373 theme=self._template_data.theme, |
358 template=name) | 374 template=name, |
375 ) | |
359 | 376 |
360 return super(Environment, self).get_template(name, parent, globals) | 377 return super(Environment, self).get_template(name, parent, globals) |
361 | 378 |
362 | 379 |
363 class Renderer(object): | 380 class Renderer(object): |
378 self.host = host | 395 self.host = host |
379 self.trusted = trusted | 396 self.trusted = trusted |
380 self.sites_paths = { | 397 self.sites_paths = { |
381 "": os.path.dirname(sat_templates.__file__), | 398 "": os.path.dirname(sat_templates.__file__), |
382 } | 399 } |
383 self.sites_themes = { | 400 self.sites_themes = {} |
384 } | |
385 conf = config.parse_main_conf() | 401 conf = config.parse_main_conf() |
386 public_sites = config.config_get(conf, None, "sites_path_public_dict", {}) | 402 public_sites = config.config_get(conf, None, "sites_path_public_dict", {}) |
387 sites_data = [public_sites] | 403 sites_data = [public_sites] |
388 if private: | 404 if private: |
389 private_sites = config.config_get(conf, None, "sites_path_private_dict", {}) | 405 private_sites = config.config_get(conf, None, "sites_path_private_dict", {}) |
390 sites_data.append(private_sites) | 406 sites_data.append(private_sites) |
391 for sites in sites_data: | 407 for sites in sites_data: |
392 normalised = {} | 408 normalised = {} |
393 for name, path in sites.items(): | 409 for name, path in sites.items(): |
394 if RE_TPL_RESERVED_CHARS.search(name): | 410 if RE_TPL_RESERVED_CHARS.search(name): |
395 log.warning(_("Can't add \"{name}\" site, it contains forbidden " | 411 log.warning( |
396 "characters. Forbidden characters are {forbidden}.") | 412 _( |
397 .format(name=name, forbidden=TPL_RESERVED_CHARS)) | 413 'Can\'t add "{name}" site, it contains forbidden ' |
414 "characters. Forbidden characters are {forbidden}." | |
415 ).format(name=name, forbidden=TPL_RESERVED_CHARS) | |
416 ) | |
398 continue | 417 continue |
399 path = os.path.expanduser(os.path.normpath(path)) | 418 path = os.path.expanduser(os.path.normpath(path)) |
400 if not path or not path.startswith("/"): | 419 if not path or not path.startswith("/"): |
401 log.warning(_("Can't add \"{name}\" site, it should map to an " | 420 log.warning( |
402 "absolute path").format(name=name)) | 421 _( |
422 'Can\'t add "{name}" site, it should map to an ' | |
423 "absolute path" | |
424 ).format(name=name) | |
425 ) | |
403 continue | 426 continue |
404 normalised[name] = path | 427 normalised[name] = path |
405 self.sites_paths.update(normalised) | 428 self.sites_paths.update(normalised) |
406 | 429 |
407 for site, site_path in self.sites_paths.items(): | 430 for site, site_path in self.sites_paths.items(): |
409 for p in tpl_path.iterdir(): | 432 for p in tpl_path.iterdir(): |
410 if not p.is_dir(): | 433 if not p.is_dir(): |
411 continue | 434 continue |
412 log.debug(f"theme found for {site or 'default site'}: {p.name}") | 435 log.debug(f"theme found for {site or 'default site'}: {p.name}") |
413 theme_data = self.sites_themes.setdefault(site, {})[p.name] = { | 436 theme_data = self.sites_themes.setdefault(site, {})[p.name] = { |
414 'path': p, | 437 "path": p, |
415 'settings': {}} | 438 "settings": {}, |
439 } | |
416 theme_settings = p / "settings.json" | 440 theme_settings = p / "settings.json" |
417 if theme_settings.is_file: | 441 if theme_settings.is_file: |
418 try: | 442 try: |
419 with theme_settings.open() as f: | 443 with theme_settings.open() as f: |
420 settings = json.load(f) | 444 settings = json.load(f) |
421 except Exception as e: | 445 except Exception as e: |
422 log.warning(_( | 446 log.warning( |
423 "Can't load theme settings at {path}: {e}").format( | 447 _("Can't load theme settings at {path}: {e}").format( |
424 path=theme_settings, e=e)) | 448 path=theme_settings, e=e |
449 ) | |
450 ) | |
425 else: | 451 else: |
426 log.debug( | 452 log.debug( |
427 f"found settings for theme {p.name!r} at {theme_settings}") | 453 f"found settings for theme {p.name!r} at {theme_settings}" |
454 ) | |
428 fallback = settings.get("fallback") | 455 fallback = settings.get("fallback") |
429 if fallback is None: | 456 if fallback is None: |
430 settings["fallback"] = [] | 457 settings["fallback"] = [] |
431 elif isinstance(fallback, str): | 458 elif isinstance(fallback, str): |
432 settings["fallback"] = [fallback] | 459 settings["fallback"] = [fallback] |
433 elif not isinstance(fallback, list): | 460 elif not isinstance(fallback, list): |
434 raise ValueError( | 461 raise ValueError( |
435 'incorrect type for "fallback" in settings ' | 462 'incorrect type for "fallback" in settings ' |
436 f'({type(fallback)}) at {theme_settings}: {fallback}' | 463 f"({type(fallback)}) at {theme_settings}: {fallback}" |
437 ) | 464 ) |
438 theme_data['settings'] = settings | 465 theme_data["settings"] = settings |
439 browser_path = p / BROWSER_DIR | 466 browser_path = p / BROWSER_DIR |
440 if browser_path.is_dir(): | 467 if browser_path.is_dir(): |
441 theme_data['browser_path'] = browser_path | 468 theme_data["browser_path"] = browser_path |
442 browser_meta_path = browser_path / BROWSER_META_FILE | 469 browser_meta_path = browser_path / BROWSER_META_FILE |
443 if browser_meta_path.is_file(): | 470 if browser_meta_path.is_file(): |
444 try: | 471 try: |
445 with browser_meta_path.open() as f: | 472 with browser_meta_path.open() as f: |
446 theme_data['browser_meta'] = json.load(f) | 473 theme_data["browser_meta"] = json.load(f) |
447 except Exception as e: | 474 except Exception as e: |
448 log.error( | 475 log.error( |
449 f"Can't parse browser metadata at {browser_meta_path}: {e}" | 476 f"Can't parse browser metadata at {browser_meta_path}: {e}" |
450 ) | 477 ) |
451 continue | 478 continue |
452 | 479 |
453 self.env = Environment( | 480 self.env = Environment( |
454 loader=TemplateLoader( | 481 loader=TemplateLoader( |
455 sites_paths=self.sites_paths, | 482 sites_paths=self.sites_paths, |
456 sites_themes=self.sites_themes, | 483 sites_themes=self.sites_themes, |
457 trusted=trusted | 484 trusted=trusted, |
458 ), | 485 ), |
459 autoescape=jinja2.select_autoescape(["html", "xhtml", "xml"]), | 486 autoescape=jinja2.select_autoescape(["html", "xhtml", "xml"]), |
460 trim_blocks=True, | 487 trim_blocks=True, |
461 lstrip_blocks=True, | 488 lstrip_blocks=True, |
462 extensions=["jinja2.ext.i18n"], | 489 extensions=["jinja2.ext.i18n"], |
480 self.env.filters["attr_escape"] = self.attr_escape | 507 self.env.filters["attr_escape"] = self.attr_escape |
481 self.env.filters["item_filter"] = self._item_filter | 508 self.env.filters["item_filter"] = self._item_filter |
482 self.env.filters["adv_format"] = self._adv_format | 509 self.env.filters["adv_format"] = self._adv_format |
483 self.env.filters["dict_ext"] = self._dict_ext | 510 self.env.filters["dict_ext"] = self._dict_ext |
484 self.env.filters["highlight"] = self.highlight | 511 self.env.filters["highlight"] = self.highlight |
485 self.env.filters["front_url"] = (self._front_url if front_url_filter is None | 512 self.env.filters["front_url"] = ( |
486 else front_url_filter) | 513 self._front_url if front_url_filter is None else front_url_filter |
514 ) | |
487 self.env.filters["media_type_main"] = self.media_type_main | 515 self.env.filters["media_type_main"] = self.media_type_main |
488 self.env.filters["media_type_sub"] = self.media_type_sub | 516 self.env.filters["media_type_sub"] = self.media_type_sub |
489 # custom tests | 517 # custom tests |
490 self.env.tests["in_the_past"] = self._in_the_past | 518 self.env.tests["in_the_past"] = self._in_the_past |
491 self.icons_path = os.path.join(host.media_dir, "fonts/fontello/svg") | 519 self.icons_path = os.path.join(host.media_dir, "fonts/fontello/svg") |
493 # policies | 521 # policies |
494 self.env.policies["ext.i18n.trimmed"] = True | 522 self.env.policies["ext.i18n.trimmed"] = True |
495 self.env.policies["json.dumps_kwargs"] = { | 523 self.env.policies["json.dumps_kwargs"] = { |
496 "sort_keys": True, | 524 "sort_keys": True, |
497 # if object can't be serialised, we use None | 525 # if object can't be serialised, we use None |
498 "default": lambda o: o.to_json() if hasattr(o, "to_json") else None | 526 "default": lambda o: o.to_json() if hasattr(o, "to_json") else None, |
499 } | 527 } |
500 | 528 |
501 def get_front_url(self, template_data, path=None): | 529 def get_front_url(self, template_data, path=None): |
502 """Give front URL (i.e. URL seen by end-user) of a path | 530 """Give front URL (i.e. URL seen by end-user) of a path |
503 | 531 |
504 @param template_data[TemplateData]: data of current template | 532 @param template_data[TemplateData]: data of current template |
505 @param path(unicode, None): relative path of file to get, | 533 @param path(unicode, None): relative path of file to get, |
506 if set, will remplate template_data.path | 534 if set, will remplate template_data.path |
507 """ | 535 """ |
508 return self.env.filters["front_url"]({"template_data": template_data}, | 536 return self.env.filters["front_url"]( |
509 path or template_data.path) | 537 {"template_data": template_data}, path or template_data.path |
538 ) | |
510 | 539 |
511 def install_translations(self): | 540 def install_translations(self): |
512 # TODO: support multi translation | 541 # TODO: support multi translation |
513 # for now, only translations in sat_templates are handled | 542 # for now, only translations in sat_templates are handled |
514 self.translations = {} | 543 self.translations = {} |
515 for site_key, site_path in self.sites_paths.items(): | 544 for site_key, site_path in self.sites_paths.items(): |
516 site_prefix = "[{}] ".format(site_key) if site_key else '' | 545 site_prefix = "[{}] ".format(site_key) if site_key else "" |
517 i18n_dir = os.path.join(site_path, "i18n") | 546 i18n_dir = os.path.join(site_path, "i18n") |
518 for lang_dir in os.listdir(i18n_dir): | 547 for lang_dir in os.listdir(i18n_dir): |
519 lang_path = os.path.join(i18n_dir, lang_dir) | 548 lang_path = os.path.join(i18n_dir, lang_dir) |
520 if not os.path.isdir(lang_path): | 549 if not os.path.isdir(lang_path): |
521 continue | 550 continue |
530 else: | 559 else: |
531 translations.merge(support.Translations(f, "sat")) | 560 translations.merge(support.Translations(f, "sat")) |
532 except EnvironmentError: | 561 except EnvironmentError: |
533 log.error( | 562 log.error( |
534 _("Can't find template translation at {path}").format( | 563 _("Can't find template translation at {path}").format( |
535 path=po_path)) | 564 path=po_path |
565 ) | |
566 ) | |
536 except UnknownLocaleError as e: | 567 except UnknownLocaleError as e: |
537 log.error(_("{site}Invalid locale name: {msg}").format( | 568 log.error( |
538 site=site_prefix, msg=e)) | 569 _("{site}Invalid locale name: {msg}").format( |
570 site=site_prefix, msg=e | |
571 ) | |
572 ) | |
539 else: | 573 else: |
540 log.info(_("{site}loaded {lang} templates translations").format( | 574 log.info( |
541 site = site_prefix, | 575 _("{site}loaded {lang} templates translations").format( |
542 lang=lang_dir)) | 576 site=site_prefix, lang=lang_dir |
577 ) | |
578 ) | |
543 | 579 |
544 default_locale = Locale.parse(self._locale_str) | 580 default_locale = Locale.parse(self._locale_str) |
545 if default_locale not in self.translations: | 581 if default_locale not in self.translations: |
546 # default locale disable gettext, | 582 # default locale disable gettext, |
547 # so we can use None instead of a Translations instance | 583 # so we can use None instead of a Translations instance |
548 self.translations[default_locale] = None | 584 self.translations[default_locale] = None |
549 | 585 |
550 self.env.install_null_translations(True) | 586 self.env.install_null_translations(True) |
551 # we generate a tuple of locales ordered by display name that templates can access | 587 # we generate a tuple of locales ordered by display name that templates can access |
552 # through the "locales" variable | 588 # through the "locales" variable |
553 self.locales = tuple(sorted(list(self.translations.keys()), | 589 self.locales = tuple( |
554 key=lambda l: l.language_name.lower())) | 590 sorted(list(self.translations.keys()), key=lambda l: l.language_name.lower()) |
555 | 591 ) |
556 | 592 |
557 def set_locale(self, locale_str): | 593 def set_locale(self, locale_str): |
558 """set current locale | 594 """set current locale |
559 | 595 |
560 change current translation locale and self self._locale and self._locale_str | 596 change current translation locale and self self._locale and self._locale_str |
599 """ | 635 """ |
600 # FIXME: check use in CLI frontend, and include site | 636 # FIXME: check use in CLI frontend, and include site |
601 site, theme, __ = self.env.loader.parse_template(template) | 637 site, theme, __ = self.env.loader.parse_template(template) |
602 if site is None: | 638 if site is None: |
603 # absolute template | 639 # absolute template |
604 return "", os.path.dirname(template) | 640 return "", os.path.dirname(template) |
605 try: | 641 try: |
606 site_root_dir = self.sites_paths[site] | 642 site_root_dir = self.sites_paths[site] |
607 except KeyError: | 643 except KeyError: |
608 raise exceptions.NotFound | 644 raise exceptions.NotFound |
609 return theme, os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, theme) | 645 return theme, os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, theme) |
613 return self.sites_themes[site_name] | 649 return self.sites_themes[site_name] |
614 except KeyError: | 650 except KeyError: |
615 raise exceptions.NotFound(f"no theme found for {site_name}") | 651 raise exceptions.NotFound(f"no theme found for {site_name}") |
616 | 652 |
617 def get_static_path( | 653 def get_static_path( |
618 self, | 654 self, template_data: TemplateData, filename: str, settings: Optional[dict] = None |
619 template_data: TemplateData, | 655 ) -> Optional[TemplateData]: |
620 filename: str, | |
621 settings: Optional[dict]=None | |
622 ) -> Optional[TemplateData]: | |
623 """Retrieve path of a static file if it exists with current theme or default | 656 """Retrieve path of a static file if it exists with current theme or default |
624 | 657 |
625 File will be looked at <site_root_dir>/<theme_dir>/<static_dir>/filename, | 658 File will be looked at <site_root_dir>/<theme_dir>/<static_dir>/filename, |
626 then <site_root_dir>/<default_theme_dir>/<static_dir>/filename anf finally | 659 then <site_root_dir>/<default_theme_dir>/<static_dir>/filename anf finally |
627 <default_site>/<default_theme_dir>/<static_dir> (i.e. sat_templates). | 660 <default_site>/<default_theme_dir>/<static_dir> (i.e. sat_templates). |
635 the relative path to the file, from theme root dir. | 668 the relative path to the file, from theme root dir. |
636 None if not found. | 669 None if not found. |
637 """ | 670 """ |
638 if template_data.site is None: | 671 if template_data.site is None: |
639 # we have an absolue path | 672 # we have an absolue path |
640 if (not template_data.theme is None | 673 if not template_data.theme is None or not template_data.path.startswith("/"): |
641 or not template_data.path.startswith('/')): | |
642 raise exceptions.InternalError( | 674 raise exceptions.InternalError( |
643 "invalid template data, was expecting absolute URL") | 675 "invalid template data, was expecting absolute URL" |
676 ) | |
644 static_dir = os.path.dirname(template_data.path) | 677 static_dir = os.path.dirname(template_data.path) |
645 file_path = os.path.join(static_dir, filename) | 678 file_path = os.path.join(static_dir, filename) |
646 if os.path.exists(file_path): | 679 if os.path.exists(file_path): |
647 return TemplateData(site=None, theme=None, path=file_path) | 680 return TemplateData(site=None, theme=None, path=file_path) |
648 else: | 681 else: |
649 return None | 682 return None |
650 | 683 |
651 sites_and_themes = TemplateLoader.get_sites_and_themes(template_data.site, | 684 sites_and_themes = TemplateLoader.get_sites_and_themes( |
652 template_data.theme, | 685 template_data.site, template_data.theme, settings |
653 settings) | 686 ) |
654 for site, theme in sites_and_themes: | 687 for site, theme in sites_and_themes: |
655 site_root_dir = self.sites_paths[site] | 688 site_root_dir = self.sites_paths[site] |
656 relative_path = os.path.join(C.TEMPLATE_STATIC_DIR, filename) | 689 relative_path = os.path.join(C.TEMPLATE_STATIC_DIR, filename) |
657 absolute_path = os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, | 690 absolute_path = os.path.join( |
658 theme, relative_path) | 691 site_root_dir, C.TEMPLATE_TPL_DIR, theme, relative_path |
692 ) | |
659 if os.path.exists(absolute_path): | 693 if os.path.exists(absolute_path): |
660 return TemplateData(site=site, theme=theme, path=relative_path) | 694 return TemplateData(site=site, theme=theme, path=relative_path) |
661 | 695 |
662 return None | 696 return None |
663 | 697 |
664 def _append_css_paths( | 698 def _append_css_paths( |
665 self, | 699 self, |
666 template_data: TemplateData, | 700 template_data: TemplateData, |
667 css_files: list, | 701 css_files: list, |
668 css_files_noscript: list, | 702 css_files_noscript: list, |
669 name_root: str, | 703 name_root: str, |
670 settings: dict | 704 settings: dict, |
671 | 705 ) -> None: |
672 ) -> None: | |
673 """Append found css to css_files and css_files_noscript | 706 """Append found css to css_files and css_files_noscript |
674 | 707 |
675 @param css_files: list to fill of relative path to found css file | 708 @param css_files: list to fill of relative path to found css file |
676 @param css_files_noscript: list to fill of relative path to found css file | 709 @param css_files_noscript: list to fill of relative path to found css file |
677 with "_noscript" suffix | 710 with "_noscript" suffix |
679 name = name_root + ".css" | 712 name = name_root + ".css" |
680 css_path = self.get_static_path(template_data, name, settings) | 713 css_path = self.get_static_path(template_data, name, settings) |
681 if css_path is not None: | 714 if css_path is not None: |
682 css_files.append(self.get_front_url(css_path)) | 715 css_files.append(self.get_front_url(css_path)) |
683 noscript_name = name_root + "_noscript.css" | 716 noscript_name = name_root + "_noscript.css" |
684 noscript_path = self.get_static_path( | 717 noscript_path = self.get_static_path(template_data, noscript_name, settings) |
685 template_data, noscript_name, settings) | |
686 if noscript_path is not None: | 718 if noscript_path is not None: |
687 css_files_noscript.append(self.get_front_url(noscript_path)) | 719 css_files_noscript.append(self.get_front_url(noscript_path)) |
688 | 720 |
689 def get_css_files(self, template_data): | 721 def get_css_files(self, template_data): |
690 """Retrieve CSS files to use according template_data | 722 """Retrieve CSS files to use according template_data |
716 - front URLs of CSS files to use when scripts are not enabled | 748 - front URLs of CSS files to use when scripts are not enabled |
717 """ | 749 """ |
718 # TODO: some caching would be nice | 750 # TODO: some caching would be nice |
719 css_files = [] | 751 css_files = [] |
720 css_files_noscript = [] | 752 css_files_noscript = [] |
721 path_elems = template_data.path.split('/') | 753 path_elems = template_data.path.split("/") |
722 path_elems[-1] = os.path.splitext(path_elems[-1])[0] | 754 path_elems[-1] = os.path.splitext(path_elems[-1])[0] |
723 site = template_data.site | 755 site = template_data.site |
724 if site is None: | 756 if site is None: |
725 # absolute path | 757 # absolute path |
726 settings = {} | 758 settings = {} |
727 else: | 759 else: |
728 settings = self.sites_themes[site][template_data.theme]['settings'] | 760 settings = self.sites_themes[site][template_data.theme]["settings"] |
729 | 761 |
730 css_path = self.get_static_path(template_data, 'fonts.css', settings) | 762 css_path = self.get_static_path(template_data, "fonts.css", settings) |
731 if css_path is not None: | 763 if css_path is not None: |
732 css_files.append(self.get_front_url(css_path)) | 764 css_files.append(self.get_front_url(css_path)) |
733 | 765 |
734 for name_root in ('styles', 'styles_extra', 'highlight'): | 766 for name_root in ("styles", "styles_extra", "highlight"): |
735 self._append_css_paths( | 767 self._append_css_paths( |
736 template_data, css_files, css_files_noscript, name_root, settings) | 768 template_data, css_files, css_files_noscript, name_root, settings |
769 ) | |
737 | 770 |
738 for idx in range(len(path_elems)): | 771 for idx in range(len(path_elems)): |
739 name_root = "_".join(path_elems[:idx+1]) | 772 name_root = "_".join(path_elems[: idx + 1]) |
740 self._append_css_paths( | 773 self._append_css_paths( |
741 template_data, css_files, css_files_noscript, name_root, settings) | 774 template_data, css_files, css_files_noscript, name_root, settings |
775 ) | |
742 | 776 |
743 return css_files, css_files_noscript | 777 return css_files, css_files_noscript |
744 | 778 |
745 ## custom filters ## | 779 ## custom filters ## |
746 | 780 |
748 def _front_url(self, ctx, relative_url): | 782 def _front_url(self, ctx, relative_url): |
749 """Get front URL (URL seen by end-user) from a relative URL | 783 """Get front URL (URL seen by end-user) from a relative URL |
750 | 784 |
751 This default method return absolute full path | 785 This default method return absolute full path |
752 """ | 786 """ |
753 template_data = ctx['template_data'] | 787 template_data = ctx["template_data"] |
754 if template_data.site is None: | 788 if template_data.site is None: |
755 assert template_data.theme is None | 789 assert template_data.theme is None |
756 assert template_data.path.startswith("/") | 790 assert template_data.path.startswith("/") |
757 return os.path.join(os.path.dirname(template_data.path, relative_url)) | 791 return os.path.join(os.path.dirname(template_data.path, relative_url)) |
758 | 792 |
759 site_root_dir = self.sites_paths[template_data.site] | 793 site_root_dir = self.sites_paths[template_data.site] |
760 return os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, template_data.theme, | 794 return os.path.join( |
761 relative_url) | 795 site_root_dir, C.TEMPLATE_TPL_DIR, template_data.theme, relative_url |
762 | 796 ) |
763 def _bare_jid(self, full_jid: str|jid.JID) -> str: | 797 |
798 def _bare_jid(self, full_jid: str | jid.JID) -> str: | |
764 """Return the bare JID""" | 799 """Return the bare JID""" |
765 return str(jid.JID(str(full_jid)).bare) | 800 return str(jid.JID(str(full_jid)).bare) |
766 | 801 |
767 @pass_context | 802 @pass_context |
768 def _next_gidx(self, ctx, value): | 803 def _next_gidx(self, ctx, value): |
782 fmt: str = "short", | 817 fmt: str = "short", |
783 date_only: bool = False, | 818 date_only: bool = False, |
784 auto_limit: int = 7, | 819 auto_limit: int = 7, |
785 auto_old_fmt: str = "short", | 820 auto_old_fmt: str = "short", |
786 auto_new_fmt: str = "relative", | 821 auto_new_fmt: str = "relative", |
787 tz_name: Optional[str] = None | 822 tz_name: Optional[str] = None, |
788 ) -> str: | 823 ) -> str: |
789 if is_undefined(fmt): | 824 if is_undefined(fmt): |
790 fmt = "short" | 825 fmt = "short" |
791 | 826 |
792 try: | 827 try: |
793 return date_utils.date_fmt( | 828 return date_utils.date_fmt( |
794 timestamp, fmt, date_only, auto_limit, auto_old_fmt, | 829 timestamp, |
795 auto_new_fmt, locale_str = self._locale_str, | 830 fmt, |
796 tz_info=tz_name or date_utils.TZ_UTC | 831 date_only, |
832 auto_limit, | |
833 auto_old_fmt, | |
834 auto_new_fmt, | |
835 locale_str=self._locale_str, | |
836 tz_info=tz_name or date_utils.TZ_UTC, | |
797 ) | 837 ) |
798 except Exception as e: | 838 except Exception as e: |
799 log.warning(_("Can't parse date: {msg}").format(msg=e)) | 839 log.warning(_("Can't parse date: {msg}").format(msg=e)) |
800 return str(timestamp) | 840 return str(timestamp) |
801 | 841 |
984 extra_attrs = " ".join(f'{k}="{html.escape(str(v))}"' for k, v in kwargs.items()) | 1024 extra_attrs = " ".join(f'{k}="{html.escape(str(v))}"' for k, v in kwargs.items()) |
985 return safe( | 1025 return safe( |
986 '<svg class="svg-icon{cls}"{extra_attrs} xmlns="http://www.w3.org/2000/svg" ' | 1026 '<svg class="svg-icon{cls}"{extra_attrs} xmlns="http://www.w3.org/2000/svg" ' |
987 'viewBox="0 0 100 100">\n' | 1027 'viewBox="0 0 100 100">\n' |
988 ' <use href="#{name}"/>' | 1028 ' <use href="#{name}"/>' |
989 '</svg>\n'.format( | 1029 "</svg>\n".format( |
990 name=name, | 1030 name=name, |
991 cls=(" " + cls) if cls else "", | 1031 cls=(" " + cls) if cls else "", |
992 extra_attrs=" " + extra_attrs if extra_attrs else "" | 1032 extra_attrs=" " + extra_attrs if extra_attrs else "", |
993 ) | 1033 ) |
994 ) | 1034 ) |
995 | 1035 |
996 def _icon_from_client(self, client): | 1036 def _icon_from_client(self, client): |
997 """Get icon name to represent a disco client""" | 1037 """Get icon name to represent a disco client""" |
998 if client is None: | 1038 if client is None: |
999 return 'desktop' | 1039 return "desktop" |
1000 elif 'pc' in client: | 1040 elif "pc" in client: |
1001 return 'desktop' | 1041 return "desktop" |
1002 elif 'phone' in client: | 1042 elif "phone" in client: |
1003 return 'mobile' | 1043 return "mobile" |
1004 elif 'web' in client: | 1044 elif "web" in client: |
1005 return 'globe' | 1045 return "globe" |
1006 elif 'console' in client: | 1046 elif "console" in client: |
1007 return 'terminal' | 1047 return "terminal" |
1008 else: | 1048 else: |
1009 return 'desktop' | 1049 return "desktop" |
1010 | 1050 |
1011 def render(self, template, site=None, theme=None, locale=C.DEFAULT_LOCALE, | 1051 def render( |
1012 media_path="", css_files=None, css_inline=False, **kwargs): | 1052 self, |
1053 template, | |
1054 site=None, | |
1055 theme=None, | |
1056 locale=C.DEFAULT_LOCALE, | |
1057 media_path="", | |
1058 css_files=None, | |
1059 css_inline=False, | |
1060 **kwargs, | |
1061 ): | |
1013 """Render a template | 1062 """Render a template |
1014 | 1063 |
1015 @param template(unicode): template to render (e.g. blog/articles.html) | 1064 @param template(unicode): template to render (e.g. blog/articles.html) |
1016 @param site(unicode): site name | 1065 @param site(unicode): site name |
1017 None or empty string for defaut site (i.e. SàT templates) | 1066 None or empty string for defaut site (i.e. SàT templates) |
1029 if not template: | 1078 if not template: |
1030 raise ValueError("template can't be empty") | 1079 raise ValueError("template can't be empty") |
1031 if site is not None or theme is not None: | 1080 if site is not None or theme is not None: |
1032 # user wants to set site and/or theme, so we add it to the template path | 1081 # user wants to set site and/or theme, so we add it to the template path |
1033 if site is None: | 1082 if site is None: |
1034 site = '' | 1083 site = "" |
1035 if theme is None: | 1084 if theme is None: |
1036 theme = C.TEMPLATE_THEME_DEFAULT | 1085 theme = C.TEMPLATE_THEME_DEFAULT |
1037 if template[0] == "(": | 1086 if template[0] == "(": |
1038 raise ValueError( | 1087 raise ValueError( |
1039 "you can't specify site or theme in template path and in argument " | 1088 "you can't specify site or theme in template path and in argument " |
1040 "at the same time" | 1089 "at the same time" |
1041 ) | 1090 ) |
1042 | 1091 |
1043 template_data = TemplateData(site, theme, template) | 1092 template_data = TemplateData(site, theme, template) |
1044 template = "({site}/{theme}){template}".format( | 1093 template = "({site}/{theme}){template}".format( |
1045 site=site, theme=theme, template=template) | 1094 site=site, theme=theme, template=template |
1095 ) | |
1046 else: | 1096 else: |
1047 template_data = self.env.loader.parse_template(template) | 1097 template_data = self.env.loader.parse_template(template) |
1048 | 1098 |
1049 # we need to save template_data in environment, to load right templates when they | 1099 # we need to save template_data in environment, to load right templates when they |
1050 # are referenced from other templates (e.g. import) | 1100 # are referenced from other templates (e.g. import) |
1064 kwargs["icon"] = self._icon_use | 1114 kwargs["icon"] = self._icon_use |
1065 kwargs["icon_from_client"] = self._icon_from_client | 1115 kwargs["icon_from_client"] = self._icon_from_client |
1066 | 1116 |
1067 if css_inline: | 1117 if css_inline: |
1068 css_contents = [] | 1118 css_contents = [] |
1069 for files, suffix in ((css_files, ""), | 1119 for files, suffix in ((css_files, ""), (css_files_noscript, "_noscript")): |
1070 (css_files_noscript, "_noscript")): | |
1071 site_root_dir = self.sites_paths[template_data.site] | 1120 site_root_dir = self.sites_paths[template_data.site] |
1072 for css_file in files: | 1121 for css_file in files: |
1073 css_file_path = os.path.join(site_root_dir, css_file) | 1122 css_file_path = os.path.join(site_root_dir, css_file) |
1074 with open(css_file_path) as f: | 1123 with open(css_file_path) as f: |
1075 css_contents.append(f.read()) | 1124 css_contents.append(f.read()) |
1088 css_files_noscript=css_files_noscript, | 1137 css_files_noscript=css_files_noscript, |
1089 locale=self._locale, | 1138 locale=self._locale, |
1090 locales=self.locales, | 1139 locales=self.locales, |
1091 gidx=Indexer(), | 1140 gidx=Indexer(), |
1092 script=scripts_handler, | 1141 script=scripts_handler, |
1093 **kwargs | 1142 **kwargs, |
1094 ) | 1143 ) |
1095 self.env._template_data = None | 1144 self.env._template_data = None |
1096 return rendered | 1145 return rendered |