[wf engine] support for subwf exit point with no destination state: go back to state from which we entered into the subworkflow stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 09 Oct 2009 16:31:06 +0200
branchstable
changeset 3628 440931181322
parent 3627 70dbba754c11
child 3629 559cad62c786
child 3631 6176ef2f6488
[wf engine] support for subwf exit point with no destination state: go back to state from which we entered into the subworkflow
entities/test/unittest_wfobjs.py
entities/wfobjs.py
schemas/workflow.py
server/hooks.py
--- a/entities/test/unittest_wfobjs.py	Fri Oct 09 15:53:42 2009 +0200
+++ b/entities/test/unittest_wfobjs.py	Fri Oct 09 16:31:06 2009 +0200
@@ -158,36 +158,7 @@
                      'WHERE T name "deactivate"')
         self._test_stduser_deactivate()
 
-    def test_swf_fire_in_a_row(self):
-        # sub-workflow
-        subwf = add_wf(self, 'CWGroup', name='subworkflow')
-        xsigning = subwf.add_state('xsigning', initial=True)
-        xaborted = subwf.add_state('xaborted')
-        xsigned = subwf.add_state('xsigned')
-        xabort = subwf.add_transition('xabort', (xsigning,), xaborted)
-        xsign = subwf.add_transition('xsign', (xsigning,), xsigning)
-        xcomplete = subwf.add_transition('xcomplete', (xsigning,), xsigned,
-                                         type=u'auto')
-        # main workflow
-        twf = add_wf(self, 'CWGroup', name='mainwf', default=True)
-        created    = twf.add_state(_('created'), initial=True)
-        identified = twf.add_state(_('identified'))
-        released   = twf.add_state(_('released'))
-        closed   = twf.add_state(_('closed'))
-        twf.add_wftransition(_('identify'), subwf, (created,),
-                             [(xsigned, identified), (xaborted, created)])
-        twf.add_wftransition(_('release'), subwf, (identified,),
-                             [(xsigned, released), (xaborted, identified)])
-        twf.add_wftransition(_('close'), subwf, (released,),
-                             [(xsigned, closed), (xaborted, released)])
-        self.commit()
-        group = self.add_entity('CWGroup', name=u'grp1')
-        self.commit()
-        for trans in ('identify', 'release', 'close'):
-            group.fire_transition(trans)
-            self.commit()
-
-    def test_subworkflow_base(self):
+    def test_swf_base(self):
         """subworkflow
 
         +-----------+  tr1   +-----------+
@@ -268,7 +239,7 @@
                                ('swfstate3', 'state3', 'swftr1', 'exiting from subworkflow subworkflow'),
                                ])
 
-    def test_subworkflow_exit_consistency(self):
+    def test_swf_exit_consistency(self):
         # sub-workflow
         swf = add_wf(self, 'CWGroup', name='subworkflow')
         swfstate1 = swf.add_state(u'swfstate1', initial=True)
@@ -284,6 +255,68 @@
         ex = self.assertRaises(ValidationError, self.commit)
         self.assertEquals(ex.errors, {'subworkflow_exit': u"can't have multiple exits on the same state"})
 
+    def test_swf_fire_in_a_row(self):
+        # sub-workflow
+        subwf = add_wf(self, 'CWGroup', name='subworkflow')
+        xsigning = subwf.add_state('xsigning', initial=True)
+        xaborted = subwf.add_state('xaborted')
+        xsigned = subwf.add_state('xsigned')
+        xabort = subwf.add_transition('xabort', (xsigning,), xaborted)
+        xsign = subwf.add_transition('xsign', (xsigning,), xsigning)
+        xcomplete = subwf.add_transition('xcomplete', (xsigning,), xsigned,
+                                         type=u'auto')
+        # main workflow
+        twf = add_wf(self, 'CWGroup', name='mainwf', default=True)
+        created    = twf.add_state(_('created'), initial=True)
+        identified = twf.add_state(_('identified'))
+        released   = twf.add_state(_('released'))
+        closed   = twf.add_state(_('closed'))
+        twf.add_wftransition(_('identify'), subwf, (created,),
+                             [(xsigned, identified), (xaborted, created)])
+        twf.add_wftransition(_('release'), subwf, (identified,),
+                             [(xsigned, released), (xaborted, identified)])
+        twf.add_wftransition(_('close'), subwf, (released,),
+                             [(xsigned, closed), (xaborted, released)])
+        self.commit()
+        group = self.add_entity('CWGroup', name=u'grp1')
+        self.commit()
+        for trans in ('identify', 'release', 'close'):
+            group.fire_transition(trans)
+            self.commit()
+
+
+    def test_swf_magic_tr(self):
+        # sub-workflow
+        subwf = add_wf(self, 'CWGroup', name='subworkflow')
+        xsigning = subwf.add_state('xsigning', initial=True)
+        xaborted = subwf.add_state('xaborted')
+        xsigned = subwf.add_state('xsigned')
+        xabort = subwf.add_transition('xabort', (xsigning,), xaborted)
+        xsign = subwf.add_transition('xsign', (xsigning,), xsigned)
+        # main workflow
+        twf = add_wf(self, 'CWGroup', name='mainwf', default=True)
+        created    = twf.add_state(_('created'), initial=True)
+        identified = twf.add_state(_('identified'))
+        released   = twf.add_state(_('released'))
+        twf.add_wftransition(_('identify'), subwf, created,
+                             [(xaborted, None), (xsigned, identified)])
+        twf.add_wftransition(_('release'), subwf, identified,
+                             [(xaborted, None)])
+        self.commit()
+        group = self.add_entity('CWGroup', name=u'grp1')
+        self.commit()
+        for trans, nextstate in (('identify', 'xsigning'),
+                                 ('xabort', 'created'),
+                                 ('identify', 'xsigning'),
+                                 ('xsign', 'identified'),
+                                 ('release', 'xsigning'),
+                                 ('xabort', 'identified')
+                                 ):
+            group.fire_transition(trans)
+            self.commit()
+            group.clear_all_caches()
+            self.assertEquals(group.state, nextstate)
+
 
 class CustomWorkflowTC(EnvBasedTC):
 
--- a/entities/wfobjs.py	Fri Oct 09 15:53:42 2009 +0200
+++ b/entities/wfobjs.py	Fri Oct 09 16:31:06 2009 +0200
@@ -124,19 +124,20 @@
         tr.set_transition_permissions(requiredgroups, conditions, reset=False)
         return tr
 
-    def add_transition(self, name, fromstates, tostate,
+    def add_transition(self, name, fromstates, tostate=None,
                        requiredgroups=(), conditions=(), **kwargs):
         """add a transition to this workflow from some state(s) to another"""
         tr = self._add_transition('Transition', name, fromstates,
                                   requiredgroups, conditions, **kwargs)
-        if hasattr(tostate, 'eid'):
-            tostate = tostate.eid
-        self.req.execute('SET T destination_state S '
-                         'WHERE S eid %(s)s, T eid %(t)s',
-                         {'t': tr.eid, 's': tostate}, ('s', 't'))
+        if tostate is not None:
+            if hasattr(tostate, 'eid'):
+                tostate = tostate.eid
+            self.req.execute('SET T destination_state S '
+                             'WHERE S eid %(s)s, T eid %(t)s',
+                             {'t': tr.eid, 's': tostate}, ('s', 't'))
         return tr
 
-    def add_wftransition(self, name, subworkflow, fromstates, exitpoints,
+    def add_wftransition(self, name, subworkflow, fromstates, exitpoints=(),
                          requiredgroups=(), conditions=(), **kwargs):
         """add a workflow transition to this workflow"""
         tr = self._add_transition('WorkflowTransition', name, fromstates,
@@ -257,28 +258,37 @@
     def add_exit_point(self, fromstate, tostate):
         if hasattr(fromstate, 'eid'):
             fromstate = fromstate.eid
-        if hasattr(tostate, 'eid'):
-            tostate = tostate.eid
-        self.req.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, '
-                         'X subworkflow_state FS, X destination_state TS '
-                         'WHERE T eid %(t)s, FS eid %(fs)s, TS eid %(ts)s',
-                         {'t': self.eid, 'fs': fromstate, 'ts': tostate},
-                         ('t', 'fs', 'ts'))
+        if tostate is None:
+            self.req.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, '
+                             'X subworkflow_state FS WHERE T eid %(t)s, FS eid %(fs)s',
+                             {'t': self.eid, 'fs': fromstate}, ('t', 'fs'))
+        else:
+            if hasattr(tostate, 'eid'):
+                tostate = tostate.eid
+            self.req.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, '
+                             'X subworkflow_state FS, X destination_state TS '
+                             'WHERE T eid %(t)s, FS eid %(fs)s, TS eid %(ts)s',
+                             {'t': self.eid, 'fs': fromstate, 'ts': tostate},
+                             ('t', 'fs', 'ts'))
 
-    def get_exit_point(self, state):
+    def get_exit_point(self, entity, stateeid):
         """if state is an exit point, return its associated destination state"""
-        if hasattr(state, 'eid'):
-            state = state.eid
-        stateeid = self.exit_points().get(state)
-        if stateeid is not None:
-            return self.req.entity_from_eid(stateeid)
-        return None
+        if hasattr(stateeid, 'eid'):
+            stateeid = stateeid.eid
+        try:
+            tostateeid = self.exit_points()[stateeid]
+        except KeyError:
+            return None
+        if tostateeid is None:
+            # go back to state from which we've entered the subworkflow
+            return entity.subworkflow_input_trinfo().previous_state
+        return self.req.entity_from_eid(tostateeid)
 
     @cached
     def exit_points(self):
         result = {}
         for ep in self.subworkflow_exit:
-            result[ep.subwf_state.eid] = ep.destination.eid
+            result[ep.subwf_state.eid] = ep.destination and ep.destination.eid
         return result
 
     def clear_all_caches(self):
@@ -296,7 +306,7 @@
 
     @property
     def destination(self):
-        return self.destination_state[0]
+        return self.destination_state and self.destination_state[0] or None
 
 
 class State(AnyEntity):
@@ -457,8 +467,9 @@
         """
         assert self.current_workflow
         if isinstance(tr, basestring):
