wcs/wcs/admin/settings.py

1243 lines
51 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 copy
import hashlib
import mimetypes
import os
try:
import lasso
except ImportError:
lasso = None
import zipfile
import shutil
import xml.etree.ElementTree as ET
from django.utils.encoding import force_bytes
from django.utils.six import BytesIO, StringIO
from quixote import get_publisher, get_request, get_response, redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from wcs.qommon import _
from wcs.qommon import get_cfg
from wcs.qommon import errors
from wcs.qommon import misc
from wcs.qommon import template
from wcs.qommon.form import *
from wcs.qommon.sms import SMS
from wcs.qommon.afterjobs import AfterJob
from wcs.qommon.backoffice.menu import html_top
from wcs.qommon.admin.menu import error_page
from wcs.qommon.admin.cfg import cfg_submit
from wcs.qommon.admin.emails import EmailsDirectory
from wcs.qommon.admin.texts import TextsDirectory
from wcs.qommon.admin.settings import SettingsDirectory as QommonSettingsDirectory
from wcs.qommon.admin.logger import LoggerDirectory
from wcs.qommon import ident
from wcs.formdef import FormDef
from wcs.carddef import CardDef
from wcs.workflows import Workflow
from wcs.roles import Role
from wcs.backoffice.studio import StudioDirectory
from .fields import FieldDefPage, FieldsDirectory
from .data_sources import NamedDataSourcesDirectory
from .wscalls import NamedWsCallsDirectory
class UserFormDirectory(Directory):
_q_exports = ['']
class IdentificationDirectory(Directory):
_q_exports = ['']
def _q_index(self):
get_response().breadcrumb.append( ('identification/', _('Identification')) )
identification_cfg = get_cfg('identification', {})
form = Form(enctype='multipart/form-data')
methods = [ ('password', _('Simple local username / password'), 'password'), ]
if lasso is not None:
methods.insert(0,
('idp', _('Delegated to SAML identity provider'), 'idp'))
methods.append(('fc', _('Delegated to FranceConnect'), 'fc'))
form.add(CheckboxesWidget, 'methods', title = _('Methods'),
value=identification_cfg.get('methods'),
options=methods,
inline=False,
required=True)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('..')
if form.is_submitted() and not form.has_errors():
cfg_submit(form, 'identification', ['methods'])
if not form.has_errors():
return redirect('..')
html_top('settings', title = _('Identification'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Identification')
if lasso is None:
cls = 'infonotice'
if identification_cfg.get('methods') and 'idp' in identification_cfg.get('methods'):
cls = 'errornotice'
r += htmltext('<p class="%s">%s</p>') % (cls, _('Delegated to SAML identity provider \
authentication is unavailable. Lasso must be installed to use it.'))
r += form.render()
return r.getvalue()
def _q_lookup(self, component):
get_response().breadcrumb.append( ('identification/', _('Identification')) )
return ident.get_method_admin_directory(component)
class UserFieldDefPage(FieldDefPage):
section = 'settings'
blacklisted_attributes = ['condition']
class UserFieldsDirectory(FieldsDirectory):
_q_exports = ['', 'update_order', 'new', 'mapping']
section = 'settings'
field_def_page_class = UserFieldDefPage
support_import = False
blacklisted_types = ['page']
field_var_prefix = '..._user_var_'
def index_bottom(self):
r = TemplateIO(html=True)
r += htmltext('<div class="bo-block">')
r += htmltext('<h2>%s</h2>') % _('Fields Mapping')
r += htmltext('<p>%s</p>') % _('These settings make it possible to assign custom user fields to standard user fields.')
form = self.mapping_form()
r += form.render()
r += htmltext('</div>')
return r.getvalue()
def mapping_form(self):
users_cfg = get_cfg('users', {})
options = [(None, _('None'), '')] + [
(x.id, x.label, x.id) for x in self.objectdef.fields]
form = Form(action = 'mapping', enctype='multipart/form-data')
field_name_value = users_cfg.get('field_name')
if type(field_name_value) is str:
field_name_value = [field_name_value]
form.add(WidgetList, 'field_name', title = _('Field(s) for Name'),
element_type = SingleSelectWidget, value = field_name_value,
element_kwargs = {
'render_br': False,
'options': options})
form.add(SingleSelectWidget, 'field_email', title = _('Field for Email'),
value = users_cfg.get('field_email'),
options = options)
form.add_submit('submit', _('Submit'))
return form
def mapping(self):
form = self.mapping_form()
cfg_submit(form, 'users', ['field_name', 'field_email'])
return redirect('.')
class UserFieldsFormDef(FormDef):
'''Class to handle custom user fields, it loads and saves from/to
an XML string stored in the configuration (at users/formdef)'''
def __init__(self, publisher=None):
if publisher is None:
publisher = get_publisher()
self.publisher = publisher
users_cfg = publisher.cfg.get('users', {})
xml_import = users_cfg.get('formdef')
self.fields = [] # make sure fields is a list
self.id = None # required for XML import/export
if xml_import:
try:
tree = ET.fromstring(xml_import)
except:
pass
else:
obj = FormDef.import_from_xml_tree(tree, include_id=True)
self.fields = obj.fields
self.max_field_id = obj.max_field_id
else:
# compatibility with older location
filename = os.path.join(publisher.app_dir, 'config', 'user')
if os.path.exists(filename):
try:
formdef = FormDef.get_filename(filename)
except KeyError:
pass
else:
self.fields = formdef.fields
# and remove old file, to keep clean
self.store()
os.unlink(filename)
@property
def name(self):
return _('User Fields')
def get_admin_url(self):
base_url = get_publisher().get_backoffice_url()
return '%s/settings/users/fields/' % base_url
def store(self):
xml_export = self.export_to_xml(include_id=True)
users_cfg = self.publisher.cfg.get('users', {})
users_cfg['formdef'] = ET.tostring(xml_export)
self.publisher.cfg['users'] = users_cfg
self.publisher.write_cfg()
if self.publisher.is_using_postgresql():
from wcs import sql
sql.do_user_table()
sql.do_tracking_code_table()
sql.do_session_table()
sql.do_meta_table()
class UsersDirectory(Directory):
_q_exports = ['', 'fields']
def _q_index(self):
return redirect('fields/')
def _q_traverse(self, path):
get_response().breadcrumb.append(('users/', _('Users')))
self.fields = UserFieldsDirectory(UserFieldsFormDef())
return Directory._q_traverse(self, path)
class FileTypesDirectory(Directory):
_q_exports = ['', 'add']
def get_form(self, filetype={}):
form = Form(enctype='multipart/form-data')
form.add(StringWidget, 'label', title=_('Label'), required=True,
value=filetype.get('label'))
form.add(StringWidget, 'mimetypes', title=_('Mime types'),
required=True, size=70,
value=', '.join(filetype.get('mimetypes', [])))
form.add_submit('submit', _('Save'))
return form
@classmethod
def parse_mimetypes(cls, value):
def ensure_mimetype(x):
x = x.strip()
if x.startswith('.'):
mime_type, encoding = mimetypes.guess_type('foobar' + x)
if mime_type:
return mime_type
return x
return [ensure_mimetype(x) for x in value.split(',')]
@classmethod
def format_mimetypes(cls, types):
if not types:
return ''
l = []
ellipsis = '...'
for mimetype in types:
if sum([len(x) for x in l]) > 80:
# string got too long already, stop this now and we'll get an
# ellipsis
break
ext = mimetypes.guess_extension(mimetype)
if ext:
l.append('%s (%s)' % (mimetype, ext))
else:
l.append(mimetype)
else:
# we got to the end of the list, we won't need an ellipsis
ellipsis = ''
return ', '.join(l) + ellipsis
def _q_index(self):
html_top('settings', title=_('File Types'))
filetypes_cfg = get_cfg('filetypes', {})
form = self.get_form()
if form.get_submit() == 'submit':
if filetypes_cfg:
new_filetype_id = max(filetypes_cfg.keys()) + 1
else:
new_filetype_id = 1
new_filetype = {
'label': form.get_widget('label').parse(),
'mimetypes': self.parse_mimetypes(form.get_widget('mimetypes').parse())
}
filetypes_cfg[new_filetype_id] = new_filetype
get_publisher().cfg['filetypes'] = filetypes_cfg
get_publisher().write_cfg()
return redirect('.')
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('File Types')
if filetypes_cfg:
r += htmltext('<ul class="objects-list biglist filetypes">')
for filetype_id, filetype in filetypes_cfg.items():
r += htmltext('<li>')
r += htmltext(' <a href="%s">%s <span class="extra-info">(%s)</span></a>') % (
filetype_id,
filetype.get('label'),
self.format_mimetypes(filetype.get('mimetypes'))
)
r += htmltext('</li>')
r += htmltext('</ul>')
else:
r += htmltext('<div class="infonotice"><p>')
r += _('There are no file type defined at the moment.')
r += htmltext('</p></div>')
r += htmltext('<h3>%s</h3>') % _('New file type')
r += form.render()
return r.getvalue()
def _q_traverse(self, path):
get_response().breadcrumb.append(('filetypes/', _('File Types')))
return Directory._q_traverse(self, path)
def _q_lookup(self, component):
filetypes_cfg = get_cfg('filetypes', {})
try:
filetype_id = int(component)
filetype = filetypes_cfg[filetype_id]
except (ValueError, KeyError):
raise errors.TraversalError()
form = self.get_form(filetype)
form.add_submit('cancel', _('Cancel'))
form.add_submit('delete', _('Delete'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.get_submit() == 'submit':
old_filetype = filetype.copy()
filetype['label'] = form.get_widget('label').parse()
filetype['mimetypes'] = self.parse_mimetypes(form.get_widget('mimetypes').parse())
FormDef.update_filetype(filetype_id, old_filetype, filetype)
get_publisher().write_cfg()
return redirect('.')
if form.get_submit() == 'delete':
del filetypes_cfg[filetype_id]
get_publisher().write_cfg()
return redirect('.')
html_top('settings', title=_('File Types'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s - %s</h2>') % (_('File Type'), filetype['label'])
r += form.render()
return r.getvalue()
class ThemePreviewDirectory(Directory):
def _q_traverse(self, path):
if len(path) < 2:
return error_page('settings', _('Invalid URL'))
theme_id = path[0]
branding = get_publisher().cfg.get('branding', {})
original_branding = branding.copy()
get_publisher().cfg['branding'] = branding
get_publisher().cfg['branding']['theme'] = theme_id
if 'template' in get_publisher().cfg['branding']:
del get_publisher().cfg['branding']['template']
root_directory = get_publisher().root_directory_class()
response = get_response()
response.reset_includes()
response.filter = {}
del(response.breadcrumb)
if path[1] in ('backoffice', 'admin') or \
get_request().get_method() == 'POST':
from wcs.qommon.template import error_page as base_error_page
output = base_error_page(_("The theme preview doesn't support this."))
else:
output = root_directory._q_traverse(path[1:])
from wcs.qommon.template import decorate
if isinstance(output, template.QommonTemplateResponse):
output = template.render(output.templates, output.context)
theme_preview = decorate(output, response)
# restore original branding in case it has been changed
get_publisher().cfg['branding'] = original_branding
get_publisher().write_cfg()
response.filter['raw'] = True
return theme_preview
class SettingsDirectory(QommonSettingsDirectory):
_q_exports = ['', 'themes', 'users',
'template', 'misc', 'emails', 'debug_options', 'language',
('import', 'p_import'), 'export', 'identification', 'sitename',
'sms', 'certificates', 'texts', 'install_theme',
'session', 'download_theme', 'smstest', 'postgresql',
('admin-permissions', 'admin_permissions'), 'geolocation',
'theme_preview', 'filetypes',
('data-sources', 'data_sources'), 'wscalls', 'logs']
emails = EmailsDirectory()
identification = IdentificationDirectory()
users = UsersDirectory()
texts = TextsDirectory()
theme_preview = ThemePreviewDirectory()
filetypes = FileTypesDirectory()
data_sources = NamedDataSourcesDirectory()
wscalls = NamedWsCallsDirectory()
logs = LoggerDirectory()
def _q_index(self):
html_top('settings', title = _('Settings'))
r = TemplateIO(html=True)
disabled_screens_option = get_publisher().get_site_option('settings-disabled-screens') or ''
disabled_screens = [x.strip() for x in disabled_screens_option.split(',')]
def enabled(screen):
return screen not in disabled_screens
r += htmltext('<div class="splitcontent-left">')
if enabled('storage') and (get_publisher().has_site_option('postgresql') or get_cfg('postgresql', {})):
r += htmltext('<div class="section">')
r += htmltext('<h2>%s</h2>') % _('Storage')
r += htmltext('<dl> <dt><a href="postgresql">%s</a></dt> <dd>%s</dd> </dl>') % (
_('PostgreSQL Settings'),
_('Configure access to PostgreSQL database'))
r += htmltext('</div>')
r += htmltext('<div class="section">')
r += htmltext('<h2>%s</h2>') % _('Security')
r += htmltext('<dl>')
if enabled('identification'):
r += htmltext('<dt><a href="identification/">%s</a></dt> <dd>%s</dd>') % (
_('Identification'), _('Configure identification parameters'))
identification_cfg = get_cfg('identification', {})
for method in identification_cfg.get('methods', []):
try:
method_admin = ident.get_method_admin_directory(method)
except AttributeError:
continue
r += htmltext('<dt><a href="identification/%s/">%s</a></dt> <dd>%s</dd>') % (
method, _(method_admin.title), _(method_admin.label))
if enabled('session'):
r += htmltext('<dt><a href="session">%s</a></dt> <dd>%s</dd>') % (
_('Session'), _('Configure session management'))
if enabled('permissions'):
roles = list(Role.select())
if roles:
r += htmltext('<dt><a href="admin-permissions">%s</a></dt> <dd>%s</dd>') % (
_('Admin Permissions'), _('Configure access to the administration interface'))
r += htmltext('</dl></div>')
if enabled('import-export'):
r += htmltext('<div class="section">')
r += htmltext('<h2>%s</h2>') % _('Import / Export')
r += htmltext('<dl>')
r += htmltext('<dt><a href="import">%s</a></dt> <dd>%s</dd>') % (
_('Import'), _('Import data from another site'))
r += htmltext('<dt><a href="export">%s</a></dt> <dd>%s</dd>') % (
_('Export'), _('Export data for another site'))
r += htmltext('</dl>')
r += htmltext('</div>')
if enabled('misc'):
r += htmltext('<div class="section">')
r += htmltext('<h2>%s</h2>') % _('Misc')
r += htmltext('<dl>')
r += htmltext('<dt><a href="misc">%s</a></dt> <dd>%s</dd>') % (
_('Misc'), _('Configure misc options'))
r += htmltext('<dt><a href="debug_options">%s</a></dt> <dd>%s</dd>') % (
_('Debug Options'), _('Configure options useful for debugging'))
if get_cfg('debug', {}).get('logger', True):
r += htmltext('<dt><a href="logs/">%s</a></dt> <dd>%s</dd>') % (
_('Logs'), _('Access application log files'))
r += htmltext('</dl>')
r += htmltext('</div>')
r += htmltext('</div>')
r += htmltext('<div class="splitcontent-right">')
r += htmltext('<div class="section">')
r += htmltext('<h2>%s</h2>') % _('Customisation')
r += htmltext('<div>')
r += htmltext('<dl>')
if enabled('sitename'):
r += htmltext('<dt><a href="sitename">%s</a></dt> <dd>%s</dd>') % (
_('Site Name and Addresses'), _('Configure site name and addresses'))
if enabled('language'):
r += htmltext('<dt><a href="language">%s</a></dt> <dd>%s</dd>') % (
_('Language'), _('Configure site language'))
if enabled('theme'):
r += htmltext('<dt><a href="themes">%s</a></dt> <dd>%s</dd>') % (
_('Theme'), _('Configure theme'))
if enabled('template'):
r += htmltext('<dt><a href="template">%s</a></dt> <dd>%s</dd>') % (
_('Template'), _('Configure template'))
if enabled('geolocation'):
r += htmltext('<dt><a href="geolocation">%s</a></dt> <dd>%s</dd>') % (
_('Geolocation'), _('Configure geolocation'))
if enabled('users'):
r += htmltext('<dt><a href="users/">%s</a></dt> <dd>%s</dd>') % (
_('Users'), _('Configure users'))
if enabled('emails'):
r += htmltext('<dt><a href="emails/">%s</a></dt> <dd>%s</dd>') % (
_('Emails'), _('Configure email settings'))
if enabled('sms') and get_publisher().use_sms_feature:
r += htmltext('<dt><a href="sms">%s</a></dt> <dd>%s</dd>') % (
_('SMS'), _('Configure SMS settings'))
if enabled('texts') and self.texts.texts_dict:
r += htmltext('<dt><a href="texts/">%s</a></dt> <dd>%s</dd>') % (
_('Texts'), _('Configure text that appears on some pages'))
if enabled('filetypes'):
r += htmltext('<dt><a href="filetypes/">%s</a></dt> <dd>%s</dd>') % (
_('File Types'), _('Configure known file types'))
r += htmltext('<dt><a href="data-sources/">%s</a></dt> <dd>%s</dd>') % (
_('Data sources'), _('Configure data sources'))
r += htmltext('<dt><a href="wscalls/">%s</a></dt> <dd>%s</dd>') % (
_('Webservice calls'), _('Configure webservice calls'))
r += htmltext('</dl>')
r += htmltext('</div>')
r += htmltext('</div>')
r += htmltext('</div>')
return r.getvalue()
def admin_permissions(self):
permissions_cfg = get_cfg('admin-permissions', {})
form = Form(enctype='multipart/form-data')
permissions = [_('Backoffice')]
permission_keys = []
admin_sections = [
('forms', N_('Forms')),
('carddefs', N_('Card Models')),
('workflows', N_('Workflows')),
('users', N_('Users')),
('roles', N_('Roles')),
('categories', N_('Categories')),
('settings', N_('Settings')),
]
for k, v in admin_sections:
if k == 'carddef' and not StudioDirectory.is_visible():
continue
permissions.append(_(v))
permission_keys.append(k)
rows = []
value = []
roles = [x for x in Role.select(order_by='name') if not x.is_internal()]
for role in roles:
rows.append(role.name)
value.append([role.allows_backoffice_access])
for k in permission_keys:
authorised_roles = [str(x) for x in permissions_cfg.get(k) or []]
value[-1].append(bool(str(role.id) in authorised_roles))
colrows_hash = hashlib.md5(force_bytes('%r-%r' % (rows, permissions))).hexdigest()
form.add_hidden('hash', colrows_hash)
form.add(CheckboxesTableWidget, 'permissions', rows=rows, columns=permissions)
form.get_widget('permissions').set_value(value)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.get_widget('hash').parse() != colrows_hash:
# The columns and rows are made of indices; permissions could be
# wrongly assigned if there were some changes to the columns and
# rows between the form being displayed and submitted.
form.get_widget('permissions').set_error(
_('Changes were made to roles or permissions while the table was displayed.'))
if not form.is_submitted() or form.has_errors():
get_response().breadcrumb.append(('admin-permissions', _('Admin Permissions')))
html_top('settings', title = _('Admin Permissions'))
r = TemplateIO(html=True)
r += htmltext('<div class="admin-permissions">')
r += htmltext('<h2>%s</h2>') % _('Admin Permissions')
r += form.render()
r += htmltext('</div>')
return r.getvalue()
else:
value = form.get_widget('permissions').parse()
permissions = {}
for key in permission_keys:
permissions[key] = []
for i, role in enumerate(roles):
permission_row = value[i]
if role.allows_backoffice_access != permission_row[0]:
role.allows_backoffice_access = permission_row[0]
role.store()
for j, key in enumerate(permission_keys):
if permission_row[j+1]:
permissions[key].append(role.id)
get_publisher().cfg['admin-permissions'] = permissions
get_publisher().write_cfg()
return redirect('.')
def themes(self):
request = get_request()
if 'theme' not in request.form:
current_theme = get_cfg('branding', {}).get('theme', 'default')
get_response().breadcrumb.append(('themes', _('Themes')))
html_top('settings', title = _('Themes'))
r = TemplateIO(html=True)
r += htmltext("<h2>%s</h2>") % _('Themes')
r += get_session().display_message()
r += htmltext('<a rel="popup" href="install_theme">%s</a>') % _('Install New Theme')
r += htmltext('<form action="themes" enctype="multipart/form-data" method="post">')
themes = template.get_themes_dict()
r += htmltext('<ul class="biglist themes">')
for theme, theme_dict in sorted(themes.items()):
label = theme_dict.get('label')
if 'version' in theme_dict:
label = '%s (%s)' % (label, theme_dict.get('version'))
if current_theme == theme:
checked = ' checked="checked"'
else:
checked = ''
r += htmltext('<li>')
r += htmltext('<strong class="label"><label>')
r += htmltext(' <input name="theme" value="%s" type="radio"%s>%s</input>') % (
theme, checked, label)
r += htmltext('</label></strong>')
if theme_dict.get('icon'):
r += htmltext('<img src="/themes/%s/icon.png" alt="" class="theme-icon" />') % theme
r += htmltext('<p class="details">%s') % theme_dict.get('desc', '')
r += htmltext(' [<a href="download_theme?theme=%s">%s</a>]') % (theme, _('download'))
r += htmltext(' [<a class="theme-preview" href="theme_preview/%s/">%s</a>]') % (
theme, _('preview'))
if theme_dict.get('author'):
r += htmltext('<br/>')
r += htmltext(_('by %s')) % theme_dict.get('author')
r += htmltext('</p>')
r += htmltext('</li>')
r += htmltext('</ul>')
r += htmltext('<div class="buttons">')
r += htmltext('<input type="submit" name="submit" value="%s" />') % _('Submit')
r += htmltext('</div>')
r += htmltext('</form>')
return r.getvalue()
else:
themes = template.get_themes()
if str(request.form['theme']) in themes:
branding_cfg = get_cfg('branding', {})
branding_cfg[str('theme')] = str(request.form['theme'])
get_publisher().cfg[str('branding')] = branding_cfg
get_publisher().write_cfg()
return redirect('.')
def download_theme(self):
theme_id = get_request().form.get('theme')
if not theme_id:
return redirect('themes')
theme_directory = template.get_theme_directory(theme_id)
if not theme_directory:
return redirect('themes')
parent_theme_directory = os.path.dirname(theme_directory)
c = BytesIO()
z = zipfile.ZipFile(c, 'w')
for base, dirnames, filenames in os.walk(theme_directory):
basetheme = base[len(parent_theme_directory)+1:]
for filename in filenames:
z.write(os.path.join(base, filename), os.path.join(basetheme, filename))
z.close()
response = get_response()
response.set_content_type('application/zip')
response.set_header('content-disposition', 'attachment; filename=%s.zip' % theme_id)
return c.getvalue()
def install_theme(self):
form = Form(enctype='multipart/form-data')
form.add(FileWidget, 'file', title=_('Theme File'), required=False)
form.add(UrlWidget, 'url', title=_('Theme Address'), required=False,
size=50)
form.add_submit('submit', _('Install'))
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('.')
if form.is_submitted() and not form.has_errors():
try:
return self.install_theme_submit(form)
except ValueError:
form.get_widget('file').set_error(_('Invalid Theme'))
get_response().breadcrumb.append( ('install_theme', _('Install Theme')) )
html_top('forms', title = _('Install Theme'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Install Theme')
r += htmltext('<p>%s</p>') % _(
'You can install a new theme by uploading a file '\
'or by pointing to the theme URL.')
r += form.render()
return r.getvalue()
def install_theme_submit(self, form):
if form.get_widget('url').parse():
return self.install_theme_from_url(form.get_widget('url').parse())
if form.get_widget('file').parse():
return self.install_theme_from_file(form.get_widget('file').parse().fp)
get_session().message = ('error', _('You have to enter a file or a URL.'))
return redirect('themes')
def install_theme_from_file(self, fp):
try:
z = zipfile.ZipFile(fp, 'r')
except Exception as e:
get_session().message = ('error', _('Failed to read theme file. (%s)') % str(e))
return redirect('themes')
theme_dir = os.path.join(get_publisher().app_dir, 'themes')
filename_list = [x for x in z.namelist() if x[0] != '/' and x[-1] != '/']
if len(filename_list) == 0:
get_session().message = ('error', _('Empty theme file.'))
return redirect('themes')
theme_name = filename_list[0].split('/')[0]
if not ('%s/desc.xml' % theme_name) in filename_list:
get_session().message = ('error', _('Theme is missing a desc.xml file.'))
return redirect('themes')
desc_xml = z.read('%s/desc.xml' % theme_name)
theme_dict = template.get_theme_dict(StringIO(force_text(desc_xml)))
if theme_dict.get('name') != theme_name:
get_session().message = ('error', _('desc.xml is missing a name attribute.'))
return redirect('themes')
if os.path.exists(os.path.join(theme_dir, theme_name)):
shutil.rmtree(os.path.join(theme_dir, theme_name))
for f in z.namelist():
if f[-1] == '/':
continue
path = os.path.join(theme_dir, f)
data = z.read(f)
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
open(path, 'wb').write(data)
z.close()
return redirect('themes')
def install_theme_from_url(self, url):
try:
fp = misc.urlopen(url)
except misc.ConnectionError as e:
get_session().message = ('error', _('Error loading theme (%s).') % str(e))
return redirect('themes')
return self.install_theme_from_file(StringIO(fp.read()))
def template(self):
from wcs.qommon.template import get_default_ezt_template
default_template_ezt = get_default_ezt_template()
branding_cfg = get_cfg('branding', {})
template = branding_cfg.get('template', default_template_ezt)
form = Form(enctype="multipart/form-data")
form.add(TextWidget, 'template', title = _('Site Template'), value = template,
cols = 80, rows = 25)
form.add_submit('submit', _('Submit'))
form.add_submit('restore-default', _('Restore default template'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.get_submit() == 'cancel':
return redirect('.')
if form.get_submit() == 'restore-default':
self.template_submit()
return redirect('.')
if form.is_submitted() and not form.has_errors():
self.template_submit(form)
return redirect('.')
get_response().breadcrumb.append(('template', _('Template')))
html_top('settings', title = _('Template'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Template')
r += form.render()
return r.getvalue()
def template_submit(self, form = None):
from wcs.qommon.template import DEFAULT_TEMPLATE_EZT, get_default_ezt_template
theme_default_template_ezt = get_default_ezt_template()
get_publisher().reload_cfg()
branding_cfg = get_cfg('branding', {})
if not form:
template = None
else:
template = form.get_widget('template').parse()
if template in (DEFAULT_TEMPLATE_EZT, theme_default_template_ezt) or not template:
if 'template' in branding_cfg:
del branding_cfg['template']
else:
branding_cfg['template'] = template
get_publisher().cfg['branding'] = branding_cfg
get_publisher().write_cfg()
def misc(self):
misc_cfg = get_cfg('misc', {})
form = Form(enctype="multipart/form-data")
form.add(WidgetDict, 'namespaces',
title = _('Namespaces for prefilling'),
value = misc_cfg.get('namespaces', {}))
form.add_submit("submit", _("Submit"))
form.add_submit("cancel", _("Cancel"))
if form.get_widget('cancel').parse():
return redirect('.')
if not form.is_submitted() or form.has_errors():
get_response().breadcrumb.append(('misc', _('Misc')))
html_top('settings', title = _('Misc'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Misc')
r += form.render()
return r.getvalue()
else:
cfg_submit(form, 'misc', ('namespaces',))
return redirect('.')
def export(self):
if get_request().form.get('job') or get_request().form.get('download'):
return self.export_pending()
form = Form(enctype="multipart/form-data")
form.add(CheckboxWidget, 'formdefs', title = _('Forms'), value = True)
if StudioDirectory.is_visible():
form.add(CheckboxWidget, 'carddefs', title=_('Card Models'), value=True)
form.add(CheckboxWidget, 'workflows', title = _('Workflows'), value = True)
if not get_cfg('sp', {}).get('idp-manage-roles'):
form.add(CheckboxWidget, 'roles', title = _('Roles'), value = True)
form.add(CheckboxWidget, 'categories', title = _('Categories'), value = True)
form.add(CheckboxWidget, 'settings', title = _('Settings'), value = False)
form.add(CheckboxWidget, 'datasources', title=_('Data sources'), value=True)
form.add(CheckboxWidget, 'wscalls', title=_('Webservice calls'), value=True)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('.')
if not form.is_submitted():
get_response().breadcrumb.append(('export', _('Export')))
html_top('settings', title = _('Export'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Export')
r += form.render()
return r.getvalue()
class Exporter(object):
def __init__(self, dirs, settings):
self.app_dir = get_publisher().app_dir
self.dirs = dirs
self.settings = settings
def export(self, job):
c = BytesIO()
z = zipfile.ZipFile(c, 'w')
for d in self.dirs:
if d not in ('roles', 'categories', 'datasources', 'wscalls'):
continue
path = os.path.join(self.app_dir, d)
if not os.path.exists(path):
continue
for f in os.listdir(path):
if f == '.indexes':
continue
z.write(os.path.join(path, f), os.path.join(d, f))
if 'formdefs' in self.dirs:
for formdef in FormDef.select():
node = formdef.export_to_xml(include_id=True)
misc.indent_xml(node)
z.writestr(os.path.join('formdefs_xml', str(formdef.id)),
b'<?xml version="1.0"?>\n' + ET.tostring(node))
if 'carddefs' in self.dirs:
for formdef in CardDef.select():
node = formdef.export_to_xml(include_id=True)
misc.indent_xml(node)
z.writestr(os.path.join('carddefs_xml', str(formdef.id)),
b'<?xml version="1.0"?>\n' + ET.tostring(node))
if 'workflows' in self.dirs:
for workflow in Workflow.select():
node = workflow.export_to_xml(include_id=True)
misc.indent_xml(node)
z.writestr(os.path.join('workflows_xml', str(workflow.id)),
b'<?xml version="1.0"?>\n' + ET.tostring(node))
if self.settings:
z.write(os.path.join(self.app_dir, 'config.pck'), 'config.pck')
for f in os.listdir(self.app_dir):
if f.startswith('idp-') and os.path.splitext(f)[-1] in ('.pem', '.xml'):
z.write(os.path.join(self.app_dir, f), f)
if os.path.exists(os.path.join(self.app_dir, 'config')):
for f in os.listdir(os.path.join(self.app_dir, 'config')):
z.write(os.path.join(self.app_dir, 'config', f), os.path.join('config', f))
z.close()
job.file_content = c.getvalue()
job.store()
dirs = []
for w in ('formdefs', 'carddefs', 'workflows', 'roles', 'categories',
'datasources', 'wscalls'):
if form.get_widget(w) and form.get_widget(w).parse():
dirs.append(w)
if not dirs and not form.get_widget('settings').parse():
return redirect('.')
exporter = Exporter(dirs, settings=form.get_widget('settings').parse())
job = get_response().add_after_job(
N_('Exporting site settings'),
exporter.export)
job.store()
return redirect('export?job=%s' % job.id)
def export_pending(self):
job_id = get_request().form.get('job') or get_request().form.get('download')
try:
job = AfterJob.get(job_id)
except KeyError:
return redirect('.')
if get_request().form.get('download'):
response = get_response()
response.set_content_type('application/x-wcs')
response.set_header('content-disposition', 'attachment; filename=export.wcs')
return job.file_content
html_top('settings', title=_('Exporting'))
r = TemplateIO(html=True)
get_response().add_javascript(['jquery.js', 'afterjob.js'])
r += htmltext('<h2>%s</h2>') % _('Export')
r += htmltext('<div class="section"><dl class="job-status">')
r += htmltext('<dt>')
r += _(job.label)
r += htmltext('</dt>')
r += htmltext('<dd>')
r += htmltext('<span class="afterjob" id="%s">') % job.id
r += _(job.status)
r += htmltext('</span>')
r += htmltext('</dd>')
r += htmltext('</dl>')
r += htmltext('<div class="done">')
r += htmltext('<a download="export.wcs" href="export?download=%s">%s</a>') % (
job.id, _('Download Export'))
r += htmltext('</div>')
r += htmltext('</div>')
return r.getvalue()
def p_import(self):
form = Form(enctype='multipart/form-data')
form.add(FileWidget, 'file', title = _('File'), required = True)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('.')
if not form.is_submitted() or form.has_errors():
get_response().breadcrumb.append(('import', _('Import')))
html_top('settings', title = _('Import'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Import')
r += form.render()
return r.getvalue()
else:
try:
results = self.import_submit(form)
except zipfile.BadZipfile:
results = None
html_top('settings', title = _('Import'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Import')
if results:
r += htmltext('<p>%s</p>') % _('Imported successfully:')
r += htmltext('<ul>')
if results['formdefs']:
r += htmltext('<li>%d %s</li>') % (results['formdefs'], _('forms'))
if results['carddefs']:
r += htmltext('<li>%d %s</li>') % (results['carddefs'], _('cards'))
if results['workflows']:
r += htmltext('<li>%d %s</li>') % (results['workflows'], _('workflows'))
if results['roles']:
r += htmltext('<li>%d %s</li>') % (results['roles'], _('roles'))
if results['categories']:
r += htmltext('<li>%d %s</li>') % (results['categories'], _('categories'))
if results['settings']:
r += htmltext('<li>%s</li>') % _('Settings')
if results['datasources']:
r += htmltext('<li>%d %s</li>') % (results['datasources'], _('data sources'))
if results['wscalls']:
r += htmltext('<li>%d %s</li>') % (results['wscalls'], _('webservice calls'))
r += htmltext('</ul>')
else:
r += htmltext('<p>%s</p>') % _('Error: Not a valid export file')
r += htmltext('<a href=".">%s</a>') % _('Back')
return r.getvalue()
def import_submit(self, form):
return get_publisher().import_zip(form.get_widget('file').parse().fp)
def sitename(self):
form = Form(enctype='multipart/form-data')
misc_cfg = get_cfg('misc', {})
form.add(StringWidget, 'sitename', title = _('Site Name'),
value = misc_cfg.get('sitename', ''))
form.add(StringWidget, 'frontoffice-url', size=32,
title = _('Frontoffice base URL'),
value = misc_cfg.get('frontoffice-url', ''))
form.add(StringWidget, 'backoffice-url', size=32,
title = _('Backoffice base URL'),
value = misc_cfg.get('backoffice-url', ''))
form.add(StringWidget, 'homepage-redirect-url', size=32,
title=_('Homepage redirection'),
value=misc_cfg.get('homepage-redirect-url', ''))
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if not form.is_submitted() or form.has_errors():
get_response().breadcrumb.append(('sitename', _('Site Name and Addresses')))
html_top('settings', title = _('Site Name and Addresses'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Site Name and Addresses')
r += form.render()
return r.getvalue()
else:
cfg_submit(form, 'misc', ['sitename', 'frontoffice-url',
'backoffice-url', 'homepage-redirect-url'])
return redirect('.')
def sms(self):
get_response().breadcrumb.append(('sms', _('SMS')))
html_top('settings', title = _('SMS'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('SMS Options')
sms_cfg = get_cfg('sms', {})
mode = sms_cfg.get('mode', 'none')
sms = SMS.get_sms_class(mode)
if sms:
r += htmltext('<ul>')
try:
try:
r += htmltext('<li>%s %s</li>') % (_('SMS Credit:'), sms.get_money_left())
except NotImplementedError:
pass
try:
r += htmltext('<li>%s %s</li>') % (_('SMS Left:'), sms.get_sms_left())
except NotImplementedError:
pass
except errors.SMSError:
r += htmltext("<p>%s</li>") % _("Connection with SMS provider failed")
r += htmltext('</ul>')
form = Form(enctype='multipart/form-data')
form.add(SingleSelectWidget, 'mode', title = _('SMS Mode'),
value = mode,
options = [(str('none'), _('No support'), str('none'))]+
[(str(k), _(SMS.providers.get(k)[0]), str(k)) for k in SMS.providers.keys()])
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if sms:
for widget, name, title in sms.parameters:
form.add(widget, name, title=_(title),
value=sms_cfg.get(name, ''),
required=True)
if form.get_submit() and not form.has_errors():
cfg_submit(form, 'sms', ['mode'] + [x[1] for x in sms.parameters])
if mode != form.get_widget('mode').parse():
return redirect('sms')
else:
return redirect('.')
elif mode != form.get_widget('mode').parse():
cfg_submit(form, 'sms', ['mode',])
return redirect('sms')
else:
if form.get_submit() and form.get_widget('mode').parse() == str('none'):
return redirect('.')
if form.get_submit() and not form.has_errors():
cfg_submit(form, 'sms', ['mode',])
return redirect('sms')
else:
r += form.render()
if mode != 'none':
r += htmltext('<p><a href="smstest">%s</a></p>') % _('SMS Test')
return r.getvalue()
def smstest(self):
form = Form(enctype='multipart/form-data', action='smstest')
form.add(StringWidget, 'sender', title=_('Sender'), required=True)
form.add(StringWidget, 'destinations', title=_('Destinations'), required=True)
form.add(StringWidget, 'text', title=_('Text'), required=True)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('sms')
get_response().breadcrumb.append(('sms', _('SMS')))
get_response().breadcrumb.append(('smstest', _('SMS Test')))
html_top('settings', title = _('SMS Test'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('SMS Test')
r += form.render()
if form.get_submit() and not form.has_errors():
sms_cfg = get_cfg('sms', {})
mode = sms_cfg.get('mode', 'none')
sms = SMS.get_sms_class(mode)
sender = str(form.get_widget('sender').parse())
destinations = str(form.get_widget('destinations').parse()).split(str(','))
text = str(form.get_widget('text').parse())
try:
sms.send(sender, destinations, text)
except Exception as e:
r += htmltext('<pre>')
r += repr(e)
r += htmltext('</pre>')
else:
r += htmltext('<p>')
r += _('The SMS has been sent successfully.')
r += htmltext('</p>')
return r.getvalue()
def postgresql(self):
postgresql_cfg = get_cfg('postgresql', {})
if not get_publisher().has_site_option('postgresql') and not postgresql_cfg:
raise errors.TraversalError()
form = Form(enctype='multipart/form-data')
form.add(StringWidget, 'database',
title=_('Database Name'), required=True,
value=postgresql_cfg.get('database'))
form.add(StringWidget, 'user',
title=_('User'), required=False,
value=postgresql_cfg.get('user'),
hint=_('User name used to authenticate'))
form.add(PasswordWidget, 'password',
title=_('Password'), required=False,
value=postgresql_cfg.get('password'),
hint=_('Password used to authenticate'))
form.add(StringWidget, 'host',
title=_('Host'), required=False,
value=postgresql_cfg.get('host'),
hint=_('Database host address'))
try:
port = int(postgresql_cfg.get('port'))
except (ValueError, TypeError):
port = None
form.add(IntWidget, 'port',
title=_('Port'), required=False,
value=port,
hint=_('Connection port number'))
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.is_submitted() and not form.has_errors():
postgresql_cfg = copy.copy(get_cfg('postgresql', {}))
cfg_submit(form, 'postgresql', ['database', 'user', 'password',
'host', 'port'])
try:
get_publisher().initialize_sql()
except Exception as e:
postgresql_cfg[str('postgresql')] = postgresql_cfg
form.set_error('database', str(e))
else:
return redirect('.')
get_response().breadcrumb.append(('postgresql', _('PostgreSQL Settings')))
html_top('settings', title=_('PostgreSQL Settings'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('PostgreSQL Settings')
r += form.render()
return r.getvalue()
def geolocation(self):
misc_cfg = get_cfg('misc', {})
form = Form(enctype='multipart/form-data')
form.add(MapWidget, 'default-position', title=_('Default Map Position'),
value=misc_cfg.get('default-position'),
default_zoom='9', required=False)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.is_submitted() and not form.has_errors():
cfg_submit(form, 'misc', ['default-position'])
return redirect('.')
get_response().breadcrumb.append(('geolocation', _('Geolocation Settings')))
html_top('settings', title=_('Geolocation Settings'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Geolocation Settings')
r += form.render()
return r.getvalue()