comparison sat/tools/common/template.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/tools/common/template.py@00480cf83fa1
children 5b26033c49a8
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT: a jabber client
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 """ template generation """
21
22 from sat.core.constants import Const as C
23 from sat.core.i18n import _
24 from sat.core import exceptions
25 from sat.core.log import getLogger
26 log = getLogger(__name__)
27 import os.path
28 from xml.sax.saxutils import quoteattr
29 import datetime
30 import time
31 import re
32 from babel import support
33 from babel import Locale
34 from babel.core import UnknownLocaleError
35 from babel import dates
36 import pygments
37 from pygments import lexers
38 from pygments import formatters
39 try:
40 import sat_templates
41 except ImportError:
42 raise exceptions.MissingModule(u'sat_templates module is not available, please install it or check your path to use template engine')
43 else:
44 sat_templates # to avoid pyflakes warning
45
46 try:
47 import jinja2
48 except:
49 raise exceptions.MissingModule(u'Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2')
50
51 from jinja2 import Markup as safe
52 from jinja2 import is_undefined
53 from lxml import etree
54
55 HTML_EXT = ('html', 'xhtml')
56 DEFAULT_LOCALE = u'en_GB'
57 RE_ATTR_ESCAPE = re.compile(r'[^a-z_-]')
58 # TODO: handle external path (an additional search path for templates should be settable by user
59 # TODO: handle absolute URL (should be used for trusted use cases) only (e.g. jp) for security reason
60
61
62 class TemplateLoader(jinja2.FileSystemLoader):
63
64 def __init__(self):
65 searchpath = os.path.dirname(sat_templates.__file__)
66 super(TemplateLoader, self).__init__(searchpath, followlinks=True)
67
68 def parse_template(self, template):
69 """parse template path and return theme and relative URL
70
71 @param template_path(unicode): path to template with parenthesis syntax
72 @return (tuple[(unicode,None),unicode]): theme and template_path
73 theme can be None if relative path is used
74 relative path is the path from search path with theme specified
75 e.g. default/blog/articles.html
76 """
77 if template.startswith(u'('):
78 try:
79 theme_end = template.index(u')')
80 except IndexError:
81 raise ValueError(u"incorrect theme in template")
82 theme = template[1:theme_end]
83 template = template[theme_end+1:]
84 if not template or template.startswith(u'/'):
85 raise ValueError(u"incorrect path after template name")
86 template = os.path.join(theme, template)
87 elif template.startswith(u'/'):
88 # absolute path means no template
89 theme = None
90 raise NotImplementedError(u'absolute path is not implemented yet')
91 else:
92 theme = C.TEMPLATE_THEME_DEFAULT
93 template = os.path.join(theme, template)
94 return theme, template
95
96 def get_default_template(self, theme, template_path):
97 """return default template path
98
99 @param theme(unicode): theme used
100 @param template_path(unicode): path to the not found template
101 @return (unicode, None): default path or None if there is not
102 """
103 ext = os.path.splitext(template_path)[1][1:]
104 path_elems = template_path.split(u'/')
105 if ext in HTML_EXT:
106 if path_elems[1] == u'error':
107 # if an inexisting error page is requested, we return base page
108 default_path = os.path.join(theme, u'error/base.html')
109 return default_path
110 if theme != C.TEMPLATE_THEME_DEFAULT:
111 # if template doesn't exists for this theme, we try with default
112 return os.path.join(C.TEMPLATE_THEME_DEFAULT, path_elems[1:])
113
114 def get_source(self, environment, template):
115 """relative path to template dir, with special theme handling
116
117 if the path is just relative, "default" theme is used.
118 The theme can be specified in parenthesis just before the path
119 e.g.: (some_theme)path/to/template.html
120 """
121 theme, template_path = self.parse_template(template)
122 try:
123 return super(TemplateLoader, self).get_source(environment, template_path)
124 except jinja2.exceptions.TemplateNotFound as e:
125 # in some special cases, a defaut template is returned if nothing is found
126 if theme is not None:
127 default_path = self.get_default_template(theme, template_path)
128 if default_path is not None:
129 return super(TemplateLoader, self).get_source(environment, default_path)
130 # if no default template is found, we re-raise the error
131 raise e
132
133
134 class Indexer(object):
135 """Index global to a page"""
136
137 def __init__(self):
138 self._indexes = {}
139
140 def next(self, value):
141 if value not in self._indexes:
142 self._indexes[value] = 0
143 return 0
144 self._indexes[value] += 1
145 return self._indexes[value]
146
147 def current(self, value):
148 return self._indexes.get(value)
149
150
151 class ScriptsHandler(object):
152
153 def __init__(self, renderer, template_path, template_root_dir, root_path):
154 self.renderer = renderer
155 self.template_root_dir = template_root_dir
156 self.root_path = root_path
157 self.scripts = [] # we don't use a set because order may be important
158 dummy, self.theme, self.is_default_theme = renderer.getThemeData(template_path)
159
160 def include(self, library_name, attribute='defer'):
161 """Mark that a script need to be imported.
162
163 Must be used before base.html is extended, as <script> are generated there.
164 If called several time with the same library, it will be imported once.
165 @param library_name(unicode): name of the library to import
166 @param loading:
167 """
168 if attribute not in ('defer', 'async', ''):
169 raise exceptions.DataError(_(u'Invalid attribute, please use one of "defer", "async" or ""'))
170 if library_name.endswith('.js'):
171 library_name = library_name[:-3]
172 if library_name not in self.scripts:
173 self.scripts.append((library_name, attribute))
174 return u''
175
176 def generate_scripts(self):
177 """Generate the <script> elements
178
179 @return (unicode): <scripts> HTML tags
180 """
181 scripts = []
182 tpl = u'<script src={src} {attribute}></script>'
183 for library, attribute in self.scripts:
184 path = self.renderer.getStaticPath(library, self.template_root_dir, self.theme, self.is_default_theme, '.js')
185 if path is None:
186 log.warning(_(u"Can't find {}.js javascript library").format(library))
187 continue
188 path = os.path.join(self.root_path, path)
189 scripts.append(tpl.format(
190 src = quoteattr(path),
191 attribute = attribute,
192 ))
193 return safe(u'\n'.join(scripts))
194
195
196 class Renderer(object):
197
198 def __init__(self, host):
199 self.host = host
200 self.base_dir = os.path.dirname(sat_templates.__file__) # FIXME: should be modified if we handle use extra dirs
201 self.env = jinja2.Environment(
202 loader=TemplateLoader(),
203 autoescape=jinja2.select_autoescape(['html', 'xhtml', 'xml']),
204 trim_blocks=True,
205 lstrip_blocks=True,
206 extensions=['jinja2.ext.i18n'],
207 )
208 self._locale_str = DEFAULT_LOCALE
209 self._locale = Locale.parse(self._locale_str)
210 self.installTranslations()
211 # we want to have access to SàT constants in templates
212 self.env.globals[u'C'] = C
213 # custom filters
214 self.env.filters['next_gidx'] = self._next_gidx
215 self.env.filters['cur_gidx'] = self._cur_gidx
216 self.env.filters['date_fmt'] = self._date_fmt
217 self.env.filters['xmlui_class'] = self._xmlui_class
218 self.env.filters['attr_escape'] = self.attr_escape
219 self.env.filters['item_filter'] = self._item_filter
220 self.env.filters['adv_format'] = self._adv_format
221 self.env.filters['dict_ext'] = self._dict_ext
222 self.env.filters['highlight'] = self.highlight
223 # custom tests
224 self.env.tests['in_the_past'] = self._in_the_past
225 self.icons_path = os.path.join(host.media_dir, u'fonts/fontello/svg')
226
227 def installTranslations(self):
228 i18n_dir = os.path.join(self.base_dir, 'i18n')
229 self.translations = {}
230 for lang_dir in os.listdir(i18n_dir):
231 lang_path = os.path.join(i18n_dir, lang_dir)
232 if not os.path.isdir(lang_path):
233 continue
234 po_path = os.path.join(lang_path, 'LC_MESSAGES/sat.mo')
235 try:
236 with open(po_path, 'rb') as f:
237 self.translations[Locale.parse(lang_dir)] = support.Translations(f, 'sat')
238 except EnvironmentError:
239 log.error(_(u"Can't find template translation at {path}").format(path = po_path))
240 except UnknownLocaleError as e:
241 log.error(_(u"Invalid locale name: {msg}").format(msg=e))
242 else:
243 log.info(_(u'loaded {lang} templates translations').format(lang=lang_dir))
244 self.env.install_null_translations(True)
245
246 def setLocale(self, locale_str):
247 """set current locale
248
249 change current translation locale and self self._locale and self._locale_str
250 """
251 if locale_str == self._locale_str:
252 return
253 if locale_str == 'en':
254 # we default to GB English when it's not specified
255 # one of the main reason is to avoid the nonsense U.S. short date format
256 locale_str = 'en_GB'
257 try:
258 locale = Locale.parse(locale_str)
259 except ValueError as e:
260 log.warning(_(u"invalid locale value: {msg}").format(msg=e))
261 locale_str = self._locale_str = DEFAULT_LOCALE
262 locale = Locale.parse(locale_str)
263
264 locale_str = unicode(locale)
265 if locale_str != DEFAULT_LOCALE:
266 try:
267 translations = self.translations[locale]
268 except KeyError:
269 log.warning(_(u"Can't find locale {locale}".format(locale=locale)))
270 locale_str = DEFAULT_LOCALE
271 locale = Locale.parse(self._locale_str)
272 else:
273 self.env.install_gettext_translations(translations, True)
274 log.debug(_(u'Switched to {lang}').format(lang=locale.english_name))
275
276 if locale_str == DEFAULT_LOCALE:
277 self.env.install_null_translations(True)
278
279 self._locale = locale
280 self._locale_str = locale_str
281
282 def getThemeAndRoot(self, template):
283 """retrieve theme and root dir of a given tempalte
284
285 @param template(unicode): template to parse
286 @return (tuple[unicode, unicode]): theme and absolute path to theme's root dir
287 """
288 theme, dummy = self.env.loader.parse_template(template)
289 return theme, os.path.join(self.base_dir, theme)
290
291 def getStaticPath(self, name, template_root_dir, theme, is_default, ext='.css'):
292 """retrieve path of a static file if it exists with current theme or default
293
294 File will be looked at [theme]/static/[name][ext], and then default
295 if not found.
296 @param name(unicode): name of the file to look for
297 @param template_root_dir(unicode): absolute path to template root used
298 @param theme(unicode): name of the template theme used
299 @param is_default(bool): True if theme is the default theme
300 @return (unicode, None): relative path if found, else None
301 """
302 file_ = None
303 path = os.path.join(theme, C.TEMPLATE_STATIC_DIR, name + ext)
304 if os.path.exists(os.path.join(template_root_dir, path)):
305 file_ = path
306 elif not is_default:
307 path = os.path.join(C.TEMPLATE_THEME_DEFAULT, C.TEMPLATE_STATIC_DIR, name + ext)
308 if os.path.exists(os.path.join(template_root_dir, path)):
309 file_.append(path)
310 return file_
311
312 def getThemeData(self, template_path):
313 """return template data got from template_path
314
315 @return tuple(unicode, unicode, bool):
316 path_elems: elements of the path
317 theme: theme of the page
318 is_default: True if the theme is the default theme
319 """
320 path_elems = [os.path.splitext(p)[0] for p in template_path.split(u'/')]
321 theme = path_elems.pop(0)
322 is_default = theme == C.TEMPLATE_THEME_DEFAULT
323 return (path_elems, theme, is_default)
324
325 def getCSSFiles(self, template_path, template_root_dir):
326 """retrieve CSS files to use according to theme and template path
327
328 for each element of the path, a .css file is looked for in /static, and returned if it exists.
329 previous element are kept by replacing '/' with '_', and styles.css is always returned.
330 For instance, if template_path is some_theme/blog/articles.html:
331 some_theme/static/styles.css is returned if it exists else default/static/styles.css
332 some_theme/static/blog.css is returned if it exists else default/static/blog.css (if it exists too)
333 some_theme/static/blog_articles.css is returned if it exists else default/static/blog_articles.css (if it exists too)
334 @param template_path(unicode): relative path to template file (e.g. some_theme/blog/articles.html)
335 @param template_root_dir(unicode): absolute path of the theme root dir used
336 @return list[unicode]: relative path to CSS files to use
337 """
338 # TODO: some caching would be nice
339 css_files = []
340 path_elems, theme, is_default = self.getThemeData(template_path)
341 for css in (u'fonts', u'styles'):
342 css_path = self.getStaticPath(css, template_root_dir, theme, is_default)
343 if css_path is not None:
344 css_files.append(css_path)
345
346 for idx, path in enumerate(path_elems):
347 css_path = self.getStaticPath(u'_'.join(path_elems[:idx+1]), template_root_dir, theme, is_default)
348 if css_path is not None:
349 css_files.append(css_path)
350
351 return css_files
352
353
354 ## custom filters ##
355
356 @jinja2.contextfilter
357 def _next_gidx(self, ctx, value):
358 """Use next current global index as suffix"""
359 next_ = ctx['gidx'].next(value)
360 return value if next_ == 0 else u"{}_{}".format(value, next_)
361
362 @jinja2.contextfilter
363 def _cur_gidx(self, ctx, value):
364 """Use current current global index as suffix"""
365 current = ctx['gidx'].current(value)
366 return value if not current else u"{}_{}".format(value, current)
367
368 def _date_fmt(self, timestamp, fmt='short', date_only=False, auto_limit=None, auto_old_fmt=None):
369 try:
370 return self.date_fmt(timestamp, fmt, date_only, auto_limit, auto_old_fmt)
371 except Exception as e:
372 log.warning(_(u"Can't parse date: {msg}").format(msg=e))
373 return timestamp
374
375 def date_fmt(self, timestamp, fmt='short', date_only=False, auto_limit=7, auto_old_fmt='short', auto_new_fmt='relative'):
376 """format date according to locale
377
378 @param timestamp(basestring, int): unix time
379 @param fmt(str): one of:
380 - short: e.g. u'31/12/17'
381 - medium: e.g. u'Apr 1, 2007'
382 - long: e.g. u'April 1, 2007'
383 - full: e.g. u'Sunday, April 1, 2007'
384 - relative: format in relative time
385 e.g.: 3 hours
386 note that this format is not precise
387 - iso: ISO 8601 format
388 e.g.: u'2007-04-01T19:53:23Z'
389 - auto: use auto_old_fmt if date is older than auto_limit
390 else use auto_new_fmt
391 - auto_day: shorcut to set auto format with change on day
392 old format will be short, and new format will be time only
393 or a free value which is passed to babel.dates.format_datetime
394 @param date_only(bool): if True, only display date (not datetime)
395 @param auto_limit (int): limit in days before using auto_old_fmt
396 use 0 to have a limit at last midnight (day change)
397 @param auto_old_fmt(unicode): format to use when date is older than limit
398 @param auto_new_fmt(unicode): format to use when date is equal to or more recent
399 than limit
400
401 """
402 if is_undefined(fmt):
403 fmt = u'short'
404
405 if (auto_limit is not None or auto_old_fmt is not None) and fmt != 'auto':
406 raise ValueError(u'auto argument can only be used with auto fmt')
407 if fmt == 'auto_day':
408 fmt, auto_limit, auto_old_fmt, auto_new_fmt = 'auto', 0, 'short', 'HH:mm'
409 if fmt == 'auto':
410 if auto_limit == 0:
411 today = time.mktime(datetime.date.today().timetuple())
412 if int(timestamp) < today:
413 fmt = auto_old_fmt
414 else:
415 fmt = auto_new_fmt
416 else:
417 days_delta = (time.time() - int(timestamp)) / 3600
418 if days_delta > (auto_limit or 7):
419 fmt = auto_old_fmt
420 else:
421 fmt = auto_new_fmt
422
423 if fmt == 'relative':
424 delta = int(timestamp) - time.time()
425 return dates.format_timedelta(delta, granularity="minute", add_direction=True, locale=self._locale_str)
426 elif fmt in ('short', 'long'):
427 formatter = dates.format_date if date_only else dates.format_datetime
428 return formatter(int(timestamp), format=fmt, locale=self._locale_str)
429 elif fmt == 'iso':
430 if date_only:
431 fmt = 'yyyy-MM-dd'
432 else:
433 fmt = "yyyy-MM-ddTHH:mm:ss'Z'"
434 return dates.format_datetime(int(timestamp), format=fmt)
435 else:
436 return dates.format_datetime(int(timestamp), format=fmt, locale=self._locale_str)
437
438 def attr_escape(self, text):
439 """escape a text to a value usable as an attribute
440
441 remove spaces, and put in lower case
442 """
443 return RE_ATTR_ESCAPE.sub(u'_', text.strip().lower())[:50]
444
445 def _xmlui_class(self, xmlui_item, fields):
446 """return classes computed from XMLUI fields name
447
448 will return a string with a series of escaped {name}_{value} separated by spaces.
449 @param xmlui_item(xmlui.XMLUIPanel): XMLUI containing the widgets to use
450 @param fields(iterable(unicode)): names of the widgets to use
451 @return (unicode, None): computer string to use as class attribute value
452 None if no field was specified
453 """
454 classes = []
455 for name in fields:
456 escaped_name = self.attr_escape(name)
457 try:
458 for value in xmlui_item.widgets[name].values:
459 classes.append(escaped_name + '_' + self.attr_escape(value))
460 except KeyError:
461 log.debug(_(u"ignoring field \"{name}\": it doesn't exists").format(name=name))
462 continue
463 return u' '.join(classes) or None
464
465 @jinja2.contextfilter
466 def _item_filter(self, ctx, item, filters):
467 """return item's value, filtered if suitable
468
469 @param item(object): item to filter
470 value must have name and value attributes,
471 mostly used for XMLUI items
472 @param filters(dict[unicode, (callable, dict, None)]): map of name => filter
473 if filter is None, return the value unchanged
474 if filter is a callable, apply it
475 if filter is a dict, it can have following keys:
476 - filters: iterable of filters to apply
477 - filters_args: kwargs of filters in the same order as filters (use empty dict if needed)
478 - template: template to format where {value} is the filtered value
479 """
480 value = item.value
481 filter_ = filters.get(item.name, None)
482 if filter_ is None:
483 return value
484 elif isinstance(filter_, dict):
485 filters_args = filter_.get(u'filters_args')
486 for idx, f_name in enumerate(filter_.get(u'filters', [])):
487 kwargs = filters_args[idx] if filters_args is not None else {}
488 filter_func = self.env.filters[f_name]
489 try:
490 eval_context_filter = filter_func.evalcontextfilter
491 except AttributeError:
492 eval_context_filter = False
493
494 if eval_context_filter:
495 value = filter_func(ctx.eval_ctx, value, **kwargs)
496 else:
497 value = filter_func(value, **kwargs)
498 template = filter_.get(u'template')
499 if template:
500 # format will return a string, so we need to check first
501 # if the value is safe or not, and re-mark it after formatting
502 is_safe = isinstance(value, safe)
503 value = template.format(value=value)
504 if is_safe:
505 value = safe(value)
506 return value
507
508 def _adv_format(self, value, template, **kwargs):
509 """Advancer formatter
510
511 like format() method, but take care or special values like None
512 @param value(unicode): value to format
513 @param template(None, unicode): template to use with format() method.
514 It will be formatted using value=value and **kwargs
515 None to return value unchanged
516 @return (unicode): formatted value
517 """
518 if template is None:
519 return value
520 # jinja use string when no special char is used, so we have to convert to unicode
521 return unicode(template).format(value=value, **kwargs)
522
523 def _dict_ext(self, source_dict, extra_dict, key=None):
524 """extend source_dict with extra dict and return the result
525
526 @param source_dict(dict): dictionary to extend
527 @param extra_dict(dict, None): dictionary to use to extend first one
528 None to return source_dict unmodified
529 @param key(unicode, None): if specified extra_dict[key] will be used
530 if it doesn't exists, a copy of unmodified source_dict is returned
531 @return (dict): resulting dictionary
532 """
533 if extra_dict is None:
534 return source_dict
535 if key is not None:
536 extra_dict = extra_dict.get(key, {})
537 ret = source_dict.copy()
538 ret.update(extra_dict)
539 return ret
540
541 def highlight(self, code, lexer_name=None, lexer_opts=None, html_fmt_opts=None):
542 """Do syntax highlighting on code
543
544 under the hood, pygments is used, check its documentation for options possible values
545 @param code(unicode): code or markup to highlight
546 @param lexer_name(unicode, None): name of the lexer to use
547 None to autodetect it
548 @param html_fmt_opts(dict, None): kword arguments to use for HtmlFormatter
549 @return (unicode): HTML markup with highlight classes
550 """
551 if lexer_opts is None:
552 lexer_opts = {}
553 if html_fmt_opts is None:
554 html_fmt_opts = {}
555 if lexer_name is None:
556 lexer = lexers.guess_lexer(code, **lexer_opts)
557 else:
558 lexer = lexers.get_lexer_by_name(lexer_name, **lexer_opts)
559 formatter = formatters.HtmlFormatter(**html_fmt_opts)
560 return safe(pygments.highlight(code, lexer, formatter))
561
562 ## custom tests ##
563
564 def _in_the_past(self, timestamp):
565 """check if a date is in the past
566
567 @param timestamp(unicode, int): unix time
568 @return (bool): True if date is in the past
569 """
570 return time.time() > int(timestamp)
571
572 ## template methods ##
573
574 def _icon_defs(self, *names):
575 """Define svg icons which will be used in the template, and use their name as id"""
576 svg_elt = etree.Element('svg', nsmap={None: 'http://www.w3.org/2000/svg'},
577 width='0', height='0', style='display: block'
578 )
579 defs_elt = etree.SubElement(svg_elt, 'defs')
580 for name in names:
581 path = os.path.join(self.icons_path, name + u'.svg')
582 icon_svg_elt = etree.parse(path).getroot()
583 # we use icon name as id, so we can retrieve them easily
584 icon_svg_elt.set('id', name)
585 if not icon_svg_elt.tag == '{http://www.w3.org/2000/svg}svg':
586 raise exceptions.DataError(u'invalid SVG element')
587 defs_elt.append(icon_svg_elt)
588 return safe(etree.tostring(svg_elt, encoding='unicode'))
589
590 def _icon_use(self, name, cls=''):
591 return safe(u"""<svg class="svg-icon{cls}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
592 <use href="#{name}"/>
593 </svg>
594 """.format(
595 name=name,
596 cls=(' ' + cls) if cls else ''))
597
598 def render(self, template, theme=None, locale=DEFAULT_LOCALE, root_path=u'', media_path=u'', css_files=None, css_inline=False, **kwargs):
599 """render a template
600 .
601 @param template(unicode): template to render (e.g. blog/articles.html)
602 @param theme(unicode): template theme
603 @param root_path(unicode): prefix of the path/URL to use for template root
604 must end with a u'/'
605 @param media_path(unicode): prefix of the SàT media path/URL to use for template root
606 must end with a u'/'
607 @param css_files(list[unicode],None): CSS files to used
608 CSS files must be in static dir of the template
609 use None for automatic selection of CSS files based on template category
610 None is recommended. General static/style.css and theme file name will be used.
611 @param css_inline(bool): if True, CSS will be embedded in the HTML page
612 @param **kwargs: variable to transmit to the template
613 """
614 if not template:
615 raise ValueError(u"template can't be empty")
616 if theme is not None:
617 # use want to set a theme, we add it to the template path
618 if template[0] == u'(':
619 raise ValueError(u"you can't specify theme in template path and in argument at the same time")
620 elif template[0] == u'/':
621 raise ValueError(u"you can't specify theme with absolute paths")
622 template= u'(' + theme + u')' + template
623 else:
624 theme, dummy = self.env.loader.parse_template(template)
625
626 template_source = self.env.get_template(template)
627 template_root_dir = os.path.normpath(self.base_dir) # FIXME: should be modified if we handle use extra dirs
628 # XXX: template_path may have a different theme as first element than theme if a default page is used
629 template_path = template_source.filename[len(template_root_dir)+1:]
630
631 if css_files is None:
632 css_files = self.getCSSFiles(template_path, template_root_dir)
633
634 kwargs['icon_defs'] = self._icon_defs
635 kwargs['icon'] = self._icon_use
636
637 if css_inline:
638 css_contents = []
639 for css_file in css_files:
640 css_file_path = os.path.join(template_root_dir, css_file)
641 with open(css_file_path) as f:
642 css_contents.append(f.read())
643 if css_contents:
644 kwargs['css_content'] = '\n'.join(css_contents)
645
646 scripts_handler = ScriptsHandler(self, template_path, template_root_dir, root_path)
647 self.setLocale(locale)
648 # XXX: theme used in template arguments is the requested theme, which may differ from actual theme
649 # if the template doesn't exist in the requested theme.
650 return template_source.render(theme=theme, root_path=root_path, media_path=media_path,
651 css_files=css_files, locale=self._locale,
652 gidx=Indexer(), script=scripts_handler,
653 **kwargs)