-            tr = self.current_workflow.transition_by_name(tr)
-        assert tr is not None, 'not a %s transition: %s' % (self.id, tr)
+            _tr = self.current_workflow.transition_by_name(tr)
+            assert _tr is not None, 'not a %s transition: %s' % (self.id, tr)
+            tr = _tr
         return self._add_trinfo(comment, commentformat, tr.eid)
 
     def change_state(self, statename, comment=None, commentformat=None, tr=None):
@@ -483,8 +494,9 @@
         # XXX try to find matching transition?
         return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid)
 
-    def subworkflow_input_transition(self):
-        """return the transition which has went through the current sub-workflow
+    def subworkflow_input_trinfo(self):
+        """return the TrInfo which has be recorded when this entity went into
+        the current sub-workflow
         """
         if self.main_workflow.eid == self.current_workflow.eid:
             return # doesn't make sense
@@ -503,7 +515,12 @@
                     subwfentries.append(trinfo)
         if not subwfentries:
             return None
-        return subwfentries[-1].transition
+        return subwfentries[-1]
+
+    def subworkflow_input_transition(self):
+        """return the transition which has went through the current sub-workflow
+        """
+        return getattr(self.subworkflow_input_trinfo(), 'transition', None)
 
     def clear_all_caches(self):
         super(WorkflowableMixIn, self).clear_all_caches()
--- a/schemas/workflow.py	Fri Oct 09 15:53:42 2009 +0200
+++ b/schemas/workflow.py	Fri Oct 09 16:31:06 2009 +0200
@@ -92,9 +92,10 @@
     """
     __specializes_schema__ = True
 
