wcs/wcs/qommon/template.py

518 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/>.
from cStringIO import StringIO
import os
import glob
import xml.etree.ElementTree as ET
from django.template import (Context as DjangoContext, Template as DjangoTemplate,
TemplateSyntaxError as DjangoTemplateSyntaxError,
VariableDoesNotExist as DjangoVariableDoesNotExist)
from django.template.loader import render_to_string
from quixote import get_session, get_request, get_response, get_publisher
from quixote.directory import Directory
from quixote.util import StaticDirectory, StaticFile
from quixote.html import htmltext, htmlescape, TemplateIO
import errors
import ezt
def get_template_from_script(filename):
local_result = {}
execfile(filename, {
'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):
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: # parse error
return None
publisher = get_publisher()
def encode_string(x):
if publisher:
return unicode(x).encode(publisher.site_charset)
return x
name = encode_string(tree.attrib['name'])
version = tree.attrib.get('version')
label = encode_string(tree.findtext('label'))
desc = encode_string(tree.findtext('desc'))
author = encode_string(tree.findtext('author'))
icon = None
if type(theme_xml) is 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 qommon import _
if not error_title:
error_title = _('Error')
if exception:
root_url = get_publisher().get_root_url()
get_response().add_javascript(['jquery.js', 'exception.js'])
kwargs = {'title': error_title}
if location_hint == 'backoffice':
import qommon.backoffice.menu
error_html_top = qommon.backoffice.menu.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>') % exception.encode(get_publisher().site_charset)
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 file(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 file(filename).read()
filename = os.path.join(get_publisher().app_dir, 'themes', current_theme, 'template.ezt')
if os.path.exists(filename):
return file(filename).read()
filename = os.path.join(get_publisher().data_dir, 'themes', current_theme, 'template.ezt')
if os.path.exists(filename):
return file(filename).read()
return DEFAULT_TEMPLATE_EZT
def get_decorate_vars(body, response, generate_breadcrumb=True):
from publisher import get_cfg
body = str(body)
if get_request().get_header('x-popup') == 'true' or response.content_type != 'text/html':
return {'body': body}
kwargs = {}
for k, v in response.filter.items():
if v:
kwargs[k] = str(v)
if not kwargs.has_key('lang') and hasattr(get_request(), 'language'):
response.filter['lang'] = get_request().language
if 'rel="popup"' in body or 'rel="popup"' in kwargs.get('sidebar', ''):
response.add_javascript(['jquery.js', 'jquery-ui.js', 'popup.js', 'widget_list.js'])
if 'data-geolocation' in body:
response.add_javascript(['qommon.geolocation.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:
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 response.page_template_key or not template_ezt:
# the theme can provide a default template
possible_filenames = []
if response.page_template_key:
possible_filenames.append('template.%s.%s.ezt' % (
get_publisher().APP_NAME, response.page_template_key))
possible_filenames.append('template.%s.ezt' % response.page_template_key)
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 = file(filename).read()
break
else:
continue
break
if template_ezt:
generate_breadcrumb = ('[breadcrumb]' in template_ezt)
template = ezt.Template()
template.parse(template_ezt)
else:
if response.page_template_key == 'iframe':
template = default_iframe_template
else:
template = default_template
fd = StringIO()
vars = get_decorate_vars(body, response,
generate_breadcrumb=generate_breadcrumb)
template.generate(fd, vars)
return fd.getvalue()
def render(template_name, context):
return htmltext(render_to_string(template_name, context).encode('utf-8'))
class TemplateError(Exception):
def __init__(self, msg, params=()):
self.msg = msg
self.params = params
def __str__(self):
return self.msg % self.params
def ezt_raises(exception, on_parse=False):
from qommon 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 UnicodeDjangoContext(DjangoContext):
def __getitem__(self, key):
s = super(UnicodeDjangoContext, self).__getitem__(key)
if isinstance(s, str):
return unicode(s, 'utf-8')
return s
class Template(object):
def __init__(self, value, raises=False, ezt_format=ezt.FORMAT_RAW, ezt_only=False):
'''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
try:
self.template = DjangoTemplate(value)
except DjangoTemplateSyntaxError as e:
if raises:
from qommon 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={}):
return str(self.value)
def django_render(self, context={}):
context = UnicodeDjangoContext(context)
try:
rendered = self.template.render(context)
except (DjangoTemplateSyntaxError, DjangoVariableDoesNotExist) as e:
if self.raises:
from qommon import _
raise TemplateError(_('failure to render Django template: %s'), e)
else:
return self.value
return rendered.encode(get_publisher().site_charset)
def ezt_render(self, context={}):
fd = StringIO()
try:
self.template.generate(fd, context)
except ezt.EZTException as e:
if self.raises:
ezt_raises(e)
else:
return self.value
return fd.getvalue()
@classmethod
def is_template_string(cls, string):
return string and ('{{' in string or '{%' in string or '[' in string)