[wf] test and hooks for WorkflowTransition support 3.5
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 25 Aug 2009 18:31:16 +0200
branch3.5
changeset 2992 a5b8bf107a1a
parent 2991 dab951c08896
child 2993 2d06451013ac
[wf] test and hooks for WorkflowTransition support
entities/test/unittest_wfobjs.py
server/hooks.py
--- a/entities/test/unittest_wfobjs.py	Tue Aug 25 18:30:44 2009 +0200
+++ b/entities/test/unittest_wfobjs.py	Tue Aug 25 18:31:16 2009 +0200
@@ -1,12 +1,15 @@
 from cubicweb.devtools.apptest import EnvBasedTC
 from cubicweb import ValidationError
 
-def add_wf(self, etype, name=None):
+def add_wf(self, etype, name=None, default=False):
     if name is None:
-        name = unicode(etype)
-    wf = self.execute('INSERT Workflow X: X name %(n)s', {'n': name}).get_entity(0, 0)
+        name = etype
+    wf = self.execute('INSERT Workflow X: X name %(n)s', {'n': unicode(name)}).get_entity(0, 0)
     self.execute('SET WF workflow_of ET WHERE WF eid %(wf)s, ET name %(et)s',
                  {'wf': wf.eid, 'et': etype})
+    if default:
+        self.execute('SET ET default_workflow WF WHERE WF eid %(wf)s, ET name %(et)s',
+                     {'wf': wf.eid, 'et': etype})
     return wf
 
 def parse_hist(wfhist):
@@ -155,6 +158,102 @@
                      'WHERE T name "deactivate"')
         self._test_stduser_deactivate()
 
+    def test_subworkflow_base(self):
+        """subworkflow
+
+        +-----------+  tr1   +-----------+
+        | swfstate1 | ------>| swfstate2 |
+        +-----------+        +-----------+
+                  |  tr2  +-----------+
+                  `------>| swfstate3 |
+                          +-----------+
+
+        main workflow
+
+        +--------+  swftr1             +--------+
+        | state1 | -------[swfstate2]->| state2 |
+        +--------+     |               +--------+
+                       |               +--------+
+                       `-[swfstate3]-->| state3 |
+                                       +--------+
+        """
+        # sub-workflow
+        swf = add_wf(self, 'CWGroup', name='subworkflow')
+        swfstate1 = swf.add_state(u'swfstate1', initial=True)
+        swfstate2 = swf.add_state(u'swfstate2')
+        swfstate3 = swf.add_state(u'swfstate3')
+        tr1 = swf.add_transition(u'tr1', (swfstate1,), swfstate2)
+        tr2 = swf.add_transition(u'tr2', (swfstate1,), swfstate3)
+        # main workflow
+        mwf = add_wf(self, 'CWGroup', name='main workflow', default=True)
+        state1 = mwf.add_state(u'state1', initial=True)
+        state2 = mwf.add_state(u'state2')
+        state3 = mwf.add_state(u'state3')
+        swftr1 = mwf.add_wftransition(u'swftr1', swf, state1,
+                                      [(swfstate2, state2), (swfstate3, state3)])
+        self.assertEquals(swftr1.destination().eid, swfstate1.eid)
+        # workflows built, begin test
+        self.group = self.add_entity('CWGroup', name=u'grp1')
+        self.commit()
+        self.assertEquals(self.group.current_state.eid, state1.eid)
+        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.subworkflow_input_transition(), None)
+        self.group.fire_transition('swftr1', u'go')
+        self.commit()
+        self.group.clear_all_caches()
+        self.assertEquals(self.group.current_state.eid, swfstate1.eid)
+        self.assertEquals(self.group.current_workflow.eid, swf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.subworkflow_input_transition().eid, swftr1.eid)
+        self.group.fire_transition('tr1', u'go')
+        self.commit()
+        self.group.clear_all_caches()
+        self.assertEquals(self.group.current_state.eid, state2.eid)
+        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.subworkflow_input_transition(), None)
+        # force back to swfstate1 is impossible since we can't any more find
+        # subworkflow input transition
+        ex = self.assertRaises(ValidationError,
+                               self.group.change_state, swfstate1, u'gadget')
+        self.assertEquals(ex.errors, {'to_state': "state doesn't belong to entity's current workflow"})
+        self.rollback()
+        # force back to state1
+        self.group.change_state('state1', u'gadget')
+        self.group.fire_transition('swftr1', u'au')
+        self.group.clear_all_caches()
+        self.group.fire_transition('tr2', u'chapeau')
+        self.commit()
+        self.group.clear_all_caches()
+        self.assertEquals(self.group.current_state.eid, state3.eid)
+        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertListEquals(parse_hist(self.group.workflow_history),
+                              [('state1', 'swfstate1', 'swftr1', 'go'),
+                               ('swfstate1', 'swfstate2', 'tr1', 'go'),
+                               ('swfstate2', 'state2', 'swftr1', 'exiting from subworkflow subworkflow'),
+                               ('state2', 'state1', None, 'gadget'),
+                               ('state1', 'swfstate1', 'swftr1', 'au'),
+                               ('swfstate1', 'swfstate3', 'tr2', 'chapeau'),
+                               ('swfstate3', 'state3', 'swftr1', 'exiting from subworkflow subworkflow'),
+                               ])
+
+    def test_subworkflow_exit_consistency(self):
+        # sub-workflow
+        swf = add_wf(self, 'CWGroup', name='subworkflow')
+        swfstate1 = swf.add_state(u'swfstate1', initial=True)
+        swfstate2 = swf.add_state(u'swfstate2')
+        tr1 = swf.add_transition(u'tr1', (swfstate1,), swfstate2)
+        # main workflow
+        mwf = add_wf(self, 'CWGroup', name='main workflow', default=True)
+        state1 = mwf.add_state(u'state1', initial=True)
+        state2 = mwf.add_state(u'state2')
+        state3 = mwf.add_state(u'state3')
+        mwf.add_wftransition(u'swftr1', swf, state1,
+                             [(swfstate2, state2), (swfstate2, state3)])
+        ex = self.assertRaises(ValidationError, self.commit)
+        self.assertEquals(ex.errors, {'subworkflow_exit': u"can't have multiple exits on the same state"})
 
 
 class CustomWorkflowTC(EnvBasedTC):
--- a/server/hooks.py	Tue Aug 25 18:30:44 2009 +0200
+++ b/server/hooks.py	Tue Aug 25 18:31:16 2009 +0200
@@ -418,6 +418,8 @@
 
 # workflow handling ###########################################################
 
+from cubicweb.entities.wfobjs import WorkflowTransition, WorkflowException
+
 def _change_state(session, x, oldstate, newstate):
     nocheck = session.transaction_data.setdefault('skip-security', set())
     nocheck.add((x, 'in_state', oldstate))
@@ -439,7 +441,8 @@
         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
+    # then check it has a workflow set, unless we're in the process of changing
+    # entity's workflow
     if session.transaction_data.get((forentity.eid, 'customwf')):
         wfeid = session.transaction_data[(forentity.eid, 'customwf')]
         wf = session.entity_from_eid(wfeid)
@@ -453,13 +456,16 @@
     if fromstate is None:
         msg = session._('related entity has no state')
         raise ValidationError(entity.eid, {None: msg})
+    # True if we are coming back from subworkflow
+    swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
+    cowpowers = session.is_super_session or 'managers' in session.user.groups
     # 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):
+        if not cowpowers:
             msg = session._('mandatory relation')
             raise ValidationError(entity.eid, {'by_transition': msg})
         deststateeid = entity.get('to_state')
@@ -467,22 +473,35 @@
             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")
+        if not cowpowers and deststate is None:
+            msg = entity.req._("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
+        # check transition is valid and allowed, unless we're coming back from
+        # subworkflow
+        tr = session.entity_from_eid(treid)
+        if swtr is None:
+            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})
+        if entity.get('to_state'):
+            deststateeid = entity['to_state']
+            if not cowpowers and deststateeid != tr.destination().eid:
+                msg = session._("transition isn't allowed")
+                raise ValidationError(entity.eid, {'by_transition': msg})
+            if swtr is None:
+                deststate = session.entity_from_eid(deststateeid)
+                if not cowpowers and deststate is None:
+                    msg = entity.req._("state doesn't belong to entity's workflow")
+                    raise ValidationError(entity.eid, {'to_state': msg})
+        else:
+            deststateeid = tr.destination().eid
     # everything is ok, add missing information on the trinfo entity
     entity['from_state'] = fromstate.eid
     entity['to_state'] = deststateeid
@@ -490,11 +509,33 @@
     nocheck.add((entity.eid, 'from_state', fromstate.eid))
     nocheck.add((entity.eid, 'to_state', deststateeid))
 
-
 def after_add_trinfo(session, entity):
     """change related entity state"""
     _change_state(session, entity['wf_info_for'],
                   entity['from_state'], entity['to_state'])
+    forentity = session.entity_from_eid(entity['wf_info_for'])
+    assert forentity.current_state.eid == entity['to_state']
+    if forentity.main_workflow.eid != forentity.current_workflow.eid:
+        # we're in a subworkflow, check if we've reached an exit point
+        wftr = forentity.subworkflow_input_transition()
+        if wftr is None:
+            # inconsistency detected
+            msg = entity.req._("state doesn't belong to entity's current workflow")
+            raise ValidationError(entity.eid, {'to_state': msg})
+        tostate = wftr.get_exit_point(entity['to_state'])
+        if tostate is not None:
+            # reached an exit point
+            msg = session._('exiting from subworkflow %s')
+            msg %= session._(forentity.current_workflow.name)
+            session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
+            # XXX iirk
+            req = forentity.req
+            forentity.req = session.super_session
+            try:
+                trinfo = forentity.change_state(tostate, msg, u'text/plain',
+                                                tr=wftr)
+            finally:
+                forentity.req = req
 
 
 class SetInitialStateOp(PreCommitOperation):
@@ -520,11 +561,11 @@
 
 
 def before_add_in_state(session, eidfrom, rtype, eidto):
-    """check state apply"""
+    """check state apply, in case of direct in_state change using unsafe_execute
+    """
     nocheck = session.transaction_data.setdefault('skip-security', ())
     if (eidfrom, 'in_state', eidto) in nocheck:
         # state changed through TrInfo insertion, so we already know it's ok
-        print 'skip in_state check'
         return
     entity = session.entity_from_eid(eidfrom)
     mainwf = entity.main_workflow
@@ -538,6 +579,25 @@
         msg = session._("state doesn't belong to entity's workflow. You may "
                         "want to set a custom workflow for this entity first.")
         raise ValidationError(eidfrom, {'in_state': msg})
+    if entity.current_workflow and wf.eid != entity.current_workflow.eid:
+        msg = session._("state doesn't belong to entity's current workflow")
+        raise ValidationError(eidfrom, {'in_state': msg})
+
+
+class CheckTrExitPoint(PreCommitOperation):
+
+    def precommit_event(self):
+        tr = self.session.entity_from_eid(self.treid)
+        outputs = set()
+        for ep in tr.subworkflow_exit:
+            if ep.subwf_state.eid in outputs:
+                msg = self.session._("can't have multiple exits on the same state")
+                raise ValidationError(self.treid, {'subworkflow_exit': msg})
+            outputs.add(ep.subwf_state.eid)
+
+
+def after_add_subworkflow_exit(session, eidfrom, rtype, eidto):
+    CheckTrExitPoint(session, treid=eidfrom)
 
 
 class WorkflowChangedOp(PreCommitOperation):
@@ -571,7 +631,7 @@
             msg = session._('workflow changed to "%s"')
             msg %= session._(mainwf.name)
             session.transaction_data[(entity.eid, 'customwf')] = self.wfeid
-            entity.change_state(deststate, msg)
+            entity.change_state(deststate, msg, u'text/plain')
 
 
 def set_custom_workflow(session, eidfrom, rtype, eidto):
@@ -605,6 +665,7 @@
         hm.register_hook(del_custom_workflow, 'after_delete_relation', 'custom_workflow')
         hm.register_hook(after_del_workflow, 'after_delete_entity', 'Workflow')
         hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state')
+        hm.register_hook(after_add_subworkflow_exit, 'after_add_relation', 'subworkflow_exit')
 
 
 # CWProperty hooks #############################################################