567 lines
19 KiB
Python
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">></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
|