hooks/workflow.py
author Sylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 17 Aug 2009 20:02:57 +0200
changeset 2890 fdcb8a2bb6eb
parent 2880 bfc8e1831290
child 2968 0e3460341023
permissions -rw-r--r--
fix __init__ parameters

"""Core hooks: workflow related hooks

:organization: Logilab
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
"""
__docformat__ = "restructuredtext en"

from datetime import datetime

from cubicweb import RepositoryError, ValidationError
from cubicweb.interfaces import IWorkflowable
from cubicweb.selectors import entity_implements
from cubicweb.server import hook



def previous_state(session, eid):
    """return the state of the entity with the given eid,
    usually since it's changing in the current transaction. Due to internal
    relation hooks, the relation may has been deleted at this point, so
    we have handle that
    """
    # don't check eid has been added in the current transaction, we don't want
    # to miss previous state of entity whose state change in the same
    # transaction as it's being created
    pending = session.transaction_data.get('pendingrelations', ())
    for eidfrom, rtype, eidto in reversed(pending):
        if rtype == 'in_state' and eidfrom == eid:
            rset = session.execute('Any S,N WHERE S eid %(x)s, S name N',
                                   {'x': eidto}, 'x')
            return rset.get_entity(0, 0)
    rset = session.execute('Any S,N WHERE X eid %(x)s, X in_state S, S name N',
                           {'x': eid}, 'x')
    if rset:
        return rset.get_entity(0, 0)


def relation_deleted(session, eidfrom, rtype, eidto):
    session.transaction_data.setdefault('pendingrelations', []).append(
        (eidfrom, rtype, eidto))


class _SetInitialStateOp(hook.Operation):
    """make initial state be a default state"""

    def precommit_event(self):
        session = self.session
        entity = self.entity
        # if there is an initial state and the entity's state is not set,
        # use the initial state as a default state
        if not session.deleted_in_transaction(entity.eid) and not entity.in_state:
            rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s',
                                   {'name': entity.id})
            if rset:
                session.add_relation(entity.eid, 'in_state', rset[0][0])

class WorkflowHook(hook.Hook):
    __abstract__ = True
    category = 'worfklow'


class SetInitialStateHook(WorkflowHook):
    __id__ = 'wfsetinitial'
    __select__ = WorkflowHook.__select__ & entity_implements(IWorkflowable)
    events = ('after_add_entity',)

    def __call__(self):
        _SetInitialStateOp(self._cw, entity=self.entity)


class PrepareStateChangeHook(WorkflowHook):
    """record previous state information"""
    __id__ = 'cwdelstate'
    __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
    events = ('before_delete_relation',)

    def __call__(self):
        self._cw.transaction_data.setdefault('pendingrelations', []).append(
            (self.eidfrom, self.rtype, self.eidto))


class FireTransitionHook(PrepareStateChangeHook):
    """check the transition is allowed and record transition information"""
    __id__ = 'wffiretransition'
    events = ('before_add_relation',)

    def __call__(self):
        session = self._cw
        eidfrom = self.eidfrom
        eidto = self.eidto
        state = previous_state(session, eidfrom)
        etype = session.describe(eidfrom)[0]
        if not (session.is_super_session or 'managers' in session.user.groups):
            if not state is None:
                entity = session.entity_from_eid(eidfrom)
                # we should find at least one transition going to this state
                try:
                    iter(state.transitions(entity, eidto)).next()
                except StopIteration:
                    msg = session._('transition is not allowed')
                    raise ValidationError(eidfrom, {'in_state': msg})
            else:
                # not a transition
                # check state is initial state if the workflow defines one
                isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s',
                                                {'etype': etype})
                if isrset and not eidto == isrset[0][0]:
                    msg = session._('not the initial state for this entity')
                    raise ValidationError(eidfrom, {'in_state': msg})
        eschema = session.repo.schema[etype]
        if not 'wf_info_for' in eschema.object_relations():
            # workflow history not activated for this entity type
            return
        rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s'
        args = {'comment': session.get_shared_data('trcomment', None, pop=True),
                'e': eidfrom, 'ds': eidto}
        cformat = session.get_shared_data('trcommentformat', None, pop=True)
        if cformat is not None:
            args['comment_format'] = cformat
            rql += ', T comment_format %(comment_format)s'
        restriction = ['DS eid %(ds)s, E eid %(e)s']
        if not state is None: # not a transition
            rql += ', T from_state FS'
            restriction.append('FS eid %(fs)s')
            args['fs'] = state.eid
        rql = '%s WHERE %s' % (rql, ', '.join(restriction))
        session.unsafe_execute(rql, args, 'e')


class SetModificationDateOnStateChange(WorkflowHook):
    """update entity's modification date after changing its state"""
    __id__ = 'wfsyncmdate'
    __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
    events = ('after_add_relation',)

    def __call__(self):
        if self._cw.added_in_transaction(self.eidfrom):
            # new entity, not needed
            return
        entity = self._cw.entity_from_eid(self.eidfrom)
        try:
            entity.set_attributes(modification_date=datetime.now(),
                                  _cw_unsafe=True)
        except RepositoryError, ex:
            # usually occurs if entity is coming from a read-only source
            # (eg ldap user)
            self.warning('cant change modification date for %s: %s', entity, ex)