Mercurial > libervia-backend
comparison sat/tools/common/template.py @ 3478:b65175eb7769
tools (common/template): new `fallback` settings:
`fallback` can be used to change fallback behaviour. By default, fallback is done on
`default` theme, this can be set to an other theme with a string, or to a list of
fallback. The list can also be empty is no fallback is desired (notably usefull for "main"
themes, on which other themes may fallback).
This setting replaces `css_default_fallback`.
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 19 Mar 2021 14:01:52 +0100 |
parents | 29f8122f00f3 |
children | be6d91572633 |
comparison
equal
deleted
inserted
replaced
3477:9498f32ba6f7 | 3478:b65175eb7769 |
---|---|
80 | 80 |
81 class TemplateLoader(jinja2.BaseLoader): | 81 class TemplateLoader(jinja2.BaseLoader): |
82 """A template loader which handle site, theme and absolute paths""" | 82 """A template loader which handle site, theme and absolute paths""" |
83 # TODO: list_templates should be implemented | 83 # TODO: list_templates should be implemented |
84 | 84 |
85 def __init__(self, sites_paths, trusted=False): | 85 def __init__(self, sites_paths, sites_themes, trusted=False): |
86 """ | 86 """ |
87 @param trusted(bool): if True, absolue template paths will be allowed | 87 @param trusted(bool): if True, absolue template paths will be allowed |
88 be careful when using this option and sure that you can trust the template, | 88 be careful when using this option and sure that you can trust the template, |
89 as this allow the template to open any file on the system that the | 89 as this allow the template to open any file on the system that the |
90 launching user can access. | 90 launching user can access. |
91 """ | 91 """ |
92 if not sites_paths or not "" in sites_paths: | 92 if not sites_paths or not "" in sites_paths: |
93 raise exceptions.InternalError("Invalid sites_paths") | 93 raise exceptions.InternalError("Invalid sites_paths") |
94 super(jinja2.BaseLoader, self).__init__() | 94 super(jinja2.BaseLoader, self).__init__() |
95 self.sites_paths = sites_paths | 95 self.sites_paths = sites_paths |
96 self.sites_themes = sites_themes | |
96 self.trusted = trusted | 97 self.trusted = trusted |
97 | 98 |
98 @staticmethod | 99 @staticmethod |
99 def parse_template(template): | 100 def parse_template(template): |
100 """Parse template path and return site, theme and path | 101 """Parse template path and return site, theme and path |
161 | 162 |
162 @staticmethod | 163 @staticmethod |
163 def getSitesAndThemes( | 164 def getSitesAndThemes( |
164 site: str, | 165 site: str, |
165 theme: str, | 166 theme: str, |
166 default_fallback: bool=True | 167 settings: Optional[dict] = None, |
167 ) -> List[Tuple[str, str]]: | 168 ) -> List[Tuple[str, str]]: |
168 """Get sites and themes to check for template/file | 169 """Get sites and themes to check for template/file |
169 | 170 |
170 Will add default theme and default site in search list when suitable | 171 Will add default theme and default site in search list when suitable. Settings' |
172 `fallback` can be used to modify behaviour: themes in this list will then be used | |
173 instead of default (it can also be empty list or None, in which case no fallback | |
174 is used). | |
175 | |
171 @param site: site requested | 176 @param site: site requested |
172 @param theme: theme requested | 177 @param theme: theme requested |
173 @return: site and theme couples to check | 178 @return: site and theme couples to check |
174 """ | 179 """ |
180 if settings is None: | |
181 settings = {} | |
175 sites_and_themes = [[site, theme]] | 182 sites_and_themes = [[site, theme]] |
176 if theme != C.TEMPLATE_THEME_DEFAULT and default_fallback: | 183 fallback = settings.get("fallback", [C.TEMPLATE_THEME_DEFAULT]) |
177 sites_and_themes.append([site, C.TEMPLATE_THEME_DEFAULT]) | 184 for fb_theme in fallback: |
178 if site and default_fallback: | 185 if theme != fb_theme: |
179 # the site is not the default one, so we add default at the end | 186 sites_and_themes.append([site, fb_theme]) |
180 sites_and_themes.append(['', C.TEMPLATE_THEME_DEFAULT]) | 187 if site: |
188 for fb_theme in fallback: | |
189 sites_and_themes.append(["", fb_theme]) | |
181 return sites_and_themes | 190 return sites_and_themes |
182 | 191 |
183 def _get_template_f(self, site, theme, path_elts): | 192 def _get_template_f(self, site, theme, path_elts): |
184 """Look for template and return opened file if found | 193 """Look for template and return opened file if found |
185 | 194 |
192 - absolute file path, or None if not found | 201 - absolute file path, or None if not found |
193 """ | 202 """ |
194 if site is None: | 203 if site is None: |
195 raise exceptions.InternalError( | 204 raise exceptions.InternalError( |
196 "_get_template_f must not be used with absolute path") | 205 "_get_template_f must not be used with absolute path") |
197 for site, theme in self.getSitesAndThemes(site, theme): | 206 settings = self.sites_themes[site][theme]['settings'] |
207 for site_to_check, theme_to_check in self.getSitesAndThemes( | |
208 site, theme, settings): | |
198 try: | 209 try: |
199 base_path = self.sites_paths[site] | 210 base_path = self.sites_paths[site_to_check] |
200 except KeyError: | 211 except KeyError: |
201 log.warning(_("Unregistered site requested: {site}").format( | 212 log.warning(_("Unregistered site requested: {site_to_check}").format( |
202 site=site)) | 213 site_to_check=site_to_check)) |
203 filepath = os.path.join(base_path, C.TEMPLATE_TPL_DIR, theme, *path_elts) | 214 filepath = os.path.join( |
215 base_path, | |
216 C.TEMPLATE_TPL_DIR, | |
217 theme_to_check, | |
218 *path_elts | |
219 ) | |
204 f = utils.open_if_exists(filepath, 'r') | 220 f = utils.open_if_exists(filepath, 'r') |
205 if f is not None: | 221 if f is not None: |
206 return f, filepath | 222 return f, filepath |
207 return None, None | 223 return None, None |
208 | 224 |
396 try: | 412 try: |
397 with theme_settings.open() as f: | 413 with theme_settings.open() as f: |
398 settings = json.load(f) | 414 settings = json.load(f) |
399 except Exception as e: | 415 except Exception as e: |
400 log.warning(_( | 416 log.warning(_( |
401 "Can't load theme settings at {path}").format( | 417 "Can't load theme settings at {path}: {e}").format( |
402 path=theme_settings)) | 418 path=theme_settings, e=e)) |
403 else: | 419 else: |
404 log.debug( | 420 log.debug( |
405 f"found settings for theme {p.name!r} at {theme_settings}") | 421 f"found settings for theme {p.name!r} at {theme_settings}") |
422 fallback = settings.get("fallback") | |
423 if fallback is None: | |
424 settings["fallback"] = [] | |
425 elif isinstance(fallback, str): | |
426 settings["fallback"] = [fallback] | |
427 elif not isinstance(fallback, list): | |
428 raise ValueError( | |
429 'incorrect type for "fallback" in settings ' | |
430 f'({type(fallback)}) at {theme_settings}: {fallback}' | |
431 ) | |
406 theme_data['settings'] = settings | 432 theme_data['settings'] = settings |
407 browser_path = p / BROWSER_DIR | 433 browser_path = p / BROWSER_DIR |
408 if browser_path.is_dir(): | 434 if browser_path.is_dir(): |
409 theme_data['browser_path'] = browser_path | 435 theme_data['browser_path'] = browser_path |
410 browser_meta_path = browser_path / BROWSER_META_FILE | 436 browser_meta_path = browser_path / BROWSER_META_FILE |
417 f"Can't parse browser metadata at {browser_meta_path}: {e}" | 443 f"Can't parse browser metadata at {browser_meta_path}: {e}" |
418 ) | 444 ) |
419 continue | 445 continue |
420 | 446 |
421 self.env = Environment( | 447 self.env = Environment( |
422 loader=TemplateLoader(sites_paths=self.sites_paths, trusted=trusted), | 448 loader=TemplateLoader( |
449 sites_paths=self.sites_paths, | |
450 sites_themes=self.sites_themes, | |
451 trusted=trusted | |
452 ), | |
423 autoescape=jinja2.select_autoescape(["html", "xhtml", "xml"]), | 453 autoescape=jinja2.select_autoescape(["html", "xhtml", "xml"]), |
424 trim_blocks=True, | 454 trim_blocks=True, |
425 lstrip_blocks=True, | 455 lstrip_blocks=True, |
426 extensions=["jinja2.ext.i18n"], | 456 extensions=["jinja2.ext.i18n"], |
427 ) | 457 ) |
548 | 578 |
549 self._locale = locale | 579 self._locale = locale |
550 self._locale_str = locale_str | 580 self._locale_str = locale_str |
551 | 581 |
552 def getThemeAndRoot(self, template): | 582 def getThemeAndRoot(self, template): |
553 """retrieve theme and root dir of a given tempalte | 583 """retrieve theme and root dir of a given template |
554 | 584 |
555 @param template(unicode): template to parse | 585 @param template(unicode): template to parse |
556 @return (tuple[unicode, unicode]): theme and absolute path to theme's root dir | 586 @return (tuple[unicode, unicode]): theme and absolute path to theme's root dir |
557 @raise NotFound: requested site has not been found | 587 @raise NotFound: requested site has not been found |
558 """ | 588 """ |
575 | 605 |
576 def getStaticPath( | 606 def getStaticPath( |
577 self, | 607 self, |
578 template_data: TemplateData, | 608 template_data: TemplateData, |
579 filename: str, | 609 filename: str, |
580 default_fallback: bool=True | 610 settings: Optional[dict]=None |
581 ) -> Optional[TemplateData]: | 611 ) -> Optional[TemplateData]: |
582 """Retrieve path of a static file if it exists with current theme or default | 612 """Retrieve path of a static file if it exists with current theme or default |
583 | 613 |
584 File will be looked at <site_root_dir>/<theme_dir>/<static_dir>/filename, | 614 File will be looked at <site_root_dir>/<theme_dir>/<static_dir>/filename, |
585 then <site_root_dir>/<default_theme_dir>/<static_dir>/filename anf finally | 615 then <site_root_dir>/<default_theme_dir>/<static_dir>/filename anf finally |
587 In case of absolute URL, base dir of template is used as base. For instance if | 617 In case of absolute URL, base dir of template is used as base. For instance if |
588 template is an absolute template to "/some/path/template.html", file will be | 618 template is an absolute template to "/some/path/template.html", file will be |
589 checked at "/some/path/<filename>" | 619 checked at "/some/path/<filename>" |
590 @param template_data: data of current template | 620 @param template_data: data of current template |
591 @param filename: name of the file to retrieve | 621 @param filename: name of the file to retrieve |
592 @param default_fallback: if True, default theme will be checked if the file is | 622 @param settings: theme settings, can be used to modify behaviour |
593 not found in current theme, then default site with default theme will be used. | |
594 @return: built template data instance where .path is | 623 @return: built template data instance where .path is |
595 the relative path to the file, from theme root dir. | 624 the relative path to the file, from theme root dir. |
596 None if not found. | 625 None if not found. |
597 """ | 626 """ |
598 if template_data.site is None: | 627 if template_data.site is None: |
608 else: | 637 else: |
609 return None | 638 return None |
610 | 639 |
611 sites_and_themes = TemplateLoader.getSitesAndThemes(template_data.site, | 640 sites_and_themes = TemplateLoader.getSitesAndThemes(template_data.site, |
612 template_data.theme, | 641 template_data.theme, |
613 default_fallback) | 642 settings) |
614 for site, theme in sites_and_themes: | 643 for site, theme in sites_and_themes: |
615 site_root_dir = self.sites_paths[site] | 644 site_root_dir = self.sites_paths[site] |
616 relative_path = os.path.join(C.TEMPLATE_STATIC_DIR, filename) | 645 relative_path = os.path.join(C.TEMPLATE_STATIC_DIR, filename) |
617 absolute_path = os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, | 646 absolute_path = os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, |
618 theme, relative_path) | 647 theme, relative_path) |
625 self, | 654 self, |
626 template_data: TemplateData, | 655 template_data: TemplateData, |
627 css_files: list, | 656 css_files: list, |
628 css_files_noscript: list, | 657 css_files_noscript: list, |
629 name_root: str, | 658 name_root: str, |
630 default_fallback: bool | 659 settings: dict |
631 | 660 |
632 ) -> None: | 661 ) -> None: |
633 """Append found css to css_files and css_files_noscript | 662 """Append found css to css_files and css_files_noscript |
634 | 663 |
635 @param css_files: list to fill of relative path to found css file | 664 @param css_files: list to fill of relative path to found css file |
636 @param css_files_noscript: list to fill of relative path to found css file | 665 @param css_files_noscript: list to fill of relative path to found css file |
637 with "_noscript" suffix | 666 with "_noscript" suffix |
638 """ | 667 """ |
639 name = name_root + ".css" | 668 name = name_root + ".css" |
640 css_path = self.getStaticPath(template_data, name, default_fallback) | 669 css_path = self.getStaticPath(template_data, name, settings) |
641 if css_path is not None: | 670 if css_path is not None: |
642 css_files.append(self.getFrontURL(css_path)) | 671 css_files.append(self.getFrontURL(css_path)) |
643 noscript_name = name_root + "_noscript.css" | 672 noscript_name = name_root + "_noscript.css" |
644 noscript_path = self.getStaticPath( | 673 noscript_path = self.getStaticPath( |
645 template_data, noscript_name, default_fallback) | 674 template_data, noscript_name, settings) |
646 if noscript_path is not None: | 675 if noscript_path is not None: |
647 css_files_noscript.append(self.getFrontURL(noscript_path)) | 676 css_files_noscript.append(self.getFrontURL(noscript_path)) |
648 | 677 |
649 def getCSSFiles(self, template_data): | 678 def getCSSFiles(self, template_data): |
650 """Retrieve CSS files to use according template_data | 679 """Retrieve CSS files to use according template_data |
665 else default/static/blog.css (if it exists too) | 694 else default/static/blog.css (if it exists too) |
666 - some_theme/static/blog_articles.css is returned if it exists | 695 - some_theme/static/blog_articles.css is returned if it exists |
667 else default/static/blog_articles.css (if it exists too) | 696 else default/static/blog_articles.css (if it exists too) |
668 and for each found files, if same file with _noscript suffix exists, it is put | 697 and for each found files, if same file with _noscript suffix exists, it is put |
669 in noscript list (for instance (some_theme/static/styles_noscript.css)). | 698 in noscript list (for instance (some_theme/static/styles_noscript.css)). |
670 The behaviour may be changed with theme settings: if "css_default_fallback" is | 699 The behaviour may be changed with theme settings: if "fallback" is set, specified |
671 False, only CSS from the theme is returned if it exists, default CSS is never | 700 themes will be checked instead of default. The theme will be checked in given |
672 used. | 701 order, and "fallback" may be None or empty list to not check anything. |
673 @param template_data(TemplateData): data of the current template | 702 @param template_data(TemplateData): data of the current template |
674 @return (tuple[list[unicode], list[unicode]]): a tuple with: | 703 @return (tuple[list[unicode], list[unicode]]): a tuple with: |
675 - front URLs of CSS files to use | 704 - front URLs of CSS files to use |
676 - front URLs of CSS files to use when scripts are not enabled | 705 - front URLs of CSS files to use when scripts are not enabled |
677 """ | 706 """ |
681 path_elems = template_data.path.split('/') | 710 path_elems = template_data.path.split('/') |
682 path_elems[-1] = os.path.splitext(path_elems[-1])[0] | 711 path_elems[-1] = os.path.splitext(path_elems[-1])[0] |
683 site = template_data.site | 712 site = template_data.site |
684 if site is None: | 713 if site is None: |
685 # absolute path | 714 # absolute path |
686 default_fallback = True | 715 settings = {} |
687 else: | 716 else: |
688 default_fallback = ( | 717 settings = self.sites_themes[site][template_data.theme]['settings'] |
689 self.sites_themes[site][template_data.theme]['settings'] | 718 |
690 ).get('css_default_fallback', True) | 719 css_path = self.getStaticPath(template_data, 'fonts.css', settings) |
691 | |
692 css_path = self.getStaticPath(template_data, 'fonts.css', default_fallback) | |
693 if css_path is not None: | 720 if css_path is not None: |
694 css_files.append(self.getFrontURL(css_path)) | 721 css_files.append(self.getFrontURL(css_path)) |
695 | 722 |
696 for name_root in ('styles', 'styles_extra', 'highlight'): | 723 for name_root in ('styles', 'styles_extra', 'highlight'): |
697 self._appendCSSPaths( | 724 self._appendCSSPaths( |
698 template_data, css_files, css_files_noscript, name_root, default_fallback) | 725 template_data, css_files, css_files_noscript, name_root, settings) |
699 | 726 |
700 for idx in range(len(path_elems)): | 727 for idx in range(len(path_elems)): |
701 name_root = "_".join(path_elems[:idx+1]) | 728 name_root = "_".join(path_elems[:idx+1]) |
702 self._appendCSSPaths( | 729 self._appendCSSPaths( |
703 template_data, css_files, css_files_noscript, name_root, default_fallback) | 730 template_data, css_files, css_files_noscript, name_root, settings) |
704 | 731 |
705 return css_files, css_files_noscript | 732 return css_files, css_files_noscript |
706 | 733 |
707 ## custom filters ## | 734 ## custom filters ## |
708 | 735 |