tools (common/template), jp: refactoring to handle multiple sites:

- site can now be specified in template header before theme, for instance: (some_site/some_theme)path/to/template.ext
- absolute template paths are now implemented, but Renderer must be instanciated with trusted to True for security reason (it's the case for jp)
- a new "front_url_filter" callable can be given to Renderer, which will convert template path to URL seen by end-user (default to real path).
- the "front_url_filter" can be used in templates with… "front_url" filter
- template_data is a new named tuple available in templates, which give site, theme and template relative URL
- search order is site/theme, site/default_theme, and default/default_theme where default link to sat_pubsub templates
- when loading CSS files, files with _noscript suffixes are now loaded, and used when javascript is not available
- "styles_extra.css" is also loaded before "styles.css", useful when a theme want to reuse default style, and just override some rules
- new site can be specified in sat.conf [DEFAULT] section, using sites_path_public_dict or sites_path_private_dict (where sites_path_private_dict won't be used in public frontends, like Libervia)
- "private" argument of Renderer tells the renderer to load private sites or not
- templates are now loaded from "templates" subdirectory, to differenciate them from other data like i18n
- jp template output has been updated to handle those changes, and to manage absolute templates
This commit is contained in:
Goffi 2018-09-10 08:58:18 +02:00
commit e70405cfc2
6 changed files with 507 additions and 249 deletions

View file

@ -225,6 +225,7 @@ class Const(object):
]
## Templates ##
TEMPLATE_TPL_DIR = u"templates"
TEMPLATE_THEME_DEFAULT = u"default"
TEMPLATE_STATIC_DIR = u"static"

View file

@ -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)

View file

@ -17,16 +17,16 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" 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"<script src={src} {attribute}></script>"
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 <site_root_dir>/<theme_dir>/<static_dir>/filename,
then <site_root_dir>/<default_theme_dir>/<static_dir>/filename anf finally
<default_site>/<default_theme_dir>/<static_dir> (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/<filename>"
@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"""<svg class="svg-icon{cls}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<use href="#{name}"/>
</svg>
""".format(
name=name, cls=(" " + cls) if cls else ""
)
)
return safe(u'<svg class="svg-icon{cls}" xmlns="http://www.w3.org/2000/svg" '
u'viewBox="0 0 100 100">\n'
u' <use href="#{name}"/>'
u'</svg>\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,

View file

@ -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():

View file

@ -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()

View file

@ -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)