# -*- coding: iso-8859-15 -*-


# Glasnost
# By: Odile Bénassy <obenassy@entrouvert.com>
#     Romain Chantereau <rchantereau@entrouvert.com>
#     Nicolas Clapiès <nclapies@easter-eggs.org>
#     Pierre-Antoine Dejace <padejace@entrouvert.be>
#     Thierry Dulieu <tdulieu@easter-eggs.com>
#     Florent Monnier <monnier@codelutin.com>
#     Cédric Musso <cmusso@easter-eggs.org>
#     Frédéric Péters <fpeters@entrouvert.be>
#     Benjamin Poussin <poussin@codelutin.com>
#     Emmanuel Raviart <eraviart@entrouvert.com>
#     Sébastien Régnier <regnier@codelutin.com>
#     Emmanuel Saracco <esaracco@easter-eggs.com>
#
# Copyright (C) 2000, 2001 Easter-eggs & Emmanuel Raviart
# Copyright (C) 2002 Odile Bénassy, Code Lutin, Thierry Dulieu, Easter-eggs,
#     Entr'ouvert, Frédéric Péters, Benjamin Poussin, Emmanuel Raviart,
#     Emmanuel Saracco & Théridion
# Copyright (C) 2003 Odile Bénassy, Romain Chantereau, Nicolas Clapiès,
#     Code Lutin, Pierre-Antoine Dejace, Thierry Dulieu, Easter-eggs,
#     Entr'ouvert, Florent Monnier, Cédric Musso, Ouvaton, Frédéric Péters,
#     Benjamin Poussin, Rodolphe Quiédeville, Emmanuel Raviart, Sébastien
#     Régnier, Emmanuel Saracco, Théridion & Vecam
#
# 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, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.


__doc__ = """Glasnost Objects Web"""

__version__ = '$Revision$'[11:-2]


import md5
import time
import types

import glasnost.common.context as context
import glasnost.common.faults as faults
import glasnost.common.slots as slots
import glasnost.common.system as system
import glasnost.common.translation as translation
import glasnost.common.tools_new as commonTools
import glasnost.common.xhtmlgenerator as X

from glasnost.proxy.DispatcherProxy import MultiCall

import kinds # Do not remove!
import things
from tools import *
import WebAPI


register = things.register


class BaseObjectWebMixin(things.ThingMixin):
    id_kind_stateInEditMode = 'hidden'
    id_kind_stateInViewMode = 'hidden'
    id_kind_widget_fieldLabel = N_('ID')
    id_kind_widgetName = 'InputText'

    serverRole_kind_stateInEditMode = 'hidden'
    serverRole_kind_stateInViewMode = 'hidden'

    version_kind_defaultValue = 0
    version_kind_stateInEditMode = 'hidden'
    version_kind_stateInViewMode = 'hidden'
    version_kind_widget_fieldLabel = N_('Version Number')
    version_kind_widgetName = 'InputText'

    def getEmbeddedViewLayout(self):
        return self.getCompactViewLayout(None, parentSlot = None)

    def getLabelTranslated(self, destinationLanguages = None, multiCall = None):
        translationsProxy = getProxyForServerRole('translations')
        if translationsProxy:
            if not destinationLanguages:
                destinationLanguages = context.getVar('destinationLanguages')
            try:
                return translationsProxy.getTranslation(
                        self.getLabel(), self.getId(), 'self.getLabel()',
                        self.getLabelLanguage(), destinationLanguages,
                        multiCall = multiCall)
            except faults.UnknownStringDigest:
                return self.getLabel()
        else:
            return self.getLabel()

    def getSlotToModifyNames(self, parentSlot = None):
        return None # Modify all slots.

    def getViewLayoutSlotNames(self, fields, parentSlot = None):
        slotNames = things.ThingMixin.getViewLayoutSlotNames(
            self, fields, parentSlot = parentSlot)
        userToken = context.getVar('userToken', default = '')

        if context.getVar('useCompactLayout', default = 0) or (
                self.id and not (hasattr(self.getWeb(), 'canModifyObject')
                                 and self.getWeb().canModifyObject(self.id))):
            slotNames = slotNames[:]
            for slotName in ('language',):
                if slotName in slotNames:
                    slotNames.remove(slotName)
        return slotNames

    def getWeb(self):
        return getWebForServerRole(self.serverRole)

    def makeContentTitle(self, contentSlot, contentLabel):
        return contentLabel

    def newWidget(self, parentSlot = None):
        return commonTools.newThing('widget', 'Thing')


class AdminWithoutWritersMixin(BaseObjectWebMixin):
    adminsSet_kind_widget_fieldLabel = N_('Administrators')

    def newWidget(self, parentSlot = None):
        return commonTools.newThing('widget', 'Thing')


class AdminMixin(AdminWithoutWritersMixin):
    readersSet_kind_itemKind_value_defaultValue = system.generalPublicId

    writersSet_kind_itemKind_value_defaultValue = None


class ObjectWebMixin(BaseObjectWebMixin):
    # this class had methods to add an object to another
    pass


class WebMixin(things.ThingMixin):
    thingCategory = 'web'


