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)