'''This package contains mixin classes that are mixed in with generated classes:
   - mixins/BaseMixin is mixed in with standard Zope classes;
   - mixins/ToolMixin is mixed in with the generated application Tool class.'''

# ------------------------------------------------------------------------------
import os, os.path, re, sys, types, urllib, cgi, time

from appy.px import Px
import appy.gen as gen
from appy import Object
from appy.gen.utils import *
from appy.gen.layout import Table
from appy.gen.navigate import Batch
from appy.shared.diff import HtmlDiff
from appy.fields.ref import Initiator
from appy.gen.validate import Validator
from appy.fields.search import Criteria
from appy.shared import utils as sutils
from appy.shared.dav import JsonDecoder
from appy.shared.data import rtlLanguages
from appy.fields.workflow import UiTransition
from appy.fields.multilingual import Multilingual
from appy.shared.xml_parser import XmlMarshaller, XmlUnmarshaller
from appy.gen.descriptors import WorkflowDescriptor, ClassDescriptor

# Zope imports -----------------------------------------------------------------
try:
    from DateTime import DateTime
    from Acquisition import aq_inner, aq_parent, aq_base
except ImportError:
    pass # We must be at generation time

# ------------------------------------------------------------------------------
NUMBERED_ID = re.compile('.+\d{4}$')
GET_CONTAINER_ERROR = "Container can't be retrieved that way."

# ------------------------------------------------------------------------------
class BaseMixin:
    '''Every Zope class generated by appy.gen inherits from this class or a
       subclass of it.'''
    _appy_meta_type = 'Class'

    def _getRequestObject(self):
        '''Gets the Zope Request object. It may not be present, ie if we are at
           Zope startup.'''
        res = getattr(self, 'REQUEST', None)
        if res != None: return res
        return self.getProductConfig().fakeRequest

    def _checkLocalRoles(self):
        '''Local roles should be stored in a PersistentMapping and not a dict.
           For old Appy databases, dicts are still in use.'''
        localRoles = self.__ac_local_roles__
        if isinstance(localRoles, dict):
            from persistent.mapping import PersistentMapping
            self.__ac_local_roles__ = PersistentMapping(localRoles)

    def get_o(self):
        '''In some cases, we want the Zope object, we don't know if the current
           object is a Zope or Appy object. By defining this property,
           "someObject.o" produces always the Zope object, be someObject an Appy
           or Zope object.'''
        return self
    o = property(get_o)

    def getInitiator(self, search=False):
        '''Gets (from the request) information about a potential initiator'''
        req = getattr(self, 'REQUEST', None)
        if req is None:
            # Try to get a fake request via the Appy wrapper
            req = self.appy().request
        if not req: return
        if not search:
            # Try to get a "classic" initiator, from the "[o]nav" key
            nav = req.get('nav') or req.get('onav')
            if not nav or (nav == 'no'): return
            fieldType, info = nav.split('.', 1)
            field = eval('gen.%s' % fieldType.capitalize())
            if field.initiator:
                r = field.initiator(self.getTool().appy(), req, info)
                if r.isComplete():
                    return r
        else:
            # Try to get the initiator from the current search = a Ref field
            # referenced in key "_ref" or from search criteria.
            tool = self.getTool()
            ref = req.get('_ref') or req.get('ref')
            if not ref:
                criteria = Criteria.readFromRequest(tool)
                if criteria and ('_ref' in criteria): ref = criteria['_ref']
            if ref:
                return tool.getObject(ref.split(':')[0], appy=True)

    def createOrUpdate(self, created, validator=None, initiator=None):
        '''This method creates (if p_created is True) or updates an object.
           In the case of an object creation from the web (p_created is True
           and a REQUEST object is present), p_self is a temporary object
           created in /temp_folder, and this method moves it at its "final"
           place. In the case of an update, this method simply updates fields
           of p_self.'''
        rq = getattr(self, 'REQUEST', None)
        obj = self
        tool = self.getTool()
        if created and rq:
            # Create the final object and put it at the right place. A method
            # named "generateUid" may exist on the corresponding class, for
            # producing a database ID for the object. If no such method is
            # found, the standard Appy method is used.
            id = None
            klass = tool.getAppyClass(obj.portal_type)
            if hasattr(klass, 'generateUid'):
                id = klass.generateUid(obj.appy())
            if not id:
                id = tool.generateUid(obj.portal_type)
            if not initiator or not initiator.asFolder:
                folder = tool.getPath('/%s' % tool.getRootFolder(klass))
            else:
                folder = initiator.obj.o.getCreateFolder()
                # Check that object creation from the initiator is allowed
                initiator.checkAllowed()
            obj = createObject(folder, id, obj.portal_type, tool.getAppName())
        # Get the fields impacted by the object creation/update
        fields = validator and validator.fields or None
        # Remember the previous values of fields, for potential historization
        previousData = None
        if not created and fields:
            previousData = obj.rememberPreviousData(fields)
        # Perform the change on the object
        if fields:
            # Store in the database the new value coming from the form
            for field in fields:
                field.store(obj, getattr(validator.values, field.name))
        if previousData:
            # Keep in history potential changes on historized fields
            obj.historizeData(previousData)

        # Call the custom "onEditEarly" if available. This method is called
        # *before* potentially linking the object to its initiator. Moreover,
        # it receives the temp object as unique arg.
        appyObject = obj.appy()
        if created and hasattr(appyObject, 'onEditEarly'):
            appyObject.onEditEarly(self.appy())

        # Manage the relationship between the initiator and the new object
        if created and initiator: initiator.manage(appyObject)
        # Stop here if the object has already been deleted
        deleted = initiator and \
                  not getattr(obj.getParentNode().aq_base, obj.id, None)
        if deleted: return obj, deleted, None

        # Call the custom "onEdit" if available
        msg = None # The message to display to the user. It can be set by onEdit
        if hasattr(appyObject, 'onEdit'): msg = appyObject.onEdit(created)

        # Update last modification date
        if not created: obj.modified = DateTime()
        # Unlock the currently saved page on the object
        if rq: self.removeLock(rq['page'])
        obj.reindex()
        return obj, deleted, msg

    def updateField(self, name, value):
        '''Updates a single field p_name with new p_value'''
        field = self.getAppyType(name)
        # Remember previous value if the field is historized
        previousData = self.rememberPreviousData(field)
        # Store the new value into the database
        field.store(self, value)
        # Update the object history when relevant
        if previousData: self.historizeData(previousData)
        # Update last modification date
        self.modified = DateTime()

    def delete(self, historize=False, executeMethods=True, unindex=True):
        '''This method is self's suicide. When unlinking this object from
           others, if Ref fields are historized and p_historize is True, this
           deletion is noted in tied object's histories.'''
        # Call a custom "onDelete" if it exists
        appyObj = self.appy()
        title = self.getShownValue()
        if executeMethods and hasattr(appyObj, 'onDelete'): appyObj.onDelete()
        # Any people referencing me must forget me now
        for field in self.getAllAppyTypes():
            if field.type != 'Ref': continue
            for obj in field.getValue(self):
                back = field.back
                if not back: continue
                back.unlinkObject(obj, appyObj, back=True)
                # Historize this unlinking when relevant
                if historize and field.back.getAttribute(obj, 'historized'):
                    cName = self.getTool().getPortalType(appyObj.klass)
                    obj.o.addHistoryEvent('_datadelete_',
                             comments='%s: %s' % (self.translate(cName), title))
        # Uncatalog the object when appropriate
        if unindex: self.reindex(unindex=True)
        # Delete the wrapper in the request, if the request object exists
        try:
            del self.REQUEST.wrappers[self.id]
        except AttributeError:
            pass
        # Delete the filesystem folder corresponding to this object
        folder = os.path.join(*self.getFsFolder())
        if os.path.exists(folder):
            # Instead of deleting it, try to move it to the OS temp folder
            name = os.path.basename(folder)
            tempFolder = os.path.join(sutils.getOsTempFolder(), name)
            if os.path.exists(tempFolder):
                sutils.FolderDeleter.delete(tempFolder)
            try:
                os.rename(folder, tempFolder)
            except OSError:
                # Renaming the folder may crash if the source and target devices
                # are different.
                sutils.FolderDeleter.delete(folder)
            # Remove folder's parent if empty
            sutils.FolderDeleter.deleteEmpty(os.path.dirname(folder))
        # Delete the object
        self.getParentNode().manage_delObjects([self.id])

    def onDelete(self):
        '''Called when an object deletion is triggered from the ui'''
        rq = self.REQUEST
        tool = self.getTool()
        id = rq.get('uid')
        if not id:
            self.log('Wrong onDelete request (no object ID)', type='error')
            return tool.translate('action_ko')
        obj = tool.getObject(id)
        obj.delete(historize=True)
        msg = obj.translate('action_done')
        # If we are called from an Ajax request, simply return msg
        if hasattr(rq, 'pxContext') and rq.pxContext['ajax']: return msg
        referer = obj.getReferer()
        if (obj.getUrl(referer, mode='raw') == obj.getUrl(mode='raw')) or \
           referer.endswith('/do'):
            # We were consulting the object that has been deleted, or the
            # referer points to an action, so we can't go back to it. In these
            # cases, go back to the main page.
            urlBack = tool.getSiteUrl()
        else:
            urlBack = obj.getUrl(referer)
        obj.say(msg)
        obj.goto(urlBack)

    def onLink(self):
        '''Called when object (un)linking is triggered from the ui.'''
        rq = self.REQUEST
        tool = self.getTool()
        sourceObject = tool.getObject(rq['sourceId'])
        field = sourceObject.getAppyType(rq['fieldName'])
        return field.onUiRequest(sourceObject, rq)

    def onCreate(self):
        '''This method is called when a user wants to create a root object or an
           object through a reference field. A temporary object is created in
           /temp_folder and the edit page to it is returned.'''
        rq = self.REQUEST
        className = rq.get('className')
        if not className:
            self.log('onCreate called without classname.', type='error')
            self.raiseUnauthorized()
        # Create the params to add to the URL we will redirect the user to
        # create the object.
        urlParams = {'mode':'edit', 'page':'main', 'nav':'no',
                     'inPopup':rq.get('popup') == '1'}
        initiator = self.getInitiator()
        if initiator:
            # Transmit initiator information in the next request. This
            # information is stored in the "nav" request key.
            urlParams['nav'] = rq['nav']
            # Transmit special key "_get_" if present
            g = rq.get('_get_')
            if g: urlParams['_get_'] = g
            # Is the creation of the new object via the initiator allowed ?
            initiator.checkAllowed()
            # Let the initiator complete URL parameters when relevant
            initiator.updateParameters(urlParams)
        # Create a temp object in /temp_folder
        tool = self.getTool()
        id = tool.generateUid(className)
        appName = tool.getAppName()
        obj = createObject(tool.getPath('/temp_folder'), id, className, appName)
        # Populate p_obj fields with a template object if present
        templateId = rq.get('template')
        if templateId: urlParams['template'] = templateId
        # Call method "onCreate" if available. This method typically sets local
        # roles to the newly created object. Note that once the final object
        # will be created, local roles set here will need to be set again on the
        # final object, that is a distinct from this temp object.
        appyObject = obj.appy()
        if hasattr(appyObject, 'onCreate'): appyObject.onCreate()
        # Go to obj's "edit" page
        return self.goto(obj.getUrl(**urlParams))

    def getParentNode(self):
        '''Appy patch for this Zope method'''
        res = aq_parent(aq_inner(self))
        if res == self: # The patch: get *really* the parent
            res = res.getParentNode()
        return res

    def getPhysicalPath(self):
        '''Get the physical path of the object. Patched by Appy because, for
           some unknown reason, the Zope version (in OFS/Traversable.py)
           sometimes counts a parent several times in the path.'''
        res = (self.getId(),)
        parent = self.getParentNode()
        if parent:
            res = parent.getPhysicalPath() + res
        return res

    def getDbFolder(self):
        '''Gets the folder, on the filesystem, where the database (Data.fs and
           sub-folders) lies.'''
        return os.path.dirname(self.getTool().getApp()._p_jar.db().getName())

    def getFsFolder(self, create=False):
        '''Gets the folder where binary files tied to this object will be stored
           on the filesystem. If p_create is True and the folder does not exist,
           it is created (together with potentially missing parent folders).
           This folder is returned as a tuple (s_baseDbFolder, s_subPath).'''
        id = self.id
        # Get the root folder where Data.fs lies
        dbFolder = self.getDbFolder()
        # Build the list of path elements within this db folder
        res = []
        i = 2
        parts = self.getPhysicalPath()
        while i < len(parts):
            part = parts[i]
            # Add the 4-digits preliminary folder when relevant
            if (i == 2) and NUMBERED_ID.match(part):
                res.append(part[-4:])
            res.append(part)
            # We are done if the part corresponds to the object id
            if part == id: break
            i += 1
        # The "config" part must be in the result
        if parts[1] == 'config': res.insert(0, 'config')
        res = os.sep.join(res)
        if create:
            fullPath = os.path.join(dbFolder, res)
            if not os.path.exists(fullPath): os.makedirs(fullPath)
        return dbFolder, res

    def view(self):
        '''Returns the view PX'''
        obj = self.appy()
        self.REQUEST.set('layoutType', 'view')
        return obj.pxView({'obj': obj, 'tool': obj.tool})

    def edit(self):
        '''Returns the edit PX'''
        obj = self.appy()
        return obj.pxEdit({'obj': obj, 'tool': obj.tool})

    def ajax(self):
        '''Called via an Ajax request to render some PX whose name is in the
           request.'''
        obj = self.appy()
        return obj.pxAjax({'obj': obj, 'tool': obj.tool})

    def field(self):
        '''Returns the field PX'''
        obj = self.appy()
        return obj.pxField({'obj': obj, 'tool': obj.tool})

    def setLock(self, user, page=None, field=None):
        '''A p_user edits a given p_page on this object: we will set a lock, to
           prevent other users to edit this page at the same time.'''
        # When setting a lock from a field being inline-edited, p_page is not
        # given and p_field is given instead. In that case, the page will be
        # retrieved from the field, but the lock will not be set if the field is
        # not persistent or whose persistence is managed by a specific method.
        if field and (not field.persist or callable(field.persist)): return
        # Create the persistent mapping storing locks if it does not exist yet
        if not hasattr(self.aq_base, 'locks'):
            from persistent.mapping import PersistentMapping
            # ~{s_page: (s_userId, DateTime_lockDate)}~
            self.locks = PersistentMapping()
        # Raise an error is the page is already locked by someone else. If the
        # page is already locked by the same user, we don't mind: he could have
        # used its browser's back / forward buttons...
        userId = user.login
        locks = self.locks
        page = page or field.page.name
        if (page in locks) and (userId != locks[page][0]):
            login, date = locks[page]
            tool = self.getTool()
            map = {'user':tool.getUserName(login), 'date':tool.formatDate(date)}
            self.raiseUnauthorized(self.translate('page_locked', mapping=map))
        # Set the lock
        locks[page] = (userId, DateTime())

    def isLocked(self, user, page):
        '''Is this page locked? If the page is locked by the same user, we don't
           mind and consider the page as unlocked. If the page is locked, this
           method returns the tuple (userId, lockDate).'''
        if hasattr(self.aq_base, 'locks') and (page in self.locks):
            if (user.login != self.locks[page][0]): return self.locks[page]

    def getLockers(self):
        '''Returns the list of user logins having locked at least one page on
           p_self.'''
        r = set()
        if hasattr(self.aq_base, 'locks'):
            for login, date in self.aq_base.locks.itervalues():
                r.add(login)
        return r

    def unlockableBy(self, user):
        '''May the currently logged p_user unlock p_self ?'''
        for role in self.getProductConfig(True).unlockers:
            if isinstance(role, str):
                condition = user.hasRole(role)
            else:
                if role.local:
                    condition = user.hasRole(role.name, self)
                else:
                    condition = user.hasRole(role.name)
            if condition:
                return True

    def removeLock(self, page=None, field=None, force=False):
        '''Removes the lock on this p_page. This happens:
           - after the page has been saved: the lock must be released;
           - when an admin wants to force the deletion of a lock that was
             left on p_page for too long (p_force=True).
        '''
        # When removing a lock from a field being inline-edited, p_page is not
        # given and p_field is given instead.
        page = page or field.page.name
        locks = getattr(self.aq_base, 'locks', None)
        if not locks or (page not in locks): return
        # If there is a custom persistence for p_field, the lock was not set
        if field and (not field.persist or callable(field.persist)): return
        # Raise an error if the user that saves changes is not the one that
        # has locked the page (excepted if p_force is True).
        if not force:
            userId = self.getTool().getUser().login
            if locks[page][0] != userId:
                self.raiseUnauthorized('This page was locked by someone else.')
        # Remove the lock
        del locks[page]

    def removeMyLock(self, user, page):
        '''If p_user has set a lock on p_page, this method removes it. This
           method is called when the user that locked a page consults
           pxView for this page. In this case, we consider that the user has
           left the edit page in an unexpected way and we remove the lock.'''
        if hasattr(self.aq_base, 'locks') and (page in self.locks) and \
           (user.login == self.locks[page][0]):
            del self.locks[page]

    def onUnlock(self):
        '''Called when an admin wants to remove a lock that was left for too
           long by some user.'''
        rq = self.REQUEST
        tool = self.getTool()
        obj = tool.getObject(rq['objectUid'])
        obj.removeLock(rq['pageName'], force=True)
        urlBack = self.getUrl(self.getReferer())
        self.say(self.translate('action_done'))
        self.goto(urlBack)

    def manageCancel(self, req, inPopup, isNew, initiator):
        '''Called by m_onUpdate when a user cancels an object creation or
           update.'''
        # Call, when available, m_onCancel on the object being created or
        # updated. m_onCancel determines the message to return to the UI and
        # the URL to redirect the user to.
        appyObj = self.appy()
        msg = urlBack = None
        if hasattr(appyObj, 'onCancel'):
            r = appyObj.onCancel(isNew)
            if r: msg, urlBack = r
        self.say(msg or self.translate('object_canceled'))
        tool = self.getTool()
        if inPopup:
            back = tool.backFromPopup()
        elif not urlBack:
            if initiator:
                # Go back to the initiator page
                urlBack = initiator.getUrl()
            else:
                if isNew: urlBack = tool.getHomePage() # Go back to home page
                else:
                    # Return to the same page, excepted if unshowable on view
                    phaseObj = self.getAppyPhases(True, 'view')
                    pageInfo = phaseObj.getPageInfo(req['page'], 'view')
                    if not pageInfo: urlBack = tool.getHomePage()
                    else: urlBack = self.getUrl(page=pageInfo.page.name)
        self.removeLock(req['page'])
        if inPopup: return back
        return self.goto(urlBack)

    def onUpdate(self):
        '''This method is executed when a user wants to update an object.
           The object may be a temporary object created in /temp_folder.
           In this case, the update consists in moving it to its "final" place.
           If the object is not a temporary one, this method updates its
           fields in the database.'''
        rq = self.REQUEST
        errorMessage = self.translate('validation_error')
        isNew = self.isTemporary()
        inPopup = rq.get('popup') == '1'
        # If this object is created from an initiator, get info about it
        initiator = self.getInitiator()
        # If the user clicked on 'Cancel', go back to the previous page
        buttonClicked = rq.get('button')
        if buttonClicked == 'cancel':
            return self.manageCancel(rq, inPopup, isNew, initiator)

        # Create a validator: he will manage validation of request data
        validator = rq.validator = Validator(self)

        # Trigger field-specific validation
        validator.intraFieldValidation()
        if validator.errors:
            self.say(errorMessage)
            return self.gotoEdit()

        # Trigger inter-field validation
        msg = validator.interFieldValidation() or errorMessage
        if validator.errors:
            self.say(msg)
            return self.gotoEdit()

        # Before saving data, must we ask a confirmation by the user ?
        appyObj = self.appy()
        saveConfirmed = rq.get('confirmed') == 'True'
        if hasattr(appyObj, 'confirm') and not saveConfirmed:
            msg = appyObj.confirm(validator.values)
            if msg:
                rq.set('confirmMsg', msg.replace("'", "\\'"))
                return self.gotoEdit()

        # Create or update the object in the database
        obj, deleted, msg = self.createOrUpdate(isNew, validator, initiator)

        # Redirect the user to the appropriate page
        if not msg: msg = obj.translate('object_saved')
        # If the object has already been deleted (ie, it is a kind of transient
        # object like a one-shot form and has already been deleted in method
        # onEdit) or if the user can't access the object anymore, redirect him
        # to the user's home page.
        tool = self.getTool()
        del rq.userLogins # Empty this cache, things could have changed
        if deleted or not obj.mayView():
            if inPopup: return tool.backFromPopup(initiator)
            return obj.goto(tool.getHomePage(), msg)
        if (buttonClicked == 'save') or saveConfirmed:
            obj.say(msg)
            nav = None
            if inPopup: return tool.backFromPopup(initiator)
            if isNew and initiator:
                if initiator.goBack() == 'view':
                    # Stay on the "view" page of the newly created object
                    nav = initiator.getNavInfo(obj)
                else:
                    return obj.goto(initiator.getUrl())
            # Return to the same page, if showable on view
            phaseObj = obj.getAppyPhases(True, 'view')
            pageInfo = phaseObj.getPageInfo(rq['page'], 'view')
            if not pageInfo: return obj.goto(tool.getHomePage(), msg)
            return obj.goto(obj.getUrl(page=pageInfo.page.name, nav=nav))
        # Get the current page name. We keep it in "pageName" because rq['page']
        # can be changed by m_getAppyPhases called below.
        pageName = rq['page']
        if buttonClicked in ('previous', 'next'):
            # Go to the previous or next page for this object. We recompute the
            # list of phases and pages because things may have changed since the
            # object has been updated (ie, additional pages may be shown or
            # hidden now, so the next and previous pages may have changed).
            # Moreover, previous and next pages may not be available in "edit"
            # mode, so we return the edit or view pages depending on page.show.
            phaseObj = obj.getAppyPhases(True, 'edit')
            methodName = 'get%sPage' % buttonClicked.capitalize()
            pageName, pageInfo = getattr(phaseObj, methodName)(pageName)
            if pageName:
                # Return to the edit or view page?
                if pageInfo.showOnEdit:
                    # I do not use gotoEdit here because I really need to
                    # redirect the user to the edit page. Indeed, the object
                    # edit URL may have moved from temp_folder to another place.
                    return obj.goto(obj.getUrl(mode='edit', page=pageName,
                                                inPopup=inPopup))
                else:
                    return obj.goto(obj.getUrl(page=pageName, inPopup=inPopup))
            else:
                obj.say(msg)
                return obj.goto(obj.getUrl(inPopup=inPopup))
        return obj.gotoEdit()

    def reindex(self, indexes=None, unindex=False, exclude=False):
        '''Reindexes this object in the catalog. If index names are specified in
           p_indexes, recataloging is limited to those indexes. If p_unindex
           is True, instead of cataloguing the object, it uncatalogs it.
           If p_exclude is True (it has only sense if unindex is False),
           p_indexes is interpreted as containing the list of indexes NOT TO
           recompute: all the indexes not being in this list will be.'''
        path = '/'.join(self.getPhysicalPath())
        catalog = self.getPhysicalRoot().catalog
        if unindex:
            catalog.uncatalog_object(path)
        else:
            if indexes:
                # Get the names of the indexes to take into account
                names = indexes
                if exclude:
                    # p_indexes contains the names of the indexes to exclude
                    names = [n for n in self.wrapperClass.getIndexes().keys() \
                             if n not in indexes]
                catalog.catalog_object(self, path, idxs=names)
            else:
                if exclude: raise Exception('Specify indexes to exclude.')
                # Get the list of indexes that apply on this object. Else, Zope
                # will reindex all indexes defined in the catalog, and through
                # acquisition, wrong methods can be called on wrong objects.
                names = self.wrapperClass.getIndexes().keys()
                catalog.catalog_object(self, path, idxs=names)

    def xml(self, action=None):
        '''If no p_action is defined, this method returns the XML version of
           this object. Else, it calls method named p_action on the
           corresponding Appy wrapper and returns, as XML, its result.'''
        resp = self.REQUEST.RESPONSE
        resp.setHeader('Content-Type', 'text/xml;charset=utf-8')
        # Check if the user is allowed to consult this object
        if not self.mayView(): return XmlMarshaller().marshall('Unauthorized')
        if not action:
            marshaller = XmlMarshaller(rootTag=self.getClass().__name__,
                                       dumpUnicode=True)
            res = marshaller.marshall(self, objectType='appy')
        else:
            appyObj = self.appy()
            try:
                methodRes = getattr(appyObj, action)()
                isStr = isinstance(methodRes, basestring)
                if isStr and methodRes.startswith('<?xml'):
                    # Already XML
                    res = methodRes
                elif isStr and methodRes.startswith('<!DOCTYPE'):
                    # Already XHTML
                    resp.setHeader('Content-Type', 'text/html;charset=utf-8')
                    res = methodRes
                elif isinstance(methodRes, Px):
                    res = methodRes({'self': self.appy()})
                elif isinstance(methodRes, file):
                    res = methodRes.read()
                    methodRes.close()
                else:
                    marshaller = XmlMarshaller()
                    oType = isinstance(methodRes, Object) and 'popo' or 'appy'
                    res = marshaller.marshall(methodRes, objectType=oType)
            except Exception, e:
                tb = sutils.Traceback.get()
                res = XmlMarshaller(rootTag='exception').marshall(tb)
                self.log(tb, type='error')
                import transaction
                transaction.abort()
        return res

    def say(self, msg):
        '''Displays a p_msg in the user interface'''
        try:
            rq = self.REQUEST
        except AttributeError, ae:
            # No request object, we have nothing to say
            return
        if not hasattr(rq, 'messages'):
            plist = self.getProductConfig().PersistentList
            messages = rq.messages = plist()
        else:
            messages = rq.messages
        messages.append(msg)

    def log(self, msg, type='info', noUser=False):
        '''Logs a p_msg in the log file. p_logLevel may be "info", "warning"
           or "error".

           If p_noUser is True, we will not try to prefix the message with the
           user login. This is required when logging from an authentication
           plug-in that tries to log an error when identifying the currently
           logged user.'''
        logger = self.getProductConfig().logger
        if type == 'warning': logMethod = logger.warn
        elif type == 'error': logMethod = logger.error
        else: logMethod = logger.info
        # If p_noUser is True, log the message without prefixing it by the user
        if noUser: return logMethod(msg)
        # There could be not user at all (even not "anon") if we are trying to
        # authenticate an inexistent user for example.
        user = self.getTool().getUser()
        login = user and user.login or 'anon'
        # Add the client IP when available
        r = ''
        rq = getattr(self, 'REQUEST', None)
        if rq:
            ip = rq.get('HTTP_X_FORWARDED_FOR')
            if ip: r = '%s ' % ip
        logMethod(r + '%s: %s' % (login, msg))

    def do(self):
        '''Performs some action from the user interface'''
        rq = self.REQUEST
        action = rq.get('action')
        if not action:
            self.log('no action in request on Object::do', type='error')
            return # May happen on heavy load
        if rq.get('objectUid', None):
            obj = self.getTool().getObject(rq['objectUid'])
        else:
            obj = self
        if rq.get('appy', None) == '1': obj = obj.appy()
        # Get, on obj, the method corresponding to the action
        method = getattr(obj, 'on%s' % action, None)
        if not method:
            self.log('wrong action "%s" in request on Object::do' % action,
                     type='error')
            return
        # Initialise the request object
        self.getTool().appy().initRequest(rq)
        # Call the method
        return method()

    def raiseUnauthorized(self, msg=None):
        '''Raise an error "Unauthorized access"'''
        from AccessControl import Unauthorized
        if msg: raise Unauthorized(msg)
        raise Unauthorized()

    def raiseMessage(self, msg):
        '''Raise an error that will render as a nice message to the user'''
        raise MessageException(msg)

    def returnNotFound(self):
        '''Return a 404 error'''
        self.REQUEST.RESPONSE.setStatus(404)
        # return ''

    def rememberPreviousData(self, fields):
        '''This method is called before updating an object and remembers, for
           every historized field from p_fields, the previous value.'''
        r = {} # ~{s_fieldName: previousFieldValue}~
        # p_fields can be a list of fields or a single field
        if isinstance(fields, gen.Field): fields = [fields]
        for field in fields:
            if not field.getAttribute(self, 'historized'): continue
            r[field.name] = field.getComparableValue(self)
        return r

    def addHistoryEvent(self, action, **kw):
        '''Adds an event in the object history'''
        event = {'action': action, 'time': DateTime(), 'comments': ''}
        event.update(kw)
        if 'actor' not in event:
            user = self.getTool().getUser()
            event['actor'] = user and user.login or 'system'
        if 'review_state' not in event: event['review_state'] = self.State()
        # Add the event to the history
        self.workflow_history['appy'] += (event,)
        return event

    def setCreationComment(self, comment):
        '''Adds this p_comment on the _init_ transition in p_self's workflow'''
        entries = self.workflow_history['appy']
        if len(entries) > 1:
            raise Exception('Too late to update the creation comment')
        entry = entries[0]
        entry['comments'] = comment
        self.workflow_history['appy'] = (entry,)

    def getCreationComment(self):
        '''Return the creation comment if it exists'''
        history = self.workflow_history
        if not history: return
        return history['appy'][0]['comments']

    def getEventId(self, event):
        '''Return an ID for this history p_event, based on its "time" key.'''
        return str(event['time']._millis)

    def mayEditHistoryComment(self, event, user, isManager):
        '''May p_user edit history comment for p_event ?'''
        if not self.getProductConfig(True).editHistoryComments: return
        return isManager or (user.login == event['actor'])

    def onDeleteEvent(self):
        '''Called when an event (from object history) deletion is triggered
           from the ui.'''
        rq = self.REQUEST
        # Re-create object history, but without the event corresponding to
        # rq['eventTime']
        history = []
        eventToDelete = DateTime(rq['eventTime'])
        for event in self.workflow_history['appy']:
            action = event['action']
            if not action or not action.startswith('_data') or \
               (event['time'] != eventToDelete):
                history.append(event)
        self.workflow_history['appy'] = tuple(history)
        self.log('data change event deleted for %s.' % self.appy().id)
        return self.translate('object_saved')

    def onEditEvent(self):
        '''Called when a history comment is edited'''
        req = self.REQUEST
        # Find the corresponding history entry
        eventToEdit = DateTime(req['eventTime'])
        for event in self.workflow_history['appy']:
            if event['time'] == eventToEdit:
                event['comments'] = req.get('comment', '')
        self.log('history comment edited for %s.' % self.appy().id)
        return self.translate('object_saved')

    def addDataChange(self, changes, comment=None,
                      notForPreviouslyEmptyValues=False):
        '''This method allows to add "manually" a data change into the objet's
           history. Indeed, data changes are "automatically" recorded only when
           a HTTP form is uploaded, not if, in the code, a setter is called on
           a field. The method is also called by m_historizeData below, that
           performs "automatic" recording when a HTTP form is uploaded. Field
           changes for which the previous value was empty are not recorded into
           the history if p_notForPreviouslyEmptyValues is True.

           For a multilingual string field, p_changes can contain a key for
           every language, of the form <field name>-<language>.'''
        # Add to the p_changes dict the field labels
        for name in changes.keys():
            # "name" can contain the language for multilingual fields
            if '-' in name:
                fieldName, lg = name.split('-')
            else:
                fieldName = name
                lg = None
            field = self.getAppyType(fieldName)
            if notForPreviouslyEmptyValues:
                # Check if the previous field value was empty
                if lg:
                    isEmpty = not changes[name] or not changes[name].get(lg)
                else:
                    isEmpty = field.isEmptyValue(self, changes[name])
                if isEmpty:
                    del changes[name]
            else:
                changes[name] = (changes[name], field.labelId)
        # Add an event in the history
        self.addHistoryEvent('_datachange_', changes=changes,
                             comments=comment or '')

    def getEventText(self, action):
        '''Gets the translated text for p_action found in p_self's history'''
        if action.startswith('_data'):
            # It is a data change, addition or deletion, with specific labels
            r = self.translate('data_%s' % action[5:-1])
        elif action.startswith('_action'):
            # The event corresponds to the triggering of an action. The label to
            # use is the one from the corresponding action field.
            field = self.getAppyType(action[7:-1])
            r = self.translate('label', field=field)
        elif action == '_custom_':
            # A custom event
            r = self.translate('event_custom')
        else:
            # The event is a workflow action
            r = self.translate(self.getWorkflowLabel(action))
        return r

    def historizeData(self, previousData):
        '''Records in the object history potential changes on historized fields.
           p_previousData contains the values, before an update, of the
           historized fields, while p_self already contains the (potentially)
           modified values.'''
        # Remove from previousData all values that were not changed
        for name in previousData.keys():
            field = self.getAppyType(name)
            prev = previousData[name]
            curr = field.getValue(self)
            try:
                if (prev == curr) or ((prev is None) and (curr == '')) or \
                   ((prev == '') and (curr is None)):
                    del previousData[name]
                    continue
            except UnicodeDecodeError, ude:
                # The string comparisons above may imply silent encoding-related
                # conversions that may produce this exception.
                continue
            # In some cases the old value must be formatted
            if field.type == 'Ref':
                previousData[name] = [r.o.getShownValue('title') \
                                      for r in previousData[name]]
            elif field.type == 'String':
                languages = field.getAttribute(self, 'languages')
                if len(languages) > 1:
                    # Consider every language-specific value as a first-class
                    # value.
                    del previousData[name]
                    for lg in languages:
                        lgPrev = prev and prev.get(lg) or None
                        lgCurr = curr and curr.get(lg) or None
                        if lgPrev == lgCurr: continue
                        previousData['%s-%s' % (name, lg)] = lgPrev
        if previousData:
            self.addDataChange(previousData)

    def setMessageCookie(self):
        '''Create a cookie containing the messages to show to the user'''
        rq = getattr(self, 'REQUEST', None)
        if not rq: return
        messages = getattr(rq, 'messages', None)
        if not messages: return
        # Encode the message
        messages = urllib.quote(' '.join(messages))
        rq.RESPONSE.setCookie('AppyMessage', messages, path='/')

    def goto(self, url, msg=None):
        '''Brings the user to some p_url after an action has been executed'''
        if msg: self.say(msg)
        # Set the message cookie if messages must be carried to the browser
        self.setMessageCookie()
        return self.REQUEST.RESPONSE.redirect(url, status=303)

    def gotoEdit(self):
        '''Brings the user to the edit page for this object. This method takes
           care of not carrying any password value. Unlike m_goto above, there
           is no HTTP redirect here: we execute directly PX "edit" and we
           return the result.'''
        req = self.REQUEST
        page = req.get('page', 'main')
        for field in self.getAppyTypes('edit', page):
            if (field.type == 'String') and (field.format in (3,4)):
                req.set(field.name, '')
        return self.edit()

    def gotoTied(self):
        '''Redirects the user to an object tied to this one'''
        return self.getAppyType(self.REQUEST['field']).onGotoTied(self)

    def getCreateFolder(self):
        '''When an object must be created from this one through a Ref field, we
           must know where to put the newly create object: within this one if it
           is folderish, besides this one in its parent else.
        '''
        if self.isPrincipiaFolderish: return self
        return self.getParentNode()

    def isDebug(self):
        '''Are we in debug mode ?'''
        for arg in sys.argv:
            if arg == 'debug-mode=on': return True

    def getClass(self, reloaded=False):
        '''Returns the Appy class that dictates self's behaviour'''
        if not reloaded:
            return self.getTool().getAppyClass(self.__class__.__name__)
        else:
            klass = self.appy().klass
            moduleName = klass.__module__
            exec 'import %s' % moduleName
            exec 'reload(%s)' % moduleName
            exec 'res = %s.%s' % (moduleName, klass.__name__)
            # More manipulations may have occurred in m_update
            if hasattr(res, 'update'):
                parentName= res.__bases__[-1].__name__
                moduleName= 'Products.%s.wrappers' % self.getTool().getAppName()
                exec 'import %s' % moduleName
                exec 'parent = %s.%s' % (moduleName, parentName)
                res.update(parent)
            return res

    def getAppyType(self, name, className=None):
        '''Returns the Appy type named p_name. If no p_className is defined, the
           field is supposed to belong to self's class.'''
        isInnerType = '*' in name # An inner type lies within a List type
        subName = None
        if isInnerType:
            elems = name.split('*')
            if len(elems) == 2: name, subName = elems
            else:               name, subName, i = elems
        if not className:
            klass = self.__class__.wrapperClass
        else:
            klass = self.getTool().getAppyClass(className, wrapper=True)
        res = getattr(klass, name, None)
        if res and isInnerType: res = res.getField(subName)
        return res

    def getAllAppyTypes(self, className=None):
        '''Returns the ordered list of all Appy types for self's class if
           p_className is not specified, or for p_className else.'''
        if not className:
            klass = self.__class__.wrapperClass
        else:
            klass = self.getTool().getAppyClass(className, wrapper=True)
        return klass.__fields__

    def getGroupedFields(self, layoutType, pageName, cssJs=None, fields=None):
        '''Returns the fields sorted by group. If a dict is given in p_cssJs,
           we will add it in the CSS and JS files required by the fields.

           If p_fields are given, we use them instead of getting all fields
           defined on p_self's class.'''
        res = []
        groups = {} # The already encountered groups
        # If a dict is given in p_cssJs, we must fill it with the CSS and JS
        # files required for every returned field.
        collectCssJs = isinstance(cssJs, dict)
        css = js = None
        config = self.getProductConfig(True)
        # If param "refresh" is there, we must reload the Python class
        refresh = ('refresh' in self.REQUEST)
        if refresh: klass = self.getClass(reloaded=True)
        for field in (fields or self.getAllAppyTypes()):
            if refresh: field = field.reload(klass, self)
            if field.page.name != pageName: continue
            if not field.isShowable(self, layoutType): continue
            if collectCssJs:
                if css == None: css = []
                field.getCss(layoutType, css, config)
                if js == None: js = []
                field.getJs(layoutType, js, config)
            if not field.group:
                res.append(field)
            else:
                group = field.getGroup(layoutType)
                if not group:
                    res.append(field)
                else:
                    # Insert the UiGroup instance corresponding to field.group
                    uiGroup = group.insertInto(res, groups, field.page,
                                               self.meta_type)
                    uiGroup.addElement(field)
        if collectCssJs:
            cssJs['css'] = css or ()
            cssJs['js'] = js or ()
        return res

    def getAppyTypes(self, layoutType, pageName, type=None, fields=None):
        '''Returns the list of fields that belong to a given page (p_pageName)
           for a given p_layoutType. If p_pageName is None, fields of all pages
           are returned. If p_type is defined, only fields of this p_type are
           returned. If p_fields are given, they are used instead of using
           self's fields.'''
        res = []
        for field in (fields or self.getAllAppyTypes()):
            if pageName and (field.page.name != pageName): continue
            if type and (field.type != type): continue
            if not field.isRenderable(layoutType): continue
            if not field.isShowable(self, layoutType): continue
            res.append(field)
        return res

    def getCssJs(self, fields, layoutType, res):
        '''Gets, in p_res ~{'css':[s_css], 'js':[s_js]}~ the lists of
           Javascript and CSS files required by Appy types p_fields when shown
           on p_layoutType.'''
        # Lists css and js below are not sets, because order of Javascript
        # inclusion can be important, and this could be losed by using sets.
        css = []
        js = []
        config = self.getProductConfig(True)
        for field in fields:
            field.getCss(layoutType, css, config)
            field.getJs(layoutType, js, config)
        res['css'] = css
        res['js'] = js

    def getCssFor(self, elem):
        '''Gets the name of the CSS class to use for styling some p_elem. If
           self's class does not define a dict or method named "styles", the
           defaut CSS class to use will be named p_elem.'''
        styles = getattr(self.getClass(), 'styles', None)
        if not styles:
            return elem
        elif callable(styles): # A method
            return styles(self.appy(), elem) or elem
        else: # A dict or appy.Object instance
            return styles.get(elem) or elem

    def getAppyPhases(self, currentOnly=False, layoutType='view'):
        '''Gets the list of phases that are defined for this content type. If
           p_currentOnly is True, the search is limited to the phase where the
           current page (as defined in the request) lies.'''
        # Get the list of phases
        res = [] # Ordered list of phases
        phases = {} # Dict of phases
        for field in self.getAllAppyTypes():
            fieldPhase = field.page.phase
            if fieldPhase not in phases:
                phase = gen.Phase(fieldPhase, self)
                res.append(phase)
                phases[fieldPhase] = phase
            else:
                phase = phases[fieldPhase]
            phase.addPage(field, self, layoutType)
            if (field.type == 'Ref') and field.navigable:
                phase.addPageLinks(field, self)
        # Remove phases that have no visible page
        for i in range(len(res)-1, -1, -1):
            if not res[i].pages:
                del phases[res[i].name]
                del res[i]
        # Compute next/previous phases of every phase
        for ph in phases.itervalues():
            ph.computeNextPrevious(res)
            ph.totalNbOfPhases = len(res)
        # Restrict the result to the current phase if required
        phase = None
        if currentOnly:
            rq = self.REQUEST
            page = rq.get('page', None)
            if not page:
                if layoutType == 'edit': page = self.getDefaultEditPage()
                else:                    page = self.getDefaultViewPage()
            for phase in res:
                if page in phase.pages:
                    return phase
            # If I am here, it means that the page as defined in the request,
            # or the default page, is not existing nor visible in any phase.
            # In this case I find the first visible page among all phases.
            viewAttr = 'showOn%s' % layoutType.capitalize()
            for phase in res:
                for page in phase.pages:
                    if getattr(phase.pagesInfo[page], viewAttr):
                        rq.set('page', page)
                        pageFound = True
                        break
            return phase
        else:
            # Return an empty list if we have a single, link-free page within
            # a single phase.
            if (len(res) == 1) and (len(res[0].pages) == 1) and \
               not res[0].pagesInfo[res[0].pages[0]].links:
                return
            return res

    def highlight(self, text):
        '''This method highlights parts of p_value if we are in the context of
           a query whose keywords must be highlighted.'''
        # Must we highlight something ?
        criteria = Criteria.readFromRequest(self)
        if not criteria or ('SearchableText' not in criteria): return text
        # Highlight every variant of every keyword
        for word in criteria['SearchableText'].strip().split():
            highlighted = '<span class="highlight">%s</span>' % word
            sWord = word.strip(' *').lower()
            for variant in (sWord, sWord.capitalize(), sWord.upper()):
                text = re.sub('(?<= |\(|\>)%s' % variant, highlighted, text)
        return text

    def getSupTitle(self, navInfo=''):
        '''Gets the html code (icons,...) that can be shown besides the title
           of an object.'''
        obj = self.appy()
        if hasattr(obj, 'getSupTitle'): return obj.getSupTitle(navInfo)
        return ''

    def getSubTitle(self):
        '''Gets the content that must appear below the title of an object.'''
        obj = self.appy()
        if hasattr(obj, 'getSubTitle'): return obj.getSubTitle()
        return ''

    def getSupBreadCrumb(self):
        '''Gets the html code that can be shown besides the title of an object
           in the breadcrumb.'''
        obj = self.appy()
        if hasattr(obj, 'getSupBreadCrumb'): return obj.getSupBreadCrumb()
        return ''

    def getSubBreadCrumb(self):
        '''Gets the content that must appear below the title of an object in the
           breadcrumb.'''
        obj = self.appy()
        if hasattr(obj, 'getSubBreadCrumb'): return obj.getSubBreadCrumb()
        return ''

    def getListTitle(self, mode='link', nav='no', target=None, page='main',
                     inPopup=False, baseUrl=None, title=None, linkTitle=None,
                     css=None, selectJs=None, highlight=False, backHook=None,
                     maxChars=None):
        '''Gets the title as it must appear in lists of objects (ie in lists of
           tied objects in a Ref, in query results...).

           In most cases, a title must appear as a link that leads to the object
           view layout. In this case (p_mode == "link"):
           * p_nav       is the navigation parameter allowing navigation between
                         this object and others;
           * p_target    specifies if the link must be opened in the popup or
                         not;
           * p_page      specifies which page to show on the target object view;
           * p_inPopup   indicates if we are already in the popup or not;
           * p_baseUrl   indicates a possible alternate base URL for accessing
                         the object;
           * p_title     specifies, if given, an alternate content for the "a"
                         tag (can be a PX);
           * p_linkTitle specifies a possible value for attribute "link" for the
                         "a" tag (by default this attribute is not dumped);
           * p_css       can be the name of a CSS class to apply (also for other
                         modes).
           * p_maxChars  gives an (optional) limit to the number of visible
                         title chars.

           Another p_mode is "select". In this case, we are in a popup for
           selecting objects: every title must not be a link, but clicking on it
           must trigger Javascript code (in p_selectJs) that will select this
           object.

           The last p_mode is "text". In this case, we simply show the object
           title but with no tied action (link, select).

           If p_highlight is True, keywords will be highlighted if we are in the
           context of a query with keywords.

           If the klass whose elements must be listed has an attribute
           "uniqueLinks" being True, in "link" mode, the generated link will
           include an additional parameter named "_hash". This parameter will
           ensure that the link will be different every time it is generated.
           This is useful for short-circuiting the a:visited CSS style when an
           app wants to manage link's style in some specific way.
        '''
        # Compute CSS class
        cssClass = self.getCssFor('title')
        if css: cssClass += ' %s' % css
        # Get the title, with highlighted parts when relevant
        klass = self.getClass()
        titleIsPx = False
        if not title:
            if hasattr(klass, 'listTitle'):
                title = klass.listTitle(self.appy(), nav)
            else:
                title = self.getShownValue('title')
        elif isinstance(title, Px):
            title = title(self.REQUEST.pxContext).encode('utf-8')
            titleIsPx = True
        if highlight and not titleIsPx: title = self.highlight(title)
        if maxChars: title = self.getTool().truncateText(title, width=maxChars)
        if mode == 'link':
            inPopup = inPopup or (target.target != '_self')
            # Build the link URL. Suffix it with some info that will make him
            # unique when relevant.
            if hasattr(klass, 'uniqueLinks') and klass.uniqueLinks:
                url = self.getUrl(base=baseUrl, page=page, nav=nav,
                                  inPopup=inPopup, _hash='%f' % time.time())
            else:
                url = self.getUrl(base=baseUrl, page=page, nav=nav,
                                  inPopup=inPopup)
            # Define attribute "onClick"
            if target.onClick:
                onClick= ' onclick="%s"'% target.getOnClick(backHook or self.id)
            else:
                onClick = ''
            # Set a "title" parameter when relevant
            lt = linkTitle and (' title="%s"' % linkTitle) or ''
            r = '<a name="title" href="%s" class="%s" target="%s"%s%s>%s</a>' %\
                (url, cssClass, target.target, onClick, lt, title)
        elif mode == 'select':
            r = '<span class="%s clickable" onclick="%s">%s</span>' % \
                (cssClass, selectJs, title)
        elif mode == 'text':
            r = '<span class="%s">%s</span>' % (cssClass, title)
        return r

    # Workflow methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def initializeWorkflow(self, initialComment, initialState=None):
        '''Called when an object is created, be it temp or not, for initializing
           workflow-related data on the object.'''
        wf = self.getWorkflow()
        # Get the initial workflow state
        if initialState:
            initialState = getattr(wf, initialState)
        else:
            initialState = self.State(name=False)
        # Create a Transition instance representing the initial transition
        initialTransition = gen.Transition((initialState, initialState))
        initialTransition.trigger('_init_', self, wf, initialComment,
                                  doSay=False)

    def getWorkflow(self, name=False, className=None):
        '''Returns the workflow applicable for p_self (or for any instance of
           p_className if given), or its name, if p_name is True.'''
        if not className:
            wrapperClass = self.wrapperClass
        else:
            wrapperClass = self.getTool().getAppyClass(className, wrapper=True)
        wf = wrapperClass.getWorkflow()
        if not name: return wf
        return WorkflowDescriptor.getWorkflowName(wf)

    def getWorkflowLabel(self, name=None):
        '''Gets the i18n label for p_name (which can denote a state or a
           transition), or for the current object state if p_name is None.'''
        name = name or self.State()
        if name == 'create_from_predecessor': return name
        return '%s_%s' % (self.getWorkflow(name=True), name)

    def getTransitions(self, includeFake=True, includeNotShowable=False,
                       grouped=True):
        '''This method returns info about transitions (as UiTransition
           instances) that one can trigger from the user interface.
           * if p_includeFake is True, it retrieves transitions that the user
             can't trigger, but for which he needs to know for what reason he
             can't trigger it;
           * if p_includeNotShowable is True, it includes transitions for which
             show=False. Indeed, because "showability" is only a UI concern,
             and not a security concern, in some cases it has sense to set
             includeNotShowable=True, because those transitions are triggerable
             from a security point of view.
           * If p_grouped is True, transitions are grouped according to their
             "group" attribute, in a similar way to fields or searches.
        '''
        res = []
        groups = {} # The already encountered groups of transitions
        wfPage = gen.Page('workflow')
        wf = self.getWorkflow()
        currentState = self.State(name=False)
        # Loop on every transition
        for name in dir(wf):
            transition = getattr(wf, name)
            try:
                if (transition.__class__.__name__ != 'Transition'): continue
            except AttributeError:
                # In Python 2, classes have no attribute "__class__"
                continue
            # Filter transitions that do not have currentState as start state
            if not transition.hasState(currentState, True): continue
            # Check if the transition can be triggered
            mayTrigger = transition.isTriggerable(self, wf)
            # Compute the condition that will lead to including or not this
            # transition
            if not includeFake:
                includeIt = mayTrigger
            else:
                includeIt = mayTrigger or isinstance(mayTrigger, gen.No)
            if not includeNotShowable:
                includeIt = includeIt and transition.isShowable(wf, self)
            if not includeIt: continue
            # Create the UiTransition instance.
            info = UiTransition(name, transition, self, mayTrigger)
            # Add the transition into the result.
            if not transition.group or not grouped:
                res.append(info)
            else:
                # Insert the UiGroup instance corresponding to transition.group.
                uiGroup = transition.group.insertInto(res, groups, wfPage,
                                 self.__class__.__name__, content='transitions')
                uiGroup.addElement(info)
        return res

    def applyUserIdChange(self, oldId, newId):
        '''A user whose ID was p_oldId has now p_newId. If the old ID was
           mentioned in self's local roles or history, update it to the new ID.
           This method returns 1 if a change occurred, 0 else.'''
        r = 0
        # Check local roles
        localRoles = self.appy().localRoles
        if oldId in localRoles:
            localRoles[newId] = localRoles[oldId]
            del localRoles[oldId]
            r = 1
        # Check creator
        if self.creator == oldId:
            self.creator = newId
            r = 1
        # Check history
        history = getattr(self.aq_base, 'workflow_history', None)
        if history:
            # The tool may not have history
            history = history['appy']
            historyToChange = False
            for event in history:
                actor = event.get('actor')
                if actor and (actor == oldId):
                    historyToChange = True
                    break
            if historyToChange:
                history = list(history)
                for event in history:
                    actor = event.get('actor')
                    if actor and (actor == oldId):
                        event['actor'] = newId
                self.workflow_history['appy'] = tuple(history)
                r = 1
        # Reindex the object if a change occurred
        if r: self.reindex()
        return r

    def findNewValue(self, field, language, history, stopIndex,
                     transferTransition='transfer'):
        '''This function tries to find a more recent version of value of p_field
           on p_self. In the case of a multilingual field, p_language is
           specified. The method first tries to find it in
           history[:stopIndex+1]. If it does not find it there, it returns the
           current value on p_obj.'''
        i = stopIndex + 1
        name = language and ('%s-%s' % (field.name, language)) or field.name
        while (i-1) >= 0:
            i -= 1
            action = history[i]['action']
            # Do not compare values that span different apps
            if action == transferTransition: return
            if action != '_datachange_': continue
            changes = history[i]['changes']
            if name not in changes: continue
            # We have found it
            return changes[name][0] or ''
        # A most recent version was not found in the history: return the current
        # field value.
        val = field.getValue(self)
        if not language: return val or ''
        return val and val.get(language) or ''

    def getHistoryTexts(self, event):
        '''Returns a tuple (insertText, deleteText) containing texts to show on,
           respectively, inserted and deleted chunks of text in a XHTML diff.'''
        tool = self.getTool()
        mapping = {'userName': tool.getUserName(event['actor'])}
        res = []
        for type in ('insert', 'delete'):
            msg = self.translate('history_%s' % type, mapping=mapping)
            date = tool.formatDate(event['time'], withHour=True)
            msg = '%s: %s' % (date, msg)
            res.append(msg)
        return res

    def hasHistory(self, name=None):
        '''Has this object an history? If p_name is specified, the question
           becomes: has this object an history for field p_name?'''
        if not hasattr(self.aq_base, 'workflow_history') or \
           not self.workflow_history: return
        # Return False if the user can't consult the object history
        klass = self.getClass()
        if hasattr(klass, 'showHistory'):
            show = klass.showHistory
            if callable(show): show = klass.showHistory(self.appy())
            if not show: return
        # Get the object history
        history = self.workflow_history['appy']
        if not name:
            for event in history:
                if event['action'] and (event['comments'] != '_invisible_'):
                    return True
        else:
            field = self.getAppyType(name)
            # Is this field multilingual ?
            languages = None
            if isinstance(field, Multilingual) or (field.type == 'String'):
                languages = field.getAttribute(self, 'languages')
            multilingual = languages and (len(languages) > 1)
            for event in history:
                if event['action'] != '_datachange_': continue
                # Is there a value present for this field in this data change?
                if not multilingual:
                    if (name in event['changes']) and \
                       (event['changes'][name][0]):
                        return True
                else:
                    # At least one language-specific value must be present
                    for lg in languages:
                        lgName = '%s-%s' % (field.name, lg)
                        if (lgName in event['changes']) and \
                           event['changes'][lgName][0]:
                            return True

    def getHistoryBatch(self, size):
        '''Returns the Batch instance allowing to navigate within p_self's
           history.'''
        start = int(self.REQUEST.get('startNumber', 0))
        # Will be completed later
        return Batch('appyHistory', total=None, length=None,
                     size=size, start=start)

    def getHistory(self, batch, reverse=True, includeInvisible=False):
        '''Returns a copy of the history for this object, sorted in p_reverse
           order if specified (most recent change first), whose invisible events
           have been removed if p_includeInvisible is True.'''
        history = list(self.workflow_history['appy'][1:])
        if not includeInvisible:
            history = [e for e in history if e['comments'] != '_invisible_']
        if reverse: history.reverse()
        # Keep only events which are within the batch
        res = []
        stopIndex = batch.start + batch.size - 1
        i = -1
        while (i+1) < len(history):
            i += 1
            # Ignore events outside range startNumber:startNumber+batchSize
            if i < batch.start: continue
            if i > stopIndex: break
            if history[i]['action'] == '_datachange_':
                # Take a copy of the event: we will modify it and replace
                # fields' old values by their formatted counterparts.
                event = history[i].copy()
                event['changes'] = {}
                for name, oldValue in history[i]['changes'].iteritems():
                    # "name" can specify a language-specific part in a
                    # multilingual field. "oldValue" is a tuple
                    # (value, fieldName).
                    if '-' in name:
                        fieldName, lg = name.split('-')
                    else:
                        fieldName = name
                        lg = None
                    field = self.getAppyType(fieldName)
                    # Field 'name' may not exist, if the history has been
                    # transferred from another site. In this case we can't show
                    # this data change.
                    if not field: continue
                    if (field.type == 'String') and \
                       (field.format == gen.String.XHTML):
                        # For rich text fields, instead of simply showing the
                        # previous value, we propose a diff with the next
                        # version, excepted if the previous value is empty.
                        if lg: isEmpty = not oldValue[0]
                        else: isEmpty = field.isEmptyValue(self, oldValue[0])
                        if isEmpty:
                            val = '-'
                        else:
                            newValue= self.findNewValue(field, lg, history, i-1)
                            if type(newValue) != type(oldValue[0]):
                                # We can't compare uncomparable values
                                continue
                            else:
                                # Compute the diff between oldValue and newValue
                                iMsg, dMsg = self.getHistoryTexts(event)
                                comparator= HtmlDiff(oldValue[0], newValue,
                                                     iMsg, dMsg)
                                val = comparator.get()
                    elif field.type == 'Dict':
                        old = oldValue[0]
                        if not old:
                            val = '-'
                        else:
                            # Propose a XHTML diff with the next version
                            new = self.findNewValue(field, None, history, i-1)
                            diffTexts = self.getHistoryTexts(event)
                            val = field.getDiffValue(self, old, new, diffTexts)
                    else:
                        fmt = lg and 'getUnilingualFormattedValue' or \
                                     'getFormattedValue'
                        val = getattr(field, fmt)(self, oldValue[0]) or '-'
                        if isinstance(val, list) or isinstance(val, tuple):
                            val = '<ul>%s</ul>' % \
                                  ''.join(['<li>%s</li>' % v for v in val])
                    event['changes'][name] = (val, oldValue[1])
            else:
                event = history[i]
            res.append(event)
        return Object(events=res, totalNumber=len(history))

    def getHistoryCollapse(self):
        '''Gets a Collapsible instance for showing a collapse or expanded
           history in this object.'''
        return Collapsible('objectHistory', self.REQUEST)

    def getHistoryAjaxData(self, batch):
        '''Gets data allowing to ajax-ask paginated history data.'''
        params = {'startNumber': batch.start, 'maxPerPage': batch.size}
        # Convert params into a JS dict
        params = sutils.getStringFrom(params)
        hook = batch.hook
        return "getNode('%s',true)['ajax']=new AjaxData('%s','pxHistory', "\
               "%s, null, '%s')" % (hook, hook, params, self.absolute_url())

    def mayNavigate(self):
        '''May the currently logged user see the navigation panel linked to
           this object?'''
        appyObj = self.appy()
        if hasattr(appyObj, 'mayNavigate'): return appyObj.mayNavigate()
        return True

    def getDefaultViewPage(self):
        '''Which view page must be shown by default?'''
        appyObj = self.appy()
        if hasattr(appyObj, 'getDefaultViewPage'):
            return appyObj.getDefaultViewPage()
        return 'main'

    def getDefaultEditPage(self):
        '''Which edit page must be shown by default?'''
        appyObj = self.appy()
        if hasattr(appyObj, 'getDefaultEditPage'):
            return appyObj.getDefaultEditPage()
        return 'main'

    def mayAct(self):
        '''m_mayAct allows to hide the whole set of actions for an object.
           Indeed, beyond workflow security, it can be useful to hide controls
           like "edit" icons/buttons. For example, if a user may only edit some
           Ref fields with add=True on an object, when clicking on "edit", he
           will see an empty edit form.'''
        appyObj = self.appy()
        if hasattr(appyObj, 'mayAct'): return appyObj.mayAct()
        return True

    def mayDelete(self):
        '''May the currently logged user delete this object?'''
        res = self.allows('delete')
        if not res: return
        # An additional, user-defined condition, may refine the base permission.
        appyObj = self.appy()
        if hasattr(appyObj, 'mayDelete'): return appyObj.mayDelete()
        return True

    def mayEdit(self, permission='write', permOnly='specific',
                raiseError=False):
        '''May the currently logged user edit this object? p_permission can be a
           field-specific permission. If p_permOnly is True, the additional
           user-defined condition (custom m_mayEdit) is not evaluated. If
           p_permOnly is "specific", this condition is evaluated only if the
           permission is specific (=not "write"). If p_raiseError is True, if
           the user may not edit p_self, an error is raised.'''
        res = self.allows(permission, raiseError=raiseError)
        if not res: return
        if (permOnly == True) or \
           ((permOnly == 'specific') and (permission != 'write')): return res
        # An additional, user-defined condition, may refine the base permission
        appyObj = self.appy()
        if hasattr(appyObj, 'mayEdit'):
            res = appyObj.mayEdit()
            if not res and raiseError: self.raiseUnauthorized()
            return res
        return True

    def mayView(self, permission='read', raiseError=False):
        '''May the currently logged user view this object? p_permission can be a
           field-specific permission. If p_raiseError is True, if the user may
           not view p_self, an error is raised.'''
        res = self.allows(permission, raiseError=raiseError)
        if not res: return
        # An additional, user-defined condition, may refine the base permission
        appyObj = self.appy()
        if hasattr(appyObj, 'mayView'):
            res = appyObj.mayView()
            if not res and raiseError: self.raiseUnauthorized()
            return res
        return True

    def callOnView(self):
        '''Call "onView" method if present'''
        klass = self.getClass()
        if hasattr(klass, 'onView'): klass.onView(self.appy())

    def onExecuteAction(self):
        '''Called when a user wants to execute an Appy action on an object'''
        req = self.REQUEST
        className = req.get('className')
        field = self.getAppyType(req['fieldName'], className=className)
        return field.onUiRequest(self, req)

    def onTrigger(self):
        '''Called when a user wants to trigger a transition on an object'''
        rq = self.REQUEST
        wf = self.getWorkflow()
        # Get the transition
        name = rq['transition']
        transition = getattr(wf, name, None)
        if not transition or (transition.__class__.__name__ != 'Transition'):
            raise Exception('Transition "%s" not found.' % name)
        return transition.onUiRequest(self, wf, name, rq)

    def getRolesFor(self, permission):
        '''Gets, according to the workflow, the roles that are currently granted
           p_permission on this object.'''
        state = self.State(name=False)
        if permission not in state.permissions:
            wf = self.getWorkflow().__name__
            raise Exception('Permission "%s" not in permissions dict for ' \
                            'state %s.%s' % \
                            (permission, wf, self.State(name=True)))
        return state.permissions[permission]

    def getLocalRolesOnly(self):
        '''Must we only take care of local roles when checking security on
           p_self ?'''
        try:
            r = self.workflow_history['appy'][0].get('local', False)
        except AttributeError:
            # Some objects like the tool may not have any history
            r = False
        return r

    def appy(self):
        '''Returns a wrapper object allowing to manipulate p_self the Appy
           way.'''
        # Create the dict for storing Appy wrapper on the REQUEST if needed.
        rq = getattr(self, 'REQUEST', None)
        if not rq:
            # We are in test mode or Zope is starting. Use static variable
            # config.fakeRequest instead.
            rq = self.getProductConfig().fakeRequest
        if not hasattr(rq, 'wrappers'): rq.wrappers = {}
        # Return the Appy wrapper if already present in the cache
        uid = self.id
        if uid in rq.wrappers: return rq.wrappers[uid]
        # Create the Appy wrapper, cache it in rq.wrappers and return it
        wrapper = self.wrapperClass(self)
        rq.wrappers[uid] = wrapper
        return wrapper

    # --------------------------------------------------------------------------
    # Methods for computing values of standard Appy indexes
    # --------------------------------------------------------------------------
    def UID(self):
        '''Returns the unique identifier for this object.'''
        return self.id

    def Title(self):
        '''Returns the title for this object.'''
        title = self.getAppyType('title')
        if title: return title.getIndexValue(self)
        return self.id

    def SortableTitle(self):
        '''Returns the title as must be stored in index "SortableTitle".'''
        return sutils.normalizeText(self.Title())

    def SearchableText(self):
        '''This method concatenates the content of every field with
           searchable=True for indexing purposes.'''
        r = []
        for field in self.getAllAppyTypes():
            if not field.searchable: continue
            r.append(field.getIndexValue(self, forSearch=True))
        return r

    def Creator(self):
        '''Who has created this object ?'''
        return self.creator

    def Created(self):
        '''When was this object created ?'''
        return self.created

    def Modified(self):
        '''When was this object last modified ?'''
        if hasattr(self.aq_base, 'modified'): return self.modified
        return self.created

    def getInitialState(self, wf, name=False):
        '''Gets the initial state in workflow p_wf (or just its name if p_name
           is True.'''
        res = None
        for elem in dir(wf):
            state = getattr(wf, elem)
            try:
                if (state.__class__.__name__ == 'State') and state.initial:
                    res = state
                    break
            except AttributeError:
                # If elem is a Python class, in Python 2 if will not have
                # attribute "__class__".
                pass
        if name: return elem
        return res

    def State(self, name=True, initial=False):
        '''Returns information about the current object state. If p_name is
           True, the returned info is the state name. Else, it is the State
           instance. If p_initial is True, instead of returning info about the
           current state, it returns info about the workflow initial state.'''
        wf = self.getWorkflow()
        if initial or not hasattr(self.aq_base, 'workflow_history'):
            # No workflow information is available (yet) on this object, or
            # initial state is asked. In both cases, return info about this
            # initial state.
            stateName = self.getInitialState(wf, name=True) or 'active'
        else:
            # Return info about the current object state
            stateName = self.workflow_history['appy'][-1]['review_state']
        # Return state name or state definition ?
        if name: return stateName
        state = getattr(wf, stateName, None)
        if not state:
            # The workflow definition has no trace of this state.
            # Return the initial state and log this problem.
            self.log('State "%s" not found for %s (%s). Use initial state.' % \
                     (stateName, self.id, self.Title()))
            state = self.getInitialState(wf, name=False)
        return state

    def ClassName(self):
        '''Returns the name of the (Zope) class for self'''
        return self.portal_type

    def Allowed(self):
        '''Returns the list of roles and users that are allowed to view this
           object. This index value will be used within catalog queries for
           filtering objects the user is allowed to see.'''
        # Get, from the workflow, roles having permission 'read'
        allowedRoles = self.getRolesFor('read').keys()
        if self.getLocalRolesOnly():
            # The roles will not be mentioned as-is
            r = []
        else:
            r = allowedRoles
        # Add users or groups having, locally, at least one of these roles on
        # p_self.
        localRoles = self.appy().localRoles
        if not localRoles: return r
        for id, roles in localRoles.iteritems():
            for role in roles:
                if role in allowedRoles:
                    usr = 'user:%s' % id
                    if usr not in r: r.append(usr)
        return r

    def Container(self):
        '''The container for an object is the object linked from this one via a
           back ref whose forward ref is of type "composite".'''
        res = None
        for field in self.getAllAppyTypes():
            if (field.type == 'Ref') and field.isBack and \
               (field.back.composite) and hasattr(self.aq_base, field.name):
                composite = getattr(self, field.name)
                if composite:
                    res = '%s_%s' % (composite[0], field.name)
                    break
        if not res:
            # This object is root (ie, contained by no other object) or not
            # declared as being a component of a composite object.
            res = 'root'
        return res

    def getContainer(self, objectOnly=False, orInitiator=True, forward=False):
        '''See docstring in homonym method on the wrapper'''
        # Get container info as stored in the index (excepted if the object
        # is temp).
        tool = self.getTool()
        if self.isTemporary():
            r = None
        else:
            try:
                r = tool.getCatalogValue(self, 'Container')
            except AttributeError:
                # The object may not be temp anymore, but not indexed yet
                r = None
        if not r:
            # Probably an object under creation. Get the initiator.
            initiator = self.getInitiator()
            if not initiator or not orInitiator:
                return not objectOnly and (None, None) or None
            if not objectOnly and not forward:
                # In that case we cannot return the name of the back reference,
                # because the initiator gives the name of the forward reference
                # instead.
                raise Exception(GET_CONTAINER_ERROR)
            obj = initiator.obj
            return objectOnly and obj or (obj, initiator.field.name)
        elif r == 'root':
            # A root object: it has no container
            return not objectOnly and (None, None) or None
        else:
            id, name = r.rsplit('_', 1)
            container = tool.getObject(id, appy=True)
            if forward and not objectOnly:
                # Get the name of p_name's forward reference
                name = self.getAppyType(name).back.name
            if objectOnly:
                return container
            else:
                return (container, name)

    def showState(self):
        '''Must I show self's current state ?'''
        stateShow = self.State(name=False).show
        if callable(stateShow):
            return stateShow(self.getWorkflow(), self.appy())
        return stateShow

    def showTransitions(self, layoutType):
        '''Must we show the buttons/icons for triggering transitions on
           p_layoutType?'''
        # Never show transitions on edit pages
        if layoutType == 'edit': return
        # Use the default value if self's class does not specify it
        klass = self.getClass()
        if not hasattr(klass, 'showTransitions'): return layoutType == 'view'
        val = klass.showTransitions
        if callable(val): val = val(self.appy())
        # This value can be a single value or a tuple/list of values
        if not val or isinstance(val, basestring): return layoutType == val
        return layoutType in val

    def showNavigationStrip(self, layoutType, inPopup):
        '''Must I show the navigation strip for this object?'''
        if layoutType == 'edit': return # Never on "edit"
        req = self.REQUEST
        if req.has_key('navStrip') and (req['navStrip'] == '0'): return
        return True

    getUrlDefaults = {'page': True, 'nav': True}
    def getUrl(self, base=None, mode='view', inPopup=False, relative=False,
               useSso=False, **kwargs):
        '''Returns an URL for this object.
           * If p_base is specified, it will be the base URL for this object
             (ie, Zope's self.absolute_url()) or an URL that is relative to the
             root site if p_relative is True).
           * p_mode can be "edit", "view" or "raw" (a non-param, base URL)
           * If p_inPopup is True, the link will be opened in the Appy iframe.
             An additional param "popup=1" will be added to URL params, in order
             to tell Appy that the link target will be shown in a popup, in a
             minimalistic way (no portlet...).
           * p_kwargs can store additional parameters to add to the URL.
             In this dict, every value that is a string will be added to the
             URL as-is. Every value that is True will be replaced by the value
             in the request for the corresponding key (if existing; else, the
             param will not be included in the URL at all).'''
        # Define the URL suffix
        suffix = ''
        if mode != 'raw': suffix = '/%s' % mode
        # Define the base URL if omitted
        if not base:
            if not relative and useSso: # Get the base URL from the SSO
                sso = self.getProductConfig().appConfig.sso
                if sso and sso.appUrl:
                    base = sso.appUrl + self.absolute_url_path()
            if not base:
                base = relative and \
                       self.absolute_url_path() or self.absolute_url()
            base += suffix
            existingParams = ''
        else:
            existingParams = urllib.splitquery(base)[1] or ''
        # If a raw URL is asked, remove any param and suffix
        if mode == 'raw':
            if '?' in base: base = base[:base.index('?')]
            base = base.rstrip('/')
            for mode in ('view', 'edit'):
                if base.endswith(mode):
                    base = base[:-len(mode)].rstrip('/')
                    break
            return base
        # Manage default args
        if not kwargs: kwargs = self.getUrlDefaults
        if not kwargs.get('page'): kwargs['page'] = True
        if not kwargs.get('nav'): kwargs['nav'] = True
        # Create URL parameters from kwargs
        params = []
        for name, value in kwargs.iteritems():
            if isinstance(value, basestring):
                prefix = '%s=' % name
                if prefix not in existingParams:
                    params.append('%s%s' % (prefix, value))
            elif self.REQUEST.get(name, ''):
                params.append('%s=%s' % (name, self.REQUEST[name]))
        # Manage inPopup
        if inPopup and ('popup=' not in existingParams):
            params.append('popup=1')
        if params:
            params = '&'.join(params)
            if base.find('?') != -1: params = '&' + params
            else:                    params = '?' + params
        else:
            params = ''
        return '%s%s' % (base, params)

    def getReferer(self):
        '''Gets the referer URL = the URL from which the UI action was
           triggered.'''
        res = self.REQUEST.get('HTTP_REFERER')
        if not res: return
        # Transform this URL if requested by the presence of a SSO reverse proxy
        sso = self.getProductConfig().appConfig.sso
        if sso: res = sso.patchUrl(res)
        return res

    def getTool(self):
        '''Returns the application tool.'''
        return self.getPhysicalRoot().config

    def getProductConfig(self, app=False):
        '''Returns a reference to the config module. If p_app is True, it
           returns the application config.'''
        res = self.__class__.config
        if app: res = res.appConfig
        return res

    def getParent(self):
        '''If this object is stored within another one, this method returns it.
           Else (if the object is stored directly within the tool or the root
           data folder) it returns None.'''
        parent = self.getParentNode()
        # Not-Managers can't navigate back to the tool
        if (parent.id == 'config') and \
            not self.getTool().getUser().has_role('Manager'):
            return False
        if parent.meta_type not in ('Folder', 'Temporary Folder'): return parent

    def getShownValue(self, name='title', layoutType='view', language=None):
        '''Call field.getShownValue on field named p_name'''
        field = self.getAppyType(name)
        return field.getShownValue(self, field.getValue(self), layoutType,
                                   language=language)

    def getBreadCrumb(self, inPopup=False):
        '''Gets breadcrumb info about this object and its parents (if it must
           be shown).'''
        klass = self.getClass()
        show = getattr(klass, 'breadcrumb', True)
        if callable(show): show = show(self.appy())
        # Return an empty breadcrumb if it must not be shown
        if not show: return []
        # Compute the breadcrumb
        res = [Object(url=self.getUrl(inPopup=inPopup),
                      title=self.getShownValue('title'),
                      view=self.allows('read'))]
        # In a popup (or if "show" specifies it), limit the breadcrumb to the
        # current object.
        if inPopup or (show == 'title'): return res
        container = self.getContainer(objectOnly=True, orInitiator=False)
        if container: container = container.o
        # The tool itself can never appear in breadcrumbs
        if container and (container != self.getTool()):
            res = container.getBreadCrumb() + res
        return res

    def index_html(self):
        '''Base method called when hitting this object.
           - The standard behaviour is to redirect to /view.
           - If a parameter named "do" is present in the request, it is supposed
             to contain the name of a method to call on this object. In this
             case, we call this method and return its result as XML.
           - If method is POST, we consider the request to be XML data, that we
             marshall to Python, and we call the method in param "do" with, as
             arg, this marshalled Python object. While this could sound strange
             to expect a query string containing a param "do" in a HTTP POST,
             the HTTP spec does not prevent to do it.'''
        rq = self.REQUEST
        if (rq.REQUEST_METHOD == 'POST') and rq.QUERY_STRING:
            # A POST containing data from various formats: form-encoded, XML,
            # JSON...
            if rq.CONTENT_TYPE == 'application/x-www-form-urlencoded':
                # Nothing more to do: form elements are, as usual, unwrapped on
                # the request object.
                pass
            elif rq.CONTENT_TYPE == 'application/json':
                rq.args = JsonDecoder.decode(rq.stdin.getvalue())
            else:
                # Default format is supposed to be XML
                rq.args = XmlUnmarshaller().parse(rq.stdin.getvalue())
            # Find the name of the method to call. Query string looks like:
            #                    "do=<methodName>"
            methodName = rq.QUERY_STRING.split('=')[1]
            return self.xml(action=methodName)
        elif rq.has_key('do'):
            # The user wants to call a method on this object and get its result
            # as XML.
            return self.xml(action=rq['do'])
        else:
            # The user wants to consult the view page for this object
            return rq.RESPONSE.redirect(self.getUrl())

    def getUserLanguage(self):
        '''Gets the language (code) for the current user'''
        cfg = self.getProductConfig().appConfig
        if cfg.forcedLanguage: return cfg.forcedLanguage
        if not hasattr(self, 'REQUEST'): return cfg.languages[0]
        # Return the cached value on the request object if present
        rq = self.REQUEST
        if hasattr(rq, 'userLanguage'): return rq.userLanguage
        # Try the value which comes from the cookie. Indeed, if such a cookie is
        # present, it means that the user has explicitly chosen this language
        # via the language selector.
        if '_ZopeLg' in rq.cookies:
            res = rq.cookies['_ZopeLg']
        else:
            # Try the LANGUAGE key from the request: it corresponds to the
            # language as configured in the user's browser.
            res = rq.get('LANGUAGE', None)
            if not res:
                # Try the HTTP_ACCEPT_LANGUAGE key from the request, which
                # stores language preferences as defined in the user's browser.
                # Several languages can be listed, from most to less wanted.
                res = rq.get('HTTP_ACCEPT_LANGUAGE', None)
                if res:
                    if ',' in res: res = res[:res.find(',')]
                    if '-' in res: res = res[:res.find('-')]
                else:
                    res = self.getProductConfig().appConfig.languages[0]
        # Cache this result
        rq.userLanguage = res
        return res

    def getLanguageDirection(self, lang):
        '''Determines if p_lang is a LTR or RTL language'''
        if lang in rtlLanguages: return 'rtl'
        return 'ltr'

    def formatText(self, text, format='html'):
        '''Produces a representation of p_text into the desired p_format, which
           is "html" by default.'''
        if 'html' in format:
            if format == 'html_from_text': text = cgi.escape(text)
            res = text.replace('\r\n', '<br/>').replace('\n', '<br/>')
        elif format == 'text':
            res = text.replace('<br/>', '\n')
        else:
            res = text
        return res

    def translate(self, label, mapping=None, default=None, language=None,
                  format='html', field=None, blankOnError=False):
        '''Translates a given p_label with p_mapping.

           If p_field is given, p_label does not correspond to a full label
           name, but to a label type linked to p_field: "label", "descr"
           or "help". Indeed, in this case, a specific i18n mapping may be
           available on the field, so we must merge this mapping into
           p_mapping.'''
        # In what language must we get the translation ?
        language = language or self.getUserLanguage()
        # Get the label name, and the field-specific mapping if any
        if field:
            if field.type != 'group':
                fieldMapping = field.mapping[label]
                if fieldMapping:
                    if callable(fieldMapping):
                        fieldMapping = field.callMethod(self, fieldMapping)
                    if not mapping:
                        mapping = fieldMapping
                    else:
                        mapping.update(fieldMapping)
                # Translation may be found on a FieldTranslation instance
                if field.translations:
                    return field.translations.get(label, language, mapping)
            label = getattr(field, '%sId' % label)
        # We will get the translation from a Translation object
        tool = self.getTool()
        try:
            translation = getattr(tool, language).appy()
        except AttributeError:
            # We have no translation for this language. Fallback to 'en'.
            fallback = self.getProductConfig(True).fallbackLanguage
            translation = getattr(tool, fallback, None)
            if translation: translation = translation.appy()
        res = getattr(translation, label, '') or ''
        if not res:
            # Get the message in the fallback language
            fallback = self.getProductConfig(True).fallbackLanguage
            try:
                translation = getattr(tool, fallback).appy()
                res = getattr(translation, label, '')
            except AttributeError:
                # The fallback language may not be among app's languages
                pass
        # If still no result, put a nice name derived from the label instead of
        # a translated message.
        if not res and not blankOnError:
            return produceNiceMessage(label.rsplit('_', 1)[-1])
        # Perform variable replacements
        if mapping:
            for name, repl in mapping.iteritems():
                if not isinstance(repl, basestring): repl = str(repl)
                res = res.replace('${%s}' % name, repl)
        # Perform some conversion, according to p_format
        return self.formatText(res, format)

    def getPageLayout(self, layoutType):
        '''Returns the layout corresponding to p_layoutType for p_self'''
        res = self.wrapperClass.getPageLayouts()[layoutType]
        if isinstance(res, basestring): res = Table(res)
        return res

    def download(self, name=None):
        '''Downloads the content of the file that is in the File field whose
           name is in the request. This name can also represent an attribute
           storing an image within a rich text field. If p_name is not given, it
           is retrieved from the request.'''
        rq = self.REQUEST
        name = name or rq.get('name')
        if not name: return
        # Get the corresponding File field
        if '_img_' not in name:
            field = self.getAppyType(name)
        else:
            field = self.getAppyType(name.split('_img_')[0])
        field.onDownload(self, rq)

    def upload(self):
        '''Receives an image uploaded by the user via ckeditor and stores it in
           a special field on this object.'''
        # Get the name of the rich text field for which an image must be stored.
        params = self.REQUEST['QUERY_STRING'].split('&')
        fieldName = params[0].split('=')[1]
        ckNum = params[1].split('=')[1]
        # We will store the image in a field named [fieldName]_img_[nb].
        i = 1
        attrName = '%s_img_%d' % (fieldName, i)
        while True:
            if not hasattr(self.aq_base, attrName):
                break
            else:
                i += 1
                attrName = '%s_img_%d' % (fieldName, i)
        # Store the image. Create a fake File instance for doing the job.
        fakeFile = gen.File(isImage=True)
        fakeFile.name = attrName
        fakeFile.store(self, self.REQUEST['upload'])
        # Return the URL of the image.
        url = '%s/download?name=%s' % (self.absolute_url(), attrName)
        response = self.REQUEST.RESPONSE
        response.setHeader('Content-Type', 'text/html')
        resp = "<script type='text/javascript'>window.parent.CKEDITOR.tools" \
               ".callFunction(%s, '%s');</script>" % (ckNum, url)
        response.write(resp)

    def allows(self, permission, raiseError=False):
        '''Has the logged user p_permission on p_self ?'''
        res = self.getTool().getUser().has_permission(permission, self)
        if not res and raiseError: self.raiseUnauthorized()
        return res

    def isTemporary(self):
        '''Is this object temporary ?'''
        parent = self.getParentNode()
        if not parent: return True # Is probably being created through code
        return parent.getId() == 'temp_folder'

    def onProcess(self):
        '''This method is a general hook for transfering processing of a request
           to a given field, whose name must be in the request.'''
        return self.getAppyType(self.REQUEST['name']).process(self)

    def onCall(self):
        '''Calls a specific method on the corresponding wrapper.'''
        self.mayView(raiseError=True)
        method = self.REQUEST['method']
        obj = self.appy()
        return getattr(obj, method)()

    def onReindex(self):
        '''Called for reindexing an index or all indexes on the currently shown
           object.'''
        if not self.getTool().getUser().has_role('Manager'):
            self.raiseUnauthorized()
        rq = self.REQUEST
        indexName = rq['indexName']
        if indexName == '_all_':
            self.reindex()
        else:
            self.reindex(indexes=(indexName,))
        self.say(self.translate('action_done'))
        self.goto(self.getUrl(self.getReferer()))

    def getConfirmPopupWidth(self):
        '''Gets the width of the confirmation popup'''
        klass = self.getClass()
        return getattr(klass, 'confirmPopup', 500)
# ------------------------------------------------------------------------------
