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
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