Mercurial > libervia-backend
comparison sat/tools/common/template.py @ 3268:85c9cfcd4f5e
tools (common/template): theme settings with possibility to disable default fallback for CSS:
- themes can now have a `settings.json` file at their root to modify the renderer
behaviour
- the `css_default_fallback` boolean (true by default) can be used to disable the fallback
mechanism when CSS files are checked. Useful when a theme doesn't follow the CSS naming of
default theme.
- introduced some type hints
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 03 May 2020 17:01:39 +0200 |
parents | 2eeca6fd08f7 |
children | 430204a3cc10 |
comparison
equal
deleted
inserted
replaced
3267:2eeca6fd08f7 | 3268:85c9cfcd4f5e |
---|---|
22 import time | 22 import time |
23 import re | 23 import re |
24 import json | 24 import json |
25 from pathlib import Path | 25 from pathlib import Path |
26 from collections import namedtuple | 26 from collections import namedtuple |
27 from typing import Optional, List, Tuple | |
27 from xml.sax.saxutils import quoteattr | 28 from xml.sax.saxutils import quoteattr |
28 from babel import support | 29 from babel import support |
29 from babel import Locale | 30 from babel import Locale |
30 from babel.core import UnknownLocaleError | 31 from babel.core import UnknownLocaleError |
31 import pygments | 32 import pygments |
157 theme=theme, reserved=TPL_RESERVED_CHARS)) | 158 theme=theme, reserved=TPL_RESERVED_CHARS)) |
158 | 159 |
159 return TemplateData(site, theme, template_path) | 160 return TemplateData(site, theme, template_path) |
160 | 161 |
161 @staticmethod | 162 @staticmethod |
162 def getSitesAndThemes(site, theme): | 163 def getSitesAndThemes( |
164 site: str, | |
165 theme: str, | |
166 default_fallback: bool=True | |
167 ) -> List[Tuple[str, str]]: | |
163 """Get sites and themes to check for template/file | 168 """Get sites and themes to check for template/file |
164 | 169 |
165 Will add default theme and default site in search list when suitable | 170 Will add default theme and default site in search list when suitable |
166 @param site(unicode): site requested | 171 @param site: site requested |
167 @param theme(unicode): theme requested | 172 @param theme: theme requested |
168 @return (list[tuple[unicode, unicode]]): site and theme couples to check | 173 @return: site and theme couples to check |
169 """ | 174 """ |
170 sites_and_themes = [[site, theme]] | 175 sites_and_themes = [[site, theme]] |
171 if theme != C.TEMPLATE_THEME_DEFAULT: | 176 if theme != C.TEMPLATE_THEME_DEFAULT and default_fallback: |
172 sites_and_themes.append([site, C.TEMPLATE_THEME_DEFAULT]) | 177 sites_and_themes.append([site, C.TEMPLATE_THEME_DEFAULT]) |
173 if site: | 178 if site and default_fallback: |
174 # the site is not the default one, so we add default at the end | 179 # the site is not the default one, so we add default at the end |
175 sites_and_themes.append(['', C.TEMPLATE_THEME_DEFAULT]) | 180 sites_and_themes.append(['', C.TEMPLATE_THEME_DEFAULT]) |
176 return sites_and_themes | 181 return sites_and_themes |
177 | 182 |
178 def _get_template_f(self, site, theme, path_elts): | 183 def _get_template_f(self, site, theme, path_elts): |
381 tpl_path = Path(site_path) / C.TEMPLATE_TPL_DIR | 386 tpl_path = Path(site_path) / C.TEMPLATE_TPL_DIR |
382 for p in tpl_path.iterdir(): | 387 for p in tpl_path.iterdir(): |
383 if not p.is_dir(): | 388 if not p.is_dir(): |
384 continue | 389 continue |
385 log.debug(f"theme found for {site or 'default site'}: {p.name}") | 390 log.debug(f"theme found for {site or 'default site'}: {p.name}") |
386 theme_data = self.sites_themes.setdefault(site, {})[p.name] = {'path': p} | 391 theme_data = self.sites_themes.setdefault(site, {})[p.name] = { |
392 'path': p, | |
393 'settings': {}} | |
394 theme_settings = p / "settings.json" | |
395 if theme_settings.is_file: | |
396 try: | |
397 with theme_settings.open() as f: | |
398 settings = json.load(f) | |
399 except Exception as e: | |
400 log.warning(_( | |
401 "Can't load theme settings at {path}").format( | |
402 path=theme_settings)) | |
403 else: | |
404 log.debug( | |
405 f"found settings for theme {p.name!r} at {theme_settings}") | |
406 theme_data['settings'] = settings | |
387 browser_path = p / BROWSER_DIR | 407 browser_path = p / BROWSER_DIR |
388 if browser_path.is_dir(): | 408 if browser_path.is_dir(): |
389 theme_data['browser_path'] = browser_path | 409 theme_data['browser_path'] = browser_path |
390 browser_meta_path = browser_path / BROWSER_META_FILE | 410 browser_meta_path = browser_path / BROWSER_META_FILE |
391 if browser_meta_path.is_file(): | 411 if browser_meta_path.is_file(): |
546 try: | 566 try: |
547 return self.sites_themes[site_name] | 567 return self.sites_themes[site_name] |
548 except KeyError: | 568 except KeyError: |
549 raise exceptions.NotFound(f"no theme found for {site_name}") | 569 raise exceptions.NotFound(f"no theme found for {site_name}") |
550 | 570 |
551 def getStaticPath(self, template_data, filename): | 571 def getStaticPath( |
572 self, | |
573 template_data: TemplateData, | |
574 filename: str, | |
575 default_fallback: bool=True | |
576 ) -> Optional[TemplateData]: | |
552 """Retrieve path of a static file if it exists with current theme or default | 577 """Retrieve path of a static file if it exists with current theme or default |
553 | 578 |
554 File will be looked at <site_root_dir>/<theme_dir>/<static_dir>/filename, | 579 File will be looked at <site_root_dir>/<theme_dir>/<static_dir>/filename, |
555 then <site_root_dir>/<default_theme_dir>/<static_dir>/filename anf finally | 580 then <site_root_dir>/<default_theme_dir>/<static_dir>/filename anf finally |
556 <default_site>/<default_theme_dir>/<static_dir> (i.e. sat_templates). | 581 <default_site>/<default_theme_dir>/<static_dir> (i.e. sat_templates). |
557 In case of absolue URL, base dir of template is used as base. For instance if | 582 In case of absolute URL, base dir of template is used as base. For instance if |
558 template is an absolute template to "/some/path/template.html", file will be | 583 template is an absolute template to "/some/path/template.html", file will be |
559 checked at "/some/path/<filename>" | 584 checked at "/some/path/<filename>" |
560 @param template_data(TemplateData): data of current template | 585 @param template_data: data of current template |
561 @return (TemplateData, None): built template data instance where .path is | 586 @param filename: name of the file to retrieve |
587 @param default_fallback: if True, default theme will be checked if the file is | |
588 not found in current theme, then default site with default theme will be used. | |
589 @return: built template data instance where .path is | |
562 the relative path to the file, from theme root dir. | 590 the relative path to the file, from theme root dir. |
563 None if not found. | 591 None if not found. |
564 """ | 592 """ |
565 if template_data.site is None: | 593 if template_data.site is None: |
566 # we have and absolue path | 594 # we have an absolue path |
567 if (not template_data.theme is None | 595 if (not template_data.theme is None |
568 or not template_data.path.startswith('/')): | 596 or not template_data.path.startswith('/')): |
569 raise exceptions.InternalError( | 597 raise exceptions.InternalError( |
570 "invalid template data, was expecting absolute URL") | 598 "invalid template data, was expecting absolute URL") |
571 static_dir = os.path.dirname(template_data.path) | 599 static_dir = os.path.dirname(template_data.path) |
574 return TemplateData(site=None, theme=None, path=file_path) | 602 return TemplateData(site=None, theme=None, path=file_path) |
575 else: | 603 else: |
576 return None | 604 return None |
577 | 605 |
578 sites_and_themes = TemplateLoader.getSitesAndThemes(template_data.site, | 606 sites_and_themes = TemplateLoader.getSitesAndThemes(template_data.site, |
579 template_data.theme) | 607 template_data.theme, |
608 default_fallback) | |
580 for site, theme in sites_and_themes: | 609 for site, theme in sites_and_themes: |
581 site_root_dir = self.sites_paths[site] | 610 site_root_dir = self.sites_paths[site] |
582 relative_path = os.path.join(C.TEMPLATE_STATIC_DIR, filename) | 611 relative_path = os.path.join(C.TEMPLATE_STATIC_DIR, filename) |
583 absolute_path = os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, | 612 absolute_path = os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, |
584 theme, relative_path) | 613 theme, relative_path) |
585 if os.path.exists(absolute_path): | 614 if os.path.exists(absolute_path): |
586 return TemplateData(site=site, theme=theme, path=relative_path) | 615 return TemplateData(site=site, theme=theme, path=relative_path) |
587 | 616 |
588 return None | 617 return None |
589 | 618 |
590 def _appendCSSPaths(self, template_data, css_files, css_files_noscript, name_root): | 619 def _appendCSSPaths( |
620 self, | |
621 template_data: TemplateData, | |
622 css_files: list, | |
623 css_files_noscript: list, | |
624 name_root: str, | |
625 default_fallback: bool | |
626 | |
627 ) -> None: | |
591 """Append found css to css_files and css_files_noscript | 628 """Append found css to css_files and css_files_noscript |
592 | 629 |
593 @param css_files(list): list to fill of relative path to found css file | 630 @param css_files: list to fill of relative path to found css file |
594 @param css_files_noscript(list): list to fill of relative path to found css file | 631 @param css_files_noscript: list to fill of relative path to found css file |
595 with "_noscript" suffix | 632 with "_noscript" suffix |
596 """ | 633 """ |
597 name = name_root + ".css" | 634 name = name_root + ".css" |
598 css_path = self.getStaticPath(template_data, name) | 635 css_path = self.getStaticPath(template_data, name, default_fallback) |
599 if css_path is not None: | 636 if css_path is not None: |
600 css_files.append(self.getFrontURL(css_path)) | 637 css_files.append(self.getFrontURL(css_path)) |
601 noscript_name = name_root + "_noscript.css" | 638 noscript_name = name_root + "_noscript.css" |
602 noscript_path = self.getStaticPath(template_data, noscript_name) | 639 noscript_path = self.getStaticPath( |
640 template_data, noscript_name, default_fallback) | |
603 if noscript_path is not None: | 641 if noscript_path is not None: |
604 css_files_noscript.append(self.getFrontURL(noscript_path)) | 642 css_files_noscript.append(self.getFrontURL(noscript_path)) |
605 | 643 |
606 def getCSSFiles(self, template_data): | 644 def getCSSFiles(self, template_data): |
607 """Retrieve CSS files to use according template_data | 645 """Retrieve CSS files to use according template_data |
622 else default/static/blog.css (if it exists too) | 660 else default/static/blog.css (if it exists too) |
623 - some_theme/static/blog_articles.css is returned if it exists | 661 - some_theme/static/blog_articles.css is returned if it exists |
624 else default/static/blog_articles.css (if it exists too) | 662 else default/static/blog_articles.css (if it exists too) |
625 and for each found files, if same file with _noscript suffix exists, it is put | 663 and for each found files, if same file with _noscript suffix exists, it is put |
626 in noscript list (for instance (some_theme/static/styles_noscript.css)). | 664 in noscript list (for instance (some_theme/static/styles_noscript.css)). |
665 The behaviour may be changed with theme settings: if "css_default_fallback" is | |
666 False, only CSS from the theme is returned if it exists, default CSS is never | |
667 used. | |
627 @param template_data(TemplateData): data of the current template | 668 @param template_data(TemplateData): data of the current template |
628 @return (tuple[list[unicode], list[unicode]]): a tuple with: | 669 @return (tuple[list[unicode], list[unicode]]): a tuple with: |
629 - front URLs of CSS files to use | 670 - front URLs of CSS files to use |
630 - front URLs of CSS files to use when scripts are not enabled | 671 - front URLs of CSS files to use when scripts are not enabled |
631 """ | 672 """ |
632 # TODO: some caching would be nice | 673 # TODO: some caching would be nice |
633 css_files = [] | 674 css_files = [] |
634 css_files_noscript = [] | 675 css_files_noscript = [] |
635 path_elems = template_data.path.split('/') | 676 path_elems = template_data.path.split('/') |
636 path_elems[-1] = os.path.splitext(path_elems[-1])[0] | 677 path_elems[-1] = os.path.splitext(path_elems[-1])[0] |
637 | 678 site = template_data.site |
638 css_path = self.getStaticPath(template_data, 'fonts.css') | 679 if site is None: |
680 # absolute path | |
681 default_fallback = True | |
682 else: | |
683 default_fallback = ( | |
684 self.sites_themes[site][template_data.theme]['settings'] | |
685 ).get('css_default_fallback', True) | |
686 | |
687 css_path = self.getStaticPath(template_data, 'fonts.css', default_fallback) | |
639 if css_path is not None: | 688 if css_path is not None: |
640 css_files.append(self.getFrontURL(css_path)) | 689 css_files.append(self.getFrontURL(css_path)) |
641 | 690 |
642 for name_root in ('styles', 'styles_extra', 'highlight'): | 691 for name_root in ('styles', 'styles_extra', 'highlight'): |
643 self._appendCSSPaths(template_data, css_files, css_files_noscript, name_root) | 692 self._appendCSSPaths( |
693 template_data, css_files, css_files_noscript, name_root, default_fallback) | |
644 | 694 |
645 for idx in range(len(path_elems)): | 695 for idx in range(len(path_elems)): |
646 name_root = "_".join(path_elems[:idx+1]) | 696 name_root = "_".join(path_elems[:idx+1]) |
647 self._appendCSSPaths(template_data, css_files, css_files_noscript, name_root) | 697 self._appendCSSPaths( |
698 template_data, css_files, css_files_noscript, name_root, default_fallback) | |
648 | 699 |
649 return css_files, css_files_noscript | 700 return css_files, css_files_noscript |
650 | 701 |
651 ## custom filters ## | 702 ## custom filters ## |
652 | 703 |