# HG changeset patch # User Sylvain Thénault # Date 1250783876 -7200 # Node ID 17224e90a1c40fccb3c8ed756121e9d8883b02b0 # Parent b5aadbd3fc5ba9be9ad5553600a38d5be16c07dc# Parent d7c23b2c75383cb7a3671f74e039a58e8ad4d9f5 backport stable branch diff -r d7c23b2c7538 -r 17224e90a1c4 __init__.py --- a/__init__.py Thu Aug 20 17:57:31 2009 +0200 +++ b/__init__.py Thu Aug 20 17:57:56 2009 +0200 @@ -121,6 +121,27 @@ raise KeyError def set_entity_cache(self, entity): pass + + def create_entity(self, etype, *args, **kwargs): + """add a new entity of the given type""" + rql = 'INSERT %s X' % etype + relations = [] + restrictions = [] + cachekey = [] + for rtype, rvar in args: + relations.append('X %s %s' % (rtype, rvar)) + restrictions.append('%s eid %%(%s)s' % (rvar, rvar)) + cachekey.append(rvar) + for attr in kwargs: + if attr in cachekey: + continue + relations.append('X %s %%(%s)s' % (attr, attr)) + if relations: + rql = '%s: %s' % (rql, ', '.join(relations)) + if restrictions: + rql = '%s WHERE %s' % (rql, ', '.join(restrictions)) + return self.execute(rql, kwargs, cachekey).get_entity(0, 0) + # url generation methods ################################################## def build_url(self, *args, **kwargs): diff -r d7c23b2c7538 -r 17224e90a1c4 common/mixins.py --- a/common/mixins.py Thu Aug 20 17:57:31 2009 +0200 +++ b/common/mixins.py Thu Aug 20 17:57:56 2009 +0200 @@ -13,7 +13,7 @@ from cubicweb import typed_eid from cubicweb.selectors import implements -from cubicweb.interfaces import IWorkflowable, IEmailable, ITree +from cubicweb.interfaces import IEmailable, ITree class TreeMixIn(object): @@ -158,97 +158,6 @@ return self.req.entity_from_eid(self.path()[0]) -class WorkflowableMixIn(object): - """base mixin providing workflow helper methods for workflowable entities. - This mixin will be automatically set on class supporting the 'in_state' - relation (which implies supporting 'wf_info_for' as well) - """ - __implements__ = (IWorkflowable,) - - @property - def state(self): - try: - return self.in_state[0].name - except IndexError: - self.warning('entity %s has no state', self) - return None - - @property - def displayable_state(self): - return self.req._(self.state) - - def wf_state(self, statename): - rset = self.req.execute('Any S, SN WHERE S name SN, S name %(n)s, S state_of E, E name %(e)s', - {'n': statename, 'e': str(self.e_schema)}) - if rset: - return rset.get_entity(0, 0) - return None - - def wf_transition(self, trname): - rset = self.req.execute('Any T, TN WHERE T name TN, T name %(n)s, T transition_of E, E name %(e)s', - {'n': trname, 'e': str(self.e_schema)}) - if rset: - return rset.get_entity(0, 0) - return None - - def change_state(self, state, trcomment=None, trcommentformat=None): - """change the entity's state according to a state defined in given - parameters - """ - if isinstance(state, basestring): - state = self.wf_state(state) - assert state is not None, 'not a %s state: %s' % (self.id, state) - if hasattr(state, 'eid'): - stateeid = state.eid - else: - stateeid = state - stateeid = typed_eid(stateeid) - if trcomment: - self.req.set_shared_data('trcomment', trcomment) - if trcommentformat: - self.req.set_shared_data('trcommentformat', trcommentformat) - self.req.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': self.eid, 's': stateeid}, 'x') - - def can_pass_transition(self, trname): - """return the Transition instance if the current user can pass the - transition with the given name, else None - """ - stateeid = self.in_state[0].eid - rset = self.req.execute('Any T,N,DS WHERE S allowed_transition T,' - 'S eid %(x)s,T name %(trname)s,ET name %(et)s,' - 'T name N,T destination_state DS,T transition_of ET', - {'x': stateeid, 'et': str(self.e_schema), - 'trname': trname}, 'x') - for tr in rset.entities(): - if tr.may_be_passed(self.eid, stateeid): - return tr - - def latest_trinfo(self): - """return the latest transition information for this entity""" - return self.reverse_wf_info_for[-1] - - # __method methods ######################################################## - - def set_state(self, params=None): - """change the entity's state according to a state defined in given - parameters, used to be called using __method controler facility - """ - params = params or self.req.form - self.change_state(typed_eid(params.pop('state')), - params.get('trcomment'), - params.get('trcomment_format')) - self.req.set_message(self.req._('__msg state changed')) - - # specific vocabulary methods ############################################# - - @deprecated('use EntityFieldsForm.subject_in_state_vocabulary') - def subject_in_state_vocabulary(self, rschema, limit=None): - form = self.vreg.select('forms', 'edition', self.req, entity=self) - return form.subject_in_state_vocabulary(rschema, limit) - - - class EmailableMixIn(object): """base mixin providing the default get_email() method used by the massmailing view @@ -288,7 +197,6 @@ MI_REL_TRIGGERS = { - ('in_state', 'subject'): WorkflowableMixIn, ('primary_email', 'subject'): EmailableMixIn, ('use_email', 'subject'): EmailableMixIn, } diff -r d7c23b2c7538 -r 17224e90a1c4 common/test/unittest_mixins.py --- a/common/test/unittest_mixins.py Thu Aug 20 17:57:31 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,32 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -from logilab.common.testlib import unittest_main -from cubicweb.devtools.apptest import EnvBasedTC - -class WorkfloableMixInTC(EnvBasedTC): - def test_wf_state(self): - s = self.add_entity('State', name=u'activated') - self.execute('SET X state_of ET WHERE ET name "Bookmark", X eid %(x)s', - {'x': s.eid}) - es = self.user().wf_state('activated') - self.assertEquals(es.state_of[0].name, 'CWUser') - - def test_wf_transition(self): - t = self.add_entity('Transition', name=u'deactivate') - self.execute('SET X transition_of ET WHERE ET name "Bookmark", X eid %(x)s', - {'x': t.eid}) - et = self.user().wf_transition('deactivate') - self.assertEquals(et.transition_of[0].name, 'CWUser') - - def test_change_state(self): - user = self.user() - user.change_state(user.wf_state('deactivated').eid) - self.assertEquals(user.state, 'deactivated') - -if __name__ == '__main__': - unittest_main() diff -r d7c23b2c7538 -r 17224e90a1c4 devtools/_apptest.py --- a/devtools/_apptest.py Thu Aug 20 17:57:31 2009 +0200 +++ b/devtools/_apptest.py Thu Aug 20 17:57:56 2009 +0200 @@ -26,7 +26,7 @@ 'CWAttribute', 'CWRelation', 'CWConstraint', 'CWConstraintType', 'CWProperty', 'CWEType', 'CWRType', - 'State', 'Transition', 'TrInfo', + 'Workflow', 'State', 'BaseTransition', 'Transition', 'WorkflowTransition', 'TrInfo', 'SubWorkflowExitPoint', 'RQLExpression', ) SYSTEM_RELATIONS = ( @@ -35,9 +35,9 @@ # metadata 'is', 'is_instance_of', 'owned_by', 'created_by', 'specializes', # workflow related - 'state_of', 'transition_of', 'initial_state', 'allowed_transition', + 'workflow_of', 'state_of', 'transition_of', 'initial_state', 'allowed_transition', 'destination_state', 'in_state', 'wf_info_for', 'from_state', 'to_state', - 'condition', + 'condition', 'subworkflow', 'subworkflow_state', 'subworkflow_exit', # permission 'in_group', 'require_group', 'require_permission', 'read_permission', 'update_permission', 'delete_permission', 'add_permission', @@ -121,8 +121,7 @@ def create_user(self, login, groups=('users',), req=None): req = req or self.create_request() cursor = self._orig_cnx.cursor(req) - rset = cursor.execute('INSERT CWUser X: X login %(login)s, X upassword %(passwd)s,' - 'X in_state S WHERE S name "activated"', + rset = cursor.execute('INSERT CWUser X: X login %(login)s, X upassword %(passwd)s', {'login': unicode(login), 'passwd': login.encode('utf8')}) user = rset.get_entity(0, 0) cursor.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)' diff -r d7c23b2c7538 -r 17224e90a1c4 devtools/apptest.py --- a/devtools/apptest.py Thu Aug 20 17:57:31 2009 +0200 +++ b/devtools/apptest.py Thu Aug 20 17:57:56 2009 +0200 @@ -371,8 +371,7 @@ def create_user(self, user, groups=('users',), password=None, commit=True): if password is None: password = user - eid = self.execute('INSERT CWUser X: X login %(x)s, X upassword %(p)s,' - 'X in_state S WHERE S name "activated"', + eid = self.execute('INSERT CWUser X: X login %(x)s, X upassword %(p)s', {'x': unicode(user), 'p': password})[0][0] groups = ','.join(repr(group) for group in groups) self.execute('SET X in_group Y WHERE X eid %%(x)s, Y name IN (%s)' % groups, diff -r d7c23b2c7538 -r 17224e90a1c4 entities/test/data/migration/postcreate.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entities/test/data/migration/postcreate.py Thu Aug 20 17:57:56 2009 +0200 @@ -0,0 +1,2 @@ +wf = add_workflow(u'bmk wf', 'Bookmark') +wf.add_state(u'hop', initial=True) diff -r d7c23b2c7538 -r 17224e90a1c4 entities/test/data/schema.py --- a/entities/test/data/schema.py Thu Aug 20 17:57:31 2009 +0200 +++ b/entities/test/data/schema.py Thu Aug 20 17:57:56 2009 +0200 @@ -1,11 +1,13 @@ -""" +"""entities tests schema :organization: Logilab :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ + from yams.buildobjs import EntityType, String +from cubicweb.schema import make_workflowable class Company(EntityType): name = String() @@ -16,3 +18,7 @@ class SubDivision(Division): __specializes_schema__ = True + +from cubicweb.schemas import bootstrap, Bookmark +make_workflowable(bootstrap.CWGroup) +make_workflowable(Bookmark.Bookmark) diff -r d7c23b2c7538 -r 17224e90a1c4 entities/test/unittest_base.py --- a/entities/test/unittest_base.py Thu Aug 20 17:57:31 2009 +0200 +++ b/entities/test/unittest_base.py Thu Aug 20 17:57:56 2009 +0200 @@ -58,149 +58,6 @@ self.assertEquals(e.dc_title(), 'member') self.assertEquals(e.name(), u'bouah lôt') - -class StateAndTransitionsTC(BaseEntityTC): - - def test_transitions(self): - user = self.entity('CWUser X') - e = self.entity('State S WHERE S name "activated"') - trs = list(e.transitions(user)) - self.assertEquals(len(trs), 1) - self.assertEquals(trs[0].name, u'deactivate') - self.assertEquals(trs[0].destination().name, u'deactivated') - self.assert_(user.can_pass_transition('deactivate')) - self.assert_(not user.can_pass_transition('activate')) - # test a std user get no possible transition - self.login('member') - # fetch the entity using the new session - e = self.entity('State S WHERE S name "activated"') - trs = list(e.transitions(user)) - self.assertEquals(len(trs), 0) - user = self.entity('CWUser X') - self.assert_(not user.can_pass_transition('deactivate')) - self.assert_(not user.can_pass_transition('activate')) - - def test_transitions_with_dest_specfied(self): - user = self.entity('CWUser X') - e = self.entity('State S WHERE S name "activated"') - e2 = self.entity('State S WHERE S name "deactivated"') - trs = list(e.transitions(user, e2.eid)) - self.assertEquals(len(trs), 1) - self.assertEquals(trs[0].name, u'deactivate') - self.assertEquals(trs[0].destination().name, u'deactivated') - trs = list(e.transitions(user, e.eid)) - self.assertEquals(len(trs), 0) - - def test_transitions_maybe_passed(self): - self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", ' - 'X expression "X owned_by U", T condition X ' - 'WHERE T name "deactivate"') - self._test_deactivated() - - def test_transitions_maybe_passed_using_has_update_perm(self): - self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", ' - 'X expression "U has_update_permission X", T condition X ' - 'WHERE T name "deactivate"') - self._test_deactivated() - - - def _test_deactivated(self): - ueid = self.create_user('toto').eid - self.create_user('tutu') - cnx = self.login('tutu') - cu = cnx.cursor() - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S name "deactivated"', - {'x': ueid}, 'x') - cnx.close() - cnx = self.login('toto') - cu = cnx.cursor() - cu.execute('SET X in_state S WHERE X eid %(x)s, S name "deactivated"', - {'x': ueid}, 'x') - cnx.commit() - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S name "activated"', - {'x': ueid}, 'x') - - - def test_transitions_selection(self): - """ - ------------------------ tr1 ----------------- - | state1 (CWGroup, Bookmark) | ------> | state2 (CWGroup) | - ------------------------ ----------------- - | tr2 ------------------ - `------> | state3 (Bookmark) | - ------------------ - """ - state1 = self.add_entity('State', name=u'state1') - state2 = self.add_entity('State', name=u'state2') - state3 = self.add_entity('State', name=u'state3') - tr1 = self.add_entity('Transition', name=u'tr1') - tr2 = self.add_entity('Transition', name=u'tr2') - self.execute('SET X state_of Y WHERE X eid in (%s, %s), Y is CWEType, Y name "CWGroup"' % - (state1.eid, state2.eid)) - self.execute('SET X state_of Y WHERE X eid in (%s, %s), Y is CWEType, Y name "Bookmark"' % - (state1.eid, state3.eid)) - self.execute('SET X transition_of Y WHERE X eid %s, Y name "CWGroup"' % tr1.eid) - self.execute('SET X transition_of Y WHERE X eid %s, Y name "Bookmark"' % tr2.eid) - self.execute('SET X allowed_transition Y WHERE X eid %s, Y eid %s' % - (state1.eid, tr1.eid)) - self.execute('SET X allowed_transition Y WHERE X eid %s, Y eid %s' % - (state1.eid, tr2.eid)) - self.execute('SET X destination_state Y WHERE X eid %s, Y eid %s' % - (tr1.eid, state2.eid)) - self.execute('SET X destination_state Y WHERE X eid %s, Y eid %s' % - (tr2.eid, state3.eid)) - self.execute('SET X initial_state Y WHERE Y eid %s, X name "CWGroup"' % state1.eid) - self.execute('SET X initial_state Y WHERE Y eid %s, X name "Bookmark"' % state1.eid) - group = self.add_entity('CWGroup', name=u't1') - transitions = list(state1.transitions(group)) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr1') - bookmark = self.add_entity('Bookmark', title=u'111', path=u'/view') - transitions = list(state1.transitions(bookmark)) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr2') - - - def test_transitions_selection2(self): - """ - ------------------------ tr1 (Bookmark) ----------------------- - | state1 (CWGroup, Bookmark) | -------------> | state2 (CWGroup,Bookmark) | - ------------------------ ----------------------- - | tr2 (CWGroup) | - `---------------------------------/ - """ - state1 = self.add_entity('State', name=u'state1') - state2 = self.add_entity('State', name=u'state2') - tr1 = self.add_entity('Transition', name=u'tr1') - tr2 = self.add_entity('Transition', name=u'tr2') - self.execute('SET X state_of Y WHERE X eid in (%s, %s), Y is CWEType, Y name "CWGroup"' % - (state1.eid, state2.eid)) - self.execute('SET X state_of Y WHERE X eid in (%s, %s), Y is CWEType, Y name "Bookmark"' % - (state1.eid, state2.eid)) - self.execute('SET X transition_of Y WHERE X eid %s, Y name "CWGroup"' % tr1.eid) - self.execute('SET X transition_of Y WHERE X eid %s, Y name "Bookmark"' % tr2.eid) - self.execute('SET X allowed_transition Y WHERE X eid %s, Y eid %s' % - (state1.eid, tr1.eid)) - self.execute('SET X allowed_transition Y WHERE X eid %s, Y eid %s' % - (state1.eid, tr2.eid)) - self.execute('SET X destination_state Y WHERE X eid %s, Y eid %s' % - (tr1.eid, state2.eid)) - self.execute('SET X destination_state Y WHERE X eid %s, Y eid %s' % - (tr2.eid, state2.eid)) - self.execute('SET X initial_state Y WHERE Y eid %s, X name "CWGroup"' % state1.eid) - self.execute('SET X initial_state Y WHERE Y eid %s, X name "Bookmark"' % state1.eid) - group = self.add_entity('CWGroup', name=u't1') - transitions = list(state1.transitions(group)) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr1') - bookmark = self.add_entity('Bookmark', title=u'111', path=u'/view') - transitions = list(state1.transitions(bookmark)) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr2') - - class EmailAddressTC(BaseEntityTC): def test_canonical_form(self): eid1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"')[0][0] @@ -234,7 +91,6 @@ e = self.entity('CWUser X WHERE X login "admin"') e.complete() - def test_matching_groups(self): e = self.entity('CWUser X WHERE X login "admin"') self.failUnless(e.matching_groups('managers')) @@ -242,23 +98,6 @@ self.failUnless(e.matching_groups(('xyz', 'managers'))) self.failIf(e.matching_groups(('xyz', 'abcd'))) - def test_workflow_base(self): - e = self.create_user('toto') - self.assertEquals(e.state, 'activated') - activatedeid = self.execute('State X WHERE X name "activated"')[0][0] - deactivatedeid = self.execute('State X WHERE X name "deactivated"')[0][0] - e.change_state(deactivatedeid, u'deactivate 1') - self.commit() - e.change_state(activatedeid, u'activate 1') - self.commit() - e.change_state(deactivatedeid, u'deactivate 2') - self.commit() - # get a fresh user to avoid potential cache issues - e = self.entity('CWUser X WHERE X eid %s' % e.eid) - self.assertEquals([tr.comment for tr in e.reverse_wf_info_for], - [None, 'deactivate 1', 'activate 1', 'deactivate 2']) - self.assertEquals(e.latest_trinfo().comment, 'deactivate 2') - class InterfaceTC(EnvBasedTC): diff -r d7c23b2c7538 -r 17224e90a1c4 entities/test/unittest_wfobjs.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entities/test/unittest_wfobjs.py Thu Aug 20 17:57:56 2009 +0200 @@ -0,0 +1,251 @@ +from cubicweb.devtools.apptest import EnvBasedTC +from cubicweb import ValidationError + +class WorkflowTC(EnvBasedTC): + + def setup_database(self): + rschema = self.schema['in_state'] + for x, y in rschema.iter_rdefs(): + self.assertEquals(rschema.rproperty(x, y, 'cardinality'), '1*') + self.member = self.create_user('member') + + def test_workflow_base(self): + e = self.create_user('toto') + self.assertEquals(e.state, 'activated') + e.change_state('deactivated', u'deactivate 1') + self.commit() + e.change_state('activated', u'activate 1') + self.commit() + e.change_state('deactivated', u'deactivate 2') + self.commit() + e.clear_related_cache('wf_info_for', 'object') + self.assertEquals([tr.comment for tr in e.reverse_wf_info_for], + ['deactivate 1', 'activate 1', 'deactivate 2']) + self.assertEquals(e.latest_trinfo().comment, 'deactivate 2') + + # def test_wf_construction(self): # XXX update or kill me + # bar = self.mh.cmd_add_state(u'bar', ('Personne', 'Email'), initial=True) + # baz = self.mh.cmd_add_transition(u'baz', ('Personne', 'Email'), + # (foo,), bar, ('managers',)) + # for etype in ('Personne', 'Email'): + # t1 = self.mh.rqlexec('Any N WHERE T transition_of ET, ET name "%s", T name N' % + # etype)[0][0] + # self.assertEquals(t1, "baz") + # gn = self.mh.rqlexec('Any GN WHERE T require_group G, G name GN, T eid %s' % baz)[0][0] + # self.assertEquals(gn, 'managers') + + def test_possible_transitions(self): + user = self.entity('CWUser X') + trs = list(user.possible_transitions()) + self.assertEquals(len(trs), 1) + self.assertEquals(trs[0].name, u'deactivate') + self.assertEquals(trs[0].destination().name, u'deactivated') + # test a std user get no possible transition + cnx = self.login('member') + # fetch the entity using the new session + trs = list(cnx.user().possible_transitions()) + self.assertEquals(len(trs), 0) + + def _test_manager_deactivate(self, user): + user.clear_related_cache('in_state', 'subject') + self.assertEquals(len(user.in_state), 1) + self.assertEquals(user.state, 'deactivated') + trinfo = user.latest_trinfo() + self.assertEquals(trinfo.previous_state.name, 'activated') + self.assertEquals(trinfo.new_state.name, 'deactivated') + self.assertEquals(trinfo.comment, 'deactivate user') + self.assertEquals(trinfo.comment_format, 'text/plain') + return trinfo + + def test_change_state(self): + user = self.user() + user.change_state('deactivated', comment=u'deactivate user') + trinfo = self._test_manager_deactivate(user) + self.assertEquals(trinfo.transition, None) + + def test_fire_transition(self): + user = self.user() + user.fire_transition('deactivate', comment=u'deactivate user') + self.assertEquals(user.state, 'deactivated') + self._test_manager_deactivate(user) + trinfo = self._test_manager_deactivate(user) + self.assertEquals(trinfo.transition.name, 'deactivate') + + # XXX test managers can change state without matching transition + + def _test_stduser_deactivate(self): + ueid = self.member.eid + self.create_user('tutu') + cnx = self.login('tutu') + req = self.request() + member = req.entity_from_eid(self.member.eid) + ex = self.assertRaises(ValidationError, + member.fire_transition, 'deactivate') + self.assertEquals(ex.errors, {'by_transition': "transition may not be fired"}) + cnx.close() + cnx = self.login('member') + req = self.request() + member = req.entity_from_eid(self.member.eid) + member.fire_transition('deactivate') + cnx.commit() + ex = self.assertRaises(ValidationError, + member.fire_transition, 'activate') + self.assertEquals(ex.errors, {'by_transition': "transition may not be fired"}) + + def test_fire_transition_owned_by(self): + self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", ' + 'X expression "X owned_by U", T condition X ' + 'WHERE T name "deactivate"') + self._test_stduser_deactivate() + + def test_fire_transition_has_update_perm(self): + self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", ' + 'X expression "U has_update_permission X", T condition X ' + 'WHERE T name "deactivate"') + self._test_stduser_deactivate() + + def _init_wf_with_shared_state_or_tr(self): + req = self.request() + etypes = dict(self.execute('Any N, ET WHERE ET is CWEType, ET name N' + ', ET name IN ("CWGroup", "Bookmark")')) + self.grpwf = req.create_entity('Workflow', ('workflow_of', 'ET'), + ET=etypes['CWGroup'], name=u'group workflow') + self.bmkwf = req.execute('Any X WHERE X is Workflow, X workflow_of ET, ET name "Bookmark"').get_entity(0, 0) + self.state1 = self.grpwf.add_state(u'state1', initial=True) + self.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s', + {'s': self.state1.eid, 'wf': self.bmkwf.eid}) + self.execute('SET WF initial_state S WHERE S eid %(s)s, WF eid %(wf)s', + {'s': self.state1.eid, 'wf': self.bmkwf.eid}) + self.state2 = self.grpwf.add_state(u'state2') + self.group = self.add_entity('CWGroup', name=u't1') + self.bookmark = self.add_entity('Bookmark', title=u'111', path=u'/view') + # commit to link to the initial state + self.commit() + + def test_transitions_selection(self): + """ + ------------------------ tr1 ----------------- + | state1 (CWGroup, Bookmark) | ------> | state2 (CWGroup) | + ------------------------ ----------------- + | tr2 ------------------ + `------> | state3 (Bookmark) | + ------------------ + """ + self._init_wf_with_shared_state_or_tr() + state3 = self.bmkwf.add_state(u'state3') + tr1 = self.grpwf.add_transition(u'tr1', (self.state1,), self.state2) + tr2 = self.bmkwf.add_transition(u'tr2', (self.state1,), state3) + transitions = list(self.group.possible_transitions()) + self.assertEquals(len(transitions), 1) + self.assertEquals(transitions[0].name, 'tr1') + transitions = list(self.bookmark.possible_transitions()) + self.assertEquals(len(transitions), 1) + self.assertEquals(transitions[0].name, 'tr2') + + + def test_transitions_selection2(self): + """ + ------------------------ tr1 (Bookmark) ----------------------- + | state1 (CWGroup, Bookmark) | -------------> | state2 (CWGroup,Bookmark) | + ------------------------ ----------------------- + | tr2 (CWGroup) | + `---------------------------------/ + """ + self._init_wf_with_shared_state_or_tr() + self.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s', + {'s': self.state2.eid, 'wf': self.bmkwf.eid}) + tr1 = self.bmkwf.add_transition(u'tr1', (self.state1,), self.state2) + tr2 = self.grpwf.add_transition(u'tr2', (self.state1,), self.state2) + transitions = list(self.group.possible_transitions()) + self.assertEquals(len(transitions), 1) + self.assertEquals(transitions[0].name, 'tr2') + transitions = list(self.bookmark.possible_transitions()) + self.assertEquals(len(transitions), 1) + self.assertEquals(transitions[0].name, 'tr1') + + +from cubicweb.devtools.apptest import RepositoryBasedTC + +class WorkflowHooksTC(RepositoryBasedTC): + + def setUp(self): + RepositoryBasedTC.setUp(self) + self.wf = self.session.user.current_workflow + self.s_activated = self.wf.state_by_name('activated').eid + self.s_deactivated = self.wf.state_by_name('deactivated').eid + self.s_dummy = self.wf.add_state(u'dummy').eid + self.wf.add_transition(u'dummy', (self.s_deactivated,), self.s_dummy) + ueid = self.create_user('stduser', commit=False) + # test initial state is set + rset = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', + {'x' : ueid}) + self.failIf(rset, rset.rows) + self.commit() + initialstate = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', + {'x' : ueid})[0][0] + self.assertEquals(initialstate, u'activated') + # give access to users group on the user's wf transitions + # so we can test wf enforcing on euser (managers don't have anymore this + # enforcement + self.execute('SET X require_group G ' + 'WHERE G name "users", X transition_of WF, WF eid %(wf)s', + {'wf': self.wf.eid}) + self.commit() + + def tearDown(self): + self.execute('DELETE X require_group G ' + 'WHERE G name "users", X transition_of WF, WF eid %(wf)s', + {'wf': self.wf.eid}) + self.commit() + RepositoryBasedTC.tearDown(self) + + # XXX currently, we've to rely on hooks to set initial state, or to use unsafe_execute + # def test_initial_state(self): + # cnx = self.login('stduser') + # cu = cnx.cursor() + # self.assertRaises(ValidationError, cu.execute, + # 'INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' + # 'X in_state S WHERE S name "deactivated"', {'pwd': 'oops'}) + # cnx.close() + # # though managers can do whatever he want + # self.execute('INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' + # 'X in_state S, X in_group G WHERE S name "deactivated", G name "users"', {'pwd': 'oops'}) + # self.commit() + + # test that the workflow is correctly enforced + def test_transition_checking1(self): + cnx = self.login('stduser') + user = cnx.user(self.current_session()) + ex = self.assertRaises(ValidationError, + user.fire_transition, 'activate') + self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"}) + cnx.close() + + def test_transition_checking2(self): + cnx = self.login('stduser') + user = cnx.user(self.current_session()) + assert user.state == 'activated' + ex = self.assertRaises(ValidationError, + user.fire_transition, 'dummy') + self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"}) + cnx.close() + + def test_transition_checking3(self): + cnx = self.login('stduser') + session = self.current_session() + user = cnx.user(session) + user.fire_transition('deactivate') + cnx.commit() + session.set_pool() + ex = self.assertRaises(ValidationError, + user.fire_transition, 'deactivate') + self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"}) + # get back now + user.fire_transition('activate') + cnx.commit() + cnx.close() + + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff -r d7c23b2c7538 -r 17224e90a1c4 entities/wfobjs.py --- a/entities/wfobjs.py Thu Aug 20 17:57:31 2009 +0200 +++ b/entities/wfobjs.py Thu Aug 20 17:57:56 2009 +0200 @@ -7,23 +7,125 @@ """ __docformat__ = "restructuredtext en" +from warnings import warn + +from logilab.common.decorators import cached +from logilab.common.deprecation import deprecated + from cubicweb.entities import AnyEntity, fetch_config +from cubicweb.interfaces import IWorkflowable +from cubicweb.common.mixins import MI_REL_TRIGGERS -class Transition(AnyEntity): - """customized class for Transition entities +class Workflow(AnyEntity): + id = 'Workflow' + + @property + def initial(self): + """return the initial state for this workflow""" + return self.initial_state and self.initial_state[0] + + def is_default_workflow_of(self, etype): + """return True if this workflow is the default workflow for the given + entity type + """ + return any(et for et in self.default_workflow_of if et.name == etype) + + def after_deletion_path(self): + """return (path, parameters) which should be used as redirect + information when this entity is being deleted + """ + if self.workflow_of: + return self.workflow_of[0].rest_path(), {'vid': 'workflow'} + return super(Workflow, self).after_deletion_path() + + # state / transitions accessors ############################################ + + def state_by_name(self, statename): + rset = self.req.execute('Any S, SN WHERE S name SN, S name %(n)s, ' + 'S state_of WF, WF eid %(wf)s', + {'n': statename, 'wf': self.eid}, 'wf') + if rset: + return rset.get_entity(0, 0) + return None + + def state_by_eid(self, eid): + rset = self.req.execute('Any S, SN WHERE S name SN, S eid %(s)s, ' + 'S state_of WF, WF eid %(wf)s', + {'s': eid, 'wf': self.eid}, ('wf', 's')) + if rset: + return rset.get_entity(0, 0) + return None + + def transition_by_name(self, trname): + rset = self.req.execute('Any T, TN WHERE T name TN, T name %(n)s, ' + 'T transition_of WF, WF eid %(wf)s', + {'n': trname, 'wf': self.eid}, 'wf') + if rset: + return rset.get_entity(0, 0) + return None - provides a specific may_be_passed method to check if the relation may be - passed by the logged user + def transition_by_eid(self, eid): + rset = self.req.execute('Any T, TN WHERE T name TN, T eid %(t)s, ' + 'T transition_of WF, WF eid %(wf)s', + {'t': eid, 'wf': self.eid}, ('wf', 't')) + if rset: + return rset.get_entity(0, 0) + return None + + # wf construction methods ################################################## + + def add_state(self, name, initial=False, **kwargs): + """method to ease workflow definition: add a state for one or more + entity type(s) + """ + state = self.req.create_entity('State', name=name, **kwargs) + self.req.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s', + {'s': state.eid, 'wf': self.eid}, ('s', 'wf')) + if initial: + assert not self.initial + self.req.execute('SET WF initial_state S ' + 'WHERE S eid %(s)s, WF eid %(wf)s', + {'s': state.eid, 'wf': self.eid}, ('s', 'wf')) + return state + + def add_transition(self, name, fromstates, tostate, + requiredgroups=(), conditions=(), **kwargs): + """method to ease workflow definition: add a transition for one or more + entity type(s), from one or more state and to a single state + """ + tr = self.req.create_entity('Transition', name=name, **kwargs) + self.req.execute('SET T transition_of WF ' + 'WHERE T eid %(t)s, WF eid %(wf)s', + {'t': tr.eid, 'wf': self.eid}, ('t', 'wf')) + for state in fromstates: + if hasattr(state, 'eid'): + state = state.eid + self.req.execute('SET S allowed_transition T ' + 'WHERE S eid %(s)s, T eid %(t)s', + {'s': state, 't': tr.eid}, ('s', 't')) + 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')) + tr.set_transition_permissions(requiredgroups, conditions, reset=False) + return tr + + +class BaseTransition(AnyEntity): + """customized class for abstract transition + + provides a specific may_be_fired method to check if the relation may be + fired by the logged user """ id = 'Transition' fetch_attrs, fetch_order = fetch_config(['name']) - def may_be_passed(self, eid, stateeid): - """return true if the logged user may pass this transition + def may_be_fired(self, eid): + """return true if the logged user may fire this transition - `eid` is the eid of the object on which we may pass the transition - `stateeid` is the eid of the current object'state XXX unused + `eid` is the eid of the object on which we may fire the transition """ user = self.req.user # check user is at least in one of the required groups if any @@ -43,47 +145,79 @@ return False return True - def destination(self): - return self.destination_state[0] - def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted """ if self.transition_of: - return self.transition_of[0].rest_path(), {'vid': 'workflow'} + return self.transition_of[0].rest_path(), {} return super(Transition, self).after_deletion_path() + def set_transition_permissions(self, requiredgroups=(), conditions=(), + reset=True): + """set or add (if `reset` is False) groups and conditions for this + transition + """ + if reset: + self.req.execute('DELETE T require_group G WHERE T eid %(x)s', + {'x': self.eid}, 'x') + self.req.execute('DELETE T condition R WHERE T eid %(x)s', + {'x': self.eid}, 'x') + for gname in requiredgroups: + ### XXX ensure gname validity + rset = self.req.execute('SET T require_group G ' + 'WHERE T eid %(x)s, G name %(gn)s', + {'x': self.eid, 'gn': gname}, 'x') + assert rset, '%s is not a known group' % gname + if isinstance(conditions, basestring): + conditions = (conditions,) + for expr in conditions: + if isinstance(expr, str): + expr = unicode(expr) + self.req.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", ' + 'X expression %(expr)s, T condition X ' + 'WHERE T eid %(x)s', + {'x': self.eid, 'expr': expr}, 'x') + # XXX clear caches? + + +class Transition(BaseTransition): + """customized class for Transition entities""" + id = 'Transition' + + def destination(self): + return self.destination_state[0] + + def has_input_state(self, state): + if hasattr(state, 'eid'): + state = state.eid + return any(s for s in self.reverse_allowed_transition if s.eid == state) + + +class WorkflowTransition(BaseTransition): + """customized class for WorkflowTransition entities""" + id = 'WorkflowTransition' + + @property + def subwf(self): + return self.subworkflow[0] + + def destination(self): + return self.subwf.initial + class State(AnyEntity): - """customized class for State entities - - provides a specific transitions method returning transitions that may be - passed by the current user for the given entity - """ + """customized class for State entities""" id = 'State' fetch_attrs, fetch_order = fetch_config(['name']) rest_attr = 'eid' - def transitions(self, entity, desteid=None): - """generates transition that MAY be passed""" - rql = ('Any T,N,DS where S allowed_transition T, S eid %(x)s, ' - 'T name N, T destination_state DS, ' - 'T transition_of ET, ET name %(et)s') - if desteid is not None: - rql += ', DS eid %(ds)s' - rset = self.req.execute(rql, {'x': self.eid, 'et': str(entity.e_schema), - 'ds': desteid}, 'x') - for tr in rset.entities(): - if tr.may_be_passed(entity.eid, self.eid): - yield tr - def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted """ if self.state_of: - return self.state_of[0].rest_path(), {'vid': 'workflow'} + return self.state_of[0].rest_path(), {} return super(State, self).after_deletion_path() @@ -95,15 +229,20 @@ pclass=None) # don't want modification_date @property def for_entity(self): - return self.wf_info_for and self.wf_info_for[0] + return self.wf_info_for[0] + @property def previous_state(self): - return self.from_state and self.from_state[0] + return self.from_state[0] @property def new_state(self): return self.to_state[0] + @property + def transition(self): + return self.by_transition and self.by_transition[0] or None + def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted @@ -111,3 +250,125 @@ if self.for_entity: return self.for_entity.rest_path(), {} return 'view', {} + + +class WorkflowableMixIn(object): + """base mixin providing workflow helper methods for workflowable entities. + This mixin will be automatically set on class supporting the 'in_state' + relation (which implies supporting 'wf_info_for' as well) + """ + __implements__ = (IWorkflowable,) + + @property + @cached + def current_workflow(self): + """return current workflow applied to this entity""" + if self.custom_workflow: + return self.custom_workflow[0] + wfrset = self.req.execute('Any WF WHERE X is ET, X eid %(x)s, WF workflow_of ET', + {'x': self.eid}, 'x') + if len(wfrset) == 1: + return wfrset.get_entity(0, 0) + if len(wfrset) > 1: + for wf in wfrset.entities(): + if wf.is_default_workflow_of(self.id): + return wf + self.warning("can't find default workflow for %s", self.id) + else: + self.warning("can't find any workflow for %s", self.id) + return None + + @property + def current_state(self): + """return current state entity""" + return self.in_state and self.in_state[0] or None + + @property + def state(self): + """return current state name""" + try: + return self.in_state[0].name + except IndexError: + self.warning('entity %s has no state', self) + return None + + @property + def printable_state(self): + """return current state name translated to context's language""" + state = self.current_state + if state: + return self.req._(state.name) + return u'' + + def latest_trinfo(self): + """return the latest transition information for this entity""" + return self.reverse_wf_info_for[-1] + + def possible_transitions(self): + """generates transition that MAY be fired for the given entity, + expected to be in this state + """ + if self.current_state is None or self.current_workflow is None: + return + rset = self.req.execute( + 'Any T,N WHERE S allowed_transition T, S eid %(x)s, ' + 'T name N, T transition_of WF, WF eid %(wfeid)s', + {'x': self.current_state.eid, + 'wfeid': self.current_workflow.eid}, 'x') + for tr in rset.entities(): + if tr.may_be_fired(self.eid): + yield tr + + def _get_tr_kwargs(self, comment, commentformat): + kwargs = {} + if comment is not None: + kwargs['comment'] = comment + if commentformat is not None: + kwargs['comment_format'] = commentformat + return kwargs + + def fire_transition(self, trname, comment=None, commentformat=None): + """change the entity's state by firing transition of the given name in + entity's workflow + """ + assert self.current_workflow + tr = self.current_workflow.transition_by_name(trname) + assert tr is not None, 'not a %s transition: %s' % (self.id, state) + # XXX try to find matching transition? + self.req.create_entity('TrInfo', ('by_transition', 'T'), + ('wf_info_for', 'E'), T=tr.eid, E=self.eid, + **self._get_tr_kwargs(comment, commentformat)) + + def change_state(self, statename, comment=None, commentformat=None): + """change the entity's state to the state of the given name in entity's + workflow. This method should only by used by manager to fix an entity's + state when their is no matching transition, otherwise fire_transition + should be used. + """ + assert self.current_workflow + if not isinstance(statename, basestring): + warn('give a state name') + state = self.current_workflow.state_by_eid(statename) + assert state is not None, 'not a %s state: %s' % (self.id, state) + else: + state = self.current_workflow.state_by_name(statename) + # XXX try to find matching transition? + self.req.create_entity('TrInfo', ('to_state', 'S'), + ('wf_info_for', 'E'), S=state.eid, E=self.eid, + **self._get_tr_kwargs(comment, commentformat)) + + @deprecated('get transition from current workflow and use its may_be_fired method') + def can_pass_transition(self, trname): + """return the Transition instance if the current user can fire the + transition with the given name, else None + """ + tr = self.current_workflow and self.current_workflow.transition_by_name(trname) + if tr and tr.may_be_fired(self.eid): + return tr + + @property + @deprecated('use printable_state') + def displayable_state(self): + return self.req._(self.state) + +MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn diff -r d7c23b2c7538 -r 17224e90a1c4 entity.py --- a/entity.py Thu Aug 20 17:57:31 2009 +0200 +++ b/entity.py Thu Aug 20 17:57:56 2009 +0200 @@ -163,7 +163,7 @@ id = None rest_attr = None fetch_attrs = None - skip_copy_for = () + skip_copy_for = ('in_state',) # class attributes set automatically at registration time e_schema = None @@ -485,13 +485,6 @@ continue if rschema.type in self.skip_copy_for: continue - if rschema.type == 'in_state': - # if the workflow is defining an initial state (XXX AND we are - # not in the managers group? not done to be more consistent) - # don't try to copy in_state - if execute('Any S WHERE S state_of ET, ET initial_state S,' - 'ET name %(etype)s', {'etype': str(self.e_schema)}): - continue # skip composite relation if self.e_schema.subjrproperty(rschema, 'composite'): continue diff -r d7c23b2c7538 -r 17224e90a1c4 interfaces.py --- a/interfaces.py Thu Aug 20 17:57:31 2009 +0200 +++ b/interfaces.py Thu Aug 20 17:57:56 2009 +0200 @@ -37,25 +37,22 @@ class IWorkflowable(Interface): """interface for entities dealing with a specific workflow""" + # XXX to be completed, see cw.entities.wfobjs.WorkflowableMixIn @property def state(self): - """return current state""" + """return current state name""" def change_state(self, stateeid, trcomment=None, trcommentformat=None): - """change the entity's state according to a state defined in given - parameters - """ - - def can_pass_transition(self, trname): - """return true if the current user can pass the transition with the - given name + """change the entity's state to the state of the given name in entity's + workflow """ def latest_trinfo(self): """return the latest transition information for this entity """ + class IProgress(Interface): """something that has a cost, a state and a progression diff -r d7c23b2c7538 -r 17224e90a1c4 misc/migration/postcreate.py --- a/misc/migration/postcreate.py Thu Aug 20 17:57:31 2009 +0200 +++ b/misc/migration/postcreate.py Thu Aug 20 17:57:56 2009 +0200 @@ -15,17 +15,19 @@ (deactivatedeid,), activatedeid, requiredgroups=('managers',)) -# need this since we already have at least one user in the database (the default admin) -rql('SET X in_state S WHERE X is CWUser, S eid %s' % activatedeid) - # create anonymous user if all-in-one config and anonymous user has been specified if hasattr(config, 'anonymous_user'): anonlogin, anonpwd = config.anonymous_user() if anonlogin: rql('INSERT CWUser X: X login %(login)s, X upassword %(pwd)s,' - 'X in_state S, X in_group G WHERE G name "guests", S name "activated"', + 'X in_group G WHERE G name "guests"', {'login': unicode(anonlogin), 'pwd': anonpwd}) +# need this since we already have at least one user in the database (the default admin) +for user in rql('Any X WHERE X is CWUser').entities(): + session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', + {'x': user.eid, 's': activatedeid}, 'x') + cfg = config.persistent_options_configuration() if interactive_mode: cfg.input_config(inputlevel=0) diff -r d7c23b2c7538 -r 17224e90a1c4 schema.py --- a/schema.py Thu Aug 20 17:57:31 2009 +0200 +++ b/schema.py Thu Aug 20 17:57:56 2009 +0200 @@ -846,23 +846,30 @@ This is the default metaclass for WorkflowableEntityType """ def __new__(mcs, name, bases, classdict): - abstract = classdict.pop('abstract', False) - defclass = super(workflowable_definition, mcs).__new__(mcs, name, bases, classdict) + abstract = classdict.pop('__abstract__', False) + cls = super(workflowable_definition, mcs).__new__(mcs, name, bases, + classdict) if not abstract: - existing_rels = set(rdef.name for rdef in defclass.__relations__) - if 'in_state' not in existing_rels and 'wf_info_for' not in existing_rels: - in_state = ybo.SubjectRelation('State', cardinality='1*', - # XXX automatize this - constraints=[RQLConstraint('S is ET, O state_of ET')], - description=_('account state')) - yams_add_relation(defclass.__relations__, in_state, 'in_state') - wf_info_for = ybo.ObjectRelation('TrInfo', cardinality='1*', composite='object') - yams_add_relation(defclass.__relations__, wf_info_for, 'wf_info_for') - return defclass + make_workflowable(cls) + return cls + +def make_workflowable(cls): + existing_rels = set(rdef.name for rdef in cls.__relations__) + # let relation types defined in cw.schemas.workflow carrying + # cardinality, constraints and other relation definition properties + if 'custom_workflow' not in existing_rels: + rdef = ybo.SubjectRelation('Workflow') + yams_add_relation(cls.__relations__, rdef, 'custom_workflow') + if 'in_state' not in existing_rels: + rdef = ybo.SubjectRelation('State') + yams_add_relation(cls.__relations__, rdef, 'in_state') + if 'wf_info_for' not in existing_rels: + rdef = ybo.ObjectRelation('TrInfo') + yams_add_relation(cls.__relations__, rdef, 'wf_info_for') class WorkflowableEntityType(ybo.EntityType): __metaclass__ = workflowable_definition - abstract = True + __abstract__ = True PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType diff -r d7c23b2c7538 -r 17224e90a1c4 schemas/workflow.py --- a/schemas/workflow.py Thu Aug 20 17:57:31 2009 +0200 +++ b/schemas/workflow.py Thu Aug 20 17:57:56 2009 +0200 @@ -13,6 +13,29 @@ from cubicweb.schema import RQLConstraint from cubicweb.schemas import META_ETYPE_PERMS, META_RTYPE_PERMS, HOOKS_RTYPE_PERMS +class Workflow(EntityType): + permissions = META_ETYPE_PERMS + + name = String(required=True, indexed=True, internationalizable=True, + maxsize=256) + description = RichString(fulltextindexed=True, default_format='text/rest', + description=_('semantic description of this workflow')) + + workflow_of = SubjectRelation('CWEType', cardinality='+*', + description=_('entity types which may use this workflow'), + constraints=[RQLConstraint('O final FALSE')]) + + default_workflow_of = SubjectRelation('CWEType', cardinality='*?', + description=_('which entity types use this workflow by default'), + constraints=[RQLConstraint('O final FALSE')]) + + initial_state = SubjectRelation('State', cardinality='?*', + # S initial_state O, O state_of S + constraints=[RQLConstraint('O state_of S')], + description=_('initial state for this workflow')) + +# XXX ensure state/transition name is unique in a given workflow + class State(EntityType): """used to associate simple states to an entity type and/or to define workflows @@ -24,23 +47,17 @@ description = RichString(fulltextindexed=True, default_format='text/rest', description=_('semantic description of this state')) - state_of = SubjectRelation('CWEType', cardinality='+*', - description=_('entity types which may use this state'), - constraints=[RQLConstraint('O final FALSE')]) - allowed_transition = SubjectRelation('Transition', cardinality='**', - constraints=[RQLConstraint('S state_of ET, O transition_of ET')], + state_of = SubjectRelation('Workflow', cardinality='+*', + description=_('workflow to which this state belongs')) + # XXX should be on BaseTransition w/ AND/OR selectors when we will + # implements #345274 + allowed_transition = SubjectRelation('BaseTransition', cardinality='**', + constraints=[RQLConstraint('S state_of WF, O transition_of WF')], description=_('allowed transitions from this state')) - initial_state = ObjectRelation('CWEType', cardinality='?*', - # S initial_state O, O state_of S - constraints=[RQLConstraint('O state_of S')], - description=_('initial state for entities of this type')) - -class Transition(EntityType): - """use to define a transition from one or multiple states to a destination - states in workflow's definitions. - """ +class BaseTransition(EntityType): + """abstract base class for transitions""" permissions = META_ETYPE_PERMS name = String(required=True, indexed=True, internationalizable=True, @@ -57,47 +74,107 @@ require_group = SubjectRelation('CWGroup', cardinality='**', description=_('group in which a user should be to be ' 'allowed to pass this transition')) - transition_of = SubjectRelation('CWEType', cardinality='+*', - description=_('entity types which may use this transition'), - constraints=[RQLConstraint('O final FALSE')]) + transition_of = SubjectRelation('Workflow', cardinality='+*', + description=_('workflow to which this transition belongs')) + + +class Transition(BaseTransition): + """use to define a transition from one or multiple states to a destination + states in workflow's definitions. + """ + __specializes_schema__ = True + destination_state = SubjectRelation('State', cardinality='1*', - constraints=[RQLConstraint('S transition_of ET, O state_of ET')], + constraints=[RQLConstraint('S transition_of WF, O state_of WF')], description=_('destination state for this transition')) -class TrInfo(EntityType): - permissions = META_ETYPE_PERMS +class WorkflowTransition(BaseTransition): + """special transition allowing to go through a sub-workflow""" + __specializes_schema__ = True + + 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', + 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')) - from_state = SubjectRelation('State', cardinality='?*') + +# XXX should we allow managers to delete TrInfo? + +class TrInfo(EntityType): + """workflow history item""" + # 'add' security actually done by hooks + permissions = { + 'read': ('managers', 'users', 'guests',), # XXX U has_read_permission O ? + 'add': ('managers', 'users', 'guests',), + 'delete': (), + 'update': ('managers', 'owners',), + } + + from_state = SubjectRelation('State', cardinality='1*') to_state = SubjectRelation('State', cardinality='1*') + # make by_transition optional because we want to allow managers to set + # entity into an arbitrary state without having to respect wf transition + by_transition = SubjectRelation('Transition', cardinality='?*') comment = RichString(fulltextindexed=True) # get actor and date time using owned_by and creation_date +class from_state(RelationType): + permissions = HOOKS_RTYPE_PERMS.copy() + inlined = True -class from_state(RelationType): - permissions = HOOKS_RTYPE_PERMS +class to_state(RelationType): + permissions = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers',), + 'delete': (), + } inlined = True -class to_state(RelationType): - permissions = HOOKS_RTYPE_PERMS + +class by_transition(RelationType): + # 'add' security actually done by hooks + permissions = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers', 'users', 'guests',), + 'delete': (), + } inlined = True -class wf_info_for(RelationType): - """link a transition information to its object""" - permissions = { - 'read': ('managers', 'users', 'guests',),# RRQLExpression('U has_read_permission O')), - 'add': (), # handled automatically, no one should add one explicitly - 'delete': ('managers',), # RRQLExpression('U has_delete_permission O') - } - inlined = True - composite = 'object' - fulltext_container = composite +class workflow_of(RelationType): + """link a workflow to one or more entity type""" + permissions = META_RTYPE_PERMS class state_of(RelationType): - """link a state to one or more entity type""" + """link a state to one or more workflow""" permissions = META_RTYPE_PERMS + class transition_of(RelationType): - """link a transition to one or more entity type""" + """link a transition to one or more workflow""" + permissions = META_RTYPE_PERMS + +class subworkflow(RelationType): + """link a transition to one or more workflow""" permissions = META_RTYPE_PERMS + inlined = True + +class exit_point(RelationType): + """link a transition to one or more workflow""" + permissions = META_RTYPE_PERMS + +class subworkflow_state(RelationType): + """link a transition to one or more workflow""" + permissions = META_RTYPE_PERMS + inlined = True class initial_state(RelationType): """indicate which state should be used by default when an entity using @@ -115,16 +192,42 @@ """allowed transition from this state""" permissions = META_RTYPE_PERMS + +# "abstract" relations, set by WorkflowableEntityType ########################## + +class custom_workflow(RelationType): + """allow to set a specific workflow for an entity""" + permissions = META_RTYPE_PERMS + + cardinality = '?*' + constraints = [RQLConstraint('S is ET, O workflow_of ET')] + object = 'Workflow' + + +class wf_info_for(RelationType): + """link a transition information to its object""" + # 'add' security actually done by hooks + permissions = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers', 'users', 'guests',), + 'delete': (), + } + inlined = True + + cardinality='1*' + composite = 'object' + fulltext_container = composite + subject = 'TrInfo' + + class in_state(RelationType): """indicate the current state of an entity""" + permissions = HOOKS_RTYPE_PERMS + # not inlined intentionnaly since when using ldap sources, user'state # has to be stored outside the CWUser table inlined = False - # add/delete perms given to managers/users, after what most of the job - # is done by workflow enforcment - permissions = { - 'read': ('managers', 'users', 'guests',), - 'add': ('managers', 'users',), # XXX has_update_perm - 'delete': ('managers', 'users',), - } + cardinality = '1*' + constraints = [RQLConstraint('S is ET, O state_of WF, WF workflow_of ET')] + object = 'State' diff -r d7c23b2c7538 -r 17224e90a1c4 server/hooks.py --- a/server/hooks.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/hooks.py Thu Aug 20 17:57:56 2009 +0200 @@ -13,8 +13,8 @@ from cubicweb import UnknownProperty, ValidationError, BadConnectionId from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation -from cubicweb.server.hookhelper import (check_internal_entity, previous_state, - get_user_sessions, rproperty) +from cubicweb.server.hookhelper import (check_internal_entity, + get_user_sessions, rproperty) from cubicweb.server.repository import FTIndexEntityOp # special relations that don't have to be checked for integrity, usually @@ -37,7 +37,8 @@ # from the database (eg during tests) if eschema.eid is None: eschema.eid = session.unsafe_execute( - 'Any X WHERE X is CWEType, X name %(name)s', {'name': etype})[0][0] + 'Any X WHERE X is CWEType, X name %(name)s', + {'name': str(etype)})[0][0] return eschema.eid @@ -417,51 +418,75 @@ # workflow handling ########################################################### -def before_add_in_state(session, fromeid, rtype, toeid): - """check the transition is allowed and record transition information +def before_add_trinfo(session, entity): + """check the transition is allowed, add missing information. Expect that: + * wf_info_for inlined relation is set + * by_transition or to_state (managers only) inlined relation is set """ - assert rtype == 'in_state' - state = previous_state(session, fromeid) - etype = session.describe(fromeid)[0] - if not (session.is_super_session or 'managers' in session.user.groups): - if not state is None: - entity = session.entity_from_eid(fromeid) - # we should find at least one transition going to this state - try: - iter(state.transitions(entity, toeid)).next() - except StopIteration: - _ = session._ - msg = _('transition from %s to %s does not exist or is not allowed') % ( - _(state.name), _(session.entity_from_eid(toeid).name)) - raise ValidationError(fromeid, {'in_state': msg}) - else: - # not a transition - # check state is initial state if the workflow defines one - isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s', - {'etype': etype}) - if isrset and not toeid == isrset[0][0]: - _ = session._ - msg = _('%s is not the initial state (%s) for this entity') % ( - _(session.entity_from_eid(toeid).name), _(isrset.get_entity(0,0).name)) - raise ValidationError(fromeid, {'in_state': msg}) - eschema = session.repo.schema[etype] - if not 'wf_info_for' in eschema.object_relations(): - # workflow history not activated for this entity type - return - rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s' - args = {'comment': session.get_shared_data('trcomment', None, pop=True), - 'e': fromeid, 'ds': toeid} - cformat = session.get_shared_data('trcommentformat', None, pop=True) - if cformat is not None: - args['comment_format'] = cformat - rql += ', T comment_format %(comment_format)s' - restriction = ['DS eid %(ds)s, E eid %(e)s'] - if not state is None: # not a transition - rql += ', T from_state FS' - restriction.append('FS eid %(fs)s') - args['fs'] = state.eid - rql = '%s WHERE %s' % (rql, ', '.join(restriction)) - session.unsafe_execute(rql, args, 'e') + # first retreive entity to which the state change apply + try: + foreid = entity['wf_info_for'] + except KeyError: + 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 + wf = forentity.current_workflow + if wf is None: + msg = session._('related entity has no workflow set') + raise ValidationError(entity.eid, {None: msg}) + # then check it has a state set + fromstate = forentity.current_state + if fromstate is None: + msg = session._('related entity has no state') + raise ValidationError(entity.eid, {None: msg}) + # 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): + msg = session._('mandatory relation') + raise ValidationError(entity.eid, {'by_transition': msg}) + deststateeid = entity.get('to_state') + if not deststateeid: + 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") + 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 + # everything is ok, add missing information on the trinfo entity + entity['from_state'] = fromstate.eid + entity['to_state'] = deststateeid + nocheck = session.transaction_data.setdefault('skip-security', set()) + 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')) class SetInitialStateOp(PreCommitOperation): @@ -473,26 +498,35 @@ # if there is an initial state and the entity's state is not set, # use the initial state as a default state pendingeids = session.transaction_data.get('pendingeids', ()) - if not entity.eid in pendingeids and not entity.in_state: - rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s', - {'name': entity.id}) - if rset: - session.add_relation(entity.eid, 'in_state', rset[0][0]) + if not entity.eid in pendingeids and not entity.in_state and \ + entity.current_workflow: + state = entity.current_workflow.initial + if state: + # use super session to by-pass security checks + session.super_session.add_relation(entity.eid, 'in_state', + state.eid) def set_initial_state_after_add(session, entity): SetInitialStateOp(session, entity=entity) +def after_del_workflow(session, eid): + # workflow cleanup + session.execute('DELETE State X WHERE NOT X state_of Y') + session.execute('DELETE Transition X WHERE NOT X transition_of Y') + def _register_wf_hooks(hm): """register workflow related hooks on the hooks manager""" if 'in_state' in hm.schema: - hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state') - hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state') + hm.register_hook(before_add_trinfo, 'before_add_entity', 'TrInfo') + hm.register_hook(after_add_trinfo, 'after_add_entity', 'TrInfo') + #hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state') for eschema in hm.schema.entities(): if 'in_state' in eschema.subject_relations(): hm.register_hook(set_initial_state_after_add, 'after_add_entity', str(eschema)) + hm.register_hook(after_del_workflow, 'after_delete_entity', 'Workflow') # CWProperty hooks ############################################################# diff -r d7c23b2c7538 -r 17224e90a1c4 server/hooksmanager.py --- a/server/hooksmanager.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/hooksmanager.py Thu Aug 20 17:57:56 2009 +0200 @@ -268,3 +268,90 @@ from cubicweb import set_log_methods set_log_methods(HooksManager, getLogger('cubicweb.hooksmanager')) set_log_methods(Hook, getLogger('cubicweb.hooks')) + +# base classes for relation propagation ######################################## + +from cubicweb.server.pool import PreCommitOperation + +class RQLPrecommitOperation(PreCommitOperation): + def precommit_event(self): + execute = self.session.unsafe_execute + for rql in self.rqls: + execute(*rql) + + +class PropagateSubjectRelationHook(Hook): + """propagate permissions and nosy list when new entity are added""" + events = ('after_add_relation',) + # to set in concrete class + rtype = None + subject_relations = None + object_relations = None + accepts = None # subject_relations + object_relations + + def call(self, session, fromeid, rtype, toeid): + for eid in (fromeid, toeid): + etype = session.describe(eid)[0] + if not self.schema.eschema(etype).has_subject_relation(self.rtype): + return + if rtype in self.subject_relations: + meid, seid = fromeid, toeid + else: + assert rtype in self.object_relations + meid, seid = toeid, fromeid + rql = 'SET E %s P WHERE X %s P, X eid %%(x)s, E eid %%(e)s, NOT E %s P'\ + % (self.rtype, self.rtype, self.rtype) + rqls = [(rql, {'x': meid, 'e': seid}, ('x', 'e'))] + RQLPrecommitOperation(session, rqls=rqls) + + +class PropagateSubjectRelationAddHook(Hook): + """propagate on existing entities when a permission or nosy list is added""" + events = ('after_add_relation',) + # to set in concrete class + rtype = None + subject_relations = None + object_relations = None + accepts = None # (self.rtype,) + + def call(self, session, fromeid, rtype, toeid): + eschema = self.schema.eschema(session.describe(fromeid)[0]) + rqls = [] + for rel in self.subject_relations: + if eschema.has_subject_relation(rel): + rqls.append(('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' + 'X %s R, NOT R %s P' % (rtype, rel, rtype), + {'x': fromeid, 'p': toeid}, 'x')) + for rel in self.object_relations: + if eschema.has_object_relation(rel): + rqls.append(('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' + 'R %s X, NOT R %s P' % (rtype, rel, rtype), + {'x': fromeid, 'p': toeid}, 'x')) + if rqls: + RQLPrecommitOperation(session, rqls=rqls) + + +class PropagateSubjectRelationDelHook(Hook): + """propagate on existing entities when a permission is deleted""" + events = ('after_delete_relation',) + # to set in concrete class + rtype = None + subject_relations = None + object_relations = None + accepts = None # (self.rtype,) + + def call(self, session, fromeid, rtype, toeid): + eschema = self.schema.eschema(session.describe(fromeid)[0]) + rqls = [] + for rel in self.subject_relations: + if eschema.has_subject_relation(rel): + rqls.append(('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' + 'X %s R' % (rtype, rel), + {'x': fromeid, 'p': toeid}, 'x')) + for rel in self.object_relations: + if eschema.has_object_relation(rel): + rqls.append(('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' + 'R %s X' % (rtype, rel), + {'x': fromeid, 'p': toeid}, 'x')) + if rqls: + RQLPrecommitOperation(session, rqls=rqls) diff -r d7c23b2c7538 -r 17224e90a1c4 server/migractions.py --- a/server/migractions.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/migractions.py Thu Aug 20 17:57:56 2009 +0200 @@ -910,78 +910,78 @@ # Workflows handling ###################################################### + def cmd_add_workflow(self, name, wfof, default=True, commit=False, + **kwargs): + self.session.set_pool() # ensure pool is set + wf = self.cmd_create_entity('Workflow', name=unicode(name), + **kwargs) + if not isinstance(wfof, (list, tuple)): + wfof = (wfof,) + for etype in wfof: + rset = self.rqlexec('SET X workflow_of ET ' + 'WHERE X eid %(x)s, ET name %(et)s', + {'x': wf.eid, 'et': etype}, 'x') + assert rset, 'unexistant entity type %s' % etype + if default: + rset = self.rqlexec('SET X default_workflow_of ET ' + 'WHERE X eid %(x)s, ET name %(et)s', + {'x': wf.eid, 'et': etype}, 'x') + if commit: + self.commit() + return wf + + # XXX remove once cmd_add_[state|transition] are removed + def _get_or_create_wf(self, etypes): + self.session.set_pool() # ensure pool is set + if not isinstance(etypes, (list, tuple)): + etypes = (etypes,) + rset = self.rqlexec('Workflow X WHERE X workflow_of ET, ET name %(et)s', + {'et': etypes[0]}) + if rset: + return rset.get_entity(0, 0) + return self.cmd_add_workflow('%s workflow' % ';'.join(etypes), etypes) + + @deprecated('use add_workflow and Workflow.add_state method') def cmd_add_state(self, name, stateof, initial=False, commit=False, **kwargs): """method to ease workflow definition: add a state for one or more entity type(s) """ - stateeid = self.cmd_add_entity('State', name=name, **kwargs) - if not isinstance(stateof, (list, tuple)): - stateof = (stateof,) - for etype in stateof: - # XXX ensure etype validity - self.rqlexec('SET X state_of Y WHERE X eid %(x)s, Y name %(et)s', - {'x': stateeid, 'et': etype}, 'x', ask_confirm=False) - if initial: - self.rqlexec('SET ET initial_state S WHERE ET name %(et)s, S eid %(x)s', - {'x': stateeid, 'et': etype}, 'x', ask_confirm=False) + wf = self._get_or_create_wf(stateof) + state = wf.add_state(name, initial, **kwargs) if commit: self.commit() - return stateeid + return state.eid + @deprecated('use add_workflow and Workflow.add_transition method') def cmd_add_transition(self, name, transitionof, fromstates, tostate, requiredgroups=(), conditions=(), commit=False, **kwargs): """method to ease workflow definition: add a transition for one or more entity type(s), from one or more state and to a single state """ - treid = self.cmd_add_entity('Transition', name=name, **kwargs) - if not isinstance(transitionof, (list, tuple)): - transitionof = (transitionof,) - for etype in transitionof: - # XXX ensure etype validity - self.rqlexec('SET X transition_of Y WHERE X eid %(x)s, Y name %(et)s', - {'x': treid, 'et': etype}, 'x', ask_confirm=False) - for stateeid in fromstates: - self.rqlexec('SET X allowed_transition Y WHERE X eid %(x)s, Y eid %(y)s', - {'x': stateeid, 'y': treid}, 'x', ask_confirm=False) - self.rqlexec('SET X destination_state Y WHERE X eid %(x)s, Y eid %(y)s', - {'x': treid, 'y': tostate}, 'x', ask_confirm=False) - self.cmd_set_transition_permissions(treid, requiredgroups, conditions, - reset=False) + wf = self._get_or_create_wf(transitionof) + tr = wf.add_transition(name, fromstates, tostate, requiredgroups, + conditions, **kwargs) if commit: self.commit() - return treid + return tr.eid + @deprecated('use Transition.set_transition_permissions method') def cmd_set_transition_permissions(self, treid, requiredgroups=(), conditions=(), reset=True, commit=False): """set or add (if `reset` is False) groups and conditions for a transition """ - if reset: - self.rqlexec('DELETE T require_group G WHERE T eid %(x)s', - {'x': treid}, 'x', ask_confirm=False) - self.rqlexec('DELETE T condition R WHERE T eid %(x)s', - {'x': treid}, 'x', ask_confirm=False) - for gname in requiredgroups: - ### XXX ensure gname validity - self.rqlexec('SET T require_group G WHERE T eid %(x)s, G name %(gn)s', - {'x': treid, 'gn': gname}, 'x', ask_confirm=False) - if isinstance(conditions, basestring): - conditions = (conditions,) - for expr in conditions: - if isinstance(expr, str): - expr = unicode(expr) - self.rqlexec('INSERT RQLExpression X: X exprtype "ERQLExpression", ' - 'X expression %(expr)s, T condition X ' - 'WHERE T eid %(x)s', - {'x': treid, 'expr': expr}, 'x', ask_confirm=False) + self.session.set_pool() # ensure pool is set + tr = self.session.entity_from_eid(treid) + tr.set_transition_permissions(requiredgroups, conditions, reset) if commit: self.commit() + @deprecated('use entity.change_state("state")') def cmd_set_state(self, eid, statename, commit=False): self.session.set_pool() # ensure pool is set - entity = self.session.entity_from_eid(eid) - entity.change_state(entity.wf_state(statename).eid) + self.session.entity_from_eid(eid).change_state(statename) if commit: self.commit() @@ -998,32 +998,26 @@ prop = self.rqlexec('CWProperty X WHERE X pkey %(k)s', {'k': pkey}, ask_confirm=False).get_entity(0, 0) except: - self.cmd_add_entity('CWProperty', pkey=unicode(pkey), value=value) + self.cmd_create_entity('CWProperty', pkey=unicode(pkey), value=value) else: self.rqlexec('SET X value %(v)s WHERE X pkey %(k)s', {'k': pkey, 'v': value}, ask_confirm=False) # other data migration commands ########################################### + def cmd_create_entity(self, etype, *args, **kwargs): + """add a new entity of the given type""" + commit = kwargs.pop('commit', False) + self.session.set_pool() + entity = self.session.create_entity(etype, *args, **kwargs) + if commit: + self.commit() + return entity + + @deprecated('use create_entity') def cmd_add_entity(self, etype, *args, **kwargs): """add a new entity of the given type""" - rql = 'INSERT %s X' % etype - relations = [] - restrictions = [] - for rtype, rvar in args: - relations.append('X %s %s' % (rtype, rvar)) - restrictions.append('%s eid %s' % (rvar, kwargs.pop(rvar))) - commit = kwargs.pop('commit', False) - for attr in kwargs: - relations.append('X %s %%(%s)s' % (attr, attr)) - if relations: - rql = '%s: %s' % (rql, ', '.join(relations)) - if restrictions: - rql = '%s WHERE %s' % (rql, ', '.join(restrictions)) - eid = self.rqlexec(rql, kwargs, ask_confirm=self.verbosity>=2).rows[0][0] - if commit: - self.commit() - return eid + return self.cmd_create_entity(etype, *args, **kwargs).eid def sqlexec(self, sql, args=None, ask_confirm=True): """execute the given sql if confirmed diff -r d7c23b2c7538 -r 17224e90a1c4 server/repository.py --- a/server/repository.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/repository.py Thu Aug 20 17:57:56 2009 +0200 @@ -194,6 +194,8 @@ 'cubicweb.entities.__init__') self.vreg.load_file(join(etdirectory, 'authobjs.py'), 'cubicweb.entities.authobjs') + self.vreg.load_file(join(etdirectory, 'wfobjs.py'), + 'cubicweb.entities.wfobjs') else: # test start: use the file system schema (quicker) self.warning("set fs instance'schema") diff -r d7c23b2c7538 -r 17224e90a1c4 server/schemahooks.py --- a/server/schemahooks.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/schemahooks.py Thu Aug 20 17:57:56 2009 +0200 @@ -736,8 +736,7 @@ def after_del_eetype(session, eid): # workflow cleanup - session.execute('DELETE State X WHERE NOT X state_of Y') - session.execute('DELETE Transition X WHERE NOT X transition_of Y') + session.execute('DELETE Workflow X WHERE NOT X workflow_of Y') def before_del_ertype(session, eid): diff -r d7c23b2c7538 -r 17224e90a1c4 server/securityhooks.py --- a/server/securityhooks.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/securityhooks.py Thu Aug 20 17:57:56 2009 +0200 @@ -70,11 +70,17 @@ def before_add_relation(session, fromeid, rtype, toeid): if rtype in BEFORE_ADD_RELATIONS and not session.is_super_session: + nocheck = session.transaction_data.get('skip-security', ()) + if (fromeid, rtype, toeid) in nocheck: + return rschema = session.repo.schema[rtype] rschema.check_perm(session, 'add', fromeid, toeid) def after_add_relation(session, fromeid, rtype, toeid): if not rtype in BEFORE_ADD_RELATIONS and not session.is_super_session: + nocheck = session.transaction_data.get('skip-security', ()) + if (fromeid, rtype, toeid) in nocheck: + return rschema = session.repo.schema[rtype] if rtype in ON_COMMIT_ADD_RELATIONS: CheckRelationPermissionOp(session, action='add', rschema=rschema, @@ -84,6 +90,9 @@ def before_del_relation(session, fromeid, rtype, toeid): if not session.is_super_session: + nocheck = session.transaction_data.get('skip-security', ()) + if (fromeid, rtype, toeid) in nocheck: + return session.repo.schema[rtype].check_perm(session, 'delete', fromeid, toeid) def register_security_hooks(hm): diff -r d7c23b2c7538 -r 17224e90a1c4 server/ssplanner.py --- a/server/ssplanner.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/ssplanner.py Thu Aug 20 17:57:56 2009 +0200 @@ -377,7 +377,7 @@ previous FetchStep relations values comes from the latest result, with one columns for - each relation defined in self.r_defs + each relation defined in self.rdefs for one entity definition, we'll construct N entity, where N is the number of the latest result @@ -387,33 +387,35 @@ RELATION = 1 REVERSE_RELATION = 2 - def __init__(self, plan, e_def, r_defs): + def __init__(self, plan, edef, rdefs): Step.__init__(self, plan) # partial entity definition to expand - self.e_def = e_def + self.edef = edef # definition of relations to complete - self.r_defs = r_defs + self.rdefs = rdefs def execute(self): """execute this step""" - base_e_def = self.e_def - result = [] - for row in self.execute_child(): + base_edef = self.edef + edefs = [] + result = self.execute_child() + for row in result: # get a new entity definition for this row - e_def = copy(base_e_def) + edef = copy(base_edef) # complete this entity def using row values - for i in range(len(self.r_defs)): - rtype, rorder = self.r_defs[i] + for i in range(len(self.rdefs)): + rtype, rorder = self.rdefs[i] if rorder == RelationsStep.FINAL: - e_def[rtype] = row[i] + edef[rtype] = row[i] elif rorder == RelationsStep.RELATION: - self.plan.add_relation_def( (e_def, rtype, row[i]) ) - e_def.querier_pending_relations[(rtype, 'subject')] = row[i] + self.plan.add_relation_def( (edef, rtype, row[i]) ) + edef.querier_pending_relations[(rtype, 'subject')] = row[i] else: - self.plan.add_relation_def( (row[i], rtype, e_def) ) - e_def.querier_pending_relations[(rtype, 'object')] = row[i] - result.append(e_def) - self.plan.substitute_entity_def(base_e_def, result) + self.plan.add_relation_def( (row[i], rtype, edef) ) + edef.querier_pending_relations[(rtype, 'object')] = row[i] + edefs.append(edef) + self.plan.substitute_entity_def(base_edef, edefs) + return result class InsertStep(Step): @@ -483,7 +485,8 @@ edefs = {} # insert relations attributes = set([relation.r_type for relation in self.attribute_relations]) - for row in self.execute_child(): + result = self.execute_child() + for row in result: for relation in self.attribute_relations: lhs, rhs = relation.get_variable_parts() eid = typed_eid(row[self.selected_index[str(lhs)]]) @@ -502,8 +505,6 @@ obj = row[self.selected_index[str(relation.children[1])]] repo.glob_add_relation(session, subj, relation.r_type, obj) # update entities - result = [] for eid, edef in edefs.iteritems(): repo.glob_update_entity(session, edef, attributes) - result.append( (eid,) ) return result diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/data/migratedapp/schema.py --- a/server/test/data/migratedapp/schema.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/data/migratedapp/schema.py Thu Aug 20 17:57:56 2009 +0200 @@ -93,7 +93,7 @@ concerne2 = SubjectRelation(('Affaire', 'Note'), cardinality='1*') connait = SubjectRelation('Personne', symetric=True) -class Societe(EntityType): +class Societe(WorkflowableEntityType): permissions = { 'read': ('managers', 'users', 'guests'), 'update': ('managers', 'owners'), @@ -112,7 +112,6 @@ cp = String(maxsize=12) ville= String(maxsize=32) - in_state = SubjectRelation('State', cardinality='?*') class evaluee(RelationDefinition): subject = ('Personne', 'CWUser', 'Societe') diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/data/schema.py --- a/server/test/data/schema.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/data/schema.py Thu Aug 20 17:57:56 2009 +0200 @@ -34,7 +34,7 @@ depends_on = SubjectRelation('Affaire') require_permission = SubjectRelation('CWPermission') concerne = SubjectRelation(('Societe', 'Note')) - todo_by = SubjectRelation('Personne') + todo_by = SubjectRelation('Personne', cardinality='?*') documented_by = SubjectRelation('Card') @@ -69,7 +69,7 @@ from cubicweb.schemas.base import CWUser CWUser.get_relations('login').next().fulltextindexed = True -class Note(EntityType): +class Note(WorkflowableEntityType): date = String(maxsize=10) type = String(maxsize=6) para = String(maxsize=512) @@ -146,18 +146,6 @@ 'delete': ('managers',), 'add': ('managers',)} - -class in_state(RelationDefinition): - subject = 'Note' - object = 'State' - cardinality = '1*' - constraints=[RQLConstraint('S is ET, O state_of ET')] - -class wf_info_for(RelationDefinition): - subject = 'TrInfo' - object = 'Note' - cardinality = '1*' - class multisource_rel(RelationDefinition): subject = ('Card', 'Note') object = 'Note' diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_hookhelper.py --- a/server/test/unittest_hookhelper.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_hookhelper.py Thu Aug 20 17:57:56 2009 +0200 @@ -49,41 +49,5 @@ op5 = hooks.CheckORelationOp(session) self.assertEquals(session.pending_operations, [op1, op2, op4, op5, op3]) - - def test_in_state_notification(self): - result = [] - # test both email notification and transition_information - # whatever if we can connect to the default stmp server, transaction - # should not fail - def in_state_changed(session, eidfrom, rtype, eidto): - tr = previous_state(session, eidfrom) - if tr is None: - result.append(tr) - return - content = u'trÀnsition from %s to %s' % (tr.name, entity_name(session, eidto)) - result.append(content) - SendMailOp(session, msg=content, recipients=['test@logilab.fr']) - self.hm.register_hook(in_state_changed, - 'before_add_relation', 'in_state') - self.execute('INSERT CWUser X: X login "paf", X upassword "wouf", X in_state S, X in_group G WHERE S name "activated", G name "users"') - self.assertEquals(result, [None]) - searchedops = [op for op in self.session.pending_operations - if isinstance(op, SendMailOp)] - self.assertEquals(len(searchedops), 0, - self.session.pending_operations) - self.commit() - self.execute('SET X in_state S WHERE X login "paf", S name "deactivated"') - self.assertEquals(result, [None, u'trÀnsition from activated to deactivated']) - # one to send the mail, one to close the smtp connection - searchedops = [op for op in self.session.pending_operations - if isinstance(op, SendMailOp)] - self.assertEquals(len(searchedops), 1, - self.session.pending_operations) - self.commit() - searchedops = [op for op in self.session.pending_operations - if isinstance(op, SendMailOp)] - self.assertEquals(len(searchedops), 0, - self.session.pending_operations) - if __name__ == '__main__': unittest_main() diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_hooks.py --- a/server/test/unittest_hooks.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_hooks.py Thu Aug 20 17:57:56 2009 +0200 @@ -37,8 +37,8 @@ 'DELETE CWGroup X WHERE X name "owners"') def test_delete_required_relations_subject(self): - self.execute('INSERT CWUser X: X login "toto", X upassword "hop", X in_group Y, X in_state S ' - 'WHERE Y name "users", S name "activated"') + self.execute('INSERT CWUser X: X login "toto", X upassword "hop", X in_group Y ' + 'WHERE Y name "users"') self.commit() self.execute('DELETE X in_group Y WHERE X login "toto", Y name "users"') self.assertRaises(ValidationError, self.commit) @@ -60,18 +60,6 @@ self.assertRaises(ValidationError, self.commit) - def test_delete_if_singlecard1(self): - self.assertEquals(self.repo.schema['in_state'].inlined, False) - ueid = self.create_user('toto') - self.commit() - self.execute('SET X in_state S WHERE S name "deactivated", X eid %(x)s', {'x': ueid}) - rset = self.execute('Any S WHERE X in_state S, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 1) - self.commit() - self.assertRaises(Exception, self.execute, 'SET X in_state S WHERE S name "deactivated", X eid %s' % ueid) - rset2 = self.execute('Any S WHERE X in_state S, X eid %(x)s', {'x': ueid}) - self.assertEquals(rset.rows, rset2.rows) - def test_inlined(self): self.assertEquals(self.repo.schema['sender'].inlined, True) self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"') @@ -155,6 +143,40 @@ self.assertEquals(entity.descr, u'R&D

yo

') + def test_metadata_cwuri(self): + eid = self.execute('INSERT Note X')[0][0] + cwuri = self.execute('Any U WHERE X eid %s, X cwuri U' % eid)[0][0] + self.assertEquals(cwuri, self.repo.config['base-url'] + 'eid/%s' % eid) + + def test_metadata_creation_modification_date(self): + _now = datetime.now() + eid = self.execute('INSERT Note X')[0][0] + creation_date, modification_date = self.execute('Any CD, MD WHERE X eid %s, ' + 'X creation_date CD, ' + 'X modification_date MD' % eid)[0] + self.assertEquals((creation_date - _now).seconds, 0) + self.assertEquals((modification_date - _now).seconds, 0) + + def test_metadata__date(self): + _now = datetime.now() + eid = self.execute('INSERT Note X')[0][0] + creation_date = self.execute('Any D WHERE X eid %s, X creation_date D' % eid)[0][0] + self.assertEquals((creation_date - _now).seconds, 0) + + def test_metadata_created_by(self): + eid = self.execute('INSERT Note X')[0][0] + self.commit() # fire operations + rset = self.execute('Any U WHERE X eid %s, X created_by U' % eid) + self.assertEquals(len(rset), 1) # make sure we have only one creator + self.assertEquals(rset[0][0], self.session.user.eid) + + def test_metadata_owned_by(self): + eid = self.execute('INSERT Note X')[0][0] + self.commit() # fire operations + rset = self.execute('Any U WHERE X eid %s, X owned_by U' % eid) + self.assertEquals(len(rset), 1) # make sure we have only one owner + self.assertEquals(rset[0][0], self.session.user.eid) + class UserGroupHooksTC(RepositoryBasedTC): @@ -480,177 +502,5 @@ 'RT name "prenom", E name "Personne"') self.commit() - -class WorkflowHooksTC(RepositoryBasedTC): - - def setUp(self): - RepositoryBasedTC.setUp(self) - self.s_activated = self.execute('State X WHERE X name "activated"')[0][0] - self.s_deactivated = self.execute('State X WHERE X name "deactivated"')[0][0] - self.s_dummy = self.execute('INSERT State X: X name "dummy", X state_of E WHERE E name "CWUser"')[0][0] - self.create_user('stduser') - # give access to users group on the user's wf transitions - # so we can test wf enforcing on euser (managers don't have anymore this - # enforcement - self.execute('SET X require_group G WHERE G name "users", X transition_of ET, ET name "CWUser"') - self.commit() - - def tearDown(self): - self.execute('DELETE X require_group G WHERE G name "users", X transition_of ET, ET name "CWUser"') - self.commit() - RepositoryBasedTC.tearDown(self) - - def test_set_initial_state(self): - ueid = self.execute('INSERT CWUser E: E login "x", E upassword "x", E in_group G ' - 'WHERE G name "users"')[0][0] - self.failIf(self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', - {'x' : ueid})) - self.commit() - initialstate = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s', - {'x' : ueid})[0][0] - self.assertEquals(initialstate, u'activated') - - def test_initial_state(self): - cnx = self.login('stduser') - cu = cnx.cursor() - self.assertRaises(ValidationError, cu.execute, - 'INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' - 'X in_state S WHERE S name "deactivated"', {'pwd': 'oops'}) - cnx.close() - # though managers can do whatever he want - self.execute('INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, ' - 'X in_state S, X in_group G WHERE S name "deactivated", G name "users"', {'pwd': 'oops'}) - self.commit() - - # test that the workflow is correctly enforced - def test_transition_checking1(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - cnx.close() - - def test_transition_checking2(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_dummy}, 'x') - cnx.close() - - def test_transition_checking3(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - cnx.commit() - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - # get back now - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - cnx.commit() - cnx.close() - - def test_transition_checking4(self): - cnx = self.login('stduser') - cu = cnx.cursor() - ueid = cnx.user(self.current_session()).eid - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - cnx.commit() - self.assertRaises(ValidationError, - cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_dummy}, 'x') - # get back now - cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - cnx.commit() - cnx.close() - - def test_transition_information(self): - ueid = self.session.user.eid - self.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_deactivated}, 'x') - self.commit() - rset = self.execute('TrInfo T ORDERBY T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 2) - tr = rset.get_entity(1, 0) - #tr.complete() - self.assertEquals(tr.comment, None) - self.assertEquals(tr.from_state[0].eid, self.s_activated) - self.assertEquals(tr.to_state[0].eid, self.s_deactivated) - - self.session.set_shared_data('trcomment', u'il est pas sage celui-la') - self.session.set_shared_data('trcommentformat', u'text/plain') - self.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': ueid, 's': self.s_activated}, 'x') - self.commit() - rset = self.execute('TrInfo T ORDERBY T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 3) - tr = rset.get_entity(2, 0) - #tr.complete() - self.assertEquals(tr.comment, u'il est pas sage celui-la') - self.assertEquals(tr.comment_format, u'text/plain') - self.assertEquals(tr.from_state[0].eid, self.s_deactivated) - self.assertEquals(tr.to_state[0].eid, self.s_activated) - self.assertEquals(tr.owned_by[0].login, 'admin') - - def test_transition_information_on_creation(self): - ueid = self.create_user('toto') - rset = self.execute('TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 1) - tr = rset.get_entity(0, 0) - #tr.complete() - self.assertEquals(tr.comment, None) - self.assertEquals(tr.from_state, []) - self.assertEquals(tr.to_state[0].eid, self.s_activated) - - def test_std_users_can_create_trinfo(self): - self.create_user('toto') - cnx = self.login('toto') - cu = cnx.cursor() - self.failUnless(cu.execute("INSERT Note X: X type 'a', X in_state S WHERE S name 'todo'")) - cnx.commit() - - def test_metadata_cwuri(self): - eid = self.execute('INSERT Note X')[0][0] - cwuri = self.execute('Any U WHERE X eid %s, X cwuri U' % eid)[0][0] - self.assertEquals(cwuri, self.repo.config['base-url'] + 'eid/%s' % eid) - - def test_metadata_creation_modification_date(self): - _now = datetime.now() - eid = self.execute('INSERT Note X')[0][0] - creation_date, modification_date = self.execute('Any CD, MD WHERE X eid %s, ' - 'X creation_date CD, ' - 'X modification_date MD' % eid)[0] - self.assertEquals((creation_date - _now).seconds, 0) - self.assertEquals((modification_date - _now).seconds, 0) - - def test_metadata__date(self): - _now = datetime.now() - eid = self.execute('INSERT Note X')[0][0] - creation_date = self.execute('Any D WHERE X eid %s, X creation_date D' % eid)[0][0] - self.assertEquals((creation_date - _now).seconds, 0) - - def test_metadata_created_by(self): - eid = self.execute('INSERT Note X')[0][0] - self.commit() # fire operations - rset = self.execute('Any U WHERE X eid %s, X created_by U' % eid) - self.assertEquals(len(rset), 1) # make sure we have only one creator - self.assertEquals(rset[0][0], self.session.user.eid) - - def test_metadata_owned_by(self): - eid = self.execute('INSERT Note X')[0][0] - self.commit() # fire operations - rset = self.execute('Any U WHERE X eid %s, X owned_by U' % eid) - self.assertEquals(len(rset), 1) # make sure we have only one owner - self.assertEquals(rset[0][0], self.session.user.eid) - if __name__ == '__main__': unittest_main() diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_ldapuser.py --- a/server/test/unittest_ldapuser.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_ldapuser.py Thu Aug 20 17:57:56 2009 +0200 @@ -156,7 +156,8 @@ self.patch_authenticate() cnx = self.login('syt', 'dummypassword') cu = cnx.cursor() - cu.execute('SET X in_state S WHERE X login "alf", S name "deactivated"') + alf = cu.execute('Any X WHERE X login "alf"').get_entity(0, 0) + alf.fire_transition('deactivate') try: cnx.commit() alf = self.execute('CWUser X WHERE X login "alf"').get_entity(0, 0) @@ -172,7 +173,8 @@ finally: # restore db state self.restore_connection() - self.execute('SET X in_state S WHERE X login "alf", S name "activated"') + alf = self.execute('Any X WHERE X login "alf"').get_entity(0, 0) + alf.fire_transition('activate') self.execute('DELETE X in_group G WHERE X login "syt", G name "managers"') def test_same_column_names(self): diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_migractions.py --- a/server/test/unittest_migractions.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_migractions.py Thu Aug 20 17:57:56 2009 +0200 @@ -106,23 +106,14 @@ def test_workflow_actions(self): - foo = self.mh.cmd_add_state(u'foo', ('Personne', 'Email'), initial=True) + wf = self.mh.cmd_add_workflow(u'foo', ('Personne', 'Email')) for etype in ('Personne', 'Email'): - s1 = self.mh.rqlexec('Any N WHERE S state_of ET, ET name "%s", S name N' % - etype)[0][0] - self.assertEquals(s1, "foo") - s1 = self.mh.rqlexec('Any N WHERE ET initial_state S, ET name "%s", S name N' % + s1 = self.mh.rqlexec('Any N WHERE WF workflow_of ET, ET name "%s", WF name N' % etype)[0][0] self.assertEquals(s1, "foo") - bar = self.mh.cmd_add_state(u'bar', ('Personne', 'Email'), initial=True) - baz = self.mh.cmd_add_transition(u'baz', ('Personne', 'Email'), - (foo,), bar, ('managers',)) - for etype in ('Personne', 'Email'): - t1 = self.mh.rqlexec('Any N WHERE T transition_of ET, ET name "%s", T name N' % + s1 = self.mh.rqlexec('Any N WHERE WF default_workflow_of ET, ET name "%s", WF name N' % etype)[0][0] - self.assertEquals(t1, "baz") - gn = self.mh.rqlexec('Any GN WHERE T require_group G, G name GN, T eid %s' % baz)[0][0] - self.assertEquals(gn, 'managers') + self.assertEquals(s1, "foo") def test_add_entity_type(self): self.failIf('Folder2' in self.schema) @@ -160,8 +151,9 @@ self.failIf('Folder2' in self.schema) self.failIf(self.execute('CWEType X WHERE X name "Folder2"')) # test automatic workflow deletion - self.failIf(self.execute('State X WHERE NOT X state_of ET')) - self.failIf(self.execute('Transition X WHERE NOT X transition_of ET')) + self.failIf(self.execute('Workflow X WHERE NOT X workflow_of ET')) + self.failIf(self.execute('State X WHERE NOT X state_of WF')) + self.failIf(self.execute('Transition X WHERE NOT X transition_of WF')) def test_add_drop_relation_type(self): self.mh.cmd_add_entity_type('Folder2', auto=False) diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_msplanner.py Thu Aug 20 17:57:56 2009 +0200 @@ -43,18 +43,19 @@ def syntax_tree_search(self, *args, **kwargs): return [] -X_ALL_SOLS = sorted([{'X': 'Affaire'}, {'X': 'Basket'}, {'X': 'Bookmark'}, +X_ALL_SOLS = sorted([{'X': 'Affaire'}, {'X': 'BaseTransition'}, {'X': 'Basket'}, + {'X': 'Bookmark'}, {'X': 'CWAttribute'}, {'X': 'CWCache'}, + {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, {'X': 'CWEType'}, + {'X': 'CWGroup'}, {'X': 'CWPermission'}, {'X': 'CWProperty'}, + {'X': 'CWRType'}, {'X': 'CWRelation'}, {'X': 'CWUser'}, {'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'}, - {'X': 'CWCache'}, {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, - {'X': 'CWEType'}, {'X': 'CWAttribute'}, {'X': 'CWGroup'}, - {'X': 'CWRelation'}, {'X': 'CWPermission'}, {'X': 'CWProperty'}, - {'X': 'CWRType'}, {'X': 'CWUser'}, {'X': 'Email'}, - {'X': 'EmailAddress'}, {'X': 'EmailPart'}, {'X': 'EmailThread'}, - {'X': 'ExternalUri'}, - {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'}, - {'X': 'Note'}, {'X': 'Personne'}, {'X': 'RQLExpression'}, - {'X': 'Societe'}, {'X': 'State'}, {'X': 'SubDivision'}, - {'X': 'Tag'}, {'X': 'TrInfo'}, {'X': 'Transition'}]) + {'X': 'Email'}, {'X': 'EmailAddress'}, {'X': 'EmailPart'}, + {'X': 'EmailThread'}, {'X': 'ExternalUri'}, {'X': 'File'}, + {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Note'}, + {'X': 'Personne'}, {'X': 'RQLExpression'}, {'X': 'Societe'}, + {'X': 'State'}, {'X': 'SubDivision'}, {'X': 'SubWorkflowExitPoint'}, + {'X': 'Tag'}, {'X': 'TrInfo'}, {'X': 'Transition'}, + {'X': 'Workflow'}, {'X': 'WorkflowTransition'}]) # keep cnx so it's not garbage collected and the associated session is closed @@ -770,12 +771,13 @@ [{'X': 'Basket'}]), ('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is CWUser', [{'X': 'CWUser'}]), - ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, State, SubDivision, Tag, Transition)', - [{'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'}, - {'X': 'Email'}, {'X': 'EmailThread'}, {'X': 'File'}, - {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Note'}, - {'X': 'Personne'}, {'X': 'Societe'}, {'X': 'State'}, - {'X': 'SubDivision'}, {'X': 'Tag'}, {'X': 'Transition'}]),], + ('Any X WHERE X has_text "bla", X is IN(BaseTransition, Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, State, SubDivision, Tag, Transition, Workflow, WorkflowTransition)', + [{'X': 'BaseTransition'}, {'X': 'Card'}, {'X': 'Comment'}, + {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'}, + {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'}, + {'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'}, + {'X': 'State'}, {'X': 'SubDivision'}, {'X': 'Tag'}, + {'X': 'Transition'}, {'X': 'Workflow'}, {'X': 'WorkflowTransition'}]),], None, None, [self.system], {}, []), ]) ]) @@ -793,25 +795,27 @@ [self.system], {'E': 'table1.C0'}, {'X': 'table0.C0'}, []), ('FetchStep', [('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is Basket', - [{'X': 'Basket'}]), - ('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is CWUser', - [{'X': 'CWUser'}]), - ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, State, SubDivision, Tag, Transition)', - [{'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'}, - {'X': 'Email'}, {'X': 'EmailThread'}, {'X': 'File'}, - {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Note'}, - {'X': 'Personne'}, {'X': 'Societe'}, {'X': 'State'}, - {'X': 'SubDivision'}, {'X': 'Tag'}, {'X': 'Transition'}]),], + [{'X': 'Basket'}]), + ('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is CWUser', + [{'X': 'CWUser'}]), + ('Any X WHERE X has_text "bla", X is IN(BaseTransition, Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, State, SubDivision, Tag, Transition, Workflow, WorkflowTransition)', + [{'X': 'BaseTransition'}, {'X': 'Card'}, {'X': 'Comment'}, + {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'}, + {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'}, + {'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'}, + {'X': 'State'}, {'X': 'SubDivision'}, {'X': 'Tag'}, + {'X': 'Transition'}, {'X': 'Workflow'}, {'X': 'WorkflowTransition'}])], [self.system], {}, {'X': 'table0.C0'}, []), ]), ('OneFetchStep', [('Any X LIMIT 10 OFFSET 10', - [{'X': 'Affaire'}, {'X': 'Basket'}, {'X': 'Card'}, - {'X': 'Comment'}, {'X': 'Division'}, {'X': 'CWUser'}, - {'X': 'Email'}, {'X': 'EmailThread'}, {'X': 'File'}, - {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Note'}, - {'X': 'Personne'}, {'X': 'Societe'}, {'X': 'State'}, - {'X': 'SubDivision'}, {'X': 'Tag'}, {'X': 'Transition'}])], + [{'X': 'Affaire'}, {'X': 'BaseTransition'}, {'X': 'Basket'}, + {'X': 'CWUser'}, {'X': 'Card'}, {'X': 'Comment'}, + {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'}, + {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'}, + {'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'}, + {'X': 'State'}, {'X': 'SubDivision'}, {'X': 'Tag'}, + {'X': 'Transition'}, {'X': 'Workflow'}, {'X': 'WorkflowTransition'}])], 10, 10, [self.system], {'X': 'table0.C0'}, []) ]) @@ -874,16 +878,23 @@ [{'X': 'Card'}, {'X': 'Note'}, {'X': 'State'}])], [self.cards, self.system], {}, {'X': 'table0.C0'}, []), ('FetchStep', - [('Any X WHERE X is IN(Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Personne, RQLExpression, Societe, SubDivision, Tag, TrInfo, Transition)', - sorted([{'X': 'Bookmark'}, {'X': 'Comment'}, {'X': 'Division'}, - {'X': 'CWCache'}, {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, - {'X': 'CWEType'}, {'X': 'CWAttribute'}, {'X': 'CWGroup'}, - {'X': 'CWRelation'}, {'X': 'CWPermission'}, {'X': 'CWProperty'}, - {'X': 'CWRType'}, {'X': 'Email'}, {'X': 'EmailAddress'}, - {'X': 'EmailPart'}, {'X': 'EmailThread'}, {'X': 'ExternalUri'}, {'X': 'File'}, - {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Personne'}, - {'X': 'RQLExpression'}, {'X': 'Societe'}, {'X': 'SubDivision'}, - {'X': 'Tag'}, {'X': 'TrInfo'}, {'X': 'Transition'}]))], + [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)', + [{'X': 'BaseTransition'}, {'X': 'Bookmark'}, + {'X': 'CWAttribute'}, {'X': 'CWCache'}, + {'X': 'CWConstraint'}, {'X': 'CWConstraintType'}, + {'X': 'CWEType'}, {'X': 'CWGroup'}, + {'X': 'CWPermission'}, {'X': 'CWProperty'}, + {'X': 'CWRType'}, {'X': 'CWRelation'}, + {'X': 'Comment'}, {'X': 'Division'}, + {'X': 'Email'}, {'X': 'EmailAddress'}, + {'X': 'EmailPart'}, {'X': 'EmailThread'}, + {'X': 'ExternalUri'}, {'X': 'File'}, + {'X': 'Folder'}, {'X': 'Image'}, + {'X': 'Personne'}, {'X': 'RQLExpression'}, + {'X': 'Societe'}, {'X': 'SubDivision'}, + {'X': 'SubWorkflowExitPoint'}, {'X': 'Tag'}, + {'X': 'TrInfo'}, {'X': 'Transition'}, + {'X': 'Workflow'}, {'X': 'WorkflowTransition'}])], [self.system], {}, {'X': 'table0.C0'}, []), ]), ('FetchStep', [('Any X WHERE EXISTS(X owned_by 5), X is CWUser', [{'X': 'CWUser'}])], @@ -899,6 +910,11 @@ def test_security_complex_aggregat2(self): # use a guest user self.session = self._user_session()[1] + X_ET_ALL_SOLS = [] + for s in X_ALL_SOLS: + ets = {'ET': 'CWEType'} + ets.update(s) + X_ET_ALL_SOLS.append(ets) self._test('Any ET, COUNT(X) GROUPBY ET ORDERBY ET WHERE X is ET', [('FetchStep', [('Any X WHERE X is IN(Card, Note, State)', [{'X': 'Card'}, {'X': 'Note'}, {'X': 'State'}])], @@ -923,23 +939,24 @@ [self.system], {'X': 'table3.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []), # extra UnionFetchStep could be avoided but has no cost, so don't care ('UnionFetchStep', - [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Personne, RQLExpression, Societe, SubDivision, Tag, TrInfo, Transition)', - [{'X': 'Bookmark', 'ET': 'CWEType'}, {'X': 'Comment', 'ET': 'CWEType'}, - {'X': 'Division', 'ET': 'CWEType'}, {'X': 'CWCache', 'ET': 'CWEType'}, - {'X': 'CWConstraint', 'ET': 'CWEType'}, {'X': 'CWConstraintType', 'ET': 'CWEType'}, - {'X': 'CWEType', 'ET': 'CWEType'}, {'X': 'CWAttribute', 'ET': 'CWEType'}, - {'X': 'CWGroup', 'ET': 'CWEType'}, {'X': 'CWRelation', 'ET': 'CWEType'}, - {'X': 'CWPermission', 'ET': 'CWEType'}, {'X': 'CWProperty', 'ET': 'CWEType'}, - {'X': 'CWRType', 'ET': 'CWEType'}, {'X': 'Email', 'ET': 'CWEType'}, + [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)', + [{'X': 'BaseTransition', 'ET': 'CWEType'}, + {'X': 'Bookmark', 'ET': 'CWEType'}, {'X': 'CWAttribute', 'ET': 'CWEType'}, + {'X': 'CWCache', 'ET': 'CWEType'}, {'X': 'CWConstraint', 'ET': 'CWEType'}, + {'X': 'CWConstraintType', 'ET': 'CWEType'}, {'X': 'CWEType', 'ET': 'CWEType'}, + {'X': 'CWGroup', 'ET': 'CWEType'}, {'X': 'CWPermission', 'ET': 'CWEType'}, + {'X': 'CWProperty', 'ET': 'CWEType'}, {'X': 'CWRType', 'ET': 'CWEType'}, + {'X': 'CWRelation', 'ET': 'CWEType'}, {'X': 'Comment', 'ET': 'CWEType'}, + {'X': 'Division', 'ET': 'CWEType'}, {'X': 'Email', 'ET': 'CWEType'}, {'X': 'EmailAddress', 'ET': 'CWEType'}, {'X': 'EmailPart', 'ET': 'CWEType'}, - {'X': 'EmailThread', 'ET': 'CWEType'}, - {'ET': 'CWEType', 'X': 'ExternalUri'}, - {'X': 'File', 'ET': 'CWEType'}, - {'X': 'Folder', 'ET': 'CWEType'}, {'X': 'Image', 'ET': 'CWEType'}, - {'X': 'Personne', 'ET': 'CWEType'}, {'X': 'RQLExpression', 'ET': 'CWEType'}, - {'X': 'Societe', 'ET': 'CWEType'}, {'X': 'SubDivision', 'ET': 'CWEType'}, + {'X': 'EmailThread', 'ET': 'CWEType'}, {'X': 'ExternalUri', 'ET': 'CWEType'}, + {'X': 'File', 'ET': 'CWEType'}, {'X': 'Folder', 'ET': 'CWEType'}, + {'X': 'Image', 'ET': 'CWEType'}, {'X': 'Personne', 'ET': 'CWEType'}, + {'X': 'RQLExpression', 'ET': 'CWEType'}, {'X': 'Societe', 'ET': 'CWEType'}, + {'X': 'SubDivision', 'ET': 'CWEType'}, {'X': 'SubWorkflowExitPoint', 'ET': 'CWEType'}, {'X': 'Tag', 'ET': 'CWEType'}, {'X': 'TrInfo', 'ET': 'CWEType'}, - {'X': 'Transition', 'ET': 'CWEType'}])], + {'X': 'Transition', 'ET': 'CWEType'}, {'X': 'Workflow', 'ET': 'CWEType'}, + {'X': 'WorkflowTransition', 'ET': 'CWEType'}])], [self.system], {}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []), ('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(Card, Note, State)', @@ -950,26 +967,7 @@ ]), ]), ('OneFetchStep', - [('Any ET,COUNT(X) GROUPBY ET ORDERBY ET', - sorted([{'ET': 'CWEType', 'X': 'Affaire'}, {'ET': 'CWEType', 'X': 'Basket'}, - {'ET': 'CWEType', 'X': 'Bookmark'}, {'ET': 'CWEType', 'X': 'Card'}, - {'ET': 'CWEType', 'X': 'Comment'}, {'ET': 'CWEType', 'X': 'Division'}, - {'ET': 'CWEType', 'X': 'CWCache'}, {'ET': 'CWEType', 'X': 'CWConstraint'}, - {'ET': 'CWEType', 'X': 'CWConstraintType'}, {'ET': 'CWEType', 'X': 'CWEType'}, - {'ET': 'CWEType', 'X': 'CWAttribute'}, {'ET': 'CWEType', 'X': 'CWGroup'}, - {'ET': 'CWEType', 'X': 'CWRelation'}, {'ET': 'CWEType', 'X': 'CWPermission'}, - {'ET': 'CWEType', 'X': 'CWProperty'}, {'ET': 'CWEType', 'X': 'CWRType'}, - {'ET': 'CWEType', 'X': 'CWUser'}, {'ET': 'CWEType', 'X': 'Email'}, - {'ET': 'CWEType', 'X': 'EmailAddress'}, {'ET': 'CWEType', 'X': 'EmailPart'}, - {'ET': 'CWEType', 'X': 'EmailThread'}, - {'ET': 'CWEType', 'X': 'ExternalUri'}, - {'ET': 'CWEType', 'X': 'File'}, - {'ET': 'CWEType', 'X': 'Folder'}, {'ET': 'CWEType', 'X': 'Image'}, - {'ET': 'CWEType', 'X': 'Note'}, {'ET': 'CWEType', 'X': 'Personne'}, - {'ET': 'CWEType', 'X': 'RQLExpression'}, {'ET': 'CWEType', 'X': 'Societe'}, - {'ET': 'CWEType', 'X': 'State'}, {'ET': 'CWEType', 'X': 'SubDivision'}, - {'ET': 'CWEType', 'X': 'Tag'}, {'ET': 'CWEType', 'X': 'TrInfo'}, - {'ET': 'CWEType', 'X': 'Transition'}]))], + [('Any ET,COUNT(X) GROUPBY ET ORDERBY ET', X_ET_ALL_SOLS)], None, None, [self.system], {'ET': 'table0.C0', 'X': 'table0.C1'}, []) ]) @@ -1707,6 +1705,7 @@ ]) def test_nonregr2(self): + self.session.user.fire_transition('deactivate') treid = self.session.user.latest_trinfo().eid self._test('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D', [('FetchStep', [('Any X,D WHERE X modification_date D, X is Note', diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_multisources.py --- a/server/test/unittest_multisources.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_multisources.py Thu Aug 20 17:57:56 2009 +0200 @@ -30,7 +30,7 @@ cu = cnx2.cursor() ec1 = cu.execute('INSERT Card X: X title "C3: An external card", X wikiid "aaa"')[0][0] cu.execute('INSERT Card X: X title "C4: Ze external card", X wikiid "zzz"') -aff1 = cu.execute('INSERT Affaire X: X ref "AFFREF", X in_state S WHERE S name "pitetre"')[0][0] +aff1 = cu.execute('INSERT Affaire X: X ref "AFFREF"')[0][0] cnx2.commit() MTIME = datetime.now() - timedelta(0, 10) @@ -122,7 +122,7 @@ cu = cnx2.cursor() assert cu.execute('Any X WHERE X eid %(x)s', {'x': aff1}, 'x') cu.execute('SET X ref "BLAH" WHERE X eid %(x)s', {'x': aff1}, 'x') - aff2 = cu.execute('INSERT Affaire X: X ref "AFFREUX", X in_state S WHERE S name "pitetre"')[0][0] + aff2 = cu.execute('INSERT Affaire X: X ref "AFFREUX"')[0][0] cnx2.commit() try: # force sync @@ -267,6 +267,7 @@ {'x': affaire.eid, 'u': ueid}) def test_nonregr2(self): + self.session.user.fire_transition('deactivate') treid = self.session.user.latest_trinfo().eid rset = self.execute('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D', {'x': treid}) diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_querier.py --- a/server/test/unittest_querier.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_querier.py Thu Aug 20 17:57:56 2009 +0200 @@ -109,10 +109,10 @@ 'X': 'Affaire', 'ET': 'CWEType', 'ETN': 'String'}]) rql, solutions = partrqls[1] - self.assertEquals(rql, 'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, ' - 'X is IN(Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Note, Personne, RQLExpression, Societe, State, SubDivision, Tag, TrInfo, Transition)') + self.assertEquals(rql, 'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Note, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)') self.assertListEquals(sorted(solutions), - sorted([{'X': 'Bookmark', 'ETN': 'String', 'ET': 'CWEType'}, + sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'Bookmark', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Card', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Comment', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Division', 'ETN': 'String', 'ET': 'CWEType'}, @@ -141,9 +141,12 @@ {'X': 'Societe', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'State', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'SubDivision', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'SubWorkflowExitPoint', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Tag', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Transition', 'ETN': 'String', 'ET': 'CWEType'}, - {'X': 'TrInfo', 'ETN': 'String', 'ET': 'CWEType'}])) + {'X': 'TrInfo', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'Workflow', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'WorkflowTransition', 'ETN': 'String', 'ET': 'CWEType'}])) rql, solutions = partrqls[2] self.assertEquals(rql, 'Any ETN,X WHERE X is ET, ET name ETN, EXISTS(X owned_by %(C)s), ' @@ -285,8 +288,8 @@ self.assert_(('Personne',) in rset.description) def test_select_not_attr(self): - self.execute("INSERT Personne X: X nom 'bidule'") - self.execute("INSERT Societe X: X nom 'chouette'") + peid = self.execute("INSERT Personne X: X nom 'bidule'")[0][0] + seid = self.execute("INSERT Societe X: X nom 'chouette'")[0][0] rset = self.execute('Personne X WHERE NOT X nom "bidule"') self.assertEquals(len(rset.rows), 0, rset.rows) rset = self.execute('Personne X WHERE NOT X nom "bid"') @@ -350,27 +353,11 @@ self.assertEquals(rset.rows, [[peid1]]) def test_select_left_outer_join(self): - ueid = self.execute("INSERT CWUser X: X login 'bob', X upassword 'toto', X in_group G " - "WHERE G name 'users'")[0][0] - self.commit() - try: - rset = self.execute('Any FS,TS,C,D,U ORDERBY D DESC ' - 'WHERE WF wf_info_for X,' - 'WF from_state FS?, WF to_state TS, WF comment C,' - 'WF creation_date D, WF owned_by U, X eid %(x)s', - {'x': ueid}, 'x') - self.assertEquals(len(rset), 1) - self.execute('SET X in_state S WHERE X eid %(x)s, S name "deactivated"', - {'x': ueid}, 'x') - rset = self.execute('Any FS,TS,C,D,U ORDERBY D DESC ' - 'WHERE WF wf_info_for X,' - 'WF from_state FS?, WF to_state TS, WF comment C,' - 'WF creation_date D, WF owned_by U, X eid %(x)s', - {'x': ueid}, 'x') - self.assertEquals(len(rset), 2) - finally: - self.execute('DELETE CWUser X WHERE X eid %s' % ueid) - self.commit() + rset = self.execute('DISTINCT Any G WHERE U? in_group G') + self.assertEquals(len(rset), 4) + rset = self.execute('DISTINCT Any G WHERE U? in_group G, U eid %(x)s', + {'x': self.session.user.eid}, 'x') + self.assertEquals(len(rset), 4) def test_select_ambigous_outer_join(self): teid = self.execute("INSERT Tag X: X name 'tag'")[0][0] @@ -466,12 +453,17 @@ 'WHERE RT name N, RDEF relation_type RT ' 'HAVING COUNT(RDEF) > 10') self.assertListEquals(rset.rows, - [[u'description', 11], - [u'name', 13], [u'created_by', 34], - [u'creation_date', 34], [u'cwuri', 34], - ['in_basket', 34], - [u'is', 34], [u'is_instance_of', 34], - [u'modification_date', 34], [u'owned_by', 34]]) + [[u'description_format', 13], + [u'description', 14], + [u'name', 16], + [u'created_by', 38], + [u'creation_date', 38], + [u'cwuri', 38], + [u'in_basket', 38], + [u'is', 38], + [u'is_instance_of', 38], + [u'modification_date', 38], + [u'owned_by', 38]]) def test_select_aggregat_having_dumb(self): # dumb but should not raise an error @@ -721,9 +713,9 @@ def test_select_union(self): rset = self.execute('Any X,N ORDERBY N WITH X,N BEING ' - '((Any X,N WHERE X name N, X transition_of E, E name %(name)s)' + '((Any X,N WHERE X name N, X transition_of WF, WF workflow_of E, E name %(name)s)' ' UNION ' - '(Any X,N WHERE X name N, X state_of E, E name %(name)s))', + '(Any X,N WHERE X name N, X state_of WF, WF workflow_of E, E name %(name)s))', {'name': 'CWUser'}) self.assertEquals([x[1] for x in rset.rows], ['activate', 'activated', 'deactivate', 'deactivated']) @@ -995,20 +987,18 @@ # update queries tests #################################################### def test_update_1(self): - self.execute("INSERT Personne Y: Y nom 'toto'") + peid = self.execute("INSERT Personne Y: Y nom 'toto'")[0][0] rset = self.execute('Personne X WHERE X nom "toto"') self.assertEqual(len(rset.rows), 1) - self.execute("SET X nom 'tutu', X prenom 'original' WHERE X is Personne, X nom 'toto'") + rset = self.execute("SET X nom 'tutu', X prenom 'original' WHERE X is Personne, X nom 'toto'") + self.assertEqual(tuplify(rset.rows), [(peid, 'tutu', 'original')]) rset = self.execute('Any Y, Z WHERE X is Personne, X nom Y, X prenom Z') self.assertEqual(tuplify(rset.rows), [('tutu', 'original')]) def test_update_2(self): - self.execute("INSERT Personne X, Societe Y: X nom 'bidule', Y nom 'toto'") - #rset = self.execute('Any X, Y WHERE X nom "bidule", Y nom "toto"') - #self.assertEqual(len(rset.rows), 1) - #rset = self.execute('Any X, Y WHERE X travaille Y') - #self.assertEqual(len(rset.rows), 0) - self.execute("SET X travaille Y WHERE X nom 'bidule', Y nom 'toto'") + peid, seid = self.execute("INSERT Personne X, Societe Y: X nom 'bidule', Y nom 'toto'")[0] + rset = self.execute("SET X travaille Y WHERE X nom 'bidule', Y nom 'toto'") + self.assertEquals(tuplify(rset.rows), [(peid, seid)]) rset = self.execute('Any X, Y WHERE X travaille Y') self.assertEqual(len(rset.rows), 1) @@ -1028,9 +1018,6 @@ rset = self.execute('Any X, Y WHERE X travaille Y') self.assertEqual(len(rset.rows), 1) -## def test_update_4(self): -## self.execute("SET X know Y WHERE X ami Y") - def test_update_multiple1(self): peid1 = self.execute("INSERT Personne Y: Y nom 'tutu'")[0][0] peid2 = self.execute("INSERT Personne Y: Y nom 'toto'")[0][0] @@ -1130,7 +1117,7 @@ """bad sql generated on the second query (destination_state is not detected as an inlined relation) """ - rset = self.execute('Any S,ES,T WHERE S state_of ET, ET name "CWUser",' + rset = self.execute('Any S,ES,T WHERE S state_of WF, WF workflow_of ET, ET name "CWUser",' 'ES allowed_transition T, T destination_state S') self.assertEquals(len(rset.rows), 2) @@ -1260,9 +1247,8 @@ def test_nonregr_set_query(self): ueid = self.execute("INSERT CWUser X: X login 'bob', X upassword 'toto'")[0][0] - self.execute("SET E in_group G, E in_state S, " - "E firstname %(firstname)s, E surname %(surname)s " - "WHERE E eid %(x)s, G name 'users', S name 'activated'", + self.execute("SET E in_group G, E firstname %(firstname)s, E surname %(surname)s " + "WHERE E eid %(x)s, G name 'users'", {'x':ueid, 'firstname': u'jean', 'surname': u'paul'}, 'x') def test_nonregr_u_owned_by_u(self): diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_repository.py Thu Aug 20 17:57:56 2009 +0200 @@ -102,7 +102,7 @@ def test_login_upassword_accent(self): repo = self.repo cnxid = repo.connect(*self.default_user_password()) - repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_state S, X in_group G WHERE S name "activated", G name "users"', + repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_group G WHERE G name "users"', {'login': u"barnabé", 'passwd': u"héhéhé".encode('UTF8')}) repo.commit(cnxid) repo.close(cnxid) @@ -112,7 +112,7 @@ repo = self.repo cnxid = repo.connect(*self.default_user_password()) # no group - repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_state S WHERE S name "activated"', + repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s', {'login': u"tutetute", 'passwd': 'tutetute'}) self.assertRaises(ValidationError, repo.commit, cnxid) rset = repo.execute(cnxid, 'CWUser X WHERE X login "tutetute"') @@ -190,16 +190,13 @@ repo = self.repo cnxid = repo.connect(*self.default_user_password()) # rollback state change which trigger TrInfo insertion - ueid = repo._get_session(cnxid).user.eid - rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) + user = repo._get_session(cnxid).user + user.fire_transition('deactivate') + rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid}) self.assertEquals(len(rset), 1) - repo.execute(cnxid, 'SET X in_state S WHERE X eid %(x)s, S name "deactivated"', - {'x': ueid}, 'x') - rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 2) repo.rollback(cnxid) - rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid}) - self.assertEquals(len(rset), 1) + rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid}) + self.assertEquals(len(rset), 0) def test_transaction_interleaved(self): self.skip('implement me') @@ -342,6 +339,22 @@ # self.set_debug(False) # print 'test time: %.3f (time) %.3f (cpu)' % ((time() - t), clock() - c) + def test_delete_if_singlecard1(self): + note = self.add_entity('Affaire') + p1 = self.add_entity('Personne', nom=u'toto') + self.execute('SET A todo_by P WHERE A eid %(x)s, P eid %(p)s', + {'x': note.eid, 'p': p1.eid}) + rset = self.execute('Any P WHERE A todo_by P, A eid %(x)s', + {'x': note.eid}) + self.assertEquals(len(rset), 1) + p2 = self.add_entity('Personne', nom=u'tutu') + self.execute('SET A todo_by P WHERE A eid %(x)s, P eid %(p)s', + {'x': note.eid, 'p': p2.eid}) + rset = self.execute('Any P WHERE A todo_by P, A eid %(x)s', + {'x': note.eid}) + self.assertEquals(len(rset), 1) + self.assertEquals(rset.rows[0][0], p2.eid) + class DataHelpersTC(RepositoryBasedTC): @@ -485,11 +498,11 @@ def test_after_add_inline(self): """make sure after__relation hooks are deferred""" + p1 = self.add_entity('Personne', nom=u'toto') self.hm.register_hook(self._after_relation_hook, - 'after_add_relation', 'in_state') - eidp = self.execute('INSERT CWUser X: X login "toto", X upassword "tutu", X in_state S WHERE S name "activated"')[0][0] - eids = self.execute('State X WHERE X name "activated"')[0][0] - self.assertEquals(self.called, [(eidp, 'in_state', eids,)]) + 'after_add_relation', 'ecrit_par') + eidn = self.execute('INSERT Note N: N ecrit_par P WHERE P nom "toto"')[0][0] + self.assertEquals(self.called, [(eidn, 'ecrit_par', p1.eid,)]) def test_before_delete_inline_relation(self): """make sure before__relation hooks are called directly""" diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_rql2sql.py --- a/server/test/unittest_rql2sql.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_rql2sql.py Thu Aug 20 17:57:56 2009 +0200 @@ -339,6 +339,9 @@ ('Any XN ORDERBY XN WHERE X name XN', '''SELECT X.cw_name +FROM cw_BaseTransition AS X +UNION ALL +SELECT X.cw_name FROM cw_Basket AS X UNION ALL SELECT X.cw_name @@ -376,6 +379,12 @@ UNION ALL SELECT X.cw_name FROM cw_Transition AS X +UNION ALL +SELECT X.cw_name +FROM cw_Workflow AS X +UNION ALL +SELECT X.cw_name +FROM cw_WorkflowTransition AS X ORDER BY 1'''), # DISTINCT, can use relation under exists scope as principal @@ -462,6 +471,9 @@ ('Any MAX(X)+MIN(X), N GROUPBY N WHERE X name N;', '''SELECT (MAX(T1.C0) + MIN(T1.C0)), T1.C1 FROM (SELECT X.cw_eid AS C0, X.cw_name AS C1 +FROM cw_BaseTransition AS X +UNION ALL +SELECT X.cw_eid AS C0, X.cw_name AS C1 FROM cw_Basket AS X UNION ALL SELECT X.cw_eid AS C0, X.cw_name AS C1 @@ -498,7 +510,13 @@ FROM cw_Tag AS X UNION ALL SELECT X.cw_eid AS C0, X.cw_name AS C1 -FROM cw_Transition AS X) AS T1 +FROM cw_Transition AS X +UNION ALL +SELECT X.cw_eid AS C0, X.cw_name AS C1 +FROM cw_Workflow AS X +UNION ALL +SELECT X.cw_eid AS C0, X.cw_name AS C1 +FROM cw_WorkflowTransition AS X) AS T1 GROUP BY T1.C1'''), ('Any MAX(X)+MIN(LENGTH(D)), N GROUPBY N ORDERBY 1, N, DF WHERE X name N, X data D, X data_format DF;', @@ -1029,8 +1047,9 @@ ('Any S,ES,T WHERE S state_of ET, ET name "CWUser", ES allowed_transition T, T destination_state S', '''SELECT T.cw_destination_state, rel_allowed_transition1.eid_from, T.cw_eid -FROM allowed_transition_relation AS rel_allowed_transition1, cw_CWEType AS ET, cw_Transition AS T, state_of_relation AS rel_state_of0 +FROM allowed_transition_relation AS rel_allowed_transition1, cw_Transition AS T, cw_Workflow AS ET, state_of_relation AS rel_state_of0 WHERE T.cw_destination_state=rel_state_of0.eid_from AND rel_state_of0.eid_to=ET.cw_eid AND ET.cw_name=CWUser AND rel_allowed_transition1.eid_to=T.cw_eid'''), + ('Any O WHERE S eid 0, S in_state O', '''SELECT S.cw_in_state FROM cw_Affaire AS S @@ -1106,11 +1125,11 @@ delete = self.rqlhelper.parse( 'DELETE X read_permission READ_PERMISSIONSUBJECT,X add_permission ADD_PERMISSIONSUBJECT,' 'X in_basket IN_BASKETSUBJECT,X delete_permission DELETE_PERMISSIONSUBJECT,' - 'X initial_state INITIAL_STATESUBJECT,X update_permission UPDATE_PERMISSIONSUBJECT,' + 'X update_permission UPDATE_PERMISSIONSUBJECT,' 'X created_by CREATED_BYSUBJECT,X is ISSUBJECT,X is_instance_of IS_INSTANCE_OFSUBJECT,' 'X owned_by OWNED_BYSUBJECT,X specializes SPECIALIZESSUBJECT,ISOBJECT is X,' - 'SPECIALIZESOBJECT specializes X,STATE_OFOBJECT state_of X,IS_INSTANCE_OFOBJECT is_instance_of X,' - 'TO_ENTITYOBJECT to_entity X,TRANSITION_OFOBJECT transition_of X,FROM_ENTITYOBJECT from_entity X ' + 'SPECIALIZESOBJECT specializes X,IS_INSTANCE_OFOBJECT is_instance_of X,' + 'TO_ENTITYOBJECT to_entity X,FROM_ENTITYOBJECT from_entity X ' 'WHERE X is CWEType') self.rqlhelper.compute_solutions(delete) def var_sols(var): @@ -1379,7 +1398,7 @@ FROM appears AS appears0, entities AS X WHERE appears0.words @@ to_tsquery('default', 'hip&hop&momo') AND appears0.uid=X.eid AND X.type='Personne'"""), - ('Any X WHERE X has_text "toto tata", X name "tutu"', + ('Any X WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,File,Folder)', """SELECT X.cw_eid FROM appears AS appears0, cw_Basket AS X WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu @@ -1391,22 +1410,7 @@ SELECT X.cw_eid FROM appears AS appears0, cw_Folder AS X WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Image AS X -WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_State AS X -WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Tag AS X -WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Transition AS X -WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu"""), +"""), ('Personne X where X has_text %(text)s, X travaille S, S has_text %(text)s', """SELECT X.eid @@ -1543,7 +1547,7 @@ FROM appears AS appears0, entities AS X WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.eid AND X.type='Personne'"""), - ('Any X WHERE X has_text "toto tata", X name "tutu"', + ('Any X WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,File,Folder)', """SELECT X.cw_eid FROM appears AS appears0, cw_Basket AS X WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu @@ -1555,22 +1559,7 @@ SELECT X.cw_eid FROM appears AS appears0, cw_Folder AS X WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Image AS X -WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_State AS X -WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Tag AS X -WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Transition AS X -WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu"""), +"""), )): yield t @@ -1619,7 +1608,7 @@ """SELECT X.eid FROM appears AS appears0, entities AS X WHERE MATCH (appears0.words) AGAINST ('hip hop momo' IN BOOLEAN MODE) AND appears0.uid=X.eid AND X.type='Personne'"""), - ('Any X WHERE X has_text "toto tata", X name "tutu"', + ('Any X WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,File,Folder)', """SELECT X.cw_eid FROM appears AS appears0, cw_Basket AS X WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu @@ -1631,22 +1620,7 @@ SELECT X.cw_eid FROM appears AS appears0, cw_Folder AS X WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Image AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_State AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Tag AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu -UNION ALL -SELECT X.cw_eid -FROM appears AS appears0, cw_Transition AS X -WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu""") +""") ] for t in self._parse(queries): yield t diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_rqlrewrite.py --- a/server/test/unittest_rqlrewrite.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_rqlrewrite.py Thu Aug 20 17:57:56 2009 +0200 @@ -101,13 +101,12 @@ def test_or(self): constraint = '(X identity U) OR (X in_state ST, CL identity U, CL in_state ST, ST name "subscribed")' - rqlst = parse('Any S WHERE S owned_by C, C eid %(u)s') + rqlst = parse('Any S WHERE S owned_by C, C eid %(u)s, S is in (CWUser, CWGroup)') rewrite(rqlst, {'C': (constraint,)}, {'u':1}) self.failUnlessEqual(rqlst.as_string(), - "Any S WHERE S owned_by C, C eid %(u)s, A eid %(B)s, " + "Any S WHERE S owned_by C, C eid %(u)s, S is IN(CWUser, CWGroup), A eid %(B)s, " "EXISTS((C identity A) OR (C in_state D, E identity A, " - "E in_state D, D name 'subscribed'), D is State, E is CWUser), " - "S is IN(Affaire, Basket, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Note, Personne, RQLExpression, Societe, State, SubDivision, Tag, TrInfo, Transition)") + "E in_state D, D name 'subscribed'), D is State, E is CWUser)") def test_simplified_rqlst(self): card_constraint = ('X in_state S, U in_group G, P require_state S,' diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_schemaserial.py --- a/server/test/unittest_schemaserial.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_schemaserial.py Thu Aug 20 17:57:56 2009 +0200 @@ -33,12 +33,17 @@ {'description': u'', 'final': True, 'name': u'String'})]) def test_eschema2rql_specialization(self): - self.assertListEquals(list(specialize2rql(schema)), - [ - ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', - {'x': 'Division', 'et': 'Societe'}), - ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', - {'x': 'SubDivision', 'et': 'Division'})]) + self.assertListEquals(sorted(specialize2rql(schema)), + [('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'BaseTransition', 'x': 'Transition'}), + ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'BaseTransition', 'x': 'WorkflowTransition'}), + ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'Division', 'x': 'SubDivision'}), + # ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + # {'et': 'File', 'x': 'Image'}), + ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s', + {'et': 'Societe', 'x': 'Division'})]) def test_rschema2rql1(self): self.assertListEquals(list(rschema2rql(schema.rschema('relation_type'))), diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_security.py --- a/server/test/unittest_security.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_security.py Thu Aug 20 17:57:56 2009 +0200 @@ -265,7 +265,7 @@ self.commit() cnx = self.login('iaminusersgrouponly') cu = cnx.cursor() - aff2 = cu.execute("INSERT Affaire X: X sujet 'cool', X in_state S WHERE S name 'pitetre'")[0][0] + aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0] soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0] cu.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1}, ('a', 's')) @@ -347,25 +347,26 @@ def test_attribute_security_rqlexpr(self): # Note.para attribute editable by managers or if the note is in "todo" state - eid = self.execute("INSERT Note X: X para 'bidule', X in_state S WHERE S name 'done'")[0][0] + note = self.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0) self.commit() - self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': eid}, 'x') + note.fire_transition('markasdone') + self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid}, 'x') self.commit() cnx = self.login('iaminusersgrouponly') cu = cnx.cursor() - cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid}, 'x') + cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid}, 'x') self.assertRaises(Unauthorized, cnx.commit) - eid2 = cu.execute("INSERT Note X: X para 'bidule'")[0][0] + note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0) cnx.commit() - cu.execute("SET X in_state S WHERE X eid %(x)s, S name 'done'", {'x': eid2}, 'x') + note2.fire_transition('markasdone') cnx.commit() - self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': eid2}, 'x')), + self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid}, 'x')), 0) - cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid2}, 'x') + cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}, 'x') self.assertRaises(Unauthorized, cnx.commit) - cu.execute("SET X in_state S WHERE X eid %(x)s, S name 'todo'", {'x': eid2}, 'x') + note2.fire_transition('redoit') cnx.commit() - cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid2}, 'x') + cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}, 'x') cnx.commit() def test_attribute_read_security(self): @@ -398,16 +399,14 @@ cu.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"') cnx.commit() self.restore_connection() - self.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"') + affaire = self.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0) + affaire.fire_transition('abort') self.commit() self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01"')), - 2) + 1) self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",' 'X owned_by U, U login "admin"')), 1) # TrInfo at the above state change - self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",' - 'X owned_by U, U login "iaminusersgrouponly"')), - 1) # TrInfo created at creation time cnx = self.login('iaminusersgrouponly') cu = cnx.cursor() cu.execute('DELETE Affaire X WHERE X ref "ARCT01"') @@ -499,29 +498,34 @@ self.assertRaises(Unauthorized, self.schema['Affaire'].check_perm, session, 'update', eid) cu = cnx.cursor() - cu.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"') - cnx.commit() - # though changing a user state (even logged user) is reserved to managers - rql = u"SET X in_state S WHERE X eid %(x)s, S name 'deactivated'" - # XXX wether it should raise Unauthorized or ValidationError is not clear - # the best would probably ValidationError if the transition doesn't exist - # from the current state but Unauthorized if it exists but user can't pass it - self.assertRaises(ValidationError, cu.execute, rql, {'x': cnx.user(self.current_session()).eid}, 'x') + self.schema['Affaire'].set_groups('read', ('users',)) + try: + aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0) + aff.fire_transition('abort') + cnx.commit() + # though changing a user state (even logged user) is reserved to managers + user = cnx.user(self.current_session()) + # XXX wether it should raise Unauthorized or ValidationError is not clear + # the best would probably ValidationError if the transition doesn't exist + # from the current state but Unauthorized if it exists but user can't pass it + self.assertRaises(ValidationError, user.fire_transition, 'deactivate') + finally: + self.schema['Affaire'].set_groups('read', ('managers',)) def test_trinfo_security(self): aff = self.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0) self.commit() + aff.fire_transition('abort') + self.commit() # can change tr info comment self.execute('SET TI comment %(c)s WHERE TI wf_info_for X, X ref "ARCT01"', - {'c': u'creation'}) + {'c': u'bouh!'}) self.commit() aff.clear_related_cache('wf_info_for', 'object') - self.assertEquals(aff.latest_trinfo().comment, 'creation') + trinfo = aff.latest_trinfo() + self.assertEquals(trinfo.comment, 'bouh!') # but not from_state/to_state - self.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"') - self.commit() aff.clear_related_cache('wf_info_for', role='object') - trinfo = aff.latest_trinfo() self.assertRaises(Unauthorized, self.execute, 'SET TI from_state S WHERE TI eid %(ti)s, S name "ben non"', {'ti': trinfo.eid}, 'ti') diff -r d7c23b2c7538 -r 17224e90a1c4 server/test/unittest_ssplanner.py --- a/server/test/unittest_ssplanner.py Thu Aug 20 17:57:31 2009 +0200 +++ b/server/test/unittest_ssplanner.py Thu Aug 20 17:57:56 2009 +0200 @@ -9,7 +9,7 @@ from cubicweb.devtools.repotest import BasePlannerTC, test_plan from cubicweb.server.ssplanner import SSPlanner -# keep cnx so it's not garbage collected and the associated session is closed +# keep cnx so it's not garbage collected and the associated session closed repo, cnx = init_test_database('sqlite') class SSPlannerTC(BasePlannerTC): @@ -25,40 +25,20 @@ BasePlannerTC.tearDown(self) def test_ordered_ambigous_sol(self): - self._test('Any XN ORDERBY XN WHERE X name XN', - [('OneFetchStep', [('Any XN ORDERBY XN WHERE X name XN', + self._test('Any XN ORDERBY XN WHERE X name XN, X is IN (Basket, File, Folder)', + [('OneFetchStep', [('Any XN ORDERBY XN WHERE X name XN, X is IN(Basket, File, Folder)', [{'X': 'Basket', 'XN': 'String'}, - {'X': 'CWCache', 'XN': 'String'}, - {'X': 'CWConstraintType', 'XN': 'String'}, - {'X': 'CWEType', 'XN': 'String'}, - {'X': 'CWGroup', 'XN': 'String'}, - {'X': 'CWPermission', 'XN': 'String'}, - {'X': 'CWRType', 'XN': 'String'}, {'X': 'File', 'XN': 'String'}, - {'X': 'Folder', 'XN': 'String'}, - {'X': 'Image', 'XN': 'String'}, - {'X': 'State', 'XN': 'String'}, - {'X': 'Tag', u'XN': 'String'}, - {'X': 'Transition', 'XN': 'String'}])], + {'X': 'Folder', 'XN': 'String'}])], None, None, [self.system], None, [])]) def test_groupeded_ambigous_sol(self): - self._test('Any XN,COUNT(X) GROUPBY XN WHERE X name XN', - [('OneFetchStep', [('Any XN,COUNT(X) GROUPBY XN WHERE X name XN', + self._test('Any XN,COUNT(X) GROUPBY XN WHERE X name XN, X is IN (Basket, File, Folder)', + [('OneFetchStep', [('Any XN,COUNT(X) GROUPBY XN WHERE X name XN, X is IN(Basket, File, Folder)', [{'X': 'Basket', 'XN': 'String'}, - {'X': 'CWCache', 'XN': 'String'}, - {'X': 'CWConstraintType', 'XN': 'String'}, - {'X': 'CWEType', 'XN': 'String'}, - {'X': 'CWGroup', 'XN': 'String'}, - {'X': 'CWPermission', 'XN': 'String'}, - {'X': 'CWRType', 'XN': 'String'}, {'X': 'File', 'XN': 'String'}, - {'X': 'Folder', 'XN': 'String'}, - {'X': 'Image', 'XN': 'String'}, - {'X': 'State', 'XN': 'String'}, - {'X': 'Tag', u'XN': 'String'}, - {'X': 'Transition', 'XN': 'String'}])], + {'X': 'Folder', 'XN': 'String'}])], None, None, [self.system], None, [])]) diff -r d7c23b2c7538 -r 17224e90a1c4 sobjects/supervising.py --- a/sobjects/supervising.py Thu Aug 20 17:57:31 2009 +0200 +++ b/sobjects/supervising.py Thu Aug 20 17:57:56 2009 +0200 @@ -83,27 +83,18 @@ added.add(entity.eid) if entity.e_schema == 'TrInfo': changes.remove(change) - if entity.from_state: - try: - changes.remove( ('delete_relation', - (entity.wf_info_for[0].eid, 'in_state', - entity.from_state[0].eid)) ) - except ValueError: - pass - try: - changes.remove( ('add_relation', - (entity.wf_info_for[0].eid, 'in_state', - entity.to_state[0].eid)) ) - except ValueError: - pass - event = 'change_state' - change = (event, - (entity.wf_info_for[0], - entity.from_state[0], entity.to_state[0])) - changes.append(change) + event = 'change_state' + change = (event, + (entity.wf_info_for[0], + entity.from_state[0], entity.to_state[0])) + changes.append(change) elif event == 'delete_entity': deleted.add(changedescr[0]) index.setdefault(event, set()).add(change) + for key in ('delete_relation', 'add_relation'): + for change in index.get(key, {}).copy(): + if change[1][1] == 'in_state': + index[key].remove(change) # filter changes for eid in added: try: @@ -112,14 +103,10 @@ # skip meta-relations which are set automatically # XXX generate list below using rtags (category = 'generated') if changedescr[1] in ('created_by', 'owned_by', 'is', 'is_instance_of', - 'from_state', 'to_state', 'wf_info_for',) \ + 'from_state', 'to_state', 'by_transition', + 'wf_info_for') \ and changedescr[0] == eid: index['add_relation'].remove(change) - # skip in_state relation if the entity is being created - # XXX this may be automatized by skipping all mandatory relation - # at entity creation time - elif changedescr[1] == 'in_state' and changedescr[0] in added: - index['add_relation'].remove(change) except KeyError: break diff -r d7c23b2c7538 -r 17224e90a1c4 sobjects/test/unittest_notification.py --- a/sobjects/test/unittest_notification.py Thu Aug 20 17:57:31 2009 +0200 +++ b/sobjects/test/unittest_notification.py Thu Aug 20 17:57:56 2009 +0200 @@ -9,9 +9,9 @@ from socket import gethostname from logilab.common.testlib import unittest_main, TestCase -from cubicweb.devtools.apptest import EnvBasedTC +from cubicweb.devtools.apptest import EnvBasedTC, MAILBOX -from cubicweb.sobjects.notification import construct_message_id, parse_message_id +from cubicweb.common.mail import construct_message_id, parse_message_id class MessageIdTC(TestCase): def test_base(self): @@ -71,16 +71,14 @@ def test_status_change_view(self): req = self.session() - u = self.create_user('toto', req=req) - assert u.req - assert u.rset - self.execute('SET X in_state S WHERE X eid %s, S name "deactivated"' % u.eid) - v = self.vreg['views'].select('notif_status_change', req, rset=u.rset, row=0) - content = v.render(row=0, comment='yeah', - previous_state='activated', - current_state='deactivated') - # remove date - self.assertEquals(content, + u = self.create_user('toto', req=req)#, commit=False) XXX in cw 3.6, and remove set_pool + req.set_pool() + u.fire_transition('deactivate', comment=u'yeah') + self.failIf(MAILBOX) + self.commit() + self.assertEquals(len(MAILBOX), 1) + email = MAILBOX[0] + self.assertEquals(email.content, ''' admin changed status from to for entity 'toto' @@ -89,7 +87,7 @@ url: http://testing.fr/cubicweb/cwuser/toto ''') - self.assertEquals(v.subject(), 'status changed cwuser #%s (admin)' % u.eid) + self.assertEquals(email.subject, 'status changed cwuser #%s (admin)' % u.eid) if __name__ == '__main__': unittest_main() diff -r d7c23b2c7538 -r 17224e90a1c4 sobjects/test/unittest_supervising.py --- a/sobjects/test/unittest_supervising.py Thu Aug 20 17:57:31 2009 +0200 +++ b/sobjects/test/unittest_supervising.py Thu Aug 20 17:57:56 2009 +0200 @@ -28,12 +28,11 @@ def test_supervision(self): session = self.session() # do some modification - ueid = self.execute('INSERT CWUser X: X login "toto", X upassword "sosafe", X in_group G, X in_state S ' - 'WHERE G name "users", S name "activated"')[0][0] - self.execute('SET X last_login_time NOW WHERE X eid %(x)s', {'x': ueid}, 'x') - self.execute('SET X in_state S WHERE X login "anon", S name "deactivated"') + user = self.execute('INSERT CWUser X: X login "toto", X upassword "sosafe", X in_group G ' + 'WHERE G name "users"').get_entity(0, 0) + self.execute('SET X last_login_time NOW WHERE X eid %(x)s', {'x': user.eid}, 'x') self.execute('DELETE Card B WHERE B title "une news !"') - self.execute('SET X bookmarked_by U WHERE X is Bookmark, U eid %(x)s', {'x': ueid}, 'x') + self.execute('SET X bookmarked_by U WHERE X is Bookmark, U eid %(x)s', {'x': user.eid}, 'x') self.execute('SET X content "duh?" WHERE X is Comment') self.execute('DELETE X comments Y WHERE Y is Card, Y title "une autre news !"') # check only one supervision email operation @@ -62,17 +61,31 @@ * updated comment #EID (#EID) http://testing.fr/cubicweb/comment/EID -* deleted relation comments from comment #EID to card #EID - -* changed state of cwuser #EID (anon) - from state activated to state deactivated - http://testing.fr/cubicweb/cwuser/anon''', +* deleted relation comments from comment #EID to card #EID''', data) # check prepared email op._prepare_email() self.assertEquals(len(op.to_send), 1) self.assert_(op.to_send[0][0]) self.assertEquals(op.to_send[0][1], ['test@logilab.fr']) + self.commit() + # some other changes ####### + user.fire_transition('deactivate') + sentops = [op for op in session.pending_operations + if isinstance(op, SupervisionMailOp)] + self.assertEquals(len(sentops), 1) + # check view content + op = sentops[0] + view = sentops[0]._get_view() + data = view.render(changes=session.transaction_data.get('pendingchanges')).strip() + data = re.sub('#\d+', '#EID', data) + data = re.sub('/\d+', '/EID', data) + self.assertTextEquals('''user admin has made the following change(s): + +* changed state of cwuser #EID (toto) + from state activated to state deactivated + http://testing.fr/cubicweb/cwuser/toto''', + data) def test_nonregr1(self): session = self.session() diff -r d7c23b2c7538 -r 17224e90a1c4 test/unittest_entity.py --- a/test/unittest_entity.py Thu Aug 20 17:57:31 2009 +0200 +++ b/test/unittest_entity.py Thu Aug 20 17:57:56 2009 +0200 @@ -76,8 +76,8 @@ e = self.entity('Any X WHERE X eid %(x)s', {'x':user.eid}, 'x') self.assertEquals(e.use_email[0].address, "toto@logilab.org") self.assertEquals(e.use_email[0].eid, adeleid) - usereid = self.execute('INSERT CWUser X: X login "toto", X upassword "toto", X in_group G, X in_state S ' - 'WHERE G name "users", S name "activated"')[0][0] + usereid = self.execute('INSERT CWUser X: X login "toto", X upassword "toto", X in_group G ' + 'WHERE G name "users"')[0][0] e = self.entity('Any X WHERE X eid %(x)s', {'x':usereid}, 'x') e.copy_relations(user.eid) self.failIf(e.use_email) @@ -85,14 +85,14 @@ def test_copy_with_non_initial_state(self): user = self.user() - eid = self.execute('INSERT CWUser X: X login "toto", X upassword %(pwd)s, X in_group G WHERE G name "users"', - {'pwd': 'toto'})[0][0] + user = self.execute('INSERT CWUser X: X login "toto", X upassword %(pwd)s, X in_group G WHERE G name "users"', + {'pwd': 'toto'}).get_entity(0, 0) self.commit() - self.execute('SET X in_state S WHERE X eid %(x)s, S name "deactivated"', {'x': eid}, 'x') + user.fire_transition('deactivate') self.commit() eid2 = self.execute('INSERT CWUser X: X login "tutu", X upassword %(pwd)s', {'pwd': 'toto'})[0][0] e = self.entity('Any X WHERE X eid %(x)s', {'x': eid2}, 'x') - e.copy_relations(eid) + e.copy_relations(user.eid) self.commit() e.clear_related_cache('in_state', 'subject') self.assertEquals(e.state, 'activated') @@ -132,7 +132,8 @@ seschema.subject_relation('evaluee').set_rproperty(seschema, Note.e_schema, 'cardinality', '1*') # testing basic fetch_attrs attribute self.assertEquals(Personne.fetch_rql(user), - 'Any X,AA,AB,AC ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC') + 'Any X,AA,AB,AC ORDERBY AA ASC ' + 'WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC') pfetch_attrs = Personne.fetch_attrs sfetch_attrs = Societe.fetch_attrs try: @@ -142,18 +143,21 @@ # testing one non final relation Personne.fetch_attrs = ('nom', 'prenom', 'travaille') self.assertEquals(Personne.fetch_rql(user), - 'Any X,AA,AB,AC,AD ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB, X travaille AC, AC nom AD') + 'Any X,AA,AB,AC,AD ORDERBY AA ASC ' + 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD') # testing two non final relations Personne.fetch_attrs = ('nom', 'prenom', 'travaille', 'evaluee') self.assertEquals(Personne.fetch_rql(user), - 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC WHERE X is Personne, X nom AA, ' - 'X prenom AB, X travaille AC, AC nom AD, X evaluee AE, AE modification_date AF') + 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC ' + 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' + 'X evaluee AE?, AE modification_date AF') # testing one non final relation with recursion Personne.fetch_attrs = ('nom', 'prenom', 'travaille') Societe.fetch_attrs = ('nom', 'evaluee') self.assertEquals(Personne.fetch_rql(user), - 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC WHERE X is Personne, X nom AA, X prenom AB, ' - 'X travaille AC, AC nom AD, AC evaluee AE, AE modification_date AF' + 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC ' + 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' + 'AC evaluee AE?, AE modification_date AF' ) # testing symetric relation Personne.fetch_attrs = ('nom', 'connait') @@ -323,33 +327,17 @@ self.failUnless(not p1.reverse_evaluee) def test_complete_relation(self): - self.execute('SET RT add_permission G WHERE RT name "wf_info_for", G name "managers"') - self.commit() session = self.session() - try: - eid = session.unsafe_execute( - 'INSERT TrInfo X: X comment "zou", X wf_info_for U, X from_state S1, X to_state S2 ' - 'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0] - trinfo = self.entity('Any X WHERE X eid %(x)s', {'x': eid}, 'x') - trinfo.complete() - self.failUnless(trinfo.relation_cached('from_state', 'subject')) - self.failUnless(trinfo.relation_cached('to_state', 'subject')) - self.failUnless(trinfo.relation_cached('wf_info_for', 'subject')) - # check with a missing relation - eid = session.unsafe_execute( - 'INSERT TrInfo X: X comment "zou", X wf_info_for U,X to_state S2 ' - 'WHERE U login "admin", S2 name "activated"')[0][0] - trinfo = self.entity('Any X WHERE X eid %(x)s', {'x': eid}, 'x') - trinfo.complete() - self.failUnless(isinstance(trinfo.creation_date, datetime)) - self.failUnless(trinfo.relation_cached('from_state', 'subject')) - self.failUnless(trinfo.relation_cached('to_state', 'subject')) - self.failUnless(trinfo.relation_cached('wf_info_for', 'subject')) - self.assertEquals(trinfo.from_state, []) - finally: - self.rollback() - self.execute('DELETE RT add_permission G WHERE RT name "wf_info_for", G name "managers"') - self.commit() + eid = session.unsafe_execute( + 'INSERT TrInfo X: X comment "zou", X wf_info_for U, X from_state S1, X to_state S2 ' + 'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0] + trinfo = self.entity('Any X WHERE X eid %(x)s', {'x': eid}, 'x') + trinfo.complete() + self.failUnless(isinstance(trinfo['creation_date'], datetime)) + self.failUnless(trinfo.relation_cached('from_state', 'subject')) + self.failUnless(trinfo.relation_cached('to_state', 'subject')) + self.failUnless(trinfo.relation_cached('wf_info_for', 'subject')) + self.assertEquals(trinfo.by_transition, []) def test_request_cache(self): req = self.request() diff -r d7c23b2c7538 -r 17224e90a1c4 test/unittest_schema.py --- a/test/unittest_schema.py Thu Aug 20 17:57:31 2009 +0200 +++ b/test/unittest_schema.py Thu Aug 20 17:57:56 2009 +0200 @@ -145,7 +145,7 @@ self.assertEquals(schema.name, 'data') entities = [str(e) for e in schema.entities()] entities.sort() - expected_entities = ['Bookmark', 'Boolean', 'Bytes', 'Card', + expected_entities = ['BaseTransition', 'Bookmark', 'Boolean', 'Bytes', 'Card', 'Date', 'Datetime', 'Decimal', 'CWCache', 'CWConstraint', 'CWConstraintType', 'CWEType', 'CWAttribute', 'CWGroup', 'EmailAddress', 'CWRelation', @@ -153,19 +153,20 @@ 'ExternalUri', 'File', 'Float', 'Image', 'Int', 'Interval', 'Note', 'Password', 'Personne', 'RQLExpression', - 'Societe', 'State', 'String', 'SubNote', 'Tag', 'Time', - 'Transition', 'TrInfo'] + 'Societe', 'State', 'String', 'SubNote', 'SubWorkflowExitPoint', + 'Tag', 'Time', 'Transition', 'TrInfo', + 'Workflow', 'WorkflowTransition'] self.assertListEquals(entities, sorted(expected_entities)) relations = [str(r) for r in schema.relations()] relations.sort() - expected_relations = ['add_permission', 'address', 'alias', - 'allowed_transition', 'bookmarked_by', 'canonical', + expected_relations = ['add_permission', 'address', 'alias', 'allowed_transition', + 'bookmarked_by', 'by_transition', - 'cardinality', 'comment', 'comment_format', + 'canonical', 'cardinality', 'comment', 'comment_format', 'composite', 'condition', 'connait', 'constrained_by', 'content', - 'content_format', 'created_by', 'creation_date', 'cstrtype', 'cwuri', + 'content_format', 'created_by', 'creation_date', 'cstrtype', 'custom_workflow', 'cwuri', - 'data', 'data_encoding', 'data_format', 'defaultval', 'delete_permission', + 'data', 'data_encoding', 'data_format', 'default_workflow_of', 'defaultval', 'delete_permission', 'description', 'description_format', 'destination_state', 'ecrit_par', 'eid', 'evaluee', 'expression', 'exprtype', @@ -189,7 +190,7 @@ 'read_permission', 'relation_type', 'require_group', - 'specializes', 'state_of', 'surname', 'symetric', 'synopsis', + 'specializes', 'state_of', 'subworkflow', 'subworkflow_exit', 'subworkflow_state', 'surname', 'symetric', 'synopsis', 'tags', 'timestamp', 'title', 'to_entity', 'to_state', 'transition_of', 'travaille', 'type', @@ -197,13 +198,13 @@ 'value', - 'wf_info_for', 'wikiid'] + 'wf_info_for', 'wikiid', 'workflow_of'] self.assertListEquals(relations, expected_relations) eschema = schema.eschema('CWUser') rels = sorted(str(r) for r in eschema.subject_relations()) - self.assertListEquals(rels, ['created_by', 'creation_date', 'cwuri', 'eid', + self.assertListEquals(rels, ['created_by', 'creation_date', 'custom_workflow', 'cwuri', 'eid', 'evaluee', 'firstname', 'has_text', 'identity', 'in_group', 'in_state', 'is', 'is_instance_of', 'last_login_time', diff -r d7c23b2c7538 -r 17224e90a1c4 web/test/unittest_form.py --- a/web/test/unittest_form.py Thu Aug 20 17:57:31 2009 +0200 +++ b/web/test/unittest_form.py Thu Aug 20 17:57:56 2009 +0200 @@ -61,19 +61,19 @@ # should be default groups but owners, i.e. managers, users, guests self.assertEquals(unrelated, [u'guests', u'managers', u'users']) - def test_subject_in_state_vocabulary(self): - # on a new entity - e = self.etype_instance('CWUser') - form = EntityFieldsForm(self.request(), None, entity=e) - states = list(form.subject_in_state_vocabulary('in_state')) - self.assertEquals(len(states), 1) - self.assertEquals(states[0][0], u'activated') # list of (combobox view, state eid) - # on an existant entity - e = self.user() - form = EntityFieldsForm(self.request(), None, entity=e) - states = list(form.subject_in_state_vocabulary('in_state')) - self.assertEquals(len(states), 1) - self.assertEquals(states[0][0], u'deactivated') # list of (combobox view, state eid) + # def test_subject_in_state_vocabulary(self): + # # on a new entity + # e = self.etype_instance('CWUser') + # form = EntityFieldsForm(self.request(), None, entity=e) + # states = list(form.subject_in_state_vocabulary('in_state')) + # self.assertEquals(len(states), 1) + # self.assertEquals(states[0][0], u'activated') # list of (combobox view, state eid) + # # on an existant entity + # e = self.user() + # form = EntityFieldsForm(self.request(), None, entity=e) + # states = list(form.subject_in_state_vocabulary('in_state')) + # self.assertEquals(len(states), 1) + # self.assertEquals(states[0][0], u'deactivated') # list of (combobox view, state eid) def test_consider_req_form_params(self): e = self.etype_instance('CWUser') @@ -143,7 +143,7 @@ def _test_richtextfield(self, expected): class RTFForm(EntityFieldsForm): description = RichTextField() - state = self.execute('State X WHERE X name "activated", X state_of ET, ET name "CWUser"').get_entity(0, 0) + state = self.execute('State X WHERE X name "activated", X state_of WF, WF workflow_of ET, ET name "CWUser"').get_entity(0, 0) form = RTFForm(self.req, redirect_path='perdu.com', entity=state) # make it think it can use fck editor anyway form.form_field_format = lambda x: 'text/html' diff -r d7c23b2c7538 -r 17224e90a1c4 web/test/unittest_views_editforms.py --- a/web/test/unittest_views_editforms.py Thu Aug 20 17:57:31 2009 +0200 +++ b/web/test/unittest_views_editforms.py Thu Aug 20 17:57:56 2009 +0200 @@ -53,6 +53,7 @@ ]) self.assertListEquals(rbc(e, 'generic'), [('primary_email', 'subject'), + ('custom_workflow', 'subject'), ('connait', 'subject'), ('checked_by', 'object'), ]) diff -r d7c23b2c7538 -r 17224e90a1c4 web/views/autoform.py --- a/web/views/autoform.py Thu Aug 20 17:57:31 2009 +0200 +++ b/web/views/autoform.py Thu Aug 20 17:57:56 2009 +0200 @@ -12,9 +12,8 @@ from cubicweb import typed_eid from cubicweb.web import stdmsgs, uicfg -from cubicweb.web.form import FieldNotFound +from cubicweb.web import form, formwidgets as fwdgs from cubicweb.web.formfields import guess_field -from cubicweb.web import formwidgets from cubicweb.web.views import forms, editforms @@ -35,9 +34,9 @@ cwtarget = 'eformframe' cssclass = 'entityForm' copy_nav_params = True - form_buttons = [formwidgets.SubmitButton(), - formwidgets.Button(stdmsgs.BUTTON_APPLY, cwaction='apply'), - formwidgets.Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')] + form_buttons = [fwdgs.SubmitButton(), + fwdgs.Button(stdmsgs.BUTTON_APPLY, cwaction='apply'), + fwdgs.Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')] attrcategories = ('primary', 'secondary') # class attributes below are actually stored in the uicfg module since we # don't want them to be reloaded @@ -133,7 +132,7 @@ """ try: return super(AutomaticEntityForm, cls_or_self).field_by_name(name, role) - except FieldNotFound: # XXX should raise more explicit exception + except form.FieldNotFound: if eschema is None or not name in cls_or_self.schema: raise rschema = cls_or_self.schema.rschema(name) @@ -163,13 +162,13 @@ try: self.field_by_name(rschema.type, role) continue # explicitly specified - except FieldNotFound: + except form.FieldNotFound: # has to be guessed try: field = self.field_by_name(rschema.type, role, eschema=entity.e_schema) self.fields.append(field) - except FieldNotFound: + except form.FieldNotFound: # meta attribute such as _format continue self.maxrelitems = self.req.property_value('navigation.related-limit') @@ -330,7 +329,11 @@ uicfg.autoform_section.tag_subject_of(('*', 'identity', '*'), 'generated') uicfg.autoform_section.tag_object_of(('*', 'identity', '*'), 'generated') uicfg.autoform_section.tag_subject_of(('*', 'require_permission', '*'), 'generated') -uicfg.autoform_section.tag_subject_of(('*', 'wf_info_for', '*'), 'generated') +uicfg.autoform_section.tag_subject_of(('*', 'by_transition', '*'), 'primary') +uicfg.autoform_section.tag_object_of(('*', 'by_transition', '*'), 'generated') +uicfg.autoform_section.tag_object_of(('*', 'from_state', '*'), 'generated') +uicfg.autoform_section.tag_object_of(('*', 'to_state', '*'), 'generated') +uicfg.autoform_section.tag_subject_of(('*', 'wf_info_for', '*'), 'primary') uicfg.autoform_section.tag_object_of(('*', 'wf_info_for', '*'), 'generated') uicfg.autoform_section.tag_subject_of(('*', 'for_user', '*'), 'generated') uicfg.autoform_section.tag_object_of(('*', 'for_user', '*'), 'generated') @@ -349,9 +352,11 @@ uicfg.autoform_section.tag_subject_of(('*', 'primary_email', '*'), 'generic') uicfg.autoform_field_kwargs.tag_attribute(('RQLExpression', 'expression'), - {'widget': formwidgets.TextInput}) + {'widget': fwdgs.TextInput}) uicfg.autoform_field_kwargs.tag_attribute(('Bookmark', 'path'), - {'widget': formwidgets.TextInput}) + {'widget': fwdgs.TextInput}) +uicfg.autoform_field_kwargs.tag_subject_of(('TrInfo', 'wf_info_for', '*'), + {'widget': fwdgs.HiddenInput}) uicfg.autoform_is_inlined.tag_subject_of(('*', 'use_email', '*'), True) uicfg.autoform_is_inlined.tag_subject_of(('CWRelation', 'relation_type', '*'), True) diff -r d7c23b2c7538 -r 17224e90a1c4 web/views/boxes.py --- a/web/views/boxes.py Thu Aug 20 17:57:31 2009 +0200 +++ b/web/views/boxes.py Thu Aug 20 17:57:56 2009 +0200 @@ -142,12 +142,11 @@ def workflow_actions(self, entity, box): - if 'in_state' in entity.e_schema.subject_relations() and entity.in_state: + if entity.e_schema.has_subject_relation('in_state') and entity.in_state: _ = self.req._ - state = entity.in_state[0] - menu_title = u'%s: %s' % (_('state'), state.view('text')) + menu_title = u'%s: %s' % (_('state'), entity.printable_state) menu_items = [] - for tr in state.transitions(entity): + for tr in entity.possible_transitions(): url = entity.absolute_url(vid='statuschange', treid=tr.eid) menu_items.append(self.mk_action(_(tr.name), url)) wfurl = self.build_url('cwetype/%s'%entity.e_schema, vid='workflow') diff -r d7c23b2c7538 -r 17224e90a1c4 web/views/forms.py --- a/web/views/forms.py Thu Aug 20 17:57:31 2009 +0200 +++ b/web/views/forms.py Thu Aug 20 17:57:56 2009 +0200 @@ -529,24 +529,24 @@ break return result - def subject_in_state_vocabulary(self, rtype, limit=None): - """vocabulary method for the in_state relation, looking for relation's - object entities (i.e. self is the subject) according to initial_state, - state_of and next_state relation - """ - entity = self.edited_entity - if not entity.has_eid() or not entity.in_state: - # get the initial state - rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S' - rset = self.req.execute(rql, {'etype': str(entity.e_schema)}) - if rset: - return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])] - return [] - results = [] - for tr in entity.in_state[0].transitions(entity): - state = tr.destination_state[0] - results.append((state.view('combobox'), state.eid)) - return sorted(results) + # def subject_in_state_vocabulary(self, rtype, limit=None): + # """vocabulary method for the in_state relation, looking for relation's + # object entities (i.e. self is the subject) according to initial_state, + # state_of and next_state relation + # """ + # entity = self.edited_entity + # if not entity.has_eid() or not entity.in_state: + # # get the initial state + # rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S' + # rset = self.req.execute(rql, {'etype': str(entity.e_schema)}) + # if rset: + # return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])] + # return [] + # results = [] + # for tr in entity.in_state[0].transitions(entity): + # state = tr.destination_state[0] + # results.append((state.view('combobox'), state.eid)) + # return sorted(results) def srelations_by_category(self, categories=None, permission=None): return () @@ -556,7 +556,7 @@ class CompositeForm(FieldsForm): - """form composed for sub-forms""" + """form composed of sub-forms""" id = 'composite' form_renderer_id = id @@ -568,3 +568,18 @@ """mark given form as a subform and append it""" subform.is_subform = True self.forms.append(subform) + + +class CompositeEntityForm(EntityFieldsForm): + """form composed of sub-forms""" + id = 'composite' + form_renderer_id = id + + def __init__(self, *args, **kwargs): + super(CompositeEntityForm, self).__init__(*args, **kwargs) + self.forms = [] + + def form_add_subform(self, subform): + """mark given form as a subform and append it""" + subform.is_subform = True + self.forms.append(subform) diff -r d7c23b2c7538 -r 17224e90a1c4 web/views/primary.py --- a/web/views/primary.py Thu Aug 20 17:57:31 2009 +0200 +++ b/web/views/primary.py Thu Aug 20 17:57:56 2009 +0200 @@ -229,9 +229,9 @@ for rtype in ('eid', 'creation_date', 'modification_date', 'cwuri', 'is', 'is_instance_of', 'identity', - 'owned_by', 'created_by', - 'in_state', 'wf_info_for', 'require_permission', - 'from_entity', 'to_entity', + 'owned_by', 'created_by', 'in_state', + 'wf_info_for', 'by_transition', 'from_state', 'to_state', + 'require_permission', 'from_entity', 'to_entity', 'see_also'): uicfg.primaryview_section.tag_subject_of(('*', rtype, '*'), 'hidden') uicfg.primaryview_section.tag_object_of(('*', rtype, '*'), 'hidden') diff -r d7c23b2c7538 -r 17224e90a1c4 web/views/workflow.py --- a/web/views/workflow.py Thu Aug 20 17:57:31 2009 +0200 +++ b/web/views/workflow.py Thu Aug 20 17:57:56 2009 +0200 @@ -20,50 +20,48 @@ from cubicweb.interfaces import IWorkflowable from cubicweb.view import EntityView from cubicweb.web import stdmsgs, action, component, form -from cubicweb.web.form import FormViewMixIn -from cubicweb.web.formfields import StringField, RichTextField -from cubicweb.web.formwidgets import HiddenInput, SubmitButton, Button +from cubicweb.web import formfields as ff, formwidgets as fwdgs from cubicweb.web.views import TmpFileViewMixin, forms # IWorkflowable views ######################################################### -class ChangeStateForm(forms.EntityFieldsForm): +class ChangeStateForm(forms.CompositeEntityForm): id = 'changestate' form_renderer_id = 'base' # don't want EntityFormRenderer - form_buttons = [SubmitButton(stdmsgs.YES), - Button(stdmsgs.NO, cwaction='cancel')] - - __method = StringField(name='__method', initial='set_state', - widget=HiddenInput) - state = StringField(eidparam=True, widget=HiddenInput) - trcomment = RichTextField(label=_('comment:'), eidparam=True) + form_buttons = [fwdgs.SubmitButton(stdmsgs.YES), + fwdgs.Button(stdmsgs.NO, cwaction='cancel')] -class ChangeStateFormView(FormViewMixIn, view.EntityView): +class ChangeStateFormView(form.FormViewMixIn, view.EntityView): id = 'statuschange' title = _('status change') __select__ = implements(IWorkflowable) & match_form_params('treid') def cell_call(self, row, col): entity = self.entity(row, col) - state = entity.in_state[0] transition = self.req.entity_from_eid(self.req.form['treid']) dest = transition.destination() _ = self.req._ - form = self.vreg.select('forms', 'changestate', self.req, rset=self.rset, - row=row, col=col, entity=entity, - redirect_path=self.redirectpath(entity)) + form = self.vreg['forms'].select('changestate', self.req, entity=entity, + redirect_path=self.redirectpath(entity)) self.w(form.error_message()) self.w(u'

%s %s

\n' % (_(transition.name), entity.view('oneline'))) msg = _('status will change from %(st1)s to %(st2)s') % { - 'st1': _(state.name), + 'st1': _(entity.current_state.name), 'st2': _(dest.name)} self.w(u'

%s

\n' % msg) - self.w(form.form_render(state=dest.eid, trcomment=u'', - trcomment_format=self.req.property_value('ui.default-text-format'))) + trinfo = self.vreg['etypes'].etype_class('TrInfo')(self.req) + self.initialize_varmaker() + trinfo.eid = self.varmaker.next() + subform = self.vreg['forms'].select('edition', self.req, entity=trinfo, + mainform=False) + subform.field_by_name('by_transition').widget = fwdgs.HiddenInput() + form.form_add_subform(subform) + self.w(form.form_render(wf_info_for=entity.eid, + by_transition=transition.eid)) def redirectpath(self, entity): return entity.rest_path() @@ -135,7 +133,7 @@ class ViewWorkflowAction(action.Action): id = 'workflow' - __select__ = implements('CWEType') & has_related_entities('state_of', 'object') + __select__ = implements('CWEType') & has_related_entities('workflow_of', 'object') category = 'mainactions' title = _('view workflow')