# 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 = list(Role.select(order_by='name'))
        for role in roles:
            if role.is_internal():
                continue
            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()
