comparison sat/tools/common/template.py @ 2624:56f94936df1e

code style reformatting using black
author Goffi <goffi@goffi.org>
date Wed, 27 Jun 2018 20:14:46 +0200
parents 5b26033c49a8
children 0fa217fafabf
comparison
equal deleted inserted replaced
2623:49533de4540b 2624:56f94936df1e
22 from sat.core.constants import Const as C 22 from sat.core.constants import Const as C
23 from sat.core.i18n import _ 23 from sat.core.i18n import _
24 from sat.core import exceptions 24 from sat.core import exceptions
25 from sat.tools.common import date_utils 25 from sat.tools.common import date_utils
26 from sat.core.log import getLogger 26 from sat.core.log import getLogger
27
27 log = getLogger(__name__) 28 log = getLogger(__name__)
28 import os.path 29 import os.path
29 from xml.sax.saxutils import quoteattr 30 from xml.sax.saxutils import quoteattr
30 import time 31 import time
31 import re 32 import re
33 from babel import Locale 34 from babel import Locale
34 from babel.core import UnknownLocaleError 35 from babel.core import UnknownLocaleError
35 import pygments 36 import pygments
36 from pygments import lexers 37 from pygments import lexers
37 from pygments import formatters 38 from pygments import formatters
39
38 try: 40 try:
39 import sat_templates 41 import sat_templates
40 except ImportError: 42 except ImportError:
41 raise exceptions.MissingModule(u'sat_templates module is not available, please install it or check your path to use template engine') 43 raise exceptions.MissingModule(
44 u"sat_templates module is not available, please install it or check your path to use template engine"
45 )
42 else: 46 else:
43 sat_templates # to avoid pyflakes warning 47 sat_templates # to avoid pyflakes warning
44 48
45 try: 49 try:
46 import jinja2 50 import jinja2
47 except: 51 except:
48 raise exceptions.MissingModule(u'Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2') 52 raise exceptions.MissingModule(
53 u"Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2"
54 )
49 55
50 from jinja2 import Markup as safe 56 from jinja2 import Markup as safe
51 from jinja2 import is_undefined 57 from jinja2 import is_undefined
52 from lxml import etree 58 from lxml import etree
53 59
54 HTML_EXT = ('html', 'xhtml') 60 HTML_EXT = ("html", "xhtml")
55 RE_ATTR_ESCAPE = re.compile(r'[^a-z_-]') 61 RE_ATTR_ESCAPE = re.compile(r"[^a-z_-]")
56 # TODO: handle external path (an additional search path for templates should be settable by user 62 #  TODO: handle external path (an additional search path for templates should be settable by user
57 # TODO: handle absolute URL (should be used for trusted use cases) only (e.g. jp) for security reason 63 # TODO: handle absolute URL (should be used for trusted use cases) only (e.g. jp) for security reason
58 64
59 65
60 class TemplateLoader(jinja2.FileSystemLoader): 66 class TemplateLoader(jinja2.FileSystemLoader):
61
62 def __init__(self): 67 def __init__(self):
63 searchpath = os.path.dirname(sat_templates.__file__) 68 searchpath = os.path.dirname(sat_templates.__file__)
64 super(TemplateLoader, self).__init__(searchpath, followlinks=True) 69 super(TemplateLoader, self).__init__(searchpath, followlinks=True)
65 70
66 def parse_template(self, template): 71 def parse_template(self, template):
70 @return (tuple[(unicode,None),unicode]): theme and template_path 75 @return (tuple[(unicode,None),unicode]): theme and template_path
71 theme can be None if relative path is used 76 theme can be None if relative path is used
72 relative path is the path from search path with theme specified 77 relative path is the path from search path with theme specified
73 e.g. default/blog/articles.html 78 e.g. default/blog/articles.html
74 """ 79 """
75 if template.startswith(u'('): 80 if template.startswith(u"("):
76 try: 81 try:
77 theme_end = template.index(u')') 82 theme_end = template.index(u")")
78 except IndexError: 83 except IndexError:
79 raise ValueError(u"incorrect theme in template") 84 raise ValueError(u"incorrect theme in template")
80 theme = template[1:theme_end] 85 theme = template[1:theme_end]
81 template = template[theme_end+1:] 86 template = template[theme_end + 1 :]
82 if not template or template.startswith(u'/'): 87 if not template or template.startswith(u"/"):
83 raise ValueError(u"incorrect path after template name") 88 raise ValueError(u"incorrect path after template name")
84 template = os.path.join(theme, template) 89 template = os.path.join(theme, template)
85 elif template.startswith(u'/'): 90 elif template.startswith(u"/"):
86 # absolute path means no template 91 # absolute path means no template
87 theme = None 92 theme = None
88 raise NotImplementedError(u'absolute path is not implemented yet') 93 raise NotImplementedError(u"absolute path is not implemented yet")
89 else: 94 else:
90 theme = C.TEMPLATE_THEME_DEFAULT 95 theme = C.TEMPLATE_THEME_DEFAULT
91 template = os.path.join(theme, template) 96 template = os.path.join(theme, template)
92 return theme, template 97 return theme, template
93 98
97 @param theme(unicode): theme used 102 @param theme(unicode): theme used
98 @param template_path(unicode): path to the not found template 103 @param template_path(unicode): path to the not found template
99 @return (unicode, None): default path or None if there is not 104 @return (unicode, None): default path or None if there is not
100 """ 105 """
101 ext = os.path.splitext(template_path)[1][1:] 106 ext = os.path.splitext(template_path)[1][1:]
102 path_elems = template_path.split(u'/') 107 path_elems = template_path.split(u"/")
103 if ext in HTML_EXT: 108 if ext in HTML_EXT:
104 if path_elems[1] == u'error': 109 if path_elems[1] == u"error":
105 # if an inexisting error page is requested, we return base page 110 # if an inexisting error page is requested, we return base page
106 default_path = os.path.join(theme, u'error/base.html') 111 default_path = os.path.join(theme, u"error/base.html")
107 return default_path 112 return default_path
108 if theme != C.TEMPLATE_THEME_DEFAULT: 113 if theme != C.TEMPLATE_THEME_DEFAULT:
109 # if template doesn't exists for this theme, we try with default 114 # if template doesn't exists for this theme, we try with default
110 return os.path.join(C.TEMPLATE_THEME_DEFAULT, path_elems[1:]) 115 return os.path.join(C.TEMPLATE_THEME_DEFAULT, path_elems[1:])
111 116
122 except jinja2.exceptions.TemplateNotFound as e: 127 except jinja2.exceptions.TemplateNotFound as e:
123 # in some special cases, a defaut template is returned if nothing is found 128 # in some special cases, a defaut template is returned if nothing is found
124 if theme is not None: 129 if theme is not None:
125 default_path = self.get_default_template(theme, template_path) 130 default_path = self.get_default_template(theme, template_path)
126 if default_path is not None: 131 if default_path is not None:
127 return super(TemplateLoader, self).get_source(environment, default_path) 132 return super(TemplateLoader, self).get_source(
133 environment, default_path
134 )
128 # if no default template is found, we re-raise the error 135 # if no default template is found, we re-raise the error
129 raise e 136 raise e
130 137
131 138
132 class Indexer(object): 139 class Indexer(object):
145 def current(self, value): 152 def current(self, value):
146 return self._indexes.get(value) 153 return self._indexes.get(value)
147 154
148 155
149 class ScriptsHandler(object): 156 class ScriptsHandler(object):
150
151 def __init__(self, renderer, template_path, template_root_dir, root_path): 157 def __init__(self, renderer, template_path, template_root_dir, root_path):
152 self.renderer = renderer 158 self.renderer = renderer
153 self.template_root_dir = template_root_dir 159 self.template_root_dir = template_root_dir
154 self.root_path = root_path 160 self.root_path = root_path
155 self.scripts = [] # we don't use a set because order may be important 161 self.scripts = [] #  we don't use a set because order may be important
156 dummy, self.theme, self.is_default_theme = renderer.getThemeData(template_path) 162 dummy, self.theme, self.is_default_theme = renderer.getThemeData(template_path)
157 163
158 def include(self, library_name, attribute='defer'): 164 def include(self, library_name, attribute="defer"):
159 """Mark that a script need to be imported. 165 """Mark that a script need to be imported.
160 166
161 Must be used before base.html is extended, as <script> are generated there. 167 Must be used before base.html is extended, as <script> are generated there.
162 If called several time with the same library, it will be imported once. 168 If called several time with the same library, it will be imported once.
163 @param library_name(unicode): name of the library to import 169 @param library_name(unicode): name of the library to import
164 @param loading: 170 @param loading:
165 """ 171 """
166 if attribute not in ('defer', 'async', ''): 172 if attribute not in ("defer", "async", ""):
167 raise exceptions.DataError(_(u'Invalid attribute, please use one of "defer", "async" or ""')) 173 raise exceptions.DataError(
168 if library_name.endswith('.js'): 174 _(u'Invalid attribute, please use one of "defer", "async" or ""')
175 )
176 if library_name.endswith(".js"):
169 library_name = library_name[:-3] 177 library_name = library_name[:-3]
170 if library_name not in self.scripts: 178 if library_name not in self.scripts:
171 self.scripts.append((library_name, attribute)) 179 self.scripts.append((library_name, attribute))
172 return u'' 180 return u""
173 181
174 def generate_scripts(self): 182 def generate_scripts(self):
175 """Generate the <script> elements 183 """Generate the <script> elements
176 184
177 @return (unicode): <scripts> HTML tags 185 @return (unicode): <scripts> HTML tags
178 """ 186 """
179 scripts = [] 187 scripts = []
180 tpl = u'<script src={src} {attribute}></script>' 188 tpl = u"<script src={src} {attribute}></script>"
181 for library, attribute in self.scripts: 189 for library, attribute in self.scripts:
182 path = self.renderer.getStaticPath(library, self.template_root_dir, self.theme, self.is_default_theme, '.js') 190 path = self.renderer.getStaticPath(
191 library, self.template_root_dir, self.theme, self.is_default_theme, ".js"
192 )
183 if path is None: 193 if path is None:
184 log.warning(_(u"Can't find {}.js javascript library").format(library)) 194 log.warning(_(u"Can't find {}.js javascript library").format(library))
185 continue 195 continue
186 path = os.path.join(self.root_path, path) 196 path = os.path.join(self.root_path, path)
187 scripts.append(tpl.format( 197 scripts.append(tpl.format(src=quoteattr(path), attribute=attribute))
188 src = quoteattr(path), 198 return safe(u"\n".join(scripts))
189 attribute = attribute,
190 ))
191 return safe(u'\n'.join(scripts))
192 199
193 200
194 class Renderer(object): 201 class Renderer(object):
195
196 def __init__(self, host): 202 def __init__(self, host):
197 self.host = host 203 self.host = host
198 self.base_dir = os.path.dirname(sat_templates.__file__) # FIXME: should be modified if we handle use extra dirs 204 self.base_dir = os.path.dirname(
205 sat_templates.__file__
206 ) # FIXME: should be modified if we handle use extra dirs
199 self.env = jinja2.Environment( 207 self.env = jinja2.Environment(
200 loader=TemplateLoader(), 208 loader=TemplateLoader(),
201 autoescape=jinja2.select_autoescape(['html', 'xhtml', 'xml']), 209 autoescape=jinja2.select_autoescape(["html", "xhtml", "xml"]),
202 trim_blocks=True, 210 trim_blocks=True,
203 lstrip_blocks=True, 211 lstrip_blocks=True,
204 extensions=['jinja2.ext.i18n'], 212 extensions=["jinja2.ext.i18n"],
205 ) 213 )
206 self._locale_str = C.DEFAULT_LOCALE 214 self._locale_str = C.DEFAULT_LOCALE
207 self._locale = Locale.parse(self._locale_str) 215 self._locale = Locale.parse(self._locale_str)
208 self.installTranslations() 216 self.installTranslations()
209 # we want to have access to SàT constants in templates 217 # we want to have access to SàT constants in templates
210 self.env.globals[u'C'] = C 218 self.env.globals[u"C"] = C
211 # custom filters 219 # custom filters
212 self.env.filters['next_gidx'] = self._next_gidx 220 self.env.filters["next_gidx"] = self._next_gidx
213 self.env.filters['cur_gidx'] = self._cur_gidx 221 self.env.filters["cur_gidx"] = self._cur_gidx
214 self.env.filters['date_fmt'] = self._date_fmt 222 self.env.filters["date_fmt"] = self._date_fmt
215 self.env.filters['xmlui_class'] = self._xmlui_class 223 self.env.filters["xmlui_class"] = self._xmlui_class
216 self.env.filters['attr_escape'] = self.attr_escape 224 self.env.filters["attr_escape"] = self.attr_escape
217 self.env.filters['item_filter'] = self._item_filter 225 self.env.filters["item_filter"] = self._item_filter
218 self.env.filters['adv_format'] = self._adv_format 226 self.env.filters["adv_format"] = self._adv_format
219 self.env.filters['dict_ext'] = self._dict_ext 227 self.env.filters["dict_ext"] = self._dict_ext
220 self.env.filters['highlight'] = self.highlight 228 self.env.filters["highlight"] = self.highlight
221 # custom tests 229 # custom tests
222 self.env.tests['in_the_past'] = self._in_the_past 230 self.env.tests["in_the_past"] = self._in_the_past
223 self.icons_path = os.path.join(host.media_dir, u'fonts/fontello/svg') 231 self.icons_path = os.path.join(host.media_dir, u"fonts/fontello/svg")
224 232
225 def installTranslations(self): 233 def installTranslations(self):
226 i18n_dir = os.path.join(self.base_dir, 'i18n') 234 i18n_dir = os.path.join(self.base_dir, "i18n")
227 self.translations = {} 235 self.translations = {}
228 for lang_dir in os.listdir(i18n_dir): 236 for lang_dir in os.listdir(i18n_dir):
229 lang_path = os.path.join(i18n_dir, lang_dir) 237 lang_path = os.path.join(i18n_dir, lang_dir)
230 if not os.path.isdir(lang_path): 238 if not os.path.isdir(lang_path):
231 continue 239 continue
232 po_path = os.path.join(lang_path, 'LC_MESSAGES/sat.mo') 240 po_path = os.path.join(lang_path, "LC_MESSAGES/sat.mo")
233 try: 241 try:
234 with open(po_path, 'rb') as f: 242 with open(po_path, "rb") as f:
235 self.translations[Locale.parse(lang_dir)] = support.Translations(f, 'sat') 243 self.translations[Locale.parse(lang_dir)] = support.Translations(
244 f, "sat"
245 )
236 except EnvironmentError: 246 except EnvironmentError:
237 log.error(_(u"Can't find template translation at {path}").format(path = po_path)) 247 log.error(
248 _(u"Can't find template translation at {path}").format(path=po_path)
249 )
238 except UnknownLocaleError as e: 250 except UnknownLocaleError as e:
239 log.error(_(u"Invalid locale name: {msg}").format(msg=e)) 251 log.error(_(u"Invalid locale name: {msg}").format(msg=e))
240 else: 252 else:
241 log.info(_(u'loaded {lang} templates translations').format(lang=lang_dir)) 253 log.info(_(u"loaded {lang} templates translations").format(lang=lang_dir))
242 self.env.install_null_translations(True) 254 self.env.install_null_translations(True)
243 255
244 def setLocale(self, locale_str): 256 def setLocale(self, locale_str):
245 """set current locale 257 """set current locale
246 258
247 change current translation locale and self self._locale and self._locale_str 259 change current translation locale and self self._locale and self._locale_str
248 """ 260 """
249 if locale_str == self._locale_str: 261 if locale_str == self._locale_str:
250 return 262 return
251 if locale_str == 'en': 263 if locale_str == "en":
252 # we default to GB English when it's not specified 264 # we default to GB English when it's not specified
253 # one of the main reason is to avoid the nonsense U.S. short date format 265 # one of the main reason is to avoid the nonsense U.S. short date format
254 locale_str = 'en_GB' 266 locale_str = "en_GB"
255 try: 267 try:
256 locale = Locale.parse(locale_str) 268 locale = Locale.parse(locale_str)
257 except ValueError as e: 269 except ValueError as e:
258 log.warning(_(u"invalid locale value: {msg}").format(msg=e)) 270 log.warning(_(u"invalid locale value: {msg}").format(msg=e))
259 locale_str = self._locale_str = C.DEFAULT_LOCALE 271 locale_str = self._locale_str = C.DEFAULT_LOCALE
267 log.warning(_(u"Can't find locale {locale}".format(locale=locale))) 279 log.warning(_(u"Can't find locale {locale}".format(locale=locale)))
268 locale_str = C.DEFAULT_LOCALE 280 locale_str = C.DEFAULT_LOCALE
269 locale = Locale.parse(self._locale_str) 281 locale = Locale.parse(self._locale_str)
270 else: 282 else:
271 self.env.install_gettext_translations(translations, True) 283 self.env.install_gettext_translations(translations, True)
272 log.debug(_(u'Switched to {lang}').format(lang=locale.english_name)) 284 log.debug(_(u"Switched to {lang}").format(lang=locale.english_name))
273 285
274 if locale_str == C.DEFAULT_LOCALE: 286 if locale_str == C.DEFAULT_LOCALE:
275 self.env.install_null_translations(True) 287 self.env.install_null_translations(True)
276 288
277 self._locale = locale 289 self._locale = locale
284 @return (tuple[unicode, unicode]): theme and absolute path to theme's root dir 296 @return (tuple[unicode, unicode]): theme and absolute path to theme's root dir
285 """ 297 """
286 theme, dummy = self.env.loader.parse_template(template) 298 theme, dummy = self.env.loader.parse_template(template)
287 return theme, os.path.join(self.base_dir, theme) 299 return theme, os.path.join(self.base_dir, theme)
288 300
289 def getStaticPath(self, name, template_root_dir, theme, is_default, ext='.css'): 301 def getStaticPath(self, name, template_root_dir, theme, is_default, ext=".css"):
290 """retrieve path of a static file if it exists with current theme or default 302 """retrieve path of a static file if it exists with current theme or default
291 303
292 File will be looked at [theme]/static/[name][ext], and then default 304 File will be looked at [theme]/static/[name][ext], and then default
293 if not found. 305 if not found.
294 @param name(unicode): name of the file to look for 306 @param name(unicode): name of the file to look for
300 file_ = None 312 file_ = None
301 path = os.path.join(theme, C.TEMPLATE_STATIC_DIR, name + ext) 313 path = os.path.join(theme, C.TEMPLATE_STATIC_DIR, name + ext)
302 if os.path.exists(os.path.join(template_root_dir, path)): 314 if os.path.exists(os.path.join(template_root_dir, path)):
303 file_ = path 315 file_ = path
304 elif not is_default: 316 elif not is_default:
305 path = os.path.join(C.TEMPLATE_THEME_DEFAULT, C.TEMPLATE_STATIC_DIR, name + ext) 317 path = os.path.join(
318 C.TEMPLATE_THEME_DEFAULT, C.TEMPLATE_STATIC_DIR, name + ext
319 )
306 if os.path.exists(os.path.join(template_root_dir, path)): 320 if os.path.exists(os.path.join(template_root_dir, path)):
307 file_.append(path) 321 file_.append(path)
308 return file_ 322 return file_
309 323
310 def getThemeData(self, template_path): 324 def getThemeData(self, template_path):
313 @return tuple(unicode, unicode, bool): 327 @return tuple(unicode, unicode, bool):
314 path_elems: elements of the path 328 path_elems: elements of the path
315 theme: theme of the page 329 theme: theme of the page
316 is_default: True if the theme is the default theme 330 is_default: True if the theme is the default theme
317 """ 331 """
318 path_elems = [os.path.splitext(p)[0] for p in template_path.split(u'/')] 332 path_elems = [os.path.splitext(p)[0] for p in template_path.split(u"/")]
319 theme = path_elems.pop(0) 333 theme = path_elems.pop(0)
320 is_default = theme == C.TEMPLATE_THEME_DEFAULT 334 is_default = theme == C.TEMPLATE_THEME_DEFAULT
321 return (path_elems, theme, is_default) 335 return (path_elems, theme, is_default)
322 336
323 def getCSSFiles(self, template_path, template_root_dir): 337 def getCSSFiles(self, template_path, template_root_dir):
334 @return list[unicode]: relative path to CSS files to use 348 @return list[unicode]: relative path to CSS files to use
335 """ 349 """
336 # TODO: some caching would be nice 350 # TODO: some caching would be nice
337 css_files = [] 351 css_files = []
338 path_elems, theme, is_default = self.getThemeData(template_path) 352 path_elems, theme, is_default = self.getThemeData(template_path)
339 for css in (u'fonts', u'styles'): 353 for css in (u"fonts", u"styles"):
340 css_path = self.getStaticPath(css, template_root_dir, theme, is_default) 354 css_path = self.getStaticPath(css, template_root_dir, theme, is_default)
341 if css_path is not None: 355 if css_path is not None:
342 css_files.append(css_path) 356 css_files.append(css_path)
343 357
344 for idx, path in enumerate(path_elems): 358 for idx, path in enumerate(path_elems):
345 css_path = self.getStaticPath(u'_'.join(path_elems[:idx+1]), template_root_dir, theme, is_default) 359 css_path = self.getStaticPath(
360 u"_".join(path_elems[: idx + 1]), template_root_dir, theme, is_default
361 )
346 if css_path is not None: 362 if css_path is not None:
347 css_files.append(css_path) 363 css_files.append(css_path)
348 364
349 return css_files 365 return css_files
350
351 366
352 ## custom filters ## 367 ## custom filters ##
353 368
354 @jinja2.contextfilter 369 @jinja2.contextfilter
355 def _next_gidx(self, ctx, value): 370 def _next_gidx(self, ctx, value):
356 """Use next current global index as suffix""" 371 """Use next current global index as suffix"""
357 next_ = ctx['gidx'].next(value) 372 next_ = ctx["gidx"].next(value)
358 return value if next_ == 0 else u"{}_{}".format(value, next_) 373 return value if next_ == 0 else u"{}_{}".format(value, next_)
359 374
360 @jinja2.contextfilter 375 @jinja2.contextfilter
361 def _cur_gidx(self, ctx, value): 376 def _cur_gidx(self, ctx, value):
362 """Use current current global index as suffix""" 377 """Use current current global index as suffix"""
363 current = ctx['gidx'].current(value) 378 current = ctx["gidx"].current(value)
364 return value if not current else u"{}_{}".format(value, current) 379 return value if not current else u"{}_{}".format(value, current)
365 380
366 def _date_fmt(self, timestamp, fmt='short', date_only=False, auto_limit=None, auto_old_fmt=None): 381 def _date_fmt(
382 self, timestamp, fmt="short", date_only=False, auto_limit=None, auto_old_fmt=None
383 ):
367 if is_undefined(fmt): 384 if is_undefined(fmt):
368 fmt = u'short' 385 fmt = u"short"
369 386
370 try: 387 try:
371 return date_utils.date_fmt(timestamp, fmt, date_only, auto_limit, auto_old_fmt) 388 return date_utils.date_fmt(
389 timestamp, fmt, date_only, auto_limit, auto_old_fmt
390 )
372 except Exception as e: 391 except Exception as e:
373 log.warning(_(u"Can't parse date: {msg}").format(msg=e)) 392 log.warning(_(u"Can't parse date: {msg}").format(msg=e))
374 return timestamp 393 return timestamp
375 394
376 def attr_escape(self, text): 395 def attr_escape(self, text):
377 """escape a text to a value usable as an attribute 396 """escape a text to a value usable as an attribute
378 397
379 remove spaces, and put in lower case 398 remove spaces, and put in lower case
380 """ 399 """
381 return RE_ATTR_ESCAPE.sub(u'_', text.strip().lower())[:50] 400 return RE_ATTR_ESCAPE.sub(u"_", text.strip().lower())[:50]
382 401
383 def _xmlui_class(self, xmlui_item, fields): 402 def _xmlui_class(self, xmlui_item, fields):
384 """return classes computed from XMLUI fields name 403 """return classes computed from XMLUI fields name
385 404
386 will return a string with a series of escaped {name}_{value} separated by spaces. 405 will return a string with a series of escaped {name}_{value} separated by spaces.
392 classes = [] 411 classes = []
393 for name in fields: 412 for name in fields:
394 escaped_name = self.attr_escape(name) 413 escaped_name = self.attr_escape(name)
395 try: 414 try:
396 for value in xmlui_item.widgets[name].values: 415 for value in xmlui_item.widgets[name].values:
397 classes.append(escaped_name + '_' + self.attr_escape(value)) 416 classes.append(escaped_name + "_" + self.attr_escape(value))
398 except KeyError: 417 except KeyError:
399 log.debug(_(u"ignoring field \"{name}\": it doesn't exists").format(name=name)) 418 log.debug(
419 _(u'ignoring field "{name}": it doesn\'t exists').format(name=name)
420 )
400 continue 421 continue
401 return u' '.join(classes) or None 422 return u" ".join(classes) or None
402 423
403 @jinja2.contextfilter 424 @jinja2.contextfilter
404 def _item_filter(self, ctx, item, filters): 425 def _item_filter(self, ctx, item, filters):
405 """return item's value, filtered if suitable 426 """return item's value, filtered if suitable
406 427
418 value = item.value 439 value = item.value
419 filter_ = filters.get(item.name, None) 440 filter_ = filters.get(item.name, None)
420 if filter_ is None: 441 if filter_ is None:
421 return value 442 return value
422 elif isinstance(filter_, dict): 443 elif isinstance(filter_, dict):
423 filters_args = filter_.get(u'filters_args') 444 filters_args = filter_.get(u"filters_args")
424 for idx, f_name in enumerate(filter_.get(u'filters', [])): 445 for idx, f_name in enumerate(filter_.get(u"filters", [])):
425 kwargs = filters_args[idx] if filters_args is not None else {} 446 kwargs = filters_args[idx] if filters_args is not None else {}
426 filter_func = self.env.filters[f_name] 447 filter_func = self.env.filters[f_name]
427 try: 448 try:
428 eval_context_filter = filter_func.evalcontextfilter 449 eval_context_filter = filter_func.evalcontextfilter
429 except AttributeError: 450 except AttributeError:
431 452
432 if eval_context_filter: 453 if eval_context_filter:
433 value = filter_func(ctx.eval_ctx, value, **kwargs) 454 value = filter_func(ctx.eval_ctx, value, **kwargs)
434 else: 455 else:
435 value = filter_func(value, **kwargs) 456 value = filter_func(value, **kwargs)
436 template = filter_.get(u'template') 457 template = filter_.get(u"template")
437 if template: 458 if template:
438 # format will return a string, so we need to check first 459 # format will return a string, so we need to check first
439 # if the value is safe or not, and re-mark it after formatting 460 # if the value is safe or not, and re-mark it after formatting
440 is_safe = isinstance(value, safe) 461 is_safe = isinstance(value, safe)
441 value = template.format(value=value) 462 value = template.format(value=value)
453 None to return value unchanged 474 None to return value unchanged
454 @return (unicode): formatted value 475 @return (unicode): formatted value
455 """ 476 """
456 if template is None: 477 if template is None:
457 return value 478 return value
458 # jinja use string when no special char is used, so we have to convert to unicode 479 #  jinja use string when no special char is used, so we have to convert to unicode
459 return unicode(template).format(value=value, **kwargs) 480 return unicode(template).format(value=value, **kwargs)
460 481
461 def _dict_ext(self, source_dict, extra_dict, key=None): 482 def _dict_ext(self, source_dict, extra_dict, key=None):
462 """extend source_dict with extra dict and return the result 483 """extend source_dict with extra dict and return the result
463 484
509 530
510 ## template methods ## 531 ## template methods ##
511 532
512 def _icon_defs(self, *names): 533 def _icon_defs(self, *names):
513 """Define svg icons which will be used in the template, and use their name as id""" 534 """Define svg icons which will be used in the template, and use their name as id"""
514 svg_elt = etree.Element('svg', nsmap={None: 'http://www.w3.org/2000/svg'}, 535 svg_elt = etree.Element(
515 width='0', height='0', style='display: block' 536 "svg",
516 ) 537 nsmap={None: "http://www.w3.org/2000/svg"},
517 defs_elt = etree.SubElement(svg_elt, 'defs') 538 width="0",
539 height="0",
540 style="display: block",
541 )
542 defs_elt = etree.SubElement(svg_elt, "defs")
518 for name in names: 543 for name in names:
519 path = os.path.join(self.icons_path, name + u'.svg') 544 path = os.path.join(self.icons_path, name + u".svg")
520 icon_svg_elt = etree.parse(path).getroot() 545 icon_svg_elt = etree.parse(path).getroot()
521 # we use icon name as id, so we can retrieve them easily 546 # we use icon name as id, so we can retrieve them easily
522 icon_svg_elt.set('id', name) 547 icon_svg_elt.set("id", name)
523 if not icon_svg_elt.tag == '{http://www.w3.org/2000/svg}svg': 548 if not icon_svg_elt.tag == "{http://www.w3.org/2000/svg}svg":
524 raise exceptions.DataError(u'invalid SVG element') 549 raise exceptions.DataError(u"invalid SVG element")
525 defs_elt.append(icon_svg_elt) 550 defs_elt.append(icon_svg_elt)
526 return safe(etree.tostring(svg_elt, encoding='unicode')) 551 return safe(etree.tostring(svg_elt, encoding="unicode"))
527 552
528 def _icon_use(self, name, cls=''): 553 def _icon_use(self, name, cls=""):
529 return safe(u"""<svg class="svg-icon{cls}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> 554 return safe(
555 u"""<svg class="svg-icon{cls}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
530 <use href="#{name}"/> 556 <use href="#{name}"/>
531 </svg> 557 </svg>
532 """.format( 558 """.format(
533 name=name, 559 name=name, cls=(" " + cls) if cls else ""
534 cls=(' ' + cls) if cls else '')) 560 )
535 561 )
536 def render(self, template, theme=None, locale=C.DEFAULT_LOCALE, root_path=u'', media_path=u'', css_files=None, css_inline=False, **kwargs): 562
563 def render(
564 self,
565 template,
566 theme=None,
567 locale=C.DEFAULT_LOCALE,
568 root_path=u"",
569 media_path=u"",
570 css_files=None,
571 css_inline=False,
572 **kwargs
573 ):
537 """render a template 574 """render a template
538 . 575 .
539 @param template(unicode): template to render (e.g. blog/articles.html) 576 @param template(unicode): template to render (e.g. blog/articles.html)
540 @param theme(unicode): template theme 577 @param theme(unicode): template theme
541 @param root_path(unicode): prefix of the path/URL to use for template root 578 @param root_path(unicode): prefix of the path/URL to use for template root
551 """ 588 """
552 if not template: 589 if not template:
553 raise ValueError(u"template can't be empty") 590 raise ValueError(u"template can't be empty")
554 if theme is not None: 591 if theme is not None:
555 # use want to set a theme, we add it to the template path 592 # use want to set a theme, we add it to the template path
556 if template[0] == u'(': 593 if template[0] == u"(":
557 raise ValueError(u"you can't specify theme in template path and in argument at the same time") 594 raise ValueError(
558 elif template[0] == u'/': 595 u"you can't specify theme in template path and in argument at the same time"
596 )
597 elif template[0] == u"/":
559 raise ValueError(u"you can't specify theme with absolute paths") 598 raise ValueError(u"you can't specify theme with absolute paths")
560 template= u'(' + theme + u')' + template 599 template = u"(" + theme + u")" + template
561 else: 600 else:
562 theme, dummy = self.env.loader.parse_template(template) 601 theme, dummy = self.env.loader.parse_template(template)
563 602
564 template_source = self.env.get_template(template) 603 template_source = self.env.get_template(template)
565 template_root_dir = os.path.normpath(self.base_dir) # FIXME: should be modified if we handle use extra dirs 604 template_root_dir = os.path.normpath(
566 # XXX: template_path may have a different theme as first element than theme if a default page is used 605 self.base_dir
567 template_path = template_source.filename[len(template_root_dir)+1:] 606 ) # FIXME: should be modified if we handle use extra dirs
607 #  XXX: template_path may have a different theme as first element than theme if a default page is used
608 template_path = template_source.filename[len(template_root_dir) + 1 :]
568 609
569 if css_files is None: 610 if css_files is None:
570 css_files = self.getCSSFiles(template_path, template_root_dir) 611 css_files = self.getCSSFiles(template_path, template_root_dir)
571 612
572 kwargs['icon_defs'] = self._icon_defs 613 kwargs["icon_defs"] = self._icon_defs
573 kwargs['icon'] = self._icon_use 614 kwargs["icon"] = self._icon_use
574 615
575 if css_inline: 616 if css_inline:
576 css_contents = [] 617 css_contents = []
577 for css_file in css_files: 618 for css_file in css_files:
578 css_file_path = os.path.join(template_root_dir, css_file) 619 css_file_path = os.path.join(template_root_dir, css_file)
579 with open(css_file_path) as f: 620 with open(css_file_path) as f:
580 css_contents.append(f.read()) 621 css_contents.append(f.read())
581 if css_contents: 622 if css_contents:
582 kwargs['css_content'] = '\n'.join(css_contents) 623 kwargs["css_content"] = "\n".join(css_contents)
583 624
584 scripts_handler = ScriptsHandler(self, template_path, template_root_dir, root_path) 625 scripts_handler = ScriptsHandler(
626 self, template_path, template_root_dir, root_path
627 )
585 self.setLocale(locale) 628 self.setLocale(locale)
586 # XXX: theme used in template arguments is the requested theme, which may differ from actual theme 629 # XXX: theme used in template arguments is the requested theme, which may differ from actual theme
587 # if the template doesn't exist in the requested theme. 630 # if the template doesn't exist in the requested theme.
588 return template_source.render(theme=theme, root_path=root_path, media_path=media_path, 631 return template_source.render(
589 css_files=css_files, locale=self._locale, 632 theme=theme,
590 gidx=Indexer(), script=scripts_handler, 633 root_path=root_path,
591 **kwargs) 634 media_path=media_path,
635 css_files=css_files,
636 locale=self._locale,
637 gidx=Indexer(),
638 script=scripts_handler,
639 **kwargs
640 )