# HG changeset patch # User Sylvain Thénault # Date 1255098666 -7200 # Node ID 4409311813224858c5ffc20ebb9b4964577f6ec0 # Parent 70dbba754c1194864bb7c7294e52a03abe361195 [wf engine] support for subwf exit point with no destination state: go back to state from which we entered into the subworkflow diff -r 70dbba754c11 -r 440931181322 entities/test/unittest_wfobjs.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): diff -r 70dbba754c11 -r 440931181322 entities/wfobjs.py --- 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() diff -r 70dbba754c11 -r 440931181322 schemas/workflow.py --- 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): diff -r 70dbba754c11 -r 440931181322 server/hooks.py --- 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')