wcs/wcs/qommon/template.py

567 lines
19 KiB
Python

# w.c.s. - web application for online forms
# Copyright (C) 2005-2010 Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
import glob
import io
import os
import re
import xml.etree.ElementTree as ET
import django.template
from django.template import TemplateSyntaxError as DjangoTemplateSyntaxError
from django.template import VariableDoesNotExist as DjangoVariableDoesNotExist
from django.template import engines
from django.template.loader import render_to_string
from django.utils.encoding import force_text
from django.utils.encoding import smart_text
from quixote import get_publisher
from quixote import get_request
from quixote import get_response
from quixote import get_session
from quixote.directory import Directory
from quixote.html import TemplateIO
from quixote.html import htmlescape
from quixote.html import htmltext
from quixote.util import StaticDirectory
from quixote.util import StaticFile
from . import ezt
from . import force_str
def get_template_from_script(filename):
local_result = {}
exec(
open(filename).read(),
{'publisher': get_publisher(), 'request': get_request(), '__file__': filename},
local_result,
)
return local_result.get('template_content')
def get_theme_directory(theme_id):
system_location = os.path.join(get_publisher().data_dir, 'themes', theme_id)
local_location = os.path.join(get_publisher().app_dir, 'themes', theme_id)
if os.path.exists(local_location):
location = local_location
elif os.path.exists(system_location):
location = system_location
else:
return None
while os.path.islink(location):
location = os.path.join(os.path.dirname(location), os.readlink(location))
if not os.path.exists(location):
return None
return location
class ThemesDirectory(Directory):
def _q_lookup(self, name):
from . import errors
if name in ('.', '..'):
raise errors.TraversalError()
location = get_theme_directory(name)
if location is None:
raise errors.TraversalError()
if os.path.isdir(location):
return StaticDirectory(location)
else:
return StaticFile(location)
def get_themes_dict():
system_location = os.path.join(get_publisher().data_dir, 'themes')
local_location = os.path.join(get_publisher().app_dir, 'themes')
themes = {}
for theme_xml in glob.glob(os.path.join(system_location, '*/desc.xml')) + glob.glob(
os.path.join(local_location, '*/desc.xml')
):
theme_dict = get_theme_dict(theme_xml)
if not theme_dict:
continue
themes[theme_dict.get('name')] = theme_dict
return themes
def get_theme_dict(theme_xml):
try:
tree = ET.parse(theme_xml).getroot()
except Exception: # parse error
return None
name = force_str(tree.attrib['name'])
version = force_str(tree.attrib.get('version') or '')
label = force_str(tree.findtext('label') or '')
desc = force_str(tree.findtext('desc') or '')
author = force_str(tree.findtext('author') or '')
icon = None
if isinstance(theme_xml, str):
icon = os.path.join(os.path.dirname(theme_xml), 'icon.png')
if not os.path.exists(icon):
icon = None
theme = {'name': name, 'label': label, 'desc': desc, 'author': author, 'icon': icon, 'version': version}
theme['keywords'] = []
for keyword in tree.findall('keywords/keyword'):
theme['keywords'].append(keyword.text)
return theme
def get_themes():
# backward compatibility function, it returns a tuple with theme info,
# newer code should use get_themes_dict()
themes = {}
for k, v in get_themes_dict().items():
themes[k] = (v['label'], v['desc'], v['author'], v['icon'])
return themes
def get_current_theme():
from .publisher import get_cfg
current_theme = get_cfg('branding', {}).get('theme', 'default')
system_location = os.path.join(get_publisher().data_dir, 'themes', current_theme)
local_location = os.path.join(get_publisher().app_dir, 'themes', current_theme)
for location in (local_location, system_location):
if os.path.exists(location):
return get_theme_dict(os.path.join(location, 'desc.xml'))
default_theme_location = os.path.join(get_publisher().data_dir, 'themes', 'default')
return get_theme_dict(os.path.join(default_theme_location, 'desc.xml'))
DEFAULT_TEMPLATE_EZT = """<!DOCTYPE html>
<html lang="[site_lang]">
<head>
<title>[page_title]</title>
<link rel="stylesheet" type="text/css" href="[css]"/>
[script]
</head>
<body[if-any onload] onload="[onload]"[end]>
<div id="page">
<div id="top"> <h1>[if-any title][title][else][site_name][end]</h1> </div>
<div id="main-content">
[if-any breadcrumb]<p id="breadcrumb">[breadcrumb]</p>[end]
[body]
</div>
<div id="footer">[if-any footer][footer][end]</div>
</div>
</body>
</html>"""
DEFAULT_IFRAME_EZT = """<!DOCTYPE html>
<html lang="[site_lang]">
<head>
<title>[page_title]</title>
<link rel="stylesheet" type="text/css" href="[css]"/>
[script]
</head>
<body[if-any onload] onload="[onload]"[end]>
<div id="main-content">
[if-any breadcrumb]<p id="breadcrumb">[breadcrumb]</p>[end]
[body]
</div>
</body>
</html>"""
default_template = ezt.Template()
default_template.parse(DEFAULT_TEMPLATE_EZT)
default_iframe_template = ezt.Template()
default_iframe_template.parse(DEFAULT_IFRAME_EZT)
def html_top(title=None, default_org=None):
if not hasattr(get_response(), 'filter'):
get_response().filter = {}
get_response().filter['title'] = title
get_response().filter['default_org'] = default_org
def error_page(error_message, error_title=None, exception=None, continue_to=None, location_hint=None):
from . import _
if not error_title:
error_title = _('Error')
if exception:
get_response().add_javascript(['jquery.js', 'exception.js'])
kwargs = {'title': error_title}
if location_hint == 'backoffice':
from .backoffice.menu import html_top as error_html_top
kwargs[str('section')] = None
else:
error_html_top = html_top
r = TemplateIO(html=True)
error_html_top(**kwargs)
r += htmltext('<div class="error-page">')
r += htmltext('<p>%s</p>') % error_message
if exception and get_publisher().logger.error_email:
r += htmltext('<p>%s</p>') % _('It has been sent to the site administrator for analyse.')
if continue_to:
continue_link = htmltext('<a href="%s">%s</a>') % continue_to
r += htmltext('<p>%s</p>') % htmltext(_('Continue to %s')) % continue_link
if exception:
r += htmltext('<p><a id="display-exception">%s</a></p>') % _('View Error Details')
r += htmltext('<pre id="exception"><code>%s</code></pre>') % force_str(exception)
r += htmltext('</div>')
return htmltext(r.getvalue())
def get_default_ezt_template():
from .publisher import get_cfg
current_theme = get_cfg('branding', {}).get('theme', 'default')
filename = os.path.join(
get_publisher().app_dir, 'themes', current_theme, 'template.%s.ezt' % get_publisher().APP_NAME
)
if os.path.exists(filename):
return open(filename).read()
filename = os.path.join(
get_publisher().data_dir, 'themes', current_theme, 'template.%s.ezt' % get_publisher().APP_NAME
)
if os.path.exists(filename):
return open(filename).read()
filename = os.path.join(get_publisher().app_dir, 'themes', current_theme, 'template.ezt')
if os.path.exists(filename):
return open(filename).read()
filename = os.path.join(get_publisher().data_dir, 'themes', current_theme, 'template.ezt')
if os.path.exists(filename):
return open(filename).read()
return DEFAULT_TEMPLATE_EZT
def get_decorate_vars(body, response, generate_breadcrumb=True, **kwargs):
from .publisher import get_cfg
if response.content_type != 'text/html':
return {'body': body}
body = str(body)
if get_request().get_header('x-popup') == 'true':
return {'body': body}
kwargs = {}
for k, v in response.filter.items():
if v:
kwargs[k] = str(v)
if 'lang' not in kwargs and hasattr(get_request(), 'language'):
response.filter['lang'] = get_request().language
if ('rel="popup"' in body or 'rel="popup"' in kwargs.get('sidebar', '')) or (
'data-popup' in body or 'data-popup' in kwargs.get('sidebar', '')
):
response.add_javascript(['popup.js', 'widget_list.js'])
onload = kwargs.get('onload')
org_name = get_cfg('sp', {}).get('organization_name', kwargs.get('default_org', get_publisher().APP_NAME))
site_name = get_cfg('misc', {}).get('sitename', org_name)
current_theme = get_cfg('branding', {}).get('theme', get_publisher().default_theme)
if kwargs.get('title'):
title = kwargs.get('title')
page_title = '%s - %s' % (site_name, title)
title_or_orgname = title
else:
page_title = site_name
title = None
title_or_orgname = site_name
script = kwargs.get('script') or ''
script += response.get_css_includes_for_header()
script += response.get_javascript_for_header()
try:
user = get_request().user
except Exception:
user = None
if type(user) in (int, str) and get_session():
try:
user = get_session().get_user_object()
except KeyError:
pass
root_url = get_publisher().get_application_static_files_root_url()
theme_url = '%sthemes/%s' % (root_url, current_theme)
is_in_backoffice = response.filter.get('admin_ezt')
if is_in_backoffice:
header_menu = kwargs.get('header_menu')
user_info = kwargs.get('user_info')
page_title = kwargs.get('sitetitle', '') + kwargs.get('title', '')
subtitle = kwargs.get('subtitle')
sidebar = kwargs.get('sidebar')
css = root_url + get_publisher().qommon_static_dir + get_publisher().qommon_admin_css
app_dir_filename = os.path.join(get_publisher().app_dir, 'themes', current_theme, 'admin.css')
data_dir_filename = os.path.join(get_publisher().data_dir, 'themes', current_theme, 'admin.css')
for filename in (app_dir_filename, data_dir_filename):
if os.path.exists(filename):
extra_css = root_url + 'themes/%s/admin.css' % current_theme
break
extra_head = get_publisher().get_site_option('backoffice_extra_head')
app_label = get_publisher().get_site_option('app_label') or 'w.c.s.'
else:
if current_theme == 'default':
css = root_url + 'static/css/%s.css' % get_publisher().APP_NAME
else:
css = root_url + 'themes/%s/%s.css' % (current_theme, get_publisher().APP_NAME)
# this variable is kept in locals() as it was once part of the default
# template and existing installations may have template changes that
# still have it.
prelude = ''
if generate_breadcrumb:
breadcrumb = ''
if hasattr(response, 'breadcrumb') and response.breadcrumb:
s = []
path = root_url
if is_in_backoffice:
path += response.breadcrumb[0][0]
response.breadcrumb = response.breadcrumb[1:]
total_len = sum([len(str(x[1])) for x in response.breadcrumb if x[1] is not None])
for component, label in response.breadcrumb:
if component.startswith('http:') or component.startswith('https:'):
s.append('<a href="%s">%s</a>' % (component, label))
continue
if label is not None:
if type(label) is str:
label = htmlescape(label)
if not is_in_backoffice and (
total_len > 80 and len(label) > 10 and response.breadcrumb[-1] != (component, label)
):
s.append('<a href="%s%s" title="%s">%s</a>' % (path, component, label, '...'))
else:
s.append('<a href="%s%s">%s</a>' % (path, component, label))
path += component
breadcrumb = ' <span class="separator">&gt;</span> '.join(s)
vars = response.filter.copy()
vars.update(get_publisher().substitutions.get_context_variables())
vars.update(locals())
return vars
def decorate(body, response):
if get_request().get_header('x-popup') == 'true':
return '''<div class="popup-content"> %s </div>''' % body
from .publisher import get_cfg
generate_breadcrumb = True
if True:
template_ezt = get_cfg('branding', {}).get('template')
current_theme = get_cfg('branding', {}).get('theme', 'default')
if not template_ezt:
# the theme can provide a default template
possible_filenames = []
possible_filenames.append('template.%s.ezt' % get_publisher().APP_NAME)
possible_filenames.append('template.ezt')
possible_dirnames = [
os.path.join(get_publisher().app_dir, 'themes', current_theme),
os.path.join(get_publisher().data_dir, 'themes', current_theme),
os.path.join(get_publisher().data_dir, 'themes', 'default'),
]
for fname in possible_filenames:
for dname in possible_dirnames:
filename = os.path.join(dname, fname)
if os.path.exists(filename):
template_ezt = open(filename).read()
break
else:
continue
break
if template_ezt:
generate_breadcrumb = '[breadcrumb]' in template_ezt
template = ezt.Template()
template.parse(template_ezt)
else:
template = default_template
fd = io.StringIO()
vars = get_decorate_vars(body, response, generate_breadcrumb=generate_breadcrumb)
template.generate(fd, vars)
return fd.getvalue()
def render(template_name, context):
request = getattr(get_request(), 'django_request', None)
result = render_to_string(template_name, context, request=request)
return htmltext(force_str(result))
class QommonTemplateResponse:
def __init__(self, templates, context):
self.templates = templates
self.context = context
def add_media(self):
if 'form' in self.context:
# run add_media so we get them in the page <head>
self.context['form'].add_media()
class TemplateError(Exception):
def __init__(self, msg, params=()):
self.msg = msg
self.params = params
def __str__(self):
from . import misc
return misc.site_encode(smart_text(self.msg) % self.params)
def ezt_raises(exception, on_parse=False):
from . import _
parts = []
parts.append(
{
ezt.ArgCountSyntaxError: _('wrong number of arguments'),
ezt.UnknownReference: _('unknown reference'),
ezt.NeedSequenceError: _('sequence required'),
ezt.UnclosedBlocksError: _('unclosed block'),
ezt.UnmatchedEndError: _('unmatched [end]'),
ezt.UnmatchedElseError: _('unmatched [else]'),
ezt.BaseUnavailableError: _('unavailable base location'),
ezt.BadFormatConstantError: _('bad format constant'),
ezt.UnknownFormatConstantError: _('unknown format constant'),
}.get(exception.__class__, _('unknown error'))
)
if exception.line is not None:
parts.append(
_('at line %(line)d and column %(column)d')
% {'line': exception.line + 1, 'column': exception.column + 1}
)
if on_parse:
message = _('syntax error in ezt template: %s')
else:
message = _('failure to render ezt template: %s')
raise TemplateError(message % ' '.join(parts))
class Template:
def __init__(self, value, raises=False, ezt_format=ezt.FORMAT_RAW, ezt_only=False, autoescape=True):
'''Guess kind of template (Django or ezt), and parse it'''
self.value = value
self.raises = raises
if ('{{' in value or '{%' in value) and not ezt_only: # Django template
self.render = self.django_render
if autoescape is False:
value = '{%% autoescape off %%}%s{%% endautoescape %%}' % value
try:
self.template = engines['django'].from_string(value)
except DjangoTemplateSyntaxError as e:
if raises:
from . import _
raise TemplateError(_('syntax error in Django template: %s'), e)
self.render = self.null_render
elif '[' in value: # ezt template
self.render = self.ezt_render
self.template = ezt.Template(compress_whitespace=False)
try:
self.template.parse(value, base_format=ezt_format)
except ezt.EZTException as e:
if raises:
ezt_raises(e, on_parse=True)
self.render = self.null_render
else:
self.render = self.null_render
def null_render(self, context=None):
return str(self.value)
def django_render(self, context=None):
context = context or {}
try:
rendered = self.template.render(context)
except (DjangoTemplateSyntaxError, DjangoVariableDoesNotExist) as e:
if self.raises:
from . import _
raise TemplateError(_('failure to render Django template: %s'), e)
else:
return self.value
rendered = str(rendered)
if context.get('allow_complex'):
return rendered
return re.sub(r'[\uE000-\uF8FF]', '', rendered)
def ezt_render(self, context=None):
context = context or {}
fd = io.StringIO()
try:
self.template.generate(fd, context)
except ezt.EZTException as e:
if self.raises:
ezt_raises(e)
else:
return self.value
return force_str(fd.getvalue())
@classmethod
def is_template_string(cls, string):
return isinstance(string, str) and ('{{' in string or '{%' in string or '[' in string)
# monkey patch django template Variable resolution to convert legacy
# strings to unicode
variable_resolve_orig = django.template.base.Variable.resolve
def variable_resolve(self, context):
try:
value = variable_resolve_orig(self, context)
except UnicodeEncodeError:
# don't crash on non-ascii variable names
return context.template.engine.string_if_invalid
if isinstance(value, str):
return force_text(value, 'utf-8')
return value
if not getattr(django.template.base.Variable, 'monkey_patched', False):
django.template.base.Variable.resolve = variable_resolve
django.template.base.Variable.monkey_patched = True