hooks/workflow.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
--- a/hooks/workflow.py	Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,357 +0,0 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Core hooks: workflow related hooks"""
-
-__docformat__ = "restructuredtext en"
-from cubicweb import _
-
-from datetime import datetime
-
-
-from cubicweb import RepositoryError, validation_error
-from cubicweb.predicates import is_instance, adaptable
-from cubicweb.server import hook
-
-
-def _change_state(cnx, x, oldstate, newstate):
-    nocheck = cnx.transaction_data.setdefault('skip-security', set())
-    nocheck.add((x, 'in_state', oldstate))
-    nocheck.add((x, 'in_state', newstate))
-    # delete previous state first
-    cnx.delete_relation(x, 'in_state', oldstate)
-    cnx.add_relation(x, 'in_state', newstate)
-
-
-# operations ###################################################################
-
-class _SetInitialStateOp(hook.Operation):
-    """make initial state be a default state"""
-    entity = None # make pylint happy
-
-    def precommit_event(self):
-        cnx = self.cnx
-        entity = self.entity
-        iworkflowable = entity.cw_adapt_to('IWorkflowable')
-        # if there is an initial state and the entity's state is not set,
-        # use the initial state as a default state
-        if not (cnx.deleted_in_transaction(entity.eid) or entity.in_state) \
-               and iworkflowable.current_workflow:
-            state = iworkflowable.current_workflow.initial
-            if state:
-                cnx.add_relation(entity.eid, 'in_state', state.eid)
-                _FireAutotransitionOp(cnx, entity=entity)
-
-class _FireAutotransitionOp(hook.Operation):
-    """try to fire auto transition after state changes"""
-    entity = None # make pylint happy
-
-    def precommit_event(self):
-        entity = self.entity
-        iworkflowable = entity.cw_adapt_to('IWorkflowable')
-        autotrs = list(iworkflowable.possible_transitions('auto'))
-        if autotrs:
-            assert len(autotrs) == 1
-            iworkflowable.fire_transition(autotrs[0])
-
-
-class _WorkflowChangedOp(hook.Operation):
-    """fix entity current state when changing its workflow"""
-    eid = wfeid = None # make pylint happy
-
-    def precommit_event(self):
-        # notice that enforcement that new workflow apply to the entity's type is
-        # done by schema rule, no need to check it here
-        cnx = self.cnx
-        pendingeids = cnx.transaction_data.get('pendingeids', ())
-        if self.eid in pendingeids:
-            return
-        entity = cnx.entity_from_eid(self.eid)
-        iworkflowable = entity.cw_adapt_to('IWorkflowable')
-        # check custom workflow has not been rechanged to another one in the same
-        # transaction
-        mainwf = iworkflowable.main_workflow
-        if mainwf.eid == self.wfeid:
-            deststate = mainwf.initial
-            if not deststate:
-                msg = _('workflow has no initial state')
-                raise validation_error(entity, {('custom_workflow', 'subject'): msg})
-            if mainwf.state_by_eid(iworkflowable.current_state.eid):
-                # nothing to do
-                return
-            # if there are no history, simply go to new workflow's initial state
-            if not iworkflowable.workflow_history:
-                if iworkflowable.current_state.eid != deststate.eid:
-                    _change_state(cnx, entity.eid,
-                                  iworkflowable.current_state.eid, deststate.eid)
-                    _FireAutotransitionOp(cnx, entity=entity)
-                return
-            msg = cnx._('workflow changed to "%s"')
-            msg %= cnx._(mainwf.name)
-            cnx.transaction_data[(entity.eid, 'customwf')] = self.wfeid
-            iworkflowable.change_state(deststate, msg, u'text/plain')
-
-
-class _CheckTrExitPoint(hook.Operation):
-    treid = None # make pylint happy
-
-    def precommit_event(self):
-        tr = self.cnx.entity_from_eid(self.treid)
-        outputs = set()
-        for ep in tr.subworkflow_exit:
-            if ep.subwf_state.eid in outputs:
-                msg = _("can't have multiple exits on the same state")
-                raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg})
-            outputs.add(ep.subwf_state.eid)
-
-
-class _SubWorkflowExitOp(hook.Operation):
-    forentity = trinfo = None # make pylint happy
-
-    def precommit_event(self):
-        cnx = self.cnx
-        forentity = self.forentity
-        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
-        trinfo = self.trinfo
-        # we're in a subworkflow, check if we've reached an exit point
-        wftr = iworkflowable.subworkflow_input_transition()
-        if wftr is None:
-            # inconsistency detected
-            msg = _("state doesn't belong to entity's current workflow")
-            raise validation_error(self.trinfo, {('to_state', 'subject'): msg})
-        tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state'])
-        if tostate is not None:
-            # reached an exit point
-            msg = _('exiting from subworkflow %s')
-            msg %= cnx._(iworkflowable.current_workflow.name)
-            cnx.transaction_data[(forentity.eid, 'subwfentrytr')] = True
-            iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
-
-
-# hooks ########################################################################
-
-class WorkflowHook(hook.Hook):
-    __abstract__ = True
-    category = 'metadata'
-
-
-class SetInitialStateHook(WorkflowHook):
-    __regid__ = 'wfsetinitial'
-    __select__ = WorkflowHook.__select__ & adaptable('IWorkflowable')
-    events = ('after_add_entity',)
-
-    def __call__(self):
-        _SetInitialStateOp(self._cw, entity=self.entity)
-
-
-class FireTransitionHook(WorkflowHook):
-    """check the transition is allowed and add missing information into the
-    TrInfo entity.
-
-    Expect that:
-    * wf_info_for inlined relation is set
-    * by_transition or to_state (managers only) inlined relation is set
-
-    Check for automatic transition to be fired at the end
-    """
-    __regid__ = 'wffiretransition'
-    __select__ = WorkflowHook.__select__ & is_instance('TrInfo')
-    events = ('before_add_entity',)
-
-    def __call__(self):
-        cnx = self._cw
-        entity = self.entity
-        # first retreive entity to which the state change apply
-        try:
-            foreid = entity.cw_attr_cache['wf_info_for']
-        except KeyError:
-            msg = _('mandatory relation')
-            raise validation_error(entity, {('wf_info_for', 'subject'): msg})
-        forentity = cnx.entity_from_eid(foreid)
-        # see comment in the TrInfo entity definition
-        entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for)
-        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
-        # then check it has a workflow set, unless we're in the process of changing
-        # entity's workflow
-        if cnx.transaction_data.get((forentity.eid, 'customwf')):
-            wfeid = cnx.transaction_data[(forentity.eid, 'customwf')]
-            wf = cnx.entity_from_eid(wfeid)
-        else:
-            wf = iworkflowable.current_workflow
-        if wf is None:
-            msg = _('related entity has no workflow set')
-            raise validation_error(entity, {None: msg})
-        # then check it has a state set
-        fromstate = iworkflowable.current_state
-        if fromstate is None:
-            msg = _('related entity has no state')
-            raise validation_error(entity, {None: msg})
-        # True if we are coming back from subworkflow
-        swtr = cnx.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
-        cowpowers = (cnx.user.is_in_group('managers')
-                     or not cnx.write_security)
-        # no investigate the requested state change...
-        try:
-            treid = entity.cw_attr_cache['by_transition']
-        except KeyError:
-            # no transition set, check user is a manager and destination state
-            # is specified (and valid)
-            if not cowpowers:
-                msg = _('mandatory relation')
-                raise validation_error(entity, {('by_transition', 'subject'): msg})
-            deststateeid = entity.cw_attr_cache.get('to_state')
-            if not deststateeid:
-                msg = _('mandatory relation')
-                raise validation_error(entity, {('by_transition', 'subject'): msg})
-            deststate = wf.state_by_eid(deststateeid)
-            if deststate is None:
-                msg = _("state doesn't belong to entity's workflow")
-                raise validation_error(entity, {('to_state', 'subject'): msg})
-        else:
-            # check transition is valid and allowed, unless we're coming back
-            # from subworkflow
-            tr = cnx.entity_from_eid(treid)
-            if swtr is None:
-                qname = ('by_transition', 'subject')
-                if tr is None:
-                    msg = _("transition doesn't belong to entity's workflow")
-                    raise validation_error(entity, {qname: msg})
-                if not tr.has_input_state(fromstate):
-                    msg = _("transition %(tr)s isn't allowed from %(st)s")
-                    raise validation_error(entity, {qname: msg}, {
-                            'tr': tr.name, 'st': fromstate.name}, ['tr', 'st'])
-                if not tr.may_be_fired(foreid):
-                    msg = _("transition may not be fired")
-                    raise validation_error(entity, {qname: msg})
-            deststateeid = entity.cw_attr_cache.get('to_state')
-            if deststateeid is not None:
-                if not cowpowers and deststateeid != tr.destination(forentity).eid:
-                    msg = _("transition isn't allowed")
-                    raise validation_error(entity, {('by_transition', 'subject'): msg})
-                if swtr is None:
-                    deststate = cnx.entity_from_eid(deststateeid)
-                    if not cowpowers and deststate is None:
-                        msg = _("state doesn't belong to entity's workflow")
-                        raise validation_error(entity, {('to_state', 'subject'): msg})
-            else:
-                deststateeid = tr.destination(forentity).eid
-        # everything is ok, add missing information on the trinfo entity
-        entity.cw_edited['from_state'] = fromstate.eid
-        entity.cw_edited['to_state'] = deststateeid
-        nocheck = cnx.transaction_data.setdefault('skip-security', set())
-        nocheck.add((entity.eid, 'from_state', fromstate.eid))
-        nocheck.add((entity.eid, 'to_state', deststateeid))
-        _FireAutotransitionOp(cnx, entity=forentity)
-
-
-class FiredTransitionHook(WorkflowHook):
-    """change related entity state and handle exit of subworkflow"""
-    __regid__ = 'wffiretransition'
-    __select__ = WorkflowHook.__select__ & is_instance('TrInfo')
-    events = ('after_add_entity',)
-
-    def __call__(self):
-        trinfo = self.entity
-        rcache = trinfo.cw_attr_cache
-        _change_state(self._cw, rcache['wf_info_for'], rcache['from_state'],
-                      rcache['to_state'])
-        forentity = self._cw.entity_from_eid(rcache['wf_info_for'])
-        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
-        assert iworkflowable.current_state.eid == rcache['to_state']
-        if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid:
-            _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo)
-
-
-class CheckInStateChangeAllowed(WorkflowHook):
-    """check state apply, in case of direct in_state change using unsafe execute
-    """
-    __regid__ = 'wfcheckinstate'
-    __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
-    events = ('before_add_relation',)
-    category = 'integrity'
-
-    def __call__(self):
-        cnx = self._cw
-        nocheck = cnx.transaction_data.get('skip-security', ())
-        if (self.eidfrom, 'in_state', self.eidto) in nocheck:
-            # state changed through TrInfo insertion, so we already know it's ok
-            return
-        entity = cnx.entity_from_eid(self.eidfrom)
-        iworkflowable = entity.cw_adapt_to('IWorkflowable')
-        mainwf = iworkflowable.main_workflow
-        if mainwf is None:
-            msg = _('entity has no workflow set')
-            raise validation_error(entity, {None: msg})
-        for wf in mainwf.iter_workflows():
-            if wf.state_by_eid(self.eidto):
-                break
-        else:
-            msg = _("state doesn't belong to entity's workflow. You may "
-                    "want to set a custom workflow for this entity first.")
-            raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
-        if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
-            msg = _("state doesn't belong to entity's current workflow")
-            raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
-
-
-class SetModificationDateOnStateChange(WorkflowHook):
-    """update entity's modification date after changing its state"""
-    __regid__ = '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.cw_set(modification_date=datetime.utcnow())
-        except RepositoryError as 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)
-
-
-class CheckWorkflowTransitionExitPoint(WorkflowHook):
-    """check that there is no multiple exits from the same state"""
-    __regid__ = 'wfcheckwftrexit'
-    __select__ = WorkflowHook.__select__ & hook.match_rtype('subworkflow_exit')
-    events = ('after_add_relation',)
-
-    def __call__(self):
-        _CheckTrExitPoint(self._cw, treid=self.eidfrom)
-
-
-class SetCustomWorkflow(WorkflowHook):
-    __regid__ = 'wfsetcustom'
-    __select__ = WorkflowHook.__select__ & hook.match_rtype('custom_workflow')
-    events = ('after_add_relation',)
-
-    def __call__(self):
-        _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=self.eidto)
-
-
-class DelCustomWorkflow(SetCustomWorkflow):
-    __regid__ = 'wfdelcustom'
-    events = ('after_delete_relation',)
-
-    def __call__(self):
-        entity = self._cw.entity_from_eid(self.eidfrom)
-        typewf = entity.cw_adapt_to('IWorkflowable').cwetype_workflow()
-        if typewf is not None:
-            _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid)