test and implements workflow changes 3.5
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 21 Aug 2009 08:45:55 +0200
branch3.5
changeset 2949 a2aa2c51f3be
parent 2948 d3cd8bd20ee5
child 2950 643570a7770f
child 2952 6e871df30058
test and implements workflow changes
entities/test/unittest_wfobjs.py
server/hooks.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
 
--- 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')