--- 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):
--- 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 #############################################################