class AdministrableWebMixin(WebMixin):
    def admin(self):
        context.push(_level = 'admin',
                     defaultDispatcherId = context.getVar('dispatcherId'))
        try:
            if not self.canGetAdmin():
                return accessForbidden()
            admin = self.getAdmin()

            layout = X.array()
            layout += admin.getViewLayout(fields = None)

            buttonsBar = X.div(_class = 'buttons-bar')
            layout += buttonsBar
            navigationButtonsBar = X.span(_class = 'navigation-buttons-bar')
            buttonsBar += navigationButtonsBar
            navigationButtonsBar += X.buttonStandalone(
                    'view-list', X.actionUrl())
            if self.canModifyAdmin():
                actionButtonsBar = X.span(_class = 'action-buttons-bar')
                buttonsBar += actionButtonsBar
                actionButtonsBar += X.buttonStandalone(
                        'edit', X.actionUrl('adminEdit'))
        finally:
            context.pull(_level = 'admin')
        return writePageLayout(layout,
                _('%s Settings') % self.getTranslatedObjectsNameCapitalized())
    admin.isPublicForWeb = 1

    def adminEdit(self):
        if not self.isAdmin():
            return accessForbidden()
        admin = self.getAdmin()
        return self.adminObjectEdit(admin)
    adminEdit.isPublicForWeb = 1

    def adminObjectEdit(self, admin):

        context.push(_level = 'adminEdit',
                     defaultDispatcherId = context.getVar('dispatcherId'),
                     layoutMode = 'edit')
        try:
            layout = X.array()
            if context.getVar('error'):
                layout += admin.getErrorLayout()
            form = X.form(
                    action = X.actionUrl('adminSubmit'),
                    enctype= 'multipart/form-data', method = 'post')
            layout += form
            form += admin.getEditLayout(fields = None)

            buttonsBar = X.div(_class = 'buttons-bar')
            form += buttonsBar
            actionButtonsBar = X.span(_class = 'action-buttons-bar')
            buttonsBar += actionButtonsBar
            actionButtonsBar += X.buttonInForm('modify', 'modifyButton')
        finally:
            context.pull(_level = 'adminEdit')
        return writePageLayout(layout,
                _('Editing %s Settings') % \
                                self.getTranslatedObjectsNameCapitalized())

    def adminSubmit(self, **keywords):
        uri = None
        context.push(_level = 'adminSubmit',
                     defaultDispatcherId = context.getVar('dispatcherId'))
        try:
            if keywords is None:
                keywords = {}
            if not self.isAdmin():
                return accessForbidden()

            if isButtonSelected('applyButton', keywords):
                context.setVar('again', 1)
                context.setVar('hideErrors', 1)
            admin = self.newAdmin(keywords)
            admin.submitFields(keywords)
            if context.getVar('again'):
                return self.adminObjectEdit(admin)
            try:
                self.modifyAdmin(admin)
            except faults.WrongVersion:
                context.setVar('again', 1)
                context.setVar('error', 1)
                admin.setError('version', 1)
                return self.adminObjectEdit(admin)
            except:
                if context.getVar('debug'):
                    raise
                return accessForbidden()
            uri = X.actionUrl('admin')
            # The redirect(uri) will be returned by the finally instruction.
        finally:
            context.pull(_level = 'adminSubmit')
            if uri:
                return redirect(uri)
    adminSubmit.isPublicForWeb = 1


