diff --git a/sat/core/constants.py b/sat/core/constants.py
index ce920c84..7e5466fe 100644
--- a/sat/core/constants.py
+++ b/sat/core/constants.py
@@ -225,6 +225,7 @@ class Const(object):
]
## Templates ##
+ TEMPLATE_TPL_DIR = u"templates"
TEMPLATE_THEME_DEFAULT = u"default"
TEMPLATE_STATIC_DIR = u"static"
diff --git a/sat/core/log.py b/sat/core/log.py
index a8c55acf..56ac4acd 100644
--- a/sat/core/log.py
+++ b/sat/core/log.py
@@ -158,13 +158,16 @@ class FilterName(object):
class ConfigureBase(object):
LOGGER_CLASS = Logger
- _color_location = False # True if color location is specified in fmt (with COLOR_START)
+ # True if color location is specified in fmt (with COLOR_START)
+ _color_location = False
- def __init__(self, level=None, fmt=None, output=None, logger=None, colors=False, levels_taints_dict=None, force_colors=False, backend_data=None):
+ def __init__(self, level=None, fmt=None, output=None, logger=None, colors=False,
+ levels_taints_dict=None, force_colors=False, backend_data=None):
"""Configure a backend
@param level: one of C.LOG_LEVELS
- @param fmt: format string, pretty much as in std logging. Accept the following keywords (maybe more depending on backend):
+ @param fmt: format string, pretty much as in std logging.
+ Accept the following keywords (maybe more depending on backend):
- "message"
- "levelname"
- "name" (logger name)
diff --git a/sat/tools/common/template.py b/sat/tools/common/template.py
index 690b3d46..651b4bd1 100644
--- a/sat/tools/common/template.py
+++ b/sat/tools/common/template.py
@@ -17,16 +17,16 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-""" template generation """
+"""Template generation"""
+import os.path
+from collections import namedtuple
from sat.core.constants import Const as C
from sat.core.i18n import _
from sat.core import exceptions
+from sat.tools import config
from sat.tools.common import date_utils
from sat.core.log import getLogger
-
-log = getLogger(__name__)
-import os.path
from xml.sax.saxutils import quoteattr
import time
import re
@@ -41,7 +41,8 @@ try:
import sat_templates
except ImportError:
raise exceptions.MissingModule(
- u"sat_templates module is not available, please install it or check your path to use template engine"
+ u"sat_templates module is not available, please install it or check your path to "
+ u"use template engine"
)
else:
sat_templates # to avoid pyflakes warning
@@ -50,90 +51,206 @@ try:
import jinja2
except:
raise exceptions.MissingModule(
- u"Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2"
+ u"Missing module jinja2, please install it from http://jinja.pocoo.org or with "
+ u"pip install jinja2"
)
from jinja2 import Markup as safe
from jinja2 import is_undefined
+from jinja2 import utils
+from jinja2 import TemplateNotFound
+from jinja2.loaders import split_template_path
from lxml import etree
+log = getLogger(__name__)
+
HTML_EXT = ("html", "xhtml")
RE_ATTR_ESCAPE = re.compile(r"[^a-z_-]")
-# TODO: handle external path (an additional search path for templates should be settable by user
-# TODO: handle absolute URL (should be used for trusted use cases) only (e.g. jp) for security reason
+SITE_RESERVED_NAMES = (u"sat",)
+TPL_RESERVED_CHARS = ur"()/."
+RE_TPL_RESERVED_CHARS = re.compile(u"[" + TPL_RESERVED_CHARS + u"]")
+
+TemplateData = namedtuple("TemplateData", ['site', 'theme', 'path'])
-class TemplateLoader(jinja2.FileSystemLoader):
- def __init__(self):
- searchpath = os.path.dirname(sat_templates.__file__)
- super(TemplateLoader, self).__init__(searchpath, followlinks=True)
+class TemplateLoader(jinja2.BaseLoader):
+ """A template loader which handle site, theme and absolute paths"""
+ # TODO: list_templates should be implemented
- def parse_template(self, template):
- """parse template path and return theme and relative URL
+ def __init__(self, sites_paths, trusted=False):
+ """
+ @param trusted(bool): if True, absolue template paths will be allowed
+ be careful when using this option and sure that you can trust the template,
+ as this allow the template to open any file on the system that the
+ launching user can access.
+ """
+ if not sites_paths or not u"" in sites_paths:
+ raise exceptions.InternalError(u"Invalid sites_paths")
+ super(jinja2.BaseLoader, self).__init__()
+ self.sites_paths = sites_paths
+ self.trusted = trusted
+
+ @staticmethod
+ def parse_template(template):
+ """Parse template path and return site, theme and path
@param template_path(unicode): path to template with parenthesis syntax
- @return (tuple[(unicode,None),unicode]): theme and template_path
- theme can be None if relative path is used
- relative path is the path from search path with theme specified
- e.g. default/blog/articles.html
+ The site and/or theme can be specified in parenthesis just before the path
+ e.g.: (some_theme)path/to/template.html
+ (/some_theme)path/to/template.html (equivalent to previous one)
+ (other_site/other_theme)path/to/template.html
+ (other_site/)path/to/template.html (defaut theme for other_site)
+ /absolute/path/to/template.html (in trusted environment only)
+ @return (TemplateData):
+ site, theme and template_path.
+ if site is empty, SàT Templates are used
+ site and theme can be both None if absolute path is used
+ Relative path is the path from theme root dir e.g. blog/articles.html
"""
if template.startswith(u"("):
+ # site and/or theme are specified
try:
theme_end = template.index(u")")
except IndexError:
- raise ValueError(u"incorrect theme in template")
- theme = template[1:theme_end]
- template = template[theme_end + 1 :]
- if not template or template.startswith(u"/"):
- raise ValueError(u"incorrect path after template name")
- template = os.path.join(theme, template)
+ raise ValueError(u"incorrect site/theme in template")
+ theme_data = template[1:theme_end]
+ theme_splitted = theme_data.split(u'/')
+ if len(theme_splitted) == 1:
+ site, theme = u"", theme_splitted
+ elif len(theme_splitted) == 2:
+ site, theme = theme_splitted
+ else:
+ raise ValueError(u"incorrect site/theme in template")
+ template_path = template[theme_end+1:]
+ if not template_path or template_path.startswith(u"/"):
+ raise ValueError(u"incorrect template path")
elif template.startswith(u"/"):
- # absolute path means no template
+ # this is an absolute path, so we have no site and no theme
+ site = None
theme = None
- raise NotImplementedError(u"absolute path is not implemented yet")
+ template_path = template
else:
+ # a default template
+ site = u""
theme = C.TEMPLATE_THEME_DEFAULT
- template = os.path.join(theme, template)
- return theme, template
+ template_path = template
- def get_default_template(self, theme, template_path):
- """return default template path
+ if site is not None:
+ site = site.strip()
+ if not site:
+ site = u""
+ elif site in SITE_RESERVED_NAMES:
+ raise ValueError(_(u"{site} can't be used as site name, "
+ u"it's reserved.").format(site=site))
- @param theme(unicode): theme used
- @param template_path(unicode): path to the not found template
- @return (unicode, None): default path or None if there is not
+ if theme is not None:
+ theme = theme.strip()
+ if not theme:
+ theme = C.TEMPLATE_THEME_DEFAULT
+ if RE_TPL_RESERVED_CHARS.search(theme):
+ raise ValueError(_(u"{theme} contain forbidden char. Following chars "
+ u"are forbidden: {reserved}").format(
+ theme=theme, reserved=TPL_RESERVED_CHARS))
+
+ return TemplateData(site, theme, template_path)
+
+ @staticmethod
+ def getSitesAndThemes(site, theme):
+ """Get sites and themes to check for template/file
+
+ Will add default theme and default site in search list when suitable
+ @param site(unicode): site requested
+ @param theme(unicode): theme requested
+ @return (list[tuple[unicode, unicode]]): site and theme couples to check
"""
- ext = os.path.splitext(template_path)[1][1:]
- path_elems = template_path.split(u"/")
- if ext in HTML_EXT:
- if path_elems[1] == u"error":
- # if an inexisting error page is requested, we return base page
- default_path = os.path.join(theme, u"error/base.html")
- return default_path
+ sites_and_themes = [[site, theme]]
if theme != C.TEMPLATE_THEME_DEFAULT:
- # if template doesn't exists for this theme, we try with default
- return os.path.join(C.TEMPLATE_THEME_DEFAULT, path_elems[1:])
+ sites_and_themes.append([site, C.TEMPLATE_THEME_DEFAULT])
+ if site:
+ # the site is not the default one, so we add default at the end
+ sites_and_themes.append([u'', C.TEMPLATE_THEME_DEFAULT])
+ return sites_and_themes
+
+ def _get_template_f(self, site, theme, path_elts):
+ """Look for template and return opened file if found
+
+ @param site(unicode): names of site to check
+ (default site will also checked)
+ @param theme(unicode): theme to check (default theme will also be checked)
+ @param path_elts(iterable[str]): elements of template path
+ @return (tuple[(File, None), (str, None)]): a tuple with:
+ - opened template, or None if not found
+ - absolute file path, or None if not found
+ """
+ if site is None:
+ raise exceptions.InternalError(
+ u"_get_template_f must not be used with absolute path")
+ for site, theme in self.getSitesAndThemes(site, theme):
+ try:
+ base_path = self.sites_paths[site]
+ except KeyError:
+ log.warning(_(u"Unregistered site requested: {site}").format(
+ site=site))
+ filepath = os.path.join(base_path, C.TEMPLATE_TPL_DIR, theme, *path_elts)
+ f = utils.open_if_exists(filepath)
+ if f is not None:
+ return f, filepath
+ return None, None
def get_source(self, environment, template):
- """relative path to template dir, with special theme handling
+ """Retrieve source handling site and themes
+ If the path is absolute it is used directly if in trusted environment
+ else and exception is raised.
if the path is just relative, "default" theme is used.
- The theme can be specified in parenthesis just before the path
- e.g.: (some_theme)path/to/template.html
+ @raise PermissionError: absolute path used in untrusted environment
"""
- theme, template_path = self.parse_template(template)
- try:
- return super(TemplateLoader, self).get_source(environment, template_path)
- except jinja2.exceptions.TemplateNotFound as e:
- # in some special cases, a defaut template is returned if nothing is found
+ site, theme, template_path = self.parse_template(template)
+
+ if site is None:
+ # we have an abolute template
if theme is not None:
- default_path = self.get_default_template(theme, template_path)
- if default_path is not None:
- return super(TemplateLoader, self).get_source(
- environment, default_path
- )
- # if no default template is found, we re-raise the error
- raise e
+ raise exceptions.InternalError(u"We can't have a theme with absolute "
+ u"template.")
+ if not self.trusted:
+ log.error(_(u"Absolute template used while unsecure is disabled, hack "
+ u"attempt? Template: {template}").format(template=template))
+ raise exceptions.PermissionError(u"absolute template is not allowed")
+ filepath = template_path
+ f = utils.open_if_exists(filepath)
+ else:
+ # relative path, we have to deal with site and theme
+ assert theme and template_path
+ path_elts = split_template_path(template_path)
+ # if we have non default site, we check it first, else we only check default
+ f, filepath = self._get_template_f(site, theme, path_elts)
+
+ if f is None:
+ if (site is not None and path_elts[0] == u"error"
+ and os.path.splitext(template_path)[1][1:] in HTML_EXT):
+ # if an HTML error is requested but doesn't exist, we try again
+ # with base error.
+ f, filepath = self._get_template_f(
+ site, theme, ("error", "base.html"))
+ if f is None:
+ raise exceptions.InternalError(u"error/base.html should exist")
+ else:
+ raise TemplateNotFound(template)
+
+ try:
+ contents = f.read().decode('utf-8')
+ finally:
+ f.close()
+
+ mtime = os.path.getmtime(filepath)
+
+ def uptodate():
+ try:
+ return os.path.getmtime(filepath) == mtime
+ except OSError:
+ return False
+
+ return contents, filepath, uptodate
class Indexer(object):
@@ -154,12 +271,10 @@ class Indexer(object):
class ScriptsHandler(object):
- def __init__(self, renderer, template_path, template_root_dir, root_path):
+ def __init__(self, renderer, template_data):
self.renderer = renderer
- self.template_root_dir = template_root_dir
- self.root_path = root_path
+ self.template_data = template_data
self.scripts = [] # we don't use a set because order may be important
- dummy, self.theme, self.is_default_theme = renderer.getThemeData(template_path)
def include(self, library_name, attribute="defer"):
"""Mark that a script need to be imported.
@@ -169,13 +284,13 @@ class ScriptsHandler(object):
@param library_name(unicode): name of the library to import
@param loading:
"""
- if attribute not in ("defer", "async", ""):
+ if attribute not in (u"defer", u"async", u""):
raise exceptions.DataError(
_(u'Invalid attribute, please use one of "defer", "async" or ""')
)
- if library_name.endswith(".js"):
- library_name = library_name[:-3]
- if library_name not in self.scripts:
+ if not library_name.endswith(u".js"):
+ library_name = library_name + u".js"
+ if (library_name, attribute) not in self.scripts:
self.scripts.append((library_name, attribute))
return u""
@@ -187,25 +302,61 @@ class ScriptsHandler(object):
scripts = []
tpl = u""
for library, attribute in self.scripts:
- path = self.renderer.getStaticPath(
- library, self.template_root_dir, self.theme, self.is_default_theme, ".js"
- )
- if path is None:
- log.warning(_(u"Can't find {}.js javascript library").format(library))
+ library_path = self.renderer.getStaticPath(self.template_data, library)
+ if library_path is None:
+ log.warning(_(u"Can't find {libary} javascript library").format(
+ library=library))
continue
- path = os.path.join(self.root_path, path)
+ path = self.renderer.getFrontURL(library_path)
scripts.append(tpl.format(src=quoteattr(path), attribute=attribute))
return safe(u"\n".join(scripts))
class Renderer(object):
- def __init__(self, host):
+
+ def __init__(self, host, front_url_filter=None, trusted=False, private=False):
+ """
+ @param front_url_filter(callable): filter to retrieve real url of a directory/file
+ The callable will get a two arguments:
+ - a dict with a "template_data" key containing TemplateData instance of
+ current template. Only site and theme should be necessary.
+ - the relative URL of the file to retrieve, relative from theme root
+ None to use default filter which return real path on file
+ Need to be specified for web rendering, to reflect URL seen by end user
+ @param trusted(bool): if True, allow to access absolute path
+ Only set to True if environment is safe (e.g. command line tool)
+ @param private(bool): if True, also load sites from sites_path_private_dict
+ """
+ # TODO:
self.host = host
- self.base_dir = os.path.dirname(
- sat_templates.__file__
- ) # FIXME: should be modified if we handle use extra dirs
+ self.trusted = trusted
+ self.sites_paths = {
+ u"": os.path.dirname(sat_templates.__file__),
+ }
+ conf = config.parseMainConf()
+ public_sites = config.getConfig(conf, None, u"sites_path_public_dict", {})
+ sites_data = [public_sites]
+ if private:
+ private_sites = config.getConfig(conf, None, u"sites_path_private_dict", {})
+ sites_data.append(private_sites)
+ for sites in sites_data:
+ normalised = {}
+ for name, path in sites.iteritems():
+ if RE_TPL_RESERVED_CHARS.search(name):
+ log.warning(_(u"Can't add \"{name}\" site, it contains forbidden "
+ u"characters. Forbidden characters are {forbidden}.")
+ .format(name=name, forbidden=TPL_RESERVED_CHARS))
+ continue
+ path = os.path.expanduser(os.path.normpath(path))
+ if not path or not path.startswith(u"/"):
+ log.warning(_(u"Can't add \"{name}\" site, it should map to an "
+ u"absolute path").format(name=name))
+ continue
+ normalised[name] = path
+ self.sites_paths.update(normalised)
+
self.env = jinja2.Environment(
- loader=TemplateLoader(),
+ loader=TemplateLoader(sites_paths=self.sites_paths, trusted=trusted),
autoescape=jinja2.select_autoescape(["html", "xhtml", "xml"]),
trim_blocks=True,
lstrip_blocks=True,
@@ -217,41 +368,64 @@ class Renderer(object):
# we want to have access to SàT constants in templates
self.env.globals[u"C"] = C
# custom filters
- self.env.filters["next_gidx"] = self._next_gidx
- self.env.filters["cur_gidx"] = self._cur_gidx
- self.env.filters["date_fmt"] = self._date_fmt
- self.env.filters["xmlui_class"] = self._xmlui_class
- self.env.filters["attr_escape"] = self.attr_escape
- self.env.filters["item_filter"] = self._item_filter
- self.env.filters["adv_format"] = self._adv_format
- self.env.filters["dict_ext"] = self._dict_ext
- self.env.filters["highlight"] = self.highlight
+ self.env.filters[u"next_gidx"] = self._next_gidx
+ self.env.filters[u"cur_gidx"] = self._cur_gidx
+ self.env.filters[u"date_fmt"] = self._date_fmt
+ self.env.filters[u"xmlui_class"] = self._xmlui_class
+ self.env.filters[u"attr_escape"] = self.attr_escape
+ self.env.filters[u"item_filter"] = self._item_filter
+ self.env.filters[u"adv_format"] = self._adv_format
+ self.env.filters[u"dict_ext"] = self._dict_ext
+ self.env.filters[u"highlight"] = self.highlight
+ self.env.filters[u"front_url"] = (self._front_url if front_url_filter is None
+ else front_url_filter)
# custom tests
- self.env.tests["in_the_past"] = self._in_the_past
+ self.env.tests[u"in_the_past"] = self._in_the_past
self.icons_path = os.path.join(host.media_dir, u"fonts/fontello/svg")
+ def getFrontURL(self, template_data, path=None):
+ """Give front URL (i.e. URL seen by end-user) of a path
+
+ @param template_data[TemplateData]: data of current template
+ @param path(unicode, None): relative path of file to get,
+ if set, will remplate template_data.path
+ """
+ return self.env.filters[u"front_url"]({u"template_data": template_data},
+ path or template_data.path)
+
def installTranslations(self):
- i18n_dir = os.path.join(self.base_dir, "i18n")
- self.translations = {}
- for lang_dir in os.listdir(i18n_dir):
- lang_path = os.path.join(i18n_dir, lang_dir)
- if not os.path.isdir(lang_path):
- continue
- po_path = os.path.join(lang_path, "LC_MESSAGES/sat.mo")
- try:
- with open(po_path, "rb") as f:
- self.translations[Locale.parse(lang_dir)] = support.Translations(
- f, "sat"
- )
- except EnvironmentError:
- log.error(
- _(u"Can't find template translation at {path}").format(path=po_path)
- )
- except UnknownLocaleError as e:
- log.error(_(u"Invalid locale name: {msg}").format(msg=e))
- else:
- log.info(_(u"loaded {lang} templates translations").format(lang=lang_dir))
- self.env.install_null_translations(True)
+ # TODO: support multi translation
+ # for now, only translations in sat_templates are handled
+ for site_key, site_path in self.sites_paths.iteritems():
+ site_prefix = u"[{}] ".format(site_key) if site_key else u''
+ i18n_dir = os.path.join(site_path, "i18n")
+ self.translations = {}
+ for lang_dir in os.listdir(i18n_dir):
+ lang_path = os.path.join(i18n_dir, lang_dir)
+ if not os.path.isdir(lang_path):
+ continue
+ po_path = os.path.join(lang_path, "LC_MESSAGES/sat.mo")
+ try:
+ locale = Locale.parse(lang_dir)
+ with open(po_path, "rb") as f:
+ try:
+ translations = self.translations[locale]
+ except KeyError:
+ self.translations[locale] = support.Translations(f, "sat")
+ else:
+ translations.merge(support.Translations(f, "sat"))
+ except EnvironmentError:
+ log.error(
+ _(u"Can't find template translation at {path}").format(
+ path=po_path))
+ except UnknownLocaleError as e:
+ log.error(_(u"{site}Invalid locale name: {msg}").format(
+ site=site_prefix, msg=e))
+ else:
+ log.info(_(u"{site}loaded {lang} templates translations").format(
+ site = site_prefix,
+ lang=lang_dir))
+ self.env.install_null_translations(True)
def setLocale(self, locale_str):
"""set current locale
@@ -294,78 +468,136 @@ class Renderer(object):
@param template(unicode): template to parse
@return (tuple[unicode, unicode]): theme and absolute path to theme's root dir
+ @raise NotFound: requested site has not been found
"""
- theme, dummy = self.env.loader.parse_template(template)
- return theme, os.path.join(self.base_dir, theme)
+ # FIXME: check use in jp, and include site
+ site, theme, __ = self.env.loader.parse_template(template)
+ if site is None:
+ # absolute template
+ return u"", os.path.dirname(template)
+ try:
+ site_root_dir = self.sites_paths[site]
+ except KeyError:
+ raise exceptions.NotFound
+ return theme, os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, theme)
- def getStaticPath(self, name, template_root_dir, theme, is_default, ext=".css"):
- """retrieve path of a static file if it exists with current theme or default
+ def getStaticPath(self, template_data, filename):
+ """Retrieve path of a static file if it exists with current theme or default
- File will be looked at [theme]/static/[name][ext], and then default
- if not found.
- @param name(unicode): name of the file to look for
- @param template_root_dir(unicode): absolute path to template root used
- @param theme(unicode): name of the template theme used
- @param is_default(bool): True if theme is the default theme
- @return (unicode, None): relative path if found, else None
+ File will be looked at ///filename,
+ then ///filename anf finally
+ // (i.e. sat_templates).
+ In case of absolue URL, base dir of template is used as base. For instance if
+ template is an absolute template to "/some/path/template.html", file will be
+ checked at "/some/path/"
+ @param template_data(TemplateData): data of current template
+ @return (TemplateData, None): built template data instance where .path is
+ the relative path to the file, from theme root dir.
+ None if not found.
"""
- file_ = None
- path = os.path.join(theme, C.TEMPLATE_STATIC_DIR, name + ext)
- if os.path.exists(os.path.join(template_root_dir, path)):
- file_ = path
- elif not is_default:
- path = os.path.join(
- C.TEMPLATE_THEME_DEFAULT, C.TEMPLATE_STATIC_DIR, name + ext
- )
- if os.path.exists(os.path.join(template_root_dir, path)):
- file_.append(path)
- return file_
+ if template_data.site is None:
+ # we have and absolue path
+ if (not template_data.theme is None
+ or not template_data.path.startswith(u'/')):
+ raise exceptions.InternalError(
+ u"invalid template data, was expecting absolute URL")
+ static_dir = os.path.dirname(template_data.path)
+ file_path = os.path.join(static_dir, filename)
+ if os.path.exists(file_path):
+ return TemplateData(site=None, theme=None, path=file_path)
+ else:
+ return None
- def getThemeData(self, template_path):
- """return template data got from template_path
+ sites_and_themes = TemplateLoader.getSitesAndThemes(template_data.site,
+ template_data.theme)
+ for site, theme in sites_and_themes:
+ site_root_dir = self.sites_paths[site]
+ relative_path = os.path.join(C.TEMPLATE_STATIC_DIR, filename)
+ absolute_path = os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR,
+ theme, relative_path)
+ if os.path.exists(absolute_path):
+ return TemplateData(site=site, theme=theme, path=relative_path)
- @return tuple(unicode, unicode, bool):
- path_elems: elements of the path
- theme: theme of the page
- is_default: True if the theme is the default theme
+ return None
+
+ def _appendCSSPaths(self, template_data, css_files, css_files_noscript, name_root):
+ """Append found css to css_files and css_files_noscript
+
+ @param css_files(list): list to fill of relative path to found css file
+ @param css_files_noscript(list): list to fill of relative path to found css file
+ with "_noscript" suffix
"""
- path_elems = [os.path.splitext(p)[0] for p in template_path.split(u"/")]
- theme = path_elems.pop(0)
- is_default = theme == C.TEMPLATE_THEME_DEFAULT
- return (path_elems, theme, is_default)
+ name = name_root + u".css"
+ css_path = self.getStaticPath(template_data, name)
+ if css_path is not None:
+ css_files.append(self.getFrontURL(css_path))
+ noscript_name = name_root + u"_noscript.css"
+ noscript_path = self.getStaticPath(template_data, noscript_name)
+ if noscript_path is not None:
+ css_files_noscript.append(self.getFrontURL(noscript_path))
- def getCSSFiles(self, template_path, template_root_dir):
- """retrieve CSS files to use according to theme and template path
+ def getCSSFiles(self, template_data):
+ """Retrieve CSS files to use according template_data
- for each element of the path, a .css file is looked for in /static, and returned if it exists.
- previous element are kept by replacing '/' with '_', and styles.css is always returned.
- For instance, if template_path is some_theme/blog/articles.html:
- some_theme/static/styles.css is returned if it exists else default/static/styles.css
- some_theme/static/blog.css is returned if it exists else default/static/blog.css (if it exists too)
- some_theme/static/blog_articles.css is returned if it exists else default/static/blog_articles.css (if it exists too)
- @param template_path(unicode): relative path to template file (e.g. some_theme/blog/articles.html)
- @param template_root_dir(unicode): absolute path of the theme root dir used
- @return list[unicode]: relative path to CSS files to use
+ For each element of the path, a .css file is looked for in /static, and returned
+ if it exists.
+ Previous element are kept by replacing '/' with '_'.
+ styles_extra.css, styles.css and fonts.css are always used if they exist.
+ For each found file, if a file with the same name and "_noscript" suffix exists,
+ it will be return is second part of resulting tuple.
+ For instance, if template_data is (some_site, some_theme, blog/articles.html),
+ following files are returned, earch time trying [some_site root] first,
+ then default site (i.e. sat_templates) root:
+ - some_theme/static/styles.css is returned if it exists
+ else default/static/styles.css
+ - some_theme/static/blog.css is returned if it exists
+ else default/static/blog.css (if it exists too)
+ - some_theme/static/blog_articles.css is returned if it exists
+ else default/static/blog_articles.css (if it exists too)
+ and for each found files, if same file with _noscript suffix exists, it is put
+ in noscript list (for instance (some_theme/static/styles_noscript.css)).
+ @param template_data(TemplateData): data of the current template
+ @return (tuple[list[unicode], list[unicode]]): a tuple with:
+ - front URLs of CSS files to use
+ - front URLs of CSS files to use when scripts are not enabled
"""
# TODO: some caching would be nice
css_files = []
- path_elems, theme, is_default = self.getThemeData(template_path)
- for css in (u"fonts", u"styles"):
- css_path = self.getStaticPath(css, template_root_dir, theme, is_default)
- if css_path is not None:
- css_files.append(css_path)
+ css_files_noscript = []
+ path_elems = template_data.path.split(u'/')
+ path_elems[-1] = os.path.splitext(path_elems[-1])[0]
- for idx, path in enumerate(path_elems):
- css_path = self.getStaticPath(
- u"_".join(path_elems[: idx + 1]), template_root_dir, theme, is_default
- )
- if css_path is not None:
- css_files.append(css_path)
+ for name_root in (u'styles_extra', u'styles'):
+ self._appendCSSPaths(template_data, css_files, css_files_noscript, name_root)
- return css_files
+ css_path = self.getStaticPath(template_data, u'fonts.css')
+ if css_path is not None:
+ css_files.append(self.getFrontURL(css_path))
+
+ for idx in xrange(len(path_elems)):
+ name_root = u"_".join(path_elems[:idx+1])
+ self._appendCSSPaths(template_data, css_files, css_files_noscript, name_root)
+
+ return css_files, css_files_noscript
## custom filters ##
+ @jinja2.contextfilter
+ def _front_url(self, ctx, relative_url):
+ """Get front URL (URL seen by end-user) from a relative URL
+
+ This default method return absolute full path
+ """
+ template_data = ctx[u'template_data']
+ if template_data.site is None:
+ assert template_data.theme is None
+ assert template_data.path.startswith(u"/")
+ return os.path.join(os.path.dirname(template_data.path, relative_url))
+
+ site_root_dir = self.sites_paths[template_data.site]
+ return os.path.join(site_root_dir, C.TEMPLATE_TPL_DIR, template_data.theme,
+ relative_url)
+
@jinja2.contextfilter
def _next_gidx(self, ctx, value):
"""Use next current global index as suffix"""
@@ -433,7 +665,8 @@ class Renderer(object):
if filter is a callable, apply it
if filter is a dict, it can have following keys:
- filters: iterable of filters to apply
- - filters_args: kwargs of filters in the same order as filters (use empty dict if needed)
+ - filters_args: kwargs of filters in the same order as filters (use empty
+ dict if needed)
- template: template to format where {value} is the filtered value
"""
value = item.value
@@ -500,7 +733,8 @@ class Renderer(object):
def highlight(self, code, lexer_name=None, lexer_opts=None, html_fmt_opts=None):
"""Do syntax highlighting on code
- under the hood, pygments is used, check its documentation for options possible values
+ Under the hood, pygments is used, check its documentation for options possible
+ values.
@param code(unicode): code or markup to highlight
@param lexer_name(unicode, None): name of the lexer to use
None to autodetect it
@@ -531,7 +765,10 @@ class Renderer(object):
## template methods ##
def _icon_defs(self, *names):
- """Define svg icons which will be used in the template, and use their name as id"""
+ """Define svg icons which will be used in the template.
+
+ Their name is used as id
+ """
svg_elt = etree.Element(
"svg",
nsmap={None: "http://www.w3.org/2000/svg"},
@@ -551,88 +788,78 @@ class Renderer(object):
return safe(etree.tostring(svg_elt, encoding="unicode"))
def _icon_use(self, name, cls=""):
- return safe(
- u"""
- """.format(
- name=name, cls=(" " + cls) if cls else ""
- )
- )
+ return safe(u'\n'.format(name=name, cls=(" " + cls) if cls else ""))
+
+ def render(self, template, site=None, theme=None, locale=C.DEFAULT_LOCALE,
+ media_path=u"", css_files=None, css_inline=False, **kwargs):
+ """Render a template
- def render(
- self,
- template,
- theme=None,
- locale=C.DEFAULT_LOCALE,
- root_path=u"",
- media_path=u"",
- css_files=None,
- css_inline=False,
- **kwargs
- ):
- """render a template
-.
@param template(unicode): template to render (e.g. blog/articles.html)
+ @param site(unicide): site name
+ None or empty string for defaut site (i.e. SàT templates)
@param theme(unicode): template theme
- @param root_path(unicode): prefix of the path/URL to use for template root
- must end with a u'/'
- @param media_path(unicode): prefix of the SàT media path/URL to use for template root
- must end with a u'/'
+ @param media_path(unicode): prefix of the SàT media path/URL to use for
+ template root. Must end with a u'/'
@param css_files(list[unicode],None): CSS files to used
CSS files must be in static dir of the template
use None for automatic selection of CSS files based on template category
- None is recommended. General static/style.css and theme file name will be used.
+ None is recommended. General static/style.css and theme file name will be
+ used.
@param css_inline(bool): if True, CSS will be embedded in the HTML page
@param **kwargs: variable to transmit to the template
"""
if not template:
raise ValueError(u"template can't be empty")
- if theme is not None:
- # use want to set a theme, we add it to the template path
+ if site is not None or theme is not None:
+ # user wants to set site and/or theme, so we add it to the template path
+ if site is None:
+ site = u''
+ if theme is None:
+ theme = C.TEMPLATE_THEME_DEFAULT
if template[0] == u"(":
raise ValueError(
- u"you can't specify theme in template path and in argument at the same time"
+ u"you can't specify site or theme in template path and in argument "
+ u"at the same time"
)
- elif template[0] == u"/":
- raise ValueError(u"you can't specify theme with absolute paths")
- template = u"(" + theme + u")" + template
+
+ template_data = TemplateData(site, theme, template)
+ template = u"({site}/{theme}){template}".format(
+ site=site, theme=theme, template=template)
else:
- theme, dummy = self.env.loader.parse_template(template)
+ template_data = self.env.loader.parse_template(template)
template_source = self.env.get_template(template)
- template_root_dir = os.path.normpath(
- self.base_dir
- ) # FIXME: should be modified if we handle use extra dirs
- # XXX: template_path may have a different theme as first element than theme if a default page is used
- template_path = template_source.filename[len(template_root_dir) + 1 :]
if css_files is None:
- css_files = self.getCSSFiles(template_path, template_root_dir)
+ css_files, css_files_noscript = self.getCSSFiles(template_data)
kwargs["icon_defs"] = self._icon_defs
kwargs["icon"] = self._icon_use
if css_inline:
css_contents = []
- for css_file in css_files:
- css_file_path = os.path.join(template_root_dir, css_file)
- with open(css_file_path) as f:
- css_contents.append(f.read())
- if css_contents:
- kwargs["css_content"] = "\n".join(css_contents)
+ for files, suffix in ((css_files, u""),
+ (css_files_noscript, u"_noscript")):
+ site_root_dir = self.sites_paths[template_data.site]
+ for css_file in files:
+ css_file_path = os.path.join(site_root_dir, css_file)
+ with open(css_file_path) as f:
+ css_contents.append(f.read())
+ if css_contents:
+ kwargs[u"css_content" + suffix] = u"\n".join(css_contents)
- scripts_handler = ScriptsHandler(
- self, template_path, template_root_dir, root_path
- )
+ scripts_handler = ScriptsHandler(self, template_data)
self.setLocale(locale)
- # XXX: theme used in template arguments is the requested theme, which may differ from actual theme
- # if the template doesn't exist in the requested theme.
+ # XXX: theme used in template arguments is the requested theme, which may differ
+ # from actual theme if the template doesn't exist in the requested theme.
return template_source.render(
- theme=theme,
- root_path=root_path,
+ template_data=template_data,
media_path=media_path,
css_files=css_files,
+ css_files_noscript=css_files,
locale=self._locale,
gidx=Indexer(),
script=scripts_handler,
diff --git a/sat/tools/config.py b/sat/tools/config.py
index f5643ae9..443bcdb1 100644
--- a/sat/tools/config.py
+++ b/sat/tools/config.py
@@ -47,7 +47,8 @@ def fixConfigOption(section, option, value, silent=True):
config = SafeConfigParser()
target_file = None
for file_ in C.CONFIG_FILES[::-1]:
- # we will eventually update the existing file with the highest priority, if it's a user personal file...
+ # we will eventually update the existing file with the highest priority,
+ # if it's a user personal file...
if not silent:
log.debug(_(u"Testing file %s") % file_)
if os.path.isfile(file_):
@@ -66,12 +67,9 @@ def fixConfigOption(section, option, value, silent=True):
if not silent:
if option in ("passphrase",): # list here the options storing a password
value = "******"
- log.warning(
- _(
- u"Config auto-update: %(option)s set to %(value)s in the file %(config_file)s"
- )
- % {"option": option, "value": value, "config_file": target_file}
- )
+ log.warning(_(u"Config auto-update: {option} set to {value} in the file "
+ u"{config_file}.").format(option=option, value=value,
+ config_file=target_file))
def parseMainConf():
diff --git a/sat_frontends/jp/base.py b/sat_frontends/jp/base.py
index 7301d66d..bcb7bff9 100644
--- a/sat_frontends/jp/base.py
+++ b/sat_frontends/jp/base.py
@@ -554,7 +554,7 @@ class Jp(object):
if self._need_loop or self._auto_loop:
self._start_loop()
except KeyboardInterrupt:
- log.info(_("User interruption: good bye"))
+ self.disp(_("User interruption: good bye"))
def _start_loop(self):
self.loop = JPLoop()
diff --git a/sat_frontends/jp/output_template.py b/sat_frontends/jp/output_template.py
index 63cbdac2..7ed7603d 100644
--- a/sat_frontends/jp/output_template.py
+++ b/sat_frontends/jp/output_template.py
@@ -21,7 +21,10 @@
from sat_frontends.jp.constants import Const as C
from sat.core.i18n import _
+from sat.core import log
from sat.tools.common import template
+from functools import partial
+import logging
import webbrowser
import tempfile
import os.path
@@ -38,6 +41,19 @@ class Template(object):
self.host = jp
jp.register_output(C.OUTPUT_COMPLEX, TEMPLATE, self.render)
+ def _front_url_tmp_dir(self, ctx, relative_url, tmp_dir):
+ """Get front URL for temporary directory"""
+ template_data = ctx[u'template_data']
+ return u"file://" + os.path.join(tmp_dir, template_data.theme, relative_url)
+
+ def _do_render(self, template_path, css_inline, **kwargs):
+ try:
+ return self.renderer.render(template_path, css_inline=css_inline, **kwargs)
+ except template.TemplateNotFound:
+ self.host.disp(_(u"Can't find requested template: {template_path}")
+ .format(template_path=template_path), error=True)
+ self.host.quit(C.EXIT_NOT_FOUND)
+
def render(self, data):
"""render output data using requested template
@@ -55,23 +71,23 @@ class Template(object):
template_path = cmd.TEMPLATE
except AttributeError:
if not "template" in cmd.args.output_opts:
- self.host.disp(
- u"no default template set for this command, "
- u"you need to specify a template using --oo template=[path/to/template.html]",
+ self.host.disp(_(
+ u"no default template set for this command, you need to specify a "
+ u"template using --oo template=[path/to/template.html]"),
error=True,
)
self.host.quit(C.EXIT_BAD_ARG)
options = self.host.parse_output_options()
self.host.check_output_options(OPTIONS, options)
- self.renderer = template.Renderer(self.host)
try:
template_path = options["template"]
except KeyError:
# template is not specified, we use default one
pass
if template_path is None:
- self.host.disp(u"Can't parse template, please check its syntax", error=True)
+ self.host.disp(_(u"Can't parse template, please check its syntax"),
+ error=True)
self.host.quit(C.EXIT_BAD_ARG)
try:
@@ -82,28 +98,41 @@ class Template(object):
kwargs = mapping_cb(data)
css_inline = u"inline-css" in options
- rendered = self.renderer.render(template_path, css_inline=css_inline, **kwargs)
if "browser" in options:
template_name = os.path.basename(template_path)
tmp_dir = tempfile.mkdtemp()
- self.host.disp(
- _(
- u"Browser opening requested.\nTemporary files are put in the following directory, "
- u"you'll have to delete it yourself once finished viewing: {}"
- ).format(tmp_dir)
- )
+ front_url_filter = partial(self._front_url_tmp_dir, tmp_dir=tmp_dir)
+ self.renderer = template.Renderer(
+ self.host, front_url_filter=front_url_filter, trusted=True)
+ rendered = self._do_render(template_path, css_inline=css_inline, **kwargs)
+ self.host.disp(_(
+ u"Browser opening requested.\n"
+ u"Temporary files are put in the following directory, you'll have to "
+ u"delete it yourself once finished viewing: {}").format(tmp_dir))
tmp_file = os.path.join(tmp_dir, template_name)
with open(tmp_file, "w") as f:
f.write(rendered.encode("utf-8"))
theme, theme_root_path = self.renderer.getThemeAndRoot(template_path)
+ if theme is None:
+ # we have an absolute path
+ webbrowser
static_dir = os.path.join(theme_root_path, C.TEMPLATE_STATIC_DIR)
if os.path.exists(static_dir):
+ # we have to copy static files in a subdirectory, to avoid file download
+ # to be blocked by same origin policy
import shutil
-
shutil.copytree(
static_dir, os.path.join(tmp_dir, theme, C.TEMPLATE_STATIC_DIR)
)
webbrowser.open(tmp_file)
else:
+ # FIXME: Q&D way to disable template logging
+ # logs are overcomplicated, and need to be reworked
+ template_logger = log.getLogger(u"sat.tools.common.template")
+ template_logger.log = lambda *args: None
+
+ logging.disable(logging.WARNING)
+ self.renderer = template.Renderer(self.host, trusted=True)
+ rendered = self._do_render(template_path, css_inline=css_inline, **kwargs)
self.host.disp(rendered)