hooks/workflow.py
changeset 2968 0e3460341023
parent 2880 bfc8e1831290
child 3024 bfaf056f1029
--- a/hooks/workflow.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/hooks/workflow.py	Fri Aug 21 16:26:20 2009 +0200
@@ -15,31 +15,13 @@
 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))
+def _change_state(session, x, oldstate, newstate):
+    nocheck = session.transaction_data.setdefault('skip-security', set())
+    nocheck.add((x, 'in_state', oldstate))
+    nocheck.add((x, 'in_state', newstate))
+    # delete previous state first in case we're using a super session
+    session.delete_relation(x, 'in_state', oldstate)
+    session.add_relation(x, 'in_state', newstate)
 
 
 class _SetInitialStateOp(hook.Operation):
@@ -50,11 +32,44 @@
         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])
+        pendingeids = session.transaction_data.get('pendingeids', ())
+        if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
+               and entity.current_workflow:
+            state = entity.current_workflow.initial
+            if state:
+                # use super session to by-pass security checks
+                session.super_session.add_relation(entity.eid, 'in_state',
+                                                   state.eid)
+
+class _WorkflowChangedOp(hook.Operation):
+    """fix entity current state when changing its workflow"""
+
+    def precommit_event(self):
+        session = self.session
+        if session.deleted_in_transaction(self.eid):
+            return
+        entity = session.entity_from_eid(self.eid)
+        # notice that enforcment that new workflow apply to the entity's type is
+        # done by schema rule, no need to check it here
+        if entity.current_workflow.eid == self.wfeid:
+            deststate = entity.current_workflow.initial
+            if not deststate:
+                msg = session._('workflow has no initial state')
+                raise ValidationError(entity.eid, {'custom_workflow': msg})
+            if entity.current_workflow.state_by_eid(entity.current_state.eid):
+                # nothing to do
+                return
+            # if there are no history, simply go to new workflow's initial state
+            if not entity.workflow_history:
+                if entity.current_state.eid != deststate.eid:
+                    _change_state(session, entity.eid,
+                                  entity.current_state.eid, deststate.eid)
+                return
+            msg = session._('workflow changed to "%s"')
+            msg %= entity.current_workflow.name
+            entity.change_state(deststate.name, msg)
+
+
 
 class WorkflowHook(hook.Hook):
     __abstract__ = True
@@ -81,52 +96,82 @@
             (self.eidfrom, self.rtype, self.eidto))
 
 
-class FireTransitionHook(PrepareStateChangeHook):
-    """check the transition is allowed and record transition information"""
+class FireTransitionHook(WorkflowHook):
+    """check the transition is allowed, add missing information. Expect that:
+    * wf_info_for inlined relation is set
+    * by_transition or to_state (managers only) inlined relation is set
+    """
     __id__ = 'wffiretransition'
-    events = ('before_add_relation',)
+    __select__ = WorkflowHook.__select__ & entity_implements('TrInfo')
+    events = ('before_add_entity',)
 
     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')
+        entity = self.entity
+        # first retreive entity to which the state change apply
+        try:
+            foreid = entity['wf_info_for']
+        except KeyError:
+            msg = session._('mandatory relation')
+            raise ValidationError(entity.eid, {'wf_info_for': msg})
+        forentity = session.entity_from_eid(foreid)
+        # then check it has a workflow set
+        wf = forentity.current_workflow
+        if wf is None:
+            msg = session._('related entity has no workflow set')
+            raise ValidationError(entity.eid, {None: msg})
+        # then check it has a state set
+        fromstate = forentity.current_state
+        if fromstate is None:
+            msg = session._('related entity has no state')
+            raise ValidationError(entity.eid, {None: msg})
+        # no investigate the requested state change...
+        try:
+            treid = entity['by_transition']
+        except KeyError:
+            # no transition set, check user is a manager and destination state is
+            # specified (and valid)
+            if not (session.is_super_session or 'managers' in session.user.groups):
+                msg = session._('mandatory relation')
+                raise ValidationError(entity.eid, {'by_transition': msg})
+            deststateeid = entity.get('to_state')
+            if not deststateeid:
+                msg = session._('mandatory relation')
+                raise ValidationError(entity.eid, {'by_transition': msg})
+            deststate = wf.state_by_eid(deststateeid)
+            if deststate is None:
+                msg = session._("state doesn't belong to entity's workflow")
+                raise ValidationError(entity.eid, {'to_state': msg})
+        else:
+            # check transition is valid and allowed
+            tr = wf.transition_by_eid(treid)
+            if tr is None:
+                msg = session._("transition doesn't belong to entity's workflow")
+                raise ValidationError(entity.eid, {'by_transition': msg})
+            if not tr.has_input_state(fromstate):
+                msg = session._("transition isn't allowed")
+                raise ValidationError(entity.eid, {'by_transition': msg})
+            if not tr.may_be_fired(foreid):
+                msg = session._("transition may not be fired")
+                raise ValidationError(entity.eid, {'by_transition': msg})
+            deststateeid = tr.destination().eid
+        # everything is ok, add missing information on the trinfo entity
+        entity['from_state'] = fromstate.eid
+        entity['to_state'] = deststateeid
+        nocheck = session.transaction_data.setdefault('skip-security', set())
+        nocheck.add((entity.eid, 'from_state', fromstate.eid))
+        nocheck.add((entity.eid, 'to_state', deststateeid))
+
+
+class FiredTransitionHook(WorkflowHook):
+    """change related entity state"""
+    __id__ = 'wffiretransition'
+    __select__ = WorkflowHook.__select__ & entity_implements('TrInfo')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        _change_state(self._cw, self.entity['wf_info_for'],
+                      self.entity['from_state'], self.entity['to_state'])
 
 
 class SetModificationDateOnStateChange(WorkflowHook):
@@ -147,3 +192,36 @@
             # 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 SetCustomWorkflow(WorkflowHook):
+    __id__ = '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):
+    __id__ = 'wfdelcustom'
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        entity = self._cw.entity_from_eid(self.eidfrom)
+        typewf = entity.cwetype_workflow()
+        if typewf is not None:
+            _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid)
+
+
+
+class DelWorkflowHook(WorkflowHook):
+    __id__ = 'wfdel'
+    __select__ = WorkflowHook.__select__ & entity_implements('Workflow')
+    events = ('after_delete_entity',)
+
+    def __call__(self):
+        # cleanup unused state and transition
+        self._cw.execute('DELETE State X WHERE NOT X state_of Y')
+        self._cw.execute('DELETE Transition X WHERE NOT X transition_of Y')
+