Mercurial > libervia-backend
comparison src/tools/common/template.py @ 2169:f472179305a1
tools(templates): workflow improvments:
- template theme can be specified in parenthesis: (some_theme)path/to/template.html. Withtout parenthesis, "default" is used
- static content are supposed to be in [theme]/static, error pages in [theme]/error/[err_code].html
- default page are used in some case (2 for now): if error page with specified code doesn't exists, a base page is used, and if a page doesn't exist for a theme, the same one for default theme is looked for
- CSS files are automatically found for HTML pages
- CSS files can be split, the'll be added in the template according to the page requested.
- theme CSS file is looked for, and if not found the default theme equivalent is looked for.
- each element of a path can be associated to a CSS file, and styles.css is always there. For instance if blog/articles.html is requested, the following CSS can be included: "styles.css", "blog.css", "blog_article.css". They all must be in /static
- if the automatic finding of CSS files is not wanted, css_files arguments can be used instead, with full relative path (i.e. including theme)
- CSS files can be merged and included inline with css_inline argument
- root_path can be specified, it will be used as a prefix for static files
- requested theme (which may differ from actual theme, e.g. if the template is not found and default one is used instead) is available in template with "theme" variable
- added getThemeAndRoot method to retrieve theme and theme root path from template
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 05 Mar 2017 23:41:10 +0100 |
parents | 5734b0994cf0 |
children | e09048cb7595 |
comparison
equal
deleted
inserted
replaced
2168:255830fdb80b | 2169:f472179305a1 |
---|---|
34 try: | 34 try: |
35 import jinja2 | 35 import jinja2 |
36 except: | 36 except: |
37 raise exceptions.MissingModule(u'Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2') | 37 raise exceptions.MissingModule(u'Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2') |
38 | 38 |
39 HTML_EXT = ('html', 'xhtml') | |
40 # TODO: handle external path (an additional search path for templates should be settable by user | |
41 # TODO: handle absolute URL (should be used for trusted use cases) only (e.g. jp) for security reason | |
42 | |
43 | |
44 class TemplateLoader(jinja2.FileSystemLoader): | |
45 | |
46 def __init__(self): | |
47 searchpath = os.path.dirname(sat_templates.__file__) | |
48 super(TemplateLoader, self).__init__(searchpath, followlinks=True) | |
49 | |
50 def parse_template(self, template): | |
51 """parse template path and return theme and relative URL | |
52 | |
53 @param template_path(unicode): path to template with parenthesis syntax | |
54 @return (tuple[(unicode,None),unicode]): theme and template_path | |
55 theme can be None if relative path is used | |
56 relative path is the path from search path with theme specified | |
57 e.g. default/blog/articles.html | |
58 """ | |
59 if template.startswith(u'('): | |
60 try: | |
61 theme_end = template.index(u')') | |
62 except IndexError: | |
63 raise ValueError(u"incorrect theme in template") | |
64 theme = template[1:theme_end] | |
65 template = template[theme_end+1:] | |
66 if not template or template.startswith(u'/'): | |
67 raise ValueError(u"incorrect path after template name") | |
68 template = os.path.join(theme, template) | |
69 elif template.startswith(u'/'): | |
70 # absolute path means no template | |
71 theme = None | |
72 raise NotImplementedError(u'absolute path is not implemented yet') | |
73 else: | |
74 theme = C.TEMPLATE_THEME_DEFAULT | |
75 template = os.path.join(theme, template) | |
76 return theme, template | |
77 | |
78 def get_default_template(self, theme, template_path): | |
79 """return default template path | |
80 | |
81 @param theme(unicode): theme used | |
82 @param template_path(unicode): path to the not found template | |
83 @return (unicode, None): default path or None if there is not | |
84 """ | |
85 ext = os.path.splitext(template_path)[1][1:] | |
86 path_elems = template_path.split(u'/') | |
87 if ext in HTML_EXT: | |
88 if path_elems[1] == u'error': | |
89 # if an inexisting error page is requested, we return base page | |
90 default_path = os.path.join(theme, u'error/base.html') | |
91 return default_path | |
92 if theme != C.TEMPLATE_THEME_DEFAULT: | |
93 # if template doesn't exists for this theme, we try with default | |
94 return os.path.join(C.TEMPLATE_THEME_DEFAULT, path_elems[1:]) | |
95 | |
96 def get_source(self, environment, template): | |
97 """relative path to template dir, with special theme handling | |
98 | |
99 if the path is just relative, "default" theme is used. | |
100 The theme can be specified in parenthesis just before the path | |
101 e.g.: (some_theme)path/to/template.html | |
102 """ | |
103 theme, template_path = self.parse_template(template) | |
104 try: | |
105 return super(TemplateLoader, self).get_source(environment, template_path) | |
106 except jinja2.exceptions.TemplateNotFound as e: | |
107 # in some special cases, a defaut template is returned if nothing is found | |
108 if theme is not None: | |
109 default_path = self.get_default_template(theme, template_path) | |
110 if default_path is not None: | |
111 return super(TemplateLoader, self).get_source(environment, default_path) | |
112 # if no default template is found, we re-raise the error | |
113 raise e | |
114 | |
39 | 115 |
40 class Renderer(object): | 116 class Renderer(object): |
41 | 117 |
42 def __init__(self, host): # , template_dir=None): | 118 def __init__(self, host): |
43 self.host = host | 119 self.host = host |
44 self.base_dir = os.path.dirname(sat_templates.__file__) | 120 self.base_dir = os.path.dirname(sat_templates.__file__) # FIXME: should be modified if we handle use extra dirs |
45 self.theme = u'default' # FIXME: temporary, template should be selected in render() | |
46 self.env = jinja2.Environment( | 121 self.env = jinja2.Environment( |
47 loader=jinja2.PackageLoader('sat_templates', self.theme), | 122 loader=TemplateLoader(), |
48 autoescape=jinja2.select_autoescape(['html', 'xhtml', 'xml']), | 123 autoescape=jinja2.select_autoescape(['html', 'xhtml', 'xml']), |
49 trim_blocks=True, | 124 trim_blocks=True, |
50 lstrip_blocks=True, | 125 lstrip_blocks=True, |
51 ) | 126 ) |
52 # we want to have access to SàT constants in templates | 127 # we want to have access to SàT constants in templates |
53 self.env.globals[u'C'] = C | 128 self.env.globals[u'C'] = C |
54 | 129 |
55 def render(self, template_path, theme=u"default", css_file=u"style.css", css_inline=False, **kwargs): | 130 def getThemeAndRoot(self, template): |
131 """retrieve theme and root dir of a given tempalte | |
132 | |
133 @param template(unicode): template to parse | |
134 @return (tuple[unicode, unicode]): theme and absolute path to theme's root dir | |
135 """ | |
136 theme, dummy = self.env.loader.parse_template(template) | |
137 return theme, os.path.join(self.base_dir, theme) | |
138 | |
139 def _appendCSSIfExists(self, css_files, template_root_dir, theme, name, is_default): | |
140 """append CSS file to list if it exists, else try with default theme | |
141 | |
142 CSS file will be looked at [theme]/static/[name].css, and then default | |
143 if not found. | |
144 @param css_files(list): list of CSS file to be completed | |
145 @param template_root_dir(unicode): absolute path to template root used | |
146 @param theme(unicode): name of the template theme used | |
147 @param name(unicode): name of the CSS file to look for | |
148 @param is_default(bool): True if theme is the default theme | |
149 """ | |
150 css_path = os.path.join(theme, C.TEMPLATE_STATIC_DIR, name + '.css') | |
151 if os.path.exists(os.path.join(template_root_dir, css_path)): | |
152 css_files.append(css_path) | |
153 elif not is_default: | |
154 css_path = os.path.join(C.TEMPLATE_THEME_DEFAULT, C.TEMPLATE_STATIC_DIR, name + '.css') | |
155 if os.path.exists(os.path.join(template_root_dir, css_path)): | |
156 css_files.append(css_path) | |
157 | |
158 def getCSSFiles(self, template_path, template_root_dir): | |
159 """retrieve CSS files to use according to theme and template path | |
160 | |
161 for each element of the path, a .css file is looked for in /static, and returned if it exists. | |
162 previous element are kept by replacing '/' with '_', and styles.css is always returned. | |
163 For instance, if template_path is some_theme/blog/articles.html: | |
164 some_theme/static/styles.css is returned if it exists else default/static/styles.css | |
165 some_theme/static/blog.css is returned if it exists else default/static/blog.css (if it exists too) | |
166 some_theme/static/blog_articles.css is returned if it exists else default/static/blog_articles.css (if it exists too) | |
167 @param template_path(unicode): relative path to template file (e.g. some_theme/blog/articles.html) | |
168 @param template_root_dir(unicode): absolute path of the theme root dir used | |
169 @return list[unicode]: relative path to CSS files to use | |
170 """ | |
171 # TODO: some caching would be nice | |
172 css_files = [] | |
173 path_elems = template_path.split(u'/') | |
174 theme = path_elems.pop(0) | |
175 is_default = theme == C.TEMPLATE_THEME_DEFAULT | |
176 self._appendCSSIfExists(css_files, template_root_dir, theme, u'styles', is_default) | |
177 | |
178 for idx, path in enumerate(path_elems): | |
179 self._appendCSSIfExists(css_files, template_root_dir, theme, u'_'.join(path_elems[:idx+1]), is_default) | |
180 | |
181 return css_files | |
182 | |
183 def render(self, template, theme=None, root_path=u'', css_files=None, css_inline=False, **kwargs): | |
56 """render a template | 184 """render a template |
57 | 185 |
58 @param template_path(unicode): path of the template to render (e.g. blog/articles.html) | 186 @param template(unicode): template to render (e.g. blog/articles.html) |
59 @param theme(unicode): template theme | 187 @param theme(unicode): template theme |
60 @param css_file(unicode): path to CSS file (relative to template dir, or absolute) | 188 @param root_path(unicode): prefix of the path/URL to use for template root |
189 must end with a u'/' | |
190 @param css_files(list[unicode],None): CSS files to used | |
191 CSS files must be in static dir of the template | |
192 use None for automatic selection of CSS files based on template category | |
193 None is recommended. General static/style.css and theme file name will be used. | |
61 @param css_inline(bool): if True, CSS will be embedded in the HTML page | 194 @param css_inline(bool): if True, CSS will be embedded in the HTML page |
62 @param **kwargs: variable to transmit to the template | 195 @param **kwargs: variable to transmit to the template |
63 """ | 196 """ |
64 | 197 if not template: |
65 # TODO: handle theme | 198 raise ValueError(u"template can't be empty") |
66 template = self.env.get_template(template_path) | 199 if theme is not None: |
200 # use want to set a theme, we add it to the template path | |
201 if template[0] == u'(': | |
202 raise ValueError(u"you can't specify theme in template path and in argument at the same time") | |
203 elif template[0] == u'/': | |
204 raise ValueError(u"you can't specify theme with absolute paths") | |
205 template= u'(' + theme + u')' + template | |
206 else: | |
207 theme, dummy = self.env.loader.parse_template(template) | |
208 | |
209 template_source = self.env.get_template(template) | |
210 template_root_dir = os.path.normpath(self.base_dir) # FIXME: should be modified if we handle use extra dirs | |
211 # XXX: template_path may have a different theme as first element than theme if a default page is used | |
212 template_path = template_source.filename[len(template_root_dir)+1:] | |
213 | |
214 if css_files is None: | |
215 css_files = self.getCSSFiles(template_path, template_root_dir) | |
216 | |
67 if css_inline: | 217 if css_inline: |
68 css_file_path = os.path.join(self.getStaticDir(template_path), css_file) | 218 css_contents = [] |
69 with open(css_file_path, 'r') as f: | 219 for css_file in css_files: |
70 kwargs[u"css_content"] = f.read() | 220 css_file_path = os.path.join(template_root_dir, css_file) |
71 return template.render(theme=theme, css_file=css_file, css_inline=css_inline, **kwargs) | 221 with open(css_file_path) as f: |
72 | 222 css_contents.append(f.read()) |
73 def getStaticDir(self, template_path): | 223 if css_contents: |
74 template_base = template_path.split(u'/')[0] | 224 kwargs['css_content'] = '\n'.join(css_contents) |
75 return os.path.join(self.base_dir, self.theme, template_base, "static") | 225 # XXX: theme used in template arguments is the requested theme, which may differ from actual theme |
226 # if the template doesn't exist in the requested theme. | |
227 return template_source.render(theme=theme, root_path=root_path, css_files=css_files, **kwargs) |