Commit 6dc982ce authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

[wf engine] support for subwf exit point with no destination state: go back to...

[wf engine] support for subwf exit point with no destination state: go back to state from which we entered into the subworkflow

--HG--
branch : stable
parent 70dbba754c11
......@@ -158,36 +158,7 @@ class WorkflowTC(EnvBasedTC):
'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 @@ class WorkflowTC(EnvBasedTC):
('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 @@ class WorkflowTC(EnvBasedTC):
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):
......
......@@ -124,19 +124,20 @@ class Workflow(AnyEntity):
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 @@ class WorkflowTransition(BaseTransition):
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'))
def get_exit_point(self, state):
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, 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 @@ class SubWorkflowExitPoint(AnyEntity):
@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 @@ class WorkflowableMixIn(object):
"""
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 @@ class WorkflowableMixIn(object):
# 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 @@ class WorkflowableMixIn(object):
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()
......
......@@ -92,9 +92,10 @@ class Transition(BaseTransition):
"""
__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 @@ class WorkflowTransition(BaseTransition):
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):
......
......@@ -559,7 +559,7 @@ def after_add_trinfo(session, entity):
# 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')
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment