# HG changeset patch # User Sylvain Thénault # Date 1250837155 -7200 # Node ID a2aa2c51f3bed6d060a353b67f3e984651414e2b # Parent d3cd8bd20ee5f727c0bceb80a60b11ebfdcfe443 test and implements workflow changes diff -r d3cd8bd20ee5 -r a2aa2c51f3be entities/test/unittest_wfobjs.py --- a/entities/test/unittest_wfobjs.py Fri Aug 21 08:45:16 2009 +0200 +++ b/entities/test/unittest_wfobjs.py Fri Aug 21 08:45:55 2009 +0200 @@ -1,11 +1,24 @@ from cubicweb.devtools.apptest import EnvBasedTC from cubicweb import ValidationError +def add_wf(self, etype, name=None): + if name is None: + name = unicode(etype) + wf = self.execute('INSERT Workflow X: X name %(n)s', {'n': 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}) + return wf + +def parse_hist(wfhist): + return [(ti.previous_state.name, ti.new_state.name, + ti.transition and ti.transition.name, ti.comment) + for ti in wfhist] + + class WorkflowBuildingTC(EnvBasedTC): def test_wf_construction(self): - wf = self.execute('INSERT Workflow X: X name "test"').get_entity(0, 0) - self.execute('SET WF workflow_of ET WHERE ET name "Company"') + wf = add_wf(self, 'Company') foo = wf.add_state(u'foo', initial=True) bar = wf.add_state(u'bar') self.assertEquals(wf.state_by_name('bar').eid, bar.eid) @@ -16,8 +29,7 @@ self.assertEquals(baz.require_group[0].name, 'managers') def test_duplicated_state(self): - wf = self.execute('INSERT Workflow X: X name "test"').get_entity(0, 0) - self.execute('SET WF workflow_of ET WHERE ET name "Company"') + wf = add_wf(self, 'Company') wf.add_state(u'foo', initial=True) wf.add_state(u'foo') ex = self.assertRaises(ValidationError, self.commit) @@ -25,8 +37,7 @@ self.assertEquals(ex.errors, {'state_of': 'unique constraint S name N, Y state_of O, Y name N failed'}) def test_duplicated_transition(self): - wf = self.execute('INSERT Workflow X: X name "test"').get_entity(0, 0) - self.execute('SET WF workflow_of ET WHERE ET name "Company"') + wf = add_wf(self, 'Company') foo = wf.add_state(u'foo', initial=True) bar = wf.add_state(u'bar') wf.add_transition(u'baz', (foo,), bar, ('managers',)) @@ -187,6 +198,112 @@ self.assertEquals(len(transitions), 1) self.assertEquals(transitions[0].name, 'tr1') +class CustomWorkflowTC(EnvBasedTC): + + def setup_database(self): + self.member = self.create_user('member') + + def tearDown(self): + super(CustomWorkflowTC, self).tearDown() + self.execute('DELETE X custom_workflow WF') + + def test_custom_wf_replace_state_no_history(self): + """member in inital state with no previous history, state is simply + redirected when changing workflow + """ + wf = add_wf(self, 'CWUser') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.member.clear_all_caches() + self.assertEquals(self.member.state, 'activated')# no change before commit + self.commit() + self.member.clear_all_caches() + self.assertEquals(self.member.current_workflow.eid, wf.eid) + self.assertEquals(self.member.state, 'asleep') + self.assertEquals(self.member.workflow_history, []) + + def test_custom_wf_replace_state_keep_history(self): + """member in inital state with some history, state is redirected and + state change is recorded to history + """ + self.member.fire_transition('deactivate') + self.member.fire_transition('activate') + wf = add_wf(self, 'CWUser') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.commit() + self.member.clear_all_caches() + self.assertEquals(self.member.current_workflow.eid, wf.eid) + self.assertEquals(self.member.state, 'asleep') + self.assertEquals(parse_hist(self.member.workflow_history), + [('activated', 'deactivated', 'deactivate', None), + ('deactivated', 'activated', 'activate', None), + ('activated', 'asleep', None, 'workflow changed to "CWUser"')]) + + def test_custom_wf_shared_state(self): + """member in some state shared by the new workflow, nothing has to be + done + """ + self.member.fire_transition('deactivate') + self.assertEquals(self.member.state, 'deactivated') + wf = add_wf(self, 'CWUser') + wf.add_state('asleep', initial=True) + self.execute('SET S state_of WF WHERE S name "deactivated", WF eid %(wf)s', + {'wf': wf.eid}) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.commit() + self.member.clear_all_caches() + self.assertEquals(self.member.current_workflow.eid, wf.eid) + self.assertEquals(self.member.state, 'deactivated') + self.assertEquals(parse_hist(self.member.workflow_history), + [('activated', 'deactivated', 'deactivate', None)]) + + def test_custom_wf_no_initial_state(self): + """try to set a custom workflow which has no initial state""" + self.member.fire_transition('deactivate') + wf = add_wf(self, 'CWUser') + wf.add_state('asleep') + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + ex = self.assertRaises(ValidationError, self.commit) + self.assertEquals(ex.errors, {'custom_workflow': u'workflow has no initial state'}) + + def test_custom_wf_bad_etype(self): + """try to set a custom workflow which has no initial state""" + self.member.fire_transition('deactivate') + wf = add_wf(self, 'Company') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + ex = self.assertRaises(ValidationError, self.commit) + self.assertEquals(ex.errors, {'custom_workflow': 'constraint S is ET, O workflow_of ET failed'}) + + def test_del_custom_wf(self): + """member in some state shared by the new workflow, nothing has to be + done + """ + self.member.fire_transition('deactivate') + wf = add_wf(self, 'CWUser') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.commit() + self.execute('DELETE X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.member.clear_all_caches() + self.assertEquals(self.member.state, 'asleep')# no change before commit + self.commit() + self.member.clear_all_caches() + self.assertEquals(self.member.current_workflow.name, "CWUser workflow") + self.assertEquals(self.member.state, 'activated') + self.assertEquals(parse_hist(self.member.workflow_history), + [('activated', 'deactivated', 'deactivate', None), + ('deactivated', 'asleep', None, 'workflow changed to "CWUser"'), + ('asleep', 'activated', None, 'workflow changed to "CWUser workflow"'),]) + from cubicweb.devtools.apptest import RepositoryBasedTC diff -r d3cd8bd20ee5 -r a2aa2c51f3be server/hooks.py --- a/server/hooks.py Fri Aug 21 08:45:16 2009 +0200 +++ b/server/hooks.py Fri Aug 21 08:45:55 2009 +0200 @@ -418,6 +418,15 @@ # workflow handling ########################################################### +def _change_state(session, x, oldstate, newstate): + nocheck = session.transaction_data.setdefault('skip-security', set()) + nocheck.add((x, 'in_state', oldstate)) + nocheck.add((x, 'in_state', newstate)) + # delete previous state first in case we're using a super session + session.delete_relation(x, 'in_state', oldstate) + session.add_relation(x, 'in_state', newstate) + + def before_add_trinfo(session, entity): """check the transition is allowed, add missing information. Expect that: * wf_info_for inlined relation is set @@ -477,16 +486,11 @@ 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""" - # need to delete previous state first, not done automatically since - # we're using a super session - session.unsafe_execute('DELETE X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': entity['wf_info_for'], 's': entity['from_state']}, - ('x', 's')) - session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': entity['wf_info_for'], 's': entity['to_state']}, - ('x', 's')) + _change_state(session, entity['wf_info_for'], + entity['from_state'], entity['to_state']) class SetInitialStateOp(PreCommitOperation): @@ -510,6 +514,48 @@ def set_initial_state_after_add(session, entity): SetInitialStateOp(session, entity=entity) + +class WorkflowChangedOp(PreCommitOperation): + """fix entity current state when changing its workflow""" + + def precommit_event(self): + session = self.session + pendingeids = session.transaction_data.get('pendingeids', ()) + if self.eid in pendingeids: + return + entity = session.entity_from_eid(self.eid) + # notice that enforcment that new workflow apply to the entity's type is + # done by schema rule, no need to check it here + if entity.current_workflow.eid == self.wfeid: + deststate = entity.current_workflow.initial + if not deststate: + msg = session._('workflow has no initial state') + raise ValidationError(entity.eid, {'custom_workflow': msg}) + if entity.current_workflow.state_by_eid(entity.current_state.eid): + # nothing to do + return + # if there are no history, simply go to new workflow's initial state + if not entity.workflow_history: + if entity.current_state.eid != deststate.eid: + _change_state(session, entity.eid, + entity.current_state.eid, deststate.eid) + return + msg = session._('workflow changed to "%s"') + msg %= entity.current_workflow.name + entity.change_state(deststate.name, msg) + + +def set_custom_workflow(session, eidfrom, rtype, eidto): + WorkflowChangedOp(session, eid=eidfrom, wfeid=eidto) + + +def del_custom_workflow(session, eidfrom, rtype, eidto): + entity = session.entity_from_eid(eidfrom) + typewf = entity.cwetype_workflow() + if typewf is not None: + WorkflowChangedOp(session, eid=eidfrom, wfeid=typewf.eid) + + def after_del_workflow(session, eid): # workflow cleanup session.execute('DELETE State X WHERE NOT X state_of Y') @@ -526,6 +572,8 @@ if 'in_state' in eschema.subject_relations(): hm.register_hook(set_initial_state_after_add, 'after_add_entity', str(eschema)) + hm.register_hook(set_custom_workflow, 'after_add_relation', 'custom_workflow') + hm.register_hook(del_custom_workflow, 'after_delete_relation', 'custom_workflow') hm.register_hook(after_del_workflow, 'after_delete_entity', 'Workflow')