-    destination_state = SubjectRelation('State', cardinality='1*',
-                                        constraints=[RQLConstraint('S transition_of WF, O state_of WF')],
-                                        description=_('destination state for this transition'))
+    destination_state = SubjectRelation(
+        'State', cardinality='1*',
+        constraints=[RQLConstraint('S transition_of WF, O state_of WF')],
+        description=_('destination state for this transition'))
 
 
 class WorkflowTransition(BaseTransition):
@@ -103,18 +104,23 @@
 
     subworkflow = SubjectRelation('Workflow', cardinality='1*',
                                   constraints=[RQLConstraint('S transition_of WF, WF workflow_of ET, O workflow_of ET')])
-    subworkflow_exit = SubjectRelation('SubWorkflowExitPoint', cardinality='+1',
+    # XXX use exit_of and inline it
+    subworkflow_exit = SubjectRelation('SubWorkflowExitPoint', cardinality='*1',
                                        composite='subject')
 
 
 class SubWorkflowExitPoint(EntityType):
     """define how we get out from a sub-workflow"""
-    subworkflow_state = SubjectRelation('State', cardinality='1*',
-                                        constraints=[RQLConstraint('T subworkflow_exit S, T subworkflow WF, O state_of WF')],
-                                        description=_('subworkflow state'))
-    destination_state = SubjectRelation('State', cardinality='1*',
-                                        constraints=[RQLConstraint('T subworkflow_exit S, T transition_of WF, O state_of WF')],
-                                        description=_('destination state'))
+    subworkflow_state = SubjectRelation(
+        'State', cardinality='1*',
+        constraints=[RQLConstraint('T subworkflow_exit S, T subworkflow WF, O state_of WF')],
+        description=_('subworkflow state'))
+    destination_state = SubjectRelation(
+        'State', cardinality='?*',
+        constraints=[RQLConstraint('T subworkflow_exit S, T transition_of WF, O state_of WF')],
+        description=_('destination state. No destination state means that transition '
+                      'should go back to the state from which we\'ve entered the '
+                      'subworkflow.'))
 
 
 class TrInfo(EntityType):
--- a/server/hooks.py	Fri Oct 09 15:53:42 2009 +0200
+++ b/server/hooks.py	Fri Oct 09 16:31:06 2009 +0200
@@ -559,7 +559,7 @@
             # 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'])
+        tostate = wftr.get_exit_point(forentity, entity['to_state'])
         if tostate is not None:
             # reached an exit point
             msg = session._('exiting from subworkflow %s')