# HG changeset patch # User Sylvain Thénault # Date 1251217876 -7200 # Node ID a5b8bf107a1a60d5325129cd8d3fa760e59ce7b6 # Parent dab951c088966edfdb8f6c873a952aaaf32e49df [wf] test and hooks for WorkflowTransition support diff -r dab951c08896 -r a5b8bf107a1a entities/test/unittest_wfobjs.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): diff -r dab951c08896 -r a5b8bf107a1a server/hooks.py --- 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 #############################################################