class ObjectsWebMixin(AdministrableWebMixin):
    def canViewAll(self, serverId = None):
        return hasattr(self, 'canGetObjects') \
               and self.canGetObjects(serverId = serverId)
        
    def clone(self, id):
        if not id or not self.hasObject(id):
            return pageNotFound()
        if not self.canCloneObject(id):
            return accessForbidden()
        object = self.getObject(id)
        object.id = None
        object.version = 0
        return self.editObject(object)
    clone.isPublicForWeb = 1
    
    def confirmDelete(self, id):
        if id and not self.hasObject(id):
            return pageNotFound()
        return confirmDelete(
                X.idUrl(id, 'delete'),
                id = id,
                objectName = self.objectName)
    confirmDelete.isPublicForWeb = 1

    def confirmMakeVersionCurrent(self, id, versionNumber):
        layout = X.array()
        object = self.getObject(id)
        layout += X.p(_('Are you sure you want to replace the current version (%s) of "%s" by this previous version (%s) ?') % (object.version, object.getLabel(), versionNumber))
        layout += X.div(_class = 'buttons-bar')(
                X.buttonStandalone('replace',
                        X.idUrl(id, 'makeVersionCurrent/%s' % versionNumber)),
                X.buttonStandalone('cancel',
                        X.idUrl(id, 'version/%s' % versionNumber)) )
        return writePageLayout(layout, _('Confirm Replacement'))
    confirmMakeVersionCurrent.isPublicForWeb = 1
    
    def delete(self, id):
        method = context.getVar('httpMethod')
        if id and self.hasObject(id):
            try:
                self.deleteObject(id)
            except faults.Fault:
                if context.getVar('debug'):
                    raise
                return accessForbidden()
            if method == 'DELETE':
                return HTTP_NO_CONTENT
            uri = X.actionUrl()
            return redirect(uri)
        else:
            # TODO: error page when user tries to delete an object that doesn't
            # exist.
            return pageNotFound()
    delete.isPublicForWeb = 1

    def diff(self, id, newVersionNumber, oldVersionNumber = ''):
        newVersionNumber = int(newVersionNumber)
        if oldVersionNumber:
            try:
                oldVersionNumber = int(oldVersionNumber)
            except ValueError:
                return pageNotFound()
        else:
            oldVersionNumber = newVersionNumber - 1
        try:
            newObject = self.getObjectWithVersion(id, int(newVersionNumber))
            oldObject = self.getObjectWithVersion(id, int(oldVersionNumber))
        except faults.UnknownObjectVersion:
            return pageNotFound()

        slotNames = newObject.getLayoutSlotNames(None)
        if 'modificationTime' in slotNames:
            slotNames.remove('modificationTime')
            slotNames.insert(slotNames.index('version')+1, 'modificationTime')
        layout = X.array()
        for slotName in slotNames:
            newObjectSlot = newObject.getSlot(slotName)
            newValue = newObjectSlot.getValue()
            oldObjectSlot = oldObject.getSlot(slotName)
            oldValue = oldObjectSlot.getValue()

            if newValue == oldValue:
                continue

            widget = newObjectSlot.getWidget()
            newObjectSlot.getKind().isTranslatable = 0
            oldObjectSlot.getKind().isTranslatable = 0
            diffValue = widget.getHtmlViewDiffValue(
                    oldObjectSlot, newObjectSlot)

            row = X.div(_class = 'field')
            layout += row
            row += X.span(_class = 'label')(
                    _(widget.getModelLabel(newObjectSlot)))
            cell = X.enclose(diffValue, _class = 'cell')
            row += cell

        return writePageLayout(layout,
                _('%s - Differences between versions') % newObject.getLabel())
    diff.isPublicForWeb = 1

    def download(self, id, *pathTuple):
        localId = commonTools.extractLocalId(id)
        if localId == '__admin__':
            if not self.canGetAdmin():
                return accessForbidden()
            object = self.getAdmin()
        else:
            if not self.hasObject(id):
                return pageNotFound()
            if not self.canGetObject(id):
                return accessForbidden()
            object = self.getObject(id)
            rememberObject(id)

        req = context.getVar('req')

        filename = None
        if '.' in pathTuple[-1]:
            filename = pathTuple[-1]
            pathTuple = pathTuple[:-1]

        def pathToPath(path):
            builtPath = 'self'
            for p in path:
                if p.isdigit():
                    builtPath += '[%s]' % p
                else:
                    builtPath += '.%s' % p
            return builtPath

        path = pathToPath(pathTuple)

        try:
            uploadSlot = object.getSlotByPath(path)
        except faults.NonExistentSlotPath:
            return pageNotFound()

        upload = uploadSlot.getValue()
        data = upload.getSlot('data', parentSlot = uploadSlot).getValue()
        dataFileName = upload.getSlot(
            'dataFileName', parentSlot = uploadSlot).getValue()

        if dataFileName:
            if not filename:
                uri = X.idUrl(id, 'download/%s/%s' % (
                        '/'.join(pathTuple), dataFileName))
                return redirect(uri)
            req.headers_out['Content-Disposition'] = \
                'inline; filename="%s"' % dataFileName
            # voodoo solution so that the right filename is used in MSIE
            #   (from <http://groups.google.com/groups?selm=
            #          pan.2003.12.11.14.18.31.315803%40ezglkhzglihzrhg.com>)
            req.headers_out['Cache-Control'] = ''
            req.headers_out['Pragma'] = ''

        req.headers_out['Content-Length'] = str(len(data))
        req.content_type = upload.getSlot(
            'dataType', parentSlot = uploadSlot).getValue()
        setHttpCookie()
        req.send_http_header()
        if req.method == 'HEAD':
            return OK
        req.write(data)
        return OK
    download.isPublicForWeb = 1

    def edit(self, id = ''):
        if id and not self.hasObject(id):
            return pageNotFound()

        if id:
            if not self.canModifyObject(id):
                return accessForbidden()
            object = self.getObject(id)
            rememberObject(id)
        else:
            if not self.canAddObject():
                return accessForbidden()
            object = self.newObject(None)

        if not object.id:
            object.fillWithDefaultValues()

        return self.editObject(object)
    edit.isPublicForWeb = 1

    def editObject(self, object):
        if not object.id:
            headerTitle = self.getTranslatedNewObjectNameCapitalized()
        else:
            headerTitle = _('Editing %s - %s') % (
                self.getTranslatedObjectNameCapitalized(), object.getLabel())

        if context.getVar('headerTitle'):
            headerTitle = context.getVar('headerTitle')

        context.push(_level = 'edit',
                     isCreateEditMode = not object.id,
                     layoutMode = 'edit')
        try:
            layout = X.array()
            leadIn = self.getEditLeadIn(object)
            if leadIn:
                layout += X.enclose(leadIn, _class = 'lead-in')
            if context.getVar('error'):
                layout += object.getErrorLayout()
            form = X.form(action = X.roleUrl(self.serverRole, 'submit'),
                          enctype = 'multipart/form-data', method = 'post')
            layout += form

            if context.getVar('nextUri'):
                form += X.input(name = 'nextUri', type = 'hidden',
                                value = context.getVar('nextUri'))

            slot = slots.Root(object)
            widget = slot.getWidget()
            form += widget.getModelPageBodyLayout(slot, fields = None)

            form += X.div(_class = 'buttons-bar')(self.getEditButtons(object))

            return writePageLayout(layout, headerTitle)
        finally:
            context.pull(_level = 'edit')

    def getEditButtons(self, object):
        array = X.array()
        if not object.id:
            array += X.buttonInForm('create', 'createButton')
        else:
            # The button Delete is also added here, so that the user can
            # delete the object he is editing without having to make it
            # valid, clic modify and then delete it.
            if self.canDeleteObject(object.id):
                array += X.buttonStandalone(
                        'delete', X.idUrl(object.id, 'confirmDelete'))
            array += X.buttonInForm('modify', 'modifyButton')
        return array

    def getEditLeadIn(self, object):
        return None
    
    def getObjectsLayout(self, partialObjects, objectIds,
                         slotNames = None):
        layout = None
        translationsProxy = getProxyForServerRole('translations')
        if len(partialObjects) > 0:
            if translationsProxy:
                labelsMultiCall = MultiCall()
                for objectId in objectIds:
                    partialObjects[objectId].getLabelTranslated(
                        context.getVar('readLanguages'),
                        multiCall = labelsMultiCall)

            layout = X.array()
            if slotNames:
                table = X.table(_class = 'objects-table')
                layout += table
                partialObject = partialObjects.values()[0]
                thead = X.thead()
                table += thead
                tr = X.tr()
                thead += tr
                tr += X.th()(X.nbsp)
                for slotName in slotNames:
                    slot = partialObject.getSlot(slotName)
                    tr += slot.getWidget().getHtmlColumnLabel(slot)
                tbody = X.tbody()
                table += tbody
                even = 0
            else:
                ul = X.ul()
                layout += ul
            if translationsProxy:
                lazyLabels = labelsMultiCall.call()
            else:
                lazyLabels = objectIds # fastest way to get a list of same len
            for objectId, lazyLabel in zip(objectIds, lazyLabels):
                if translationsProxy:
                    try:
                        label = lazyLabel()
                    except faults.UnknownStringDigest:
                        label = partialObjects[objectId].getLabel()
                else:
                    label = partialObjects[objectId].getLabel()
                partialObject = partialObjects[objectId]
                fields = {}
                if slotNames:
                    tr = X.tr(_class = ((even and 'even') or 'odd'))
                    even = not even
                    tbody += tr
                    tr += X.td()(
                        X.a(href = X.idUrl(partialObject.id))(label))
                    context.push(inForm = 0,
                                 readOnly = 0)
                    try:
                        for slotName in slotNames:
                            slot = partialObject.getSlot(slotName)
                            widget = slot.getWidget()
                            tr += widget.getHtmlColumnValue(slot, fields)
                    finally:
                        context.pull()
                else:
                    ul += X.li()(
                        X.a(href = X.idUrl(partialObject.id))(label))

        return layout

    def getObjectsSectionLayout(self, partialObjects, intertitle,
                                slotNames = None):
        layout = None
        translationsProxy = getProxyForServerRole('translations')
        if len(partialObjects) > 0:
            if translationsProxy:
                labelsMultiCall = MultiCall()
                for partialObject in partialObjects:
                    partialObject.getLabelTranslated(
                        context.getVar('readLanguages'),
                        multiCall = labelsMultiCall)

            layout = X.array(X.h2()(intertitle))
            table = X.table(_class = 'objects-table')
            layout += table
            partialObject = partialObjects[0]
            if slotNames:
                thead = X.thead()
                table += thead
                tr = X.tr()
                thead += tr
                tr += X.th()(X.nbsp)
                for slotName in slotNames:
                    slot = partialObject.getSlot(slotName)
                    tr += slot.getWidget().getHtmlColumnLabel(slot)
                tbody = X.tbody()
                table += tbody
            else:
                tbody = table
            even = 0
            if translationsProxy:
                lazyLabels = labelsMultiCall.call()
            else:
                lazyLabels = partialObjects # fastest way to get a list of same len
            for partialObject, lazyLabel in zip(partialObjects, lazyLabels):
                if translationsProxy:
                    label = lazyLabel()
                else:
                    label = partialObject.getLabel()
                fields = {}
                tr = X.tr(_class = ((even and 'even') or 'odd'))
                even = not even
                tbody += tr
                tr += X.td()(X.a(href = X.idUrl(partialObject.id))(label))
                if slotNames:
                    context.push(inForm = 0,
                                 readOnly = 0)
                    try:
                        for slotName in slotNames:
                            slot = partialObject.getSlot(slotName)
                            widget = slot.getWidget()
                            tr += widget.getHtmlColumnValue(slot, fields)
                    finally:
                        context.pull()
            layout += X.br()
        return layout

    def getSortedIds(self, objects):
        translationsProxy = getProxyForServerRole('translations')
        labels = {}
        ids = objects.keys()
        if not translationsProxy:
            for k, v in objects.items():
                labels[k] = v.getLabel()
        else:
            objectsByDispatcherId = {}
            for id, object in objects.items():
                dispatcherId = commonTools.extractDispatcherId(id)
                if not objectsByDispatcherId.has_key(dispatcherId):
                    objectsByDispatcherId[dispatcherId] = {}
                objectsByDispatcherId[dispatcherId][id] = object
            readLanguages = context.getVar('readLanguages')
            for sameDispatcherObjects in objectsByDispatcherId.values():
                labelsMultiCall = MultiCall()
                sameDispatcherIds = []
                for id, object in sameDispatcherObjects.items():
                    sameDispatcherIds.append(id)
                    object.getLabelTranslated(
                            readLanguages, multiCall = labelsMultiCall)
                for id, lazyLabel in zip(sameDispatcherIds,
                                         labelsMultiCall.call()):
                    try:
                        labels[id] = lazyLabel()
                    except faults.UnknownStringDigest:
                        labels[id] = objects[id].getLabel()
        ids.sort(lambda id1, id2, labels = labels:
                 locale.strcoll(labels[id1], labels[id2]))
        return ids

    def getViewAboveButtonsBarLayout(self, object, fields):
        return None

    def getViewActionButtonsBarLayout(self, object, fields):
        layout = X.array()
        if self.canDeleteObject(object.id):
            layout += X.buttonStandalone(
                    'delete', X.idUrl(object.id, 'confirmDelete'))
        if self.canCloneObject(object.id):
            layout += X.buttonStandalone(
                    'clone', X.idUrl(object.id, 'clone'))
        if self.canModifyObject(object.id):
            layout += X.buttonStandalone('edit', X.idUrl(object.id, 'edit'))
        return layout

    def getViewAllActionButtonsBarLayout(self):
        layout = X.array()
        if self.canAddObject():
            layout += X.buttonStandalone('new', X.actionUrl('edit'))
        return layout

    def getViewAllButtonsBarLayout(self):
        layout = X.div(_class = 'buttons-bar')
        layout += X.span(_class = 'navigation-buttons-bar')(
            self.getViewAllNavigationButtonsBarLayout())
        layout += X.span(_class = 'other-action-buttons-bar')(
            self.getViewAllOtherActionButtonsBarLayout())
        layout += X.span(_class = 'action-buttons-bar')(
            self.getViewAllActionButtonsBarLayout())
        return layout

    def getViewAllLeadIn(self):
        return None
    
    def getViewAllNavigationButtonsBarLayout(self):
        layout = X.array()
        translationsProxy = getProxyForServerRole('translations')
        if translationsProxy and \
                translationsProxy.getTranslatorLocalizationKeys():
            layout += X.buttonStandalone(_('Translations Status'),
                    X.roleUrl('translations', 'status/%s' % self.serverRole))
        userToken = context.getVar('userToken')
        if self.canModifyAdmin() and userToken:
            layout += X.buttonStandalone('settings', X.actionUrl('admin'))
        return layout

    def getViewAllOtherActionButtonsBarLayout(self):
        return None

    def getViewBelowButtonsBarLayout(self, object, fields):
        if hasattr(self, 'addComment') and \
                'comments' in context.getVar('knownRoles'):
            return getWebForServerRole(
                    'comments').getBothCommentsLayout(object.id)
        return None

    def getViewButtonsBarLayout(self, object, fields):
        layout = X.div(_class = 'buttons-bar')
        buttons = self.getViewNavigationButtonsBarLayout(object, fields)
        if buttons:
            layout += X.span(_class = 'navigation-buttons-bar')(buttons)
        buttons = self.getViewOtherActionButtonsBarLayout(object, fields)
        if buttons:
            layout += X.span(_class = 'other-action-buttons-bar')(buttons)
        buttons = self.getViewActionButtonsBarLayout(object, fields)
        if buttons:
            layout += X.span(_class = 'action-buttons-bar')(buttons)
        if not layout.children:
            return None
        return layout

    def getViewLeadIn(self, object):
        return None
    
    def getViewNavigationButtonsBarLayout(self, object, fields):
        layout = X.array()
        userToken = context.getVar('userToken', default = '')
        if userToken:
            layout += X.buttonStandalone('view-list', X.actionUrl())
            layout += X.buttonStandalone(
                'history', X.idUrl(object.id, 'history'))
        return layout

    def getViewOtherActionButtonsBarLayout(self, object, fields):
        return None

    def history(self, id):
        try:
            revisions = self.getRevisionsInfos(id)
        except: # error 506; check
            return pageNotFound()
        revisions.reverse()
        object = self.getObject(id)
        label = object.getLabelTranslated(context.getVar('readLanguages'))
        pageTitle = '%s - %s - %s' % (_('History'),
                self.getTranslatedObjectNameCapitalized(), label)
        if not revisions:
            layout = X.p(_("""No revision informations are available for this object."""))
            return writePageLayout(layout, pageTitle)
        table = X.table()
        table += X.tr(
                    X.th(_('Date')),
                    X.th(_('User')),
                    X.th(''))
        for revision in revisions:
            revisionTime = time.strftime('%Y-%m-%d %H:%M:%S',
                                         time.localtime(revision['time']))
            if revision['userId']:
                userLabel = X.objectHypertextLabel(revision['userId'])
            else:
                userLabel = _('Unknown')
            td = X.td(X.buttonStandalone('version',
                            X.idUrl(id,
                                'version/%d' % revision['version'])))
            if revision is not revisions[-1]:
                td += X.buttonStandalone('diff',
                            X.idUrl(id, 'diff/%d' % revision['version']))
            table += X.tr(
                    X.td(revisionTime),
                    X.td(userLabel),
                    td)
        return writePageLayout(table, pageTitle)
    history.isPublicForWeb = 1

    def id(self, id):
        if not self.hasObject(id):
            return pageNotFound()
        if not self.canGetObject(id):
            return accessForbidden()
        object = self.getObject(id)
        return writePageLayout(
                X.p(id),
                _('Id for "%s"') % object.getLabel())
    id.isPublicForWeb = 1

    image = download
    image.isPublicForWeb = 1

    def imageEdit(self, dataToken):
        sessionsProxy = getProxyForServerRole('sessions')
        data = sessionsProxy.getTemporaryData(dataToken)
        if not data:
            return pageNotFound()
        req = context.getVar('req')
        req.headers_out['Content-length'] = str(len(data['data']))
        req.content_type = data['type']
        setHttpCookie()
        req.send_http_header()
        if req.method == 'HEAD':
            return OK
        req.write(data['data'])
        return OK
    imageEdit.isPublicForWeb = 1
        
    def makeVersionCurrent(self, id, versionNumber):
        oldObject = self.getObjectWithVersion(id, int(versionNumber))
        currentObject = self.getObject(id)
        oldObject.version = currentObject.version
        self.modifyObject(oldObject)
        return redirect(X.idUrl(id))
    makeVersionCurrent.isPublicForWeb = 1

    def rss(self):
        lastObjects = self.getLastObjects(20, None, None, None)
        req = context.getVar('req')
        req.content_type = 'application/rss+xml'
        req.send_http_header()

        if req.caching:
            req.openCachePage()
        
        req.write("""\
<?xml version="1.0" encoding="iso-8859-15"?>
<rss version="2.0" 
     xmlns:dc="http://purl.org/dc/elements/1.1/"
     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
     xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>%(websiteTitle)s (%(objectsName)s)</title>
    <link>http://%(websiteUrl)s</link>
""" % {
                'websiteTitle': context.getVar('virtualHost').getLabel(),
                'websiteUrl': context.getVar('virtualHost').hostName,
                'objectsName': self.getTranslatedObjectsNameCapitalized(),
                })
        # Workaround Python Emacs mode bug: """

        for object in lastObjects:
            req.write("""\
    <item>
      <title>%s</title>
      <dc:language>%s</dc:language>
      <link>%s</link>
    </item>
""" % (
                    xmlEncode(object.getLabel()),
                    object.language,
                    X.idUrl(object.id).getAsAbsoluteUrl()))
    
        req.write("""\
  </channel>
</rss>
""")

        if req.caching:
            req.closeCachePage()
    
        return OK
    rss.isPublicForWeb = 1

    def search(self, slotNames = None, **keywords):
        if 'terms' in keywords.keys():
            terms = keywords['terms']
            del keywords['terms']
            terms = terms.split(' ')
            object = self.newObject(keywords)
            allSlotNames = object.getSlotNames()
            for slotName in ['serverRole', 'thingName', 'thingCategory']:
                if slotName in allSlotNames:
                    allSlotNames.remove(slotName)
            for slotName in allSlotNames:
                kind = object.getSlot(slotName).getKind()
                if kind.__class__.__name__ == 'String':
                    keywords['%s_PT' % slotName] = terms
        for key in keywords.keys():
            if not '_' in key:
                del keywords[key]
        if type(slotNames) is not types.ListType:
            slotNames = None
        if not self.canGetObjects():
            return accessForbidden()
        if slotNames:
            objects = self.searchPartialObjects(requiredSlotNames = slotNames,
                                                **keywords)
        else:
            objects = self.searchObjects(**keywords)
        layout = X.array()
        ids = self.getSortedIds(objects)
        layout += self.getObjectsLayout(objects, ids, slotNames = slotNames)
        layout += self.getViewAllButtonsBarLayout()
        title = self.getTranslatedObjectsNameCapitalized() + \
                    ' ' + _('Search Results')
        return writePageLayout(layout, title)
    search.isPublicForWeb = 1

    def submit(self, id = '', **keywords):
        if keywords is None:
            keywords = {}

        if id and not self.hasObject(id):
            return pageNotFound()

        keywords['id'] = id
        if isButtonSelected('applyButton', keywords):
            context.setVar('again', 1)
            context.setVar('hideErrors', 1)
        
        if id:
            object = self.getObject(id)
        else:
            object = self.newObject(keywords)
        object.submitFields(keywords)
        
        if context.getVar('again'):
            return self.editObject(object = object)

        try:
            if not id:
                result = self.submitAddObject(object)
            else:
                result = self.submitModifyObject(object)
        except:
            if context.getVar('debug'):
                raise
            return accessForbidden() # TODO: return failure ?
        if result:
            return result

        if context.getVar('nextUri'):
            return redirect(context.getVar('nextUri'))
        return redirect(X.idUrl(object.id))
    submit.isPublicForWeb = 1

    def submitAddObject(self, object):
        try:
            object.id = self.addObject(object)
        except faults.UserAccessDenied:
            if context.getVar('debug'):
                raise
            return accessForbidden()

    def submitModifyObject(self, object):
        try:
            self.modifyPartialObject(object, object.getSlotToModifyNames())
        except faults.WrongVersion:
            context.setVar('again', 1)
            context.setVar('error', 1)
            object.setError('version', 1)
            return self.editObject(object)
        except faults.UserAccessDenied:
            return accessForbidden()

    def thumbnail(self, id, path, width = '', height = ''):
        localId = commonTools.extractLocalId(id)
        if localId == '__admin__':
            if not self.canGetAdmin():
                return accessForbidden()
            object = self.getAdmin()
        else:
            if not self.hasObject(id):
                return pageNotFound()
            if not self.canGetObject(id):
                return accessForbidden()
            object = self.getObject(id)
            rememberObject(id)
        try:
            width = int(width)
        except ValueError:
            width = 128
        try:
            height = int(height)
        except ValueError:
            height = 128

        uploadSlot = object.getSlotByPath(path)
        upload = uploadSlot.getValue()
        data = upload.getSlot('data', parentSlot = uploadSlot).getValue()

        if data and upload.isType('image'):
            import cStringIO
            # From Python Imaging Library.
            try:
                import Image as PILImage
            except ImportError:
                PILImage = None
            
            if PILImage:
                imageFile = cStringIO.StringIO(data)
                imageObject = PILImage.open(imageFile)
                imageObject.thumbnail((width, height))
                thumbnailFile = cStringIO.StringIO()
                imageObject.save(thumbnailFile, imageObject.format)
                data = thumbnailFile.getvalue()
                width, height = imageObject.size

        req = context.getVar('req')
        dataFileName = upload.getSlot(
            'dataFileName', parentSlot = uploadSlot).getValue()
        if dataFileName:
            req.headers_out['Content-disposition'] = \
                'attachment; filename="%s"' % dataFileName
        req.headers_out['Content-length'] = str(len(data))
        req.content_type = upload.getSlot(
            'dataType', parentSlot = uploadSlot).getValue()
        setHttpCookie()
        req.send_http_header()
        if req.method == 'HEAD':
            return OK
        req.write(data)
        return OK
    thumbnail.isPublicForWeb = 1

    def translation(self, id, language):
        translationsProxy = getProxyForServerRole('translations')
        if not translationsProxy:
            return pageNotFound

        object = self.getObject(id)
        title = _('Translation')
        title += ' (%s -> %s)' % (
                _(translation.languageLabels[object.language]),
                _(translation.languageLabels[language]))

        context.push(_level = 'translation', layoutMode = 'edit')
        try:
            form = X.form(
                    action = X.idUrl(id, 'translationSubmit/%s' % language),
                    enctype= 'multipart/form-data', method = 'post')
            
            for slotName in object.getLayoutSlotNames(None):
                slot = object.getSlot(slotName)
                kind = slot.getKind()
                widget = slot.getWidget()
                if not kind.isTranslatable:
                    continue
                if not slot.getValue():
                    continue
            
                stringTranslated, destinationLanguage, stringState = \
                        translationsProxy.getTranslationInfos(
                            slot.getValue() or '', object.id, slot.getPath(),
                            object.getLanguage(), [language])
                if stringState not in ('translated', 'fuzzy', 'obsolete'):
                    stringTranslated = ''

                row = X.div(_class = 'field')
                form += row
                row += X.span(_class = 'label')(
                        _(widget.getModelLabel(slot)) + ' (%s):' % object.getLanguage())
                kind.isTranslatable = 0
                widget.viewInTextArea = 1 # FIXME: not clean
                cell = X.enclose(widget.getHtmlViewValue(slot, None), _class = 'cell')
                del(widget.viewInTextArea)
                kind.isTranslatable = 1
                row += cell
                
                row = X.div(_class = 'field')
                form += row
                row += X.span(_class = 'label')(
                        _(widget.getModelLabel(slot)) + ' (%s):' % language)
                widget.preview = 0 # FIXME: not clean
                class NotTotallyDummy(slots.Dummy):
                    def getFieldName(self):
                        return self.fieldName
                dummySlot = NotTotallyDummy(kind)
                dummySlot.fieldName = slot.getFieldName()
                cell = X.enclose(widget.getHtmlFormValue(dummySlot, None),
                        _class = 'cell')
                del(widget.preview)
                row += cell

            buttonsBar = X.div(_class = 'buttons-bar')
            form += buttonsBar
            actionButtonsBar = X.span(_class = 'action-buttons-bar')
            buttonsBar += actionButtonsBar
            actionButtonsBar += X.buttonInForm('translate', 'translateButton')
                
            return writePageLayout(form, title)
        finally:
            context.pull(_level = 'translation')
    translation.isPublicForWeb = 1

    def translationSubmit(self, id, language, **keywords):
        translationsProxy = getProxyForServerRole('translations')
        if not translationsProxy:
            return pageNotFound

        object = self.getObject(id)

        transObject = self.newObject()
        transObject.submitFields(keywords)

        for slotName in object.getLayoutSlotNames(None):
            slot = object.getSlot(slotName)
            kind = slot.getKind()
            widget = slot.getWidget()
            if not kind.isTranslatable:
                continue
            if not slot.getValue():
                continue
            transSlot = transObject.getSlot(slotName)
            if not transSlot.getValue():
                continue
        
            localization = commonTools.newThing('object',
                    'translations.Localization')
            localization.sourceStringDigest = \
                    md5.new(slot.getValue().replace('\r\n', '\n')).hexdigest()
            localization.destinationLanguage = language
            localization.destinationString = transSlot.getValue()
            localization.isFuzzy = 0
            localization.sourceString = slot.getValue()
            localization.sourceLanguage = object.language
            localization.isTranslatable = 1
            try:
                translationsProxy.modifyLocalization(localization)
            except Fault, fault:
                return accessForbidden()

        return redirect(X.idUrl(object.id))
    translationSubmit.isPublicForWeb = 1

    def use(self, id):
        return self.view(id)
    use.isPublicForWeb = 1

    def version(self, id, versionNumber):
        #return self.viewObject(object)
        try:
            object = self.getObjectWithVersion(id, int(versionNumber))
        except (ValueError, faults.UnknownObjectVersion):
            return pageNotFound()
        slot = slots.Root(object)
        widget = slot.getWidget()
        layout = widget.getModelPageBodyLayout(slot, None)
        buttonsBar = X.div(_class = 'buttons-bar')
        layout += buttonsBar
        buttonsBar += X.buttonStandalone('history', X.idUrl(id, 'history'))
        buttonsBar += X.buttonStandalone(_('Make this version current'),
                X.idUrl(id, 'confirmMakeVersionCurrent/%s' % versionNumber))
        return writePageLayout(layout,
                _('%s - Version %d') % (object.getLabel(), object.version))
    version.isPublicForWeb = 1

    def view(self, id):
        if not self.hasObject(id):
            return pageNotFound()
        if not self.canGetObject(id):
            return accessForbidden()
        object = self.getObject(id)
        rememberObject(id)
        return self.viewObject(object)
    view.isPublicForWeb = 1

    def viewObject(self, object):
        label = object.getLabelTranslated(context.getVar('readLanguages'))

        layout = X.array()
        leadIn = self.getViewLeadIn(object) 
        if leadIn:
            layout += X.enclose(leadIn, _class = 'lead-in')
        slot = slots.Root(object)
        widget = slot.getWidget()
        layout += widget.getModelPageBodyLayout(slot, None)
        pageTitle = context.getVar('pageTitle', default = None)
        layout += self.getViewAboveButtonsBarLayout(object, None)
        layout += self.getViewButtonsBarLayout(object, None)
        layout += self.getViewBelowButtonsBarLayout(object, None)

        context.push(
            currentObject = WebAPI.GlasnostObject(object = object),
            )
        if not pageTitle:
            pageTitle = '%s - %s' % (
                    self.getTranslatedObjectNameCapitalized(), label)
        layout = writePageLayout(layout, pageTitle)
        context.pull()
        return layout

    def viewAll(self, slotNames = None):
        if type(slotNames) is not types.ListType:
            slotNames = None
        
        context.push(_level = 'viewAll',
                     defaultDispatcherId = context.getVar('dispatcherId'))
        try:
            if not self.canGetObjects():
                return accessForbidden()
            objects = self.getObjects()
            layout = X.array()
            leadIn = self.getViewAllLeadIn()
            if leadIn:
                layout += X.enclose(leadIn, _class = 'lead-in')
            ids = self.getSortedIds(objects)
            layout += self.getObjectsLayout(
                    objects, ids, slotNames = slotNames)
            layout += self.getViewAllButtonsBarLayout()
        finally:
            context.pull(_level = 'viewAll')
        return writePageLayout(layout,
                    self.getTranslatedObjectsNameCapitalized())
    viewAll.isPublicForWeb = 1

    def viewAllPy(self, **keywords):
        context.push(_level = 'viewAllPy',
                     defaultDispatcherId = context.getVar('dispatcherId'))
        try:
            keywords['objects'] = [WebAPI.GlasnostObject(id)
                                   for id in self.getObjectIds()]
        finally:
            context.pull(_level = 'viewAllPy')
        req = context.getVar('req')
        keywords['req'] = req
        stdO = sys.stdout
        sys.stdout = req
        try:
            execfile(context.getVar('PyFile'), keywords)
            sys.stdout = stdO
        except:
            sys.stdout = stdO
            raise apache.SERVER_RETURN, HTTP_INTERNAL_SERVER_ERROR
        return OK

    def viewAllTal(self, **keywords):
        context.push(_level = 'viewAllTal',
                     defaultDispatcherId = context.getVar('dispatcherId'))
        try:
            keywords['objects'] = [WebAPI.GlasnostObject(id)
                                   for id in self.getObjectIds()]
        finally:
            context.pull(_level = 'viewAllTal')
        file = context.getVar('TALFile')
        return processTALFile(
            '',
            xtal = context.getVar('TALFileIsXTAL'),
            file = file,
            **keywords)

    def viewTal(self, id, **keywords):
        if not self.hasObject(id):
            return pageNotFound()
        if not self.canGetObject(id):
            return accessForbidden()
        file = context.getVar('TALFile')
        keywords['currentObject'] = WebAPI.GlasnostObject(id)
        rememberObject(id)
        return processTALFile(
            '',
            xtal = context.getVar('TALFileIsXTAL'),
            file = file,
            **keywords)
        
    def viewPy(self, id, **keywords):
        req = context.getVar('req')
        keywords['currentObject'] = WebAPI.GlasnostObject(id)
        keywords['req'] = context.req
        for k, v in getTemplateVars().items():
            keywords[k] = v

        rememberObject(id)
        stdO = sys.stdout
        sys.stdout = req
        try:
            execfile(context.getVar('PyFile'), keywords)
            sys.stdout = stdO
        except:
            sys.stdout = stdO
            raise apache.SERVER_RETURN, HTTP_INTERNAL_SERVER_ERROR
        return OK



class CommentableObjectMixin(things.ThingMixin):
    def addComment(self, id):
        if not self.hasObject(id):
            return pageNotFound()
        webRole = getWebForServerRole('comments')
        if not webRole:
            return pageNotFound()
        object = webRole.newObject(None)
        object.fillWithDefaultValues()
        return self.addCommentObject(id, object)
    addComment.isPublicForWeb = 1

    def addCommentObject(self, id, object):
        webRole = getWebForServerRole('comments')
        object.parentId = id
        context.push(_level = 'edit', layoutMode = 'edit')
        try:
            layout = X.array()
            leadIn = webRole.getEditLeadIn(object)
            if leadIn:
                layout += X.enclose(leadIn, _class = 'lead-in')
            if context.getVar('error'):
                layout += object.getErrorLayout()
            form = X.form(action = X.idUrl(id, 'addCommentSubmit'),
                          enctype = 'multipart/form-data', method = 'post')
            layout += form
            slot = slots.Root(object)
            widget = slot.getWidget()
            form += widget.getModelPageBodyLayout(slot, None)

            buttonsBar = X.div(_class = 'buttons-bar')(
                    X.buttonInForm('add', 'addButton'))
            form += buttonsBar

            return writePageLayout(layout,
                        webRole.getTranslatedNewObjectNameCapitalized())
        finally:
            context.pull(_level = 'edit')

    def addCommentSubmit(self, id, **keywords):
        if keywords is None:
            keywords = {}
        webRole = getWebForServerRole('comments')
        object = webRole.newObject(keywords)
        object.submitFields(keywords)
        object.parentId = id
        if context.getVar('again'):
            return self.addCommentObject(id, object)

        try:
            webRole.submitAddObject(object)
        except:
            if context.getVar('debug'):
                raise
            return accessForbidden()
        return redirect(X.idUrl(id))
    addCommentSubmit.isPublicForWeb = 1

