[wf engine] support for subwf exit point with no destination state: go back to state from which we entered into the subworkflow
--- 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')