# HG changeset patch # User Nicolas Chauvat # Date 1251193516 -7200 # Node ID 46a5a94287fa7a8ca32468964e73bd21fe579ce5 # Parent 23418c13e024bcc57b2108eec34d0ffd858751cc# Parent bc0a270622c2fe7b23755146f36f1d7ed7253f7b backport stable branch diff -r bc0a270622c2 -r 46a5a94287fa __init__.py --- a/__init__.py Mon Aug 24 20:27:05 2009 +0200 +++ b/__init__.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa __pkginfo__.py --- a/__pkginfo__.py Mon Aug 24 20:27:05 2009 +0200 +++ b/__pkginfo__.py Tue Aug 25 11:45:16 2009 +0200 @@ -7,7 +7,7 @@ distname = "cubicweb" modname = "cubicweb" -numversion = (3, 4, 4) +numversion = (3, 5, 0) version = '.'.join(str(num) for num in numversion) license = 'LGPL v2' diff -r bc0a270622c2 -r 46a5a94287fa common/mixins.py --- a/common/mixins.py Mon Aug 24 20:27:05 2009 +0200 +++ b/common/mixins.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa common/test/unittest_mixins.py --- a/common/test/unittest_mixins.py Mon Aug 24 20:27:05 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 bc0a270622c2 -r 46a5a94287fa common/test/unittest_uilib.py --- a/common/test/unittest_uilib.py Mon Aug 24 20:27:05 2009 +0200 +++ b/common/test/unittest_uilib.py Tue Aug 25 11:45:16 2009 +0200 @@ -81,45 +81,6 @@ got = uilib.text_cut(text, 30) self.assertEquals(got, expected) -tree = ('root', ( - ('child_1_1', ( - ('child_2_1', ()), ('child_2_2', ( - ('child_3_1', ()), - ('child_3_2', ()), - ('child_3_3', ()), - )))), - ('child_1_2', (('child_2_3', ()),)))) - -generated_html = """\ - - - - - - - - - - - -
root
  
child_1_1
  
child_2_1
   
      
      
child_2_2
  
child_3_1
      
         
child_3_2
      
         
child_3_3
      
   
child_1_2
  
child_2_3
   
      
\ -""" - -def make_tree(tuple): - n = Node(tuple[0]) - for child in tuple[1]: - n.append(make_tree(child)) - return n - -class UIlibHTMLGenerationTC(TestCase): - """ a basic tree node, caracterised by an id""" - def setUp(self): - """ called before each test from this class """ - self.o = make_tree(tree) - - def test_generated_html(self): - s = uilib.render_HTML_tree(self.o, selected_node="child_2_2") - self.assertTextEqual(s, generated_html) if __name__ == '__main__': diff -r bc0a270622c2 -r 46a5a94287fa common/uilib.py --- a/common/uilib.py Mon Aug 24 20:27:05 2009 +0200 +++ b/common/uilib.py Tue Aug 25 11:45:16 2009 +0200 @@ -12,7 +12,6 @@ import csv import re -from urllib import quote as urlquote from StringIO import StringIO from logilab.mtconverter import xml_escape, html_unescape @@ -264,124 +263,6 @@ res = unicode(res, 'UTF8') return res -def render_HTML_tree(tree, selected_node=None, render_node=None, caption=None): - """ - Generate a pure HTML representation of a tree given as an instance - of a logilab.common.tree.Node - - selected_node is the currently selected node (if any) which will - have its surrounding
have id="selected" (which default - to a bold border libe with the default CSS). - - render_node is a function that should take a Node content (Node.id) - as parameter and should return a string (what will be displayed - in the cell). - - Warning: proper rendering of the generated html code depends on html_tree.css - """ - tree_depth = tree.depth_down() - if render_node is None: - render_node = str - - # helper function that build a matrix from the tree, like: - # +------+-----------+-----------+ - # | root | child_1_1 | child_2_1 | - # | root | child_1_1 | child_2_2 | - # | root | child_1_2 | | - # | root | child_1_3 | child_2_3 | - # | root | child_1_3 | child_2_4 | - # +------+-----------+-----------+ - # from: - # root -+- child_1_1 -+- child_2_1 - # | | - # | +- child_2_2 - # +- child_1_2 - # | - # +- child1_3 -+- child_2_3 - # | - # +- child_2_2 - def build_matrix(path, matrix): - if path[-1].is_leaf(): - matrix.append(path[:]) - else: - for child in path[-1].children: - build_matrix(path[:] + [child], matrix) - - matrix = [] - build_matrix([tree], matrix) - - # make all lines in the matrix have the same number of columns - for line in matrix: - line.extend([None]*(tree_depth-len(line))) - for i in range(len(matrix)-1, 0, -1): - prev_line, line = matrix[i-1:i+1] - for j in range(len(line)): - if line[j] == prev_line[j]: - line[j] = None - - # We build the matrix of link types (between 2 cells on a line of the matrix) - # link types are : - link_types = {(True, True, True ): 1, # T - (False, False, True ): 2, # | - (False, True, True ): 3, # + (actually, vert. bar with horiz. bar on the right) - (False, True, False): 4, # L - (True, True, False): 5, # - - } - links = [] - for i, line in enumerate(matrix): - links.append([]) - for j in range(tree_depth-1): - cell_11 = line[j] is not None - cell_12 = line[j+1] is not None - cell_21 = line[j+1] is not None and line[j+1].next_sibling() is not None - link_type = link_types.get((cell_11, cell_12, cell_21), 0) - if link_type == 0 and i > 0 and links[i-1][j] in (1, 2, 3): - link_type = 2 - links[-1].append(link_type) - - - # We can now generate the HTML code for the - s = u'
\n' - if caption: - s += '\n' % caption - - for i, link_line in enumerate(links): - line = matrix[i] - - s += '' - for j, link_cell in enumerate(link_line): - cell = line[j] - if cell: - if cell.id == selected_node: - s += '' % (render_node(cell.id)) - else: - s += '' % (render_node(cell.id)) - else: - s += '' - s += '' % link_cell - s += '' % link_cell - - cell = line[-1] - if cell: - if cell.id == selected_node: - s += '' % (render_node(cell.id)) - else: - s += '' % (render_node(cell.id)) - else: - s += '' - - s += '\n' - if link_line: - s += '' - for j, link_cell in enumerate(link_line): - s += '' % link_cell - s += '' % link_cell - s += '\n' - - s += '
%s
%s
%s
   
%s
%s
 
  
' - return s - - # traceback formatting ######################################################## diff -r bc0a270622c2 -r 46a5a94287fa debian/control --- a/debian/control Mon Aug 24 20:27:05 2009 +0200 +++ b/debian/control Tue Aug 25 11:45:16 2009 +0200 @@ -62,7 +62,7 @@ Architecture: all XB-Python-Version: ${python:Versions} Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3), python-elementtree -Recommends: python-docutils, python-vobject, fckeditor, python-fyzz +Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-pysixt, fop Description: web interface library for the CubicWeb framework CubicWeb is a semantic web application framework. . diff -r bc0a270622c2 -r 46a5a94287fa devtools/_apptest.py --- a/devtools/_apptest.py Mon Aug 24 20:27:05 2009 +0200 +++ b/devtools/_apptest.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa devtools/apptest.py --- a/devtools/apptest.py Mon Aug 24 20:27:05 2009 +0200 +++ b/devtools/apptest.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa doc/book/en/development/testing/index.rst --- a/doc/book/en/development/testing/index.rst Mon Aug 24 20:27:05 2009 +0200 +++ b/doc/book/en/development/testing/index.rst Tue Aug 25 11:45:16 2009 +0200 @@ -16,14 +16,48 @@ * `EnvBasedTC`, to simulate a complete environment (web + repository) * `RepositoryBasedTC`, to simulate a repository environment only -Thos two classes almost have the same interface and offers numerous methods to -write tests rapidely and efficiently. +Those two classes almost have the same interface and offer numerous +methods to write tests rapidly and efficiently. XXX FILLME describe API In most of the cases, you will inherit `EnvBasedTC` to write Unittest or functional tests for your entities, views, hooks, etc... +Managing connections or users ++++++++++++++++++++++++++++++ + +Since unit tests are done with the SQLITE backend and this does not +support multiple connections at a time, you must be careful when +simulating security, changing users. + +By default, tests run with a user with admin privileges. This +user/connection must never be closed. +qwq +Before a self.login, one has to release the connection pool in use with a self.commit, self.rollback or self.close. + +When one is logged in as a normal user and wants to switch back to the admin user, one has to use self.restore_connection(). + +Usually it looks like this: + +.. sourcecode:: python + + # execute using default admin connection + self.execute(...) + # I want to login with another user, ensure to free admin connection pool + # (could have used rollback but not close here, we should never close defaut admin connection) + self.commit() + cnx = self.login('user') + # execute using user connection + self.execute(...) + # I want to login with another user or with admin user + self.commit(); cnx.close() + # restore admin connection, never use cnx = self.login('admin'), it will return + # the default admin connection and one may be tempted to close it + self.restore_connection() + +Take care of the references kept to the entities created with a connection or the other. + Email notifications tests ------------------------- diff -r bc0a270622c2 -r 46a5a94287fa entities/test/data/migration/postcreate.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entities/test/data/migration/postcreate.py Tue Aug 25 11:45:16 2009 +0200 @@ -0,0 +1,2 @@ +wf = add_workflow(u'bmk wf', 'Bookmark') +wf.add_state(u'hop', initial=True) diff -r bc0a270622c2 -r 46a5a94287fa entities/test/data/schema.py --- a/entities/test/data/schema.py Mon Aug 24 20:27:05 2009 +0200 +++ b/entities/test/data/schema.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa entities/test/unittest_base.py --- a/entities/test/unittest_base.py Mon Aug 24 20:27:05 2009 +0200 +++ b/entities/test/unittest_base.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa entities/test/unittest_wfobjs.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entities/test/unittest_wfobjs.py Tue Aug 25 11:45:16 2009 +0200 @@ -0,0 +1,393 @@ +from cubicweb.devtools.apptest import EnvBasedTC +from cubicweb import ValidationError + +def add_wf(self, etype, name=None): + if name is None: + name = unicode(etype) + wf = self.execute('INSERT Workflow X: X name %(n)s', {'n': name}).get_entity(0, 0) + self.execute('SET WF workflow_of ET WHERE WF eid %(wf)s, ET name %(et)s', + {'wf': wf.eid, 'et': etype}) + return wf + +def parse_hist(wfhist): + return [(ti.previous_state.name, ti.new_state.name, + ti.transition and ti.transition.name, ti.comment) + for ti in wfhist] + + +class WorkflowBuildingTC(EnvBasedTC): + + def test_wf_construction(self): + wf = add_wf(self, 'Company') + foo = wf.add_state(u'foo', initial=True) + bar = wf.add_state(u'bar') + self.assertEquals(wf.state_by_name('bar').eid, bar.eid) + self.assertEquals(wf.state_by_name('barrr'), None) + baz = wf.add_transition(u'baz', (foo,), bar, ('managers',)) + self.assertEquals(wf.transition_by_name('baz').eid, baz.eid) + self.assertEquals(len(baz.require_group), 1) + self.assertEquals(baz.require_group[0].name, 'managers') + + def test_duplicated_state(self): + wf = add_wf(self, 'Company') + wf.add_state(u'foo', initial=True) + wf.add_state(u'foo') + ex = self.assertRaises(ValidationError, self.commit) + # XXX enhance message + self.assertEquals(ex.errors, {'state_of': 'unique constraint S name N, Y state_of O, Y name N failed'}) + + def test_duplicated_transition(self): + wf = add_wf(self, 'Company') + foo = wf.add_state(u'foo', initial=True) + bar = wf.add_state(u'bar') + wf.add_transition(u'baz', (foo,), bar, ('managers',)) + wf.add_transition(u'baz', (bar,), foo) + ex = self.assertRaises(ValidationError, self.commit) + # XXX enhance message + self.assertEquals(ex.errors, {'transition_of': 'unique constraint S name N, Y transition_of O, Y name N failed'}) + + +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_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') + + +class CustomWorkflowTC(EnvBasedTC): + + def setup_database(self): + self.member = self.create_user('member') + + def tearDown(self): + super(CustomWorkflowTC, self).tearDown() + self.execute('DELETE X custom_workflow WF') + + def test_custom_wf_replace_state_no_history(self): + """member in inital state with no previous history, state is simply + redirected when changing workflow + """ + wf = add_wf(self, 'CWUser') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.member.clear_all_caches() + self.assertEquals(self.member.state, 'activated')# no change before commit + self.commit() + self.member.clear_all_caches() + self.assertEquals(self.member.current_workflow.eid, wf.eid) + self.assertEquals(self.member.state, 'asleep') + self.assertEquals(self.member.workflow_history, []) + + def test_custom_wf_replace_state_keep_history(self): + """member in inital state with some history, state is redirected and + state change is recorded to history + """ + self.member.fire_transition('deactivate') + self.member.fire_transition('activate') + wf = add_wf(self, 'CWUser') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.commit() + self.member.clear_all_caches() + self.assertEquals(self.member.current_workflow.eid, wf.eid) + self.assertEquals(self.member.state, 'asleep') + self.assertEquals(parse_hist(self.member.workflow_history), + [('activated', 'deactivated', 'deactivate', None), + ('deactivated', 'activated', 'activate', None), + ('activated', 'asleep', None, 'workflow changed to "CWUser"')]) + + def test_custom_wf_shared_state(self): + """member in some state shared by the new workflow, nothing has to be + done + """ + self.member.fire_transition('deactivate') + self.assertEquals(self.member.state, 'deactivated') + wf = add_wf(self, 'CWUser') + wf.add_state('asleep', initial=True) + self.execute('SET S state_of WF WHERE S name "deactivated", WF eid %(wf)s', + {'wf': wf.eid}) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.commit() + self.member.clear_all_caches() + self.assertEquals(self.member.current_workflow.eid, wf.eid) + self.assertEquals(self.member.state, 'deactivated') + self.assertEquals(parse_hist(self.member.workflow_history), + [('activated', 'deactivated', 'deactivate', None)]) + + def test_custom_wf_no_initial_state(self): + """try to set a custom workflow which has no initial state""" + self.member.fire_transition('deactivate') + wf = add_wf(self, 'CWUser') + wf.add_state('asleep') + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + ex = self.assertRaises(ValidationError, self.commit) + self.assertEquals(ex.errors, {'custom_workflow': u'workflow has no initial state'}) + + def test_custom_wf_bad_etype(self): + """try to set a custom workflow which has no initial state""" + self.member.fire_transition('deactivate') + wf = add_wf(self, 'Company') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + ex = self.assertRaises(ValidationError, self.commit) + self.assertEquals(ex.errors, {'custom_workflow': 'constraint S is ET, O workflow_of ET failed'}) + + def test_del_custom_wf(self): + """member in some state shared by the new workflow, nothing has to be + done + """ + self.member.fire_transition('deactivate') + wf = add_wf(self, 'CWUser') + wf.add_state('asleep', initial=True) + self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.commit() + self.execute('DELETE X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', + {'wf': wf.eid, 'x': self.member.eid}) + self.member.clear_all_caches() + self.assertEquals(self.member.state, 'asleep')# no change before commit + self.commit() + self.member.clear_all_caches() + self.assertEquals(self.member.current_workflow.name, "CWUser workflow") + self.assertEquals(self.member.state, 'activated') + self.assertEquals(parse_hist(self.member.workflow_history), + [('activated', 'deactivated', 'deactivate', None), + ('deactivated', 'asleep', None, 'workflow changed to "CWUser"'), + ('asleep', 'activated', None, 'workflow changed to "CWUser workflow"'),]) + + +from cubicweb.devtools.apptest import RepositoryBasedTC + +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 bc0a270622c2 -r 46a5a94287fa entities/wfobjs.py --- a/entities/wfobjs.py Mon Aug 24 20:27:05 2009 +0200 +++ b/entities/wfobjs.py Tue Aug 25 11:45:16 2009 +0200 @@ -7,23 +7,131 @@ """ __docformat__ = "restructuredtext en" +from warnings import warn + +from logilab.common.decorators import cached, clear_cache +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] or None + + 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.reverse_default_workflow + 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=unicode(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=unicode(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' + id = 'BaseTransition' 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 __init__(self, *args, **kwargs): + if self.id == 'BaseTransition': + raise Exception('should not be instantiated') + super(BaseTransition, self).__init__(*args, **kwargs) - `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 + 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 fire the transition """ user = self.req.user # check user is at least in one of the required groups if any @@ -43,47 +151,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 +235,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 +256,142 @@ 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 + def current_workflow(self): + """return current workflow applied to this entity""" + if self.custom_workflow: + return self.custom_workflow[0] + return self.cwetype_workflow() + + @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'' + + @property + def workflow_history(self): + """return the workflow history for this entity (eg ordered list of + TrInfo entities) + """ + return self.reverse_wf_info_for + + def latest_trinfo(self): + """return the latest transition information for this entity""" + return self.reverse_wf_info_for[-1] + + @cached + def cwetype_workflow(self): + """return the default workflow for entities of this type""" + # XXX CWEType method + 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 + + 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)) + + + def clear_all_caches(self): + super(WorkflowableMixIn, self).clear_all_caches() + clear_cache(self, 'cwetype_workflow') + + @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 bc0a270622c2 -r 46a5a94287fa entity.py --- a/entity.py Mon Aug 24 20:27:05 2009 +0200 +++ b/entity.py Tue Aug 25 11:45:16 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 @@ -182,15 +182,15 @@ continue setattr(cls, rschema.type, Attribute(rschema.type)) mixins = [] - for rschema, _, x in eschema.relation_definitions(): - if (rschema, x) in MI_REL_TRIGGERS: - mixin = MI_REL_TRIGGERS[(rschema, x)] + for rschema, _, role in eschema.relation_definitions(): + if (rschema, role) in MI_REL_TRIGGERS: + mixin = MI_REL_TRIGGERS[(rschema, role)] if not (issubclass(cls, mixin) or mixin in mixins): # already mixed ? mixins.append(mixin) for iface in getattr(mixin, '__implements__', ()): if not interface.implements(cls, iface): interface.extend(cls, iface) - if x == 'subject': + if role == 'subject': setattr(cls, rschema.type, SubjectRelation(rschema)) else: attr = 'reverse_%s' % rschema.type @@ -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 @@ -622,14 +615,14 @@ self[str(selected[i-1][0])] = rset[i] # handle relations for i in xrange(lastattr, len(rset)): - rtype, x = selected[i-1][0] + rtype, role = selected[i-1][0] value = rset[i] if value is None: rrset = ResultSet([], rql, {'x': self.eid}) self.req.decorate_rset(rrset) else: rrset = self.req.eid_rset(value) - self.set_related_cache(rtype, x, rrset) + self.set_related_cache(rtype, role, rrset) def get_value(self, name): """get value for the attribute relation , query the repository @@ -832,6 +825,11 @@ assert role self._related_cache.pop('%s_%s' % (rtype, role), None) + def clear_all_caches(self): + self.clear() + for rschema, _, role in self.e_schema.relation_definitions(): + self.clear_related_cache(rschema.type, role) + # raw edition utilities ################################################### def set_attributes(self, _cw_unsafe=False, **kwargs): diff -r bc0a270622c2 -r 46a5a94287fa interfaces.py --- a/interfaces.py Mon Aug 24 20:27:05 2009 +0200 +++ b/interfaces.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa misc/migration/2.42.0_Any.py --- a/misc/migration/2.42.0_Any.py Mon Aug 24 20:27:05 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +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 -""" -synchronize_rschema('created_by') -synchronize_rschema('owned_by') diff -r bc0a270622c2 -r 46a5a94287fa misc/migration/2.42.1_Any.py --- a/misc/migration/2.42.1_Any.py Mon Aug 24 20:27:05 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,25 +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 -""" -if confirm('remove deprecated database constraints?'): - execute = session.system_sql - session.set_pool() - dbhelper = session.pool.source('system').dbhelper - cu = session.pool['system'] - for table in dbhelper.list_tables(cu): - if table.endswith('_relation'): - try: - execute('ALTER TABLE %s DROP CONSTRAINT %s_fkey1' % (table, table)) - execute('ALTER TABLE %s DROP CONSTRAINT %s_fkey2' % (table, table)) - except: - continue - checkpoint() - -if 'inline_view' in schema: - # inline_view attribute should have been deleted for a while now.... - drop_attribute('CWRelation', 'inline_view') - diff -r bc0a270622c2 -r 46a5a94287fa misc/migration/2.43.0_Any.py --- a/misc/migration/2.43.0_Any.py Mon Aug 24 20:27:05 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +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 -""" -synchronize_permissions('EmailAddress') diff -r bc0a270622c2 -r 46a5a94287fa misc/migration/2.44.0_Any.py --- a/misc/migration/2.44.0_Any.py Mon Aug 24 20:27:05 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +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 -""" -change_relation_props('CWAttribute', 'cardinality', 'String', internationalizable=True) -change_relation_props('CWRelation', 'cardinality', 'String', internationalizable=True) - -drop_relation_definition('CWPermission', 'require_state', 'State') - -if confirm('cleanup require_permission relation'): - try: - newrschema = fsschema.rschema('require_permission') - except KeyError: - newrschema = None - for rsubj, robj in schema.rschema('require_permission').rdefs(): - if newrschema is None or not newrschema.has_rdef(rsubj, robj): - print 'removing', rsubj, 'require_permission', robj - drop_relation_definition(rsubj, 'require_permission', robj, ask_confirm=False) diff -r bc0a270622c2 -r 46a5a94287fa misc/migration/2.45.0_Any.py --- a/misc/migration/2.45.0_Any.py Mon Aug 24 20:27:05 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,25 +0,0 @@ -# following functions have been renamed, but keep old definition for bw compat -""" - -: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 -""" -sql('''CREATE AGGREGATE group_concat ( - basetype = anyelement, - sfunc = array_append, - stype = anyarray, - finalfunc = comma_join, - initcond = '{}' -)''') - -sql('''CREATE FUNCTION text_limit_size (fulltext text, maxsize integer) RETURNS text AS $$ -BEGIN - RETURN limit_size(fulltext, 'text/plain', maxsize); -END -$$ LANGUAGE plpgsql; -''') - - -synchronize_rschema('bookmarked_by') diff -r bc0a270622c2 -r 46a5a94287fa misc/migration/2.46.0_Any.py --- a/misc/migration/2.46.0_Any.py Mon Aug 24 20:27:05 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +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 -""" - - -rql('SET X value "navtop" WHERE X pkey ~= "contentnavigation.%.context", X value "header"') -rql('SET X value "navcontenttop" WHERE X pkey ~= "contentnavigation%.context", X value "incontext"') -rql('SET X value "navcontentbottom" WHERE X pkey ~= "contentnavigation%.context", X value "footer"') -checkpoint() - -if 'require_permission' in schema: - synchronize_rschema('require_permission') diff -r bc0a270622c2 -r 46a5a94287fa misc/migration/2.47.0_Any.py --- a/misc/migration/2.47.0_Any.py Mon Aug 24 20:27:05 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +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 -""" -synchronize_permissions('primary_email') -synchronize_rschema('wf_info_for') -synchronize_rschema('use_email') - diff -r bc0a270622c2 -r 46a5a94287fa misc/migration/2.48.8_Any.py --- a/misc/migration/2.48.8_Any.py Mon Aug 24 20:27:05 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +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 -""" -for etype in ('CWRType', 'CWAttribute', 'CWRelation', 'CWConstraint', 'CWConstraintType'): - synchronize_permissions(etype) diff -r bc0a270622c2 -r 46a5a94287fa misc/migration/2.49.3_Any.py --- a/misc/migration/2.49.3_Any.py Mon Aug 24 20:27:05 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +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 -""" -add_entity_type('Decimal') diff -r bc0a270622c2 -r 46a5a94287fa misc/migration/2.50.0_Any.py --- a/misc/migration/2.50.0_Any.py Mon Aug 24 20:27:05 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +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 -""" -add_relation_type('specializes') diff -r bc0a270622c2 -r 46a5a94287fa misc/migration/bootstrapmigration_repository.py --- a/misc/migration/bootstrapmigration_repository.py Mon Aug 24 20:27:05 2009 +0200 +++ b/misc/migration/bootstrapmigration_repository.py Tue Aug 25 11:45:16 2009 +0200 @@ -30,6 +30,37 @@ repo.hm.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '') session.set_shared_data('do-not-insert-cwuri', False) +if applcubicwebversion < (3, 5, 0) and cubicwebversion >= (3, 5, 0): + add_entity_type('Workflow') + add_entity_type('BaseTransition') + add_entity_type('WorkflowTransition') + add_entity_type('SubWorkflowExitPoint') + drop_relation_definition('State', 'allowed_transition', 'Transition') # should be infered + schema.rebuild_infered_relations() # need to be explicitly called once everything is in place + + for et in rql('DISTINCT Any ET,ETN WHERE S state_of ET, ET name ETN', + ask_confirm=False).entities(): + wf = add_workflow(u'default %s workflow' % et.name, et.name, + ask_confirm=False) + rql('SET S state_of WF WHERE S state_of ET, ET eid %(et)s, WF eid %(wf)s', + {'et': et.eid, 'wf': wf.eid}, 'et', ask_confirm=False) + rql('SET T transition_of WF WHERE T transition_of ET, ET eid %(et)s, WF eid %(wf)s', + {'et': et.eid, 'wf': wf.eid}, 'et', ask_confirm=False) + rql('SET WF initial_state S WHERE ET initial_state S, S state_of ET, ET eid %(et)s, WF eid %(wf)s', + {'et': et.eid, 'wf': wf.eid}, 'et', ask_confirm=False) + + + rql('DELETE TrInfo TI WHERE NOT TI from_state S') + rql('SET TI by_transition T WHERE TI from_state FS, TI to_state TS, ' + 'FS allowed_transition T, T destination_state TS') + checkpoint() + + drop_relation_definition('State', 'state_of', 'CWEType') + drop_relation_definition('Transition', 'transition_of', 'CWEType') + drop_relation_definition('CWEType', 'initial_state', 'State') + + sync_schema_props_perms() + if applcubicwebversion < (3, 2, 2) and cubicwebversion >= (3, 2, 1): from base64 import b64encode for table in ('entities', 'deleted_entities'): @@ -41,37 +72,3 @@ if applcubicwebversion < (3, 2, 0) and cubicwebversion >= (3, 2, 0): add_cube('card', update_database=False) - -if applcubicwebversion < (2, 47, 0) and cubicwebversion >= (2, 47, 0): - from cubicweb.server import schemaserial - schemaserial.HAS_FULLTEXT_CONTAINER = False - session.set_shared_data('do-not-insert-is_instance_of', True) - add_attribute('CWRType', 'fulltext_container') - schemaserial.HAS_FULLTEXT_CONTAINER = True - - - -if applcubicwebversion < (2, 50, 0) and cubicwebversion >= (2, 50, 0): - session.set_shared_data('do-not-insert-is_instance_of', True) - add_relation_type('is_instance_of') - # fill the relation using an efficient sql query instead of using rql - sql('INSERT INTO is_instance_of_relation ' - ' SELECT * from is_relation') - checkpoint() - session.set_shared_data('do-not-insert-is_instance_of', False) - -if applcubicwebversion < (2, 42, 0) and cubicwebversion >= (2, 42, 0): - sql('ALTER TABLE entities ADD COLUMN mtime TIMESTAMP') - sql('UPDATE entities SET mtime=CURRENT_TIMESTAMP') - sql('CREATE INDEX entities_mtime_idx ON entities(mtime)') - sql('''CREATE TABLE deleted_entities ( - eid INTEGER PRIMARY KEY NOT NULL, - type VARCHAR(64) NOT NULL, - source VARCHAR(64) NOT NULL, - dtime TIMESTAMP NOT NULL, - extid VARCHAR(256) -)''') - sql('CREATE INDEX deleted_entities_type_idx ON deleted_entities(type)') - sql('CREATE INDEX deleted_entities_dtime_idx ON deleted_entities(dtime)') - sql('CREATE INDEX deleted_entities_extid_idx ON deleted_entities(extid)') - checkpoint() diff -r bc0a270622c2 -r 46a5a94287fa misc/migration/postcreate.py --- a/misc/migration/postcreate.py Mon Aug 24 20:27:05 2009 +0200 +++ b/misc/migration/postcreate.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa schema.py --- a/schema.py Mon Aug 24 20:27:05 2009 +0200 +++ b/schema.py Tue Aug 25 11:45:16 2009 +0200 @@ -847,23 +847,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, in_state_descr=None): + 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', description=in_state_descr) + 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 bc0a270622c2 -r 46a5a94287fa schemas/workflow.py --- a/schemas/workflow.py Mon Aug 24 20:27:05 2009 +0200 +++ b/schemas/workflow.py Tue Aug 25 11:45:16 2009 +0200 @@ -10,8 +10,37 @@ from yams.buildobjs import (EntityType, RelationType, SubjectRelation, ObjectRelation, RichString, String) -from cubicweb.schema import RQLConstraint -from cubicweb.schemas import META_ETYPE_PERMS, META_RTYPE_PERMS, HOOKS_RTYPE_PERMS +from cubicweb.schema import RQLConstraint, RQLUniqueConstraint +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')]) + + 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')) + + +class default_workflow(RelationType): + """default workflow for this entity types""" + permissions = META_RTYPE_PERMS + + subject = 'CWEType' + object = 'Workflow' + cardinality = '?*' + constraints = [RQLConstraint('S final FALSE, O workflow_of S')] + class State(EntityType): """used to associate simple states to an entity type and/or to define @@ -24,23 +53,18 @@ 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')], + # 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')) + state_of = SubjectRelation('Workflow', cardinality='+*', + description=_('workflow to which this state belongs'), + constraints=[RQLUniqueConstraint('S name N, Y state_of O, Y name N')]) -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 +81,108 @@ 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'), + constraints=[RQLUniqueConstraint('S name N, Y transition_of O, Y name N')]) + + +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('BaseTransition', 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 +200,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 bc0a270622c2 -r 46a5a94287fa selectors.py diff -r bc0a270622c2 -r 46a5a94287fa server/hooks.py --- a/server/hooks.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/hooks.py Tue Aug 25 11:45:16 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,79 @@ # workflow handling ########################################################### -def before_add_in_state(session, fromeid, rtype, toeid): - """check the transition is allowed and record transition information +def _change_state(session, x, oldstate, newstate): + nocheck = session.transaction_data.setdefault('skip-security', set()) + nocheck.add((x, 'in_state', oldstate)) + nocheck.add((x, 'in_state', newstate)) + # delete previous state first in case we're using a super session + session.delete_relation(x, 'in_state', oldstate) + session.add_relation(x, 'in_state', newstate) + + +def before_add_trinfo(session, entity): + """check the transition is allowed, add missing information. Expect that: + * wf_info_for inlined relation is set + * 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""" + _change_state(session, entity['wf_info_for'], + entity['from_state'], entity['to_state']) class SetInitialStateOp(PreCommitOperation): @@ -473,26 +502,79 @@ # 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) +class WorkflowChangedOp(PreCommitOperation): + """fix entity current state when changing its workflow""" + + def precommit_event(self): + session = self.session + pendingeids = session.transaction_data.get('pendingeids', ()) + if self.eid in pendingeids: + return + entity = session.entity_from_eid(self.eid) + # notice that enforcment that new workflow apply to the entity's type is + # done by schema rule, no need to check it here + if entity.current_workflow.eid == self.wfeid: + deststate = entity.current_workflow.initial + if not deststate: + msg = session._('workflow has no initial state') + raise ValidationError(entity.eid, {'custom_workflow': msg}) + if entity.current_workflow.state_by_eid(entity.current_state.eid): + # nothing to do + return + # if there are no history, simply go to new workflow's initial state + if not entity.workflow_history: + if entity.current_state.eid != deststate.eid: + _change_state(session, entity.eid, + entity.current_state.eid, deststate.eid) + return + msg = session._('workflow changed to "%s"') + msg %= entity.current_workflow.name + entity.change_state(deststate.name, msg) + + +def set_custom_workflow(session, eidfrom, rtype, eidto): + WorkflowChangedOp(session, eid=eidfrom, wfeid=eidto) + + +def del_custom_workflow(session, eidfrom, rtype, eidto): + entity = session.entity_from_eid(eidfrom) + typewf = entity.cwetype_workflow() + if typewf is not None: + WorkflowChangedOp(session, eid=eidfrom, wfeid=typewf.eid) + + +def after_del_workflow(session, eid): + # workflow cleanup + session.execute('DELETE State X WHERE NOT X state_of Y') + 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(set_custom_workflow, 'after_add_relation', 'custom_workflow') + hm.register_hook(del_custom_workflow, 'after_delete_relation', 'custom_workflow') + hm.register_hook(after_del_workflow, 'after_delete_entity', 'Workflow') # CWProperty hooks ############################################################# diff -r bc0a270622c2 -r 46a5a94287fa server/hooksmanager.py --- a/server/hooksmanager.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/hooksmanager.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/migractions.py --- a/server/migractions.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/migractions.py Tue Aug 25 11:45:16 2009 +0200 @@ -954,78 +954,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', ask_confirm=False) + assert rset, 'unexistant entity type %s' % etype + if default: + self.rqlexec( + 'SET ET default_workflow X WHERE X eid %(x)s, ET name %(et)s', + {'x': wf.eid, 'et': etype}, 'x', ask_confirm=False) + 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() @@ -1042,32 +1042,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 bc0a270622c2 -r 46a5a94287fa server/repository.py --- a/server/repository.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/repository.py Tue Aug 25 11:45:16 2009 +0200 @@ -199,6 +199,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 bc0a270622c2 -r 46a5a94287fa server/schemahooks.py --- a/server/schemahooks.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/schemahooks.py Tue Aug 25 11:45:16 2009 +0200 @@ -759,8 +759,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 bc0a270622c2 -r 46a5a94287fa server/schemaserial.py --- a/server/schemaserial.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/schemaserial.py Tue Aug 25 11:45:16 2009 +0200 @@ -110,7 +110,6 @@ print sql sqlcu.execute(sql) # other table renaming done once schema has been read - # print 'reading schema from the database...' index = {} permsdict = deserialize_ertype_permissions(session) schema.reading_from_database = True diff -r bc0a270622c2 -r 46a5a94287fa server/securityhooks.py --- a/server/securityhooks.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/securityhooks.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/session.py --- a/server/session.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/session.py Tue Aug 25 11:45:16 2009 +0200 @@ -78,16 +78,43 @@ def schema(self): return self.repo.schema - def add_relation(self, fromeid, rtype, toeid): + def _change_relation(self, cb, fromeid, rtype, toeid): if self.is_super_session: - self.repo.glob_add_relation(self, fromeid, rtype, toeid) + cb(self, fromeid, rtype, toeid) return self.is_super_session = True try: - self.repo.glob_add_relation(self, fromeid, rtype, toeid) + cb(self, fromeid, rtype, toeid) finally: self.is_super_session = False + def add_relation(self, fromeid, rtype, toeid): + """provide direct access to the repository method to add a relation. + + This is equivalent to the following rql query: + + SET X rtype Y WHERE X eid fromeid, T eid toeid + + without read security check but also all the burden of rql execution. + You may use this in hooks when you know both eids of the relation you + want to add. + """ + self._change_relation(self.repo.glob_add_relation, + fromeid, rtype, toeid) + def delete_relation(self, fromeid, rtype, toeid): + """provide direct access to the repository method to delete a relation. + + This is equivalent to the following rql query: + + DELETE X rtype Y WHERE X eid fromeid, T eid toeid + + without read security check but also all the burden of rql execution. + You may use this in hooks when you know both eids of the relation you + want to delete. + """ + self._change_relation(self.repo.glob_delete_relation, + fromeid, rtype, toeid) + def update_rel_cache_add(self, subject, rtype, object, symetric=False): self._update_entity_rel_cache_add(subject, rtype, 'subject', object) if symetric: diff -r bc0a270622c2 -r 46a5a94287fa server/sources/native.py --- a/server/sources/native.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/sources/native.py Tue Aug 25 11:45:16 2009 +0200 @@ -94,8 +94,6 @@ """adapter for source using the native cubicweb schema (see below) """ sqlgen_class = SQLGenerator - # need default value on class since migration doesn't call init method - has_deleted_entitites_table = True passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P" auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s" @@ -226,15 +224,6 @@ def init(self): self.init_creating() - pool = self.repo._get_pool() - pool.pool_set() - # XXX cubicweb < 2.42 compat - if 'deleted_entities' in self.dbhelper.list_tables(pool['system']): - self.has_deleted_entitites_table = True - else: - self.has_deleted_entitites_table = False - pool.pool_reset() - self.repo._free_pool(pool) def map_attribute(self, etype, attr, cb): self._rql_sqlgen.attr_map['%s.%s' % (etype, attr)] = cb @@ -549,13 +538,12 @@ """ attrs = {'eid': eid} session.system_sql(self.sqlgen.delete('entities', attrs), attrs) - if self.has_deleted_entitites_table: - if extid is not None: - assert isinstance(extid, str), type(extid) - extid = b64encode(extid) - attrs = {'type': etype, 'eid': eid, 'extid': extid, - 'source': uri, 'dtime': datetime.now()} - session.system_sql(self.sqlgen.insert('deleted_entities', attrs), attrs) + if extid is not None: + assert isinstance(extid, str), type(extid) + extid = b64encode(extid) + attrs = {'type': etype, 'eid': eid, 'extid': extid, + 'source': uri, 'dtime': datetime.now()} + session.system_sql(self.sqlgen.insert('deleted_entities', attrs), attrs) def fti_unindex_entity(self, session, eid): """remove text content for entity with the given eid from the full text diff -r bc0a270622c2 -r 46a5a94287fa server/ssplanner.py --- a/server/ssplanner.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/ssplanner.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/data/migratedapp/schema.py --- a/server/test/data/migratedapp/schema.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/data/migratedapp/schema.py Tue Aug 25 11:45:16 2009 +0200 @@ -104,7 +104,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'), @@ -123,7 +123,6 @@ cp = String(maxsize=12) ville= String(maxsize=32) - in_state = SubjectRelation('State', cardinality='?*') class evaluee(RelationDefinition): subject = ('Personne', 'CWUser', 'Societe') diff -r bc0a270622c2 -r 46a5a94287fa server/test/data/schema.py --- a/server/test/data/schema.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/data/schema.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_hookhelper.py --- a/server/test/unittest_hookhelper.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_hookhelper.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_hooks.py --- a/server/test/unittest_hooks.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_hooks.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_ldapuser.py --- a/server/test/unittest_ldapuser.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_ldapuser.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_migractions.py --- a/server/test/unittest_migractions.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_migractions.py Tue Aug 25 11:45:16 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 ET default_workflow WF, 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_msplanner.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_multisources.py --- a/server/test/unittest_multisources.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_multisources.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_querier.py --- a/server/test/unittest_querier.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_querier.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_repository.py --- a/server/test/unittest_repository.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_repository.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_rql2sql.py --- a/server/test/unittest_rql2sql.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_rql2sql.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_rqlrewrite.py --- a/server/test/unittest_rqlrewrite.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_rqlrewrite.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_schemaserial.py --- a/server/test/unittest_schemaserial.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_schemaserial.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_security.py --- a/server/test/unittest_security.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_security.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa server/test/unittest_ssplanner.py --- a/server/test/unittest_ssplanner.py Mon Aug 24 20:27:05 2009 +0200 +++ b/server/test/unittest_ssplanner.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa sobjects/supervising.py --- a/sobjects/supervising.py Mon Aug 24 20:27:05 2009 +0200 +++ b/sobjects/supervising.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa sobjects/test/unittest_notification.py --- a/sobjects/test/unittest_notification.py Mon Aug 24 20:27:05 2009 +0200 +++ b/sobjects/test/unittest_notification.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa sobjects/test/unittest_supervising.py --- a/sobjects/test/unittest_supervising.py Mon Aug 24 20:27:05 2009 +0200 +++ b/sobjects/test/unittest_supervising.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa test/unittest_entity.py --- a/test/unittest_entity.py Mon Aug 24 20:27:05 2009 +0200 +++ b/test/unittest_entity.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa test/unittest_schema.py --- a/test/unittest_schema.py Mon Aug 24 20:27:05 2009 +0200 +++ b/test/unittest_schema.py Tue Aug 25 11:45:16 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', '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 bc0a270622c2 -r 46a5a94287fa utils.py --- a/utils.py Mon Aug 24 20:27:05 2009 +0200 +++ b/utils.py Tue Aug 25 11:45:16 2009 +0200 @@ -320,9 +320,27 @@ self.body.getvalue()) -class AcceptMixIn(object): - """Mixin class for appobjects defining the 'accepts' attribute describing - a set of supported entity type (Any by default). +def can_do_pdf_conversion(__answer=[None]): + """pdf conversion depends on + * pyxmltrf (python package) + * fop 0.9x """ - # XXX deprecated, no more necessary - + if __answer[0] is not None: + return __answer[0] + try: + import pysixt + except ImportError: + __answer[0] = False + return False + from subprocess import Popen, STDOUT + import os + try: + Popen(['/usr/bin/fop', '-q'], + stdout=open(os.devnull, 'w'), + stderr=STDOUT) + except OSError, e: + print e + __answer[0] = False + return False + __answer[0] = True + return True diff -r bc0a270622c2 -r 46a5a94287fa web/__init__.py --- a/web/__init__.py Mon Aug 24 20:27:05 2009 +0200 +++ b/web/__init__.py Tue Aug 25 11:45:16 2009 +0200 @@ -16,7 +16,7 @@ from logilab.common.deprecation import deprecated -from cubicweb.common.uilib import urlquote +from urllib import quote as urlquote from cubicweb.web._exceptions import * diff -r bc0a270622c2 -r 46a5a94287fa web/data/cubicweb.css --- a/web/data/cubicweb.css Mon Aug 24 20:27:05 2009 +0200 +++ b/web/data/cubicweb.css Tue Aug 25 11:45:16 2009 +0200 @@ -838,4 +838,13 @@ border: 1px solid #edecd2; border-color:#edecd2 #cfceb7 #cfceb7 #edecd2; background: #fffff8 url("button.png") bottom left repeat-x; -} \ No newline at end of file +} + + +/********************************/ +/* placement of alt. view icons */ +/********************************/ + +.otherView { + float: right; +} diff -r bc0a270622c2 -r 46a5a94287fa web/data/pdf_icon.gif Binary file web/data/pdf_icon.gif has changed diff -r bc0a270622c2 -r 46a5a94287fa web/test/data/sample1.pdf Binary file web/test/data/sample1.pdf has changed diff -r bc0a270622c2 -r 46a5a94287fa web/test/data/sample1.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/test/data/sample1.xml Tue Aug 25 11:45:16 2009 +0200 @@ -0,0 +1,138 @@ + + + + + ] > + + + + + + + +Comet 0.2.0 (unset title) + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + \ No newline at end of file diff -r bc0a270622c2 -r 46a5a94287fa web/test/unittest_form.py --- a/web/test/unittest_form.py Mon Aug 24 20:27:05 2009 +0200 +++ b/web/test/unittest_form.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa web/test/unittest_pdf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/test/unittest_pdf.py Tue Aug 25 11:45:16 2009 +0200 @@ -0,0 +1,34 @@ +from unittest import TestCase +import os.path as osp +from xml.etree.cElementTree import ElementTree, fromstring, tostring, dump + +from tempfile import NamedTemporaryFile +from subprocess import Popen as sub + +from cubicweb.utils import can_do_pdf_conversion + +from cubicweb.web.xhtml2fo import ReportTransformer + +DATADIR = osp.join(osp.dirname(__file__), 'data') + +class PDFTC(TestCase): + + def test_xhtml_to_fop_to_pdf(self): + if not can_do_pdf_conversion(): + self.skip('dependencies not available : check pysixt and fop') + xmltree = ElementTree() + xmltree.parse(osp.join(DATADIR, 'sample1.xml')) + foptree = ReportTransformer(u'contentmain').transform(xmltree) + # next + foptmp = NamedTemporaryFile() + foptree.write(foptmp) + foptmp.flush() + pdftmp = NamedTemporaryFile() + fopproc = sub(['/usr/bin/fop', foptmp.name, pdftmp.name]) + fopproc.wait() + del foptmp + pdftmp.seek(0) # a bit superstitious + reference = open(osp.join(DATADIR, 'sample1.pdf'), 'r').read() + output = pdftmp.read() + # XXX almost equals due to ID, creation date, so it seems to fail + self.assertTextEquals(output, reference) diff -r bc0a270622c2 -r 46a5a94287fa web/test/unittest_views_editforms.py --- a/web/test/unittest_views_editforms.py Mon Aug 24 20:27:05 2009 +0200 +++ b/web/test/unittest_views_editforms.py Tue Aug 25 11:45:16 2009 +0200 @@ -53,6 +53,7 @@ ]) self.assertListEquals(rbc(e, 'generic'), [('primary_email', 'subject'), + ('custom_workflow', 'subject'), ('connait', 'subject'), ('checked_by', 'object'), ]) diff -r bc0a270622c2 -r 46a5a94287fa web/uicfg.py --- a/web/uicfg.py Mon Aug 24 20:27:05 2009 +0200 +++ b/web/uicfg.py Tue Aug 25 11:45:16 2009 +0200 @@ -149,12 +149,14 @@ # * 'application' # * 'system' # * 'schema' +# * 'hidden' # * 'subobject' (not displayed by default) indexview_etype_section = {'EmailAddress': 'subobject', 'CWUser': 'system', 'CWGroup': 'system', 'CWPermission': 'system', + 'BaseTransition': 'hidden', } diff -r bc0a270622c2 -r 46a5a94287fa web/views/autoform.py --- a/web/views/autoform.py Mon Aug 24 20:27:05 2009 +0200 +++ b/web/views/autoform.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa web/views/basecomponents.py --- a/web/views/basecomponents.py Mon Aug 24 20:27:05 2009 +0200 +++ b/web/views/basecomponents.py Tue Aug 25 11:45:16 2009 +0200 @@ -2,6 +2,7 @@ * the rql input form * the logged user link +* pdf view link :organization: Logilab :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. @@ -214,6 +215,23 @@ self.w(u' | '.join(html)) self.w(u'
') +class PdfViewComponent(component.Component): + id = 'pdfview' + __select__ = yes() + + context = 'header' + property_defs = { + _('visible'): dict(type='Boolean', default=True, + help=_('display the pdf icon or not')), + } + + def call(self, vid): + self.req.add_css('cubes.confman.css') + entity = self.entity(0,0) + self.w(u'' % + (xml_escape(entity.absolute_url() + '?vid=%s&__template=pdf-main-template' % vid))) + + def registration_callback(vreg): vreg.register_all(globals().values(), __name__, (SeeAlsoVComponent,)) diff -r bc0a270622c2 -r 46a5a94287fa web/views/basetemplates.py --- a/web/views/basetemplates.py Mon Aug 24 20:27:05 2009 +0200 +++ b/web/views/basetemplates.py Tue Aug 25 11:45:16 2009 +0200 @@ -13,7 +13,7 @@ from cubicweb.appobject import objectify_selector from cubicweb.selectors import match_kwargs from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW -from cubicweb.utils import make_uid, UStringIO +from cubicweb.utils import make_uid, UStringIO, can_do_pdf_conversion # main templates ############################################################## @@ -266,6 +266,44 @@ self.w(u'\n') self.w(u'\n') +if can_do_pdf_conversion(): + from xml.etree.cElementTree import ElementTree + from subprocess import Popen as sub + from StringIO import StringIO + from tempfile import NamedTemporaryFile + from cubicweb.web.xhtml2fo import ReportTransformer + + class PdfMainTemplate(TheMainTemplate): + id = 'pdf-main-template' + + def call(self, view): + """build the standard view, then when it's all done, convert xhtml to pdf + """ + super(PdfMainTemplate, self).call(view) + pdf = self.to_pdf(self._stream) + self.req.set_content_type('application/pdf', filename='report.pdf') + self.binary = True + self.w = None + self.set_stream() + # pylint needs help + self.w(pdf) + + def to_pdf(self, stream, section='contentmain'): + # XXX see ticket/345282 + stream = stream.getvalue().replace(' ', ' ').encode('utf-8') + xmltree = ElementTree() + xmltree.parse(StringIO(stream)) + foptree = ReportTransformer(section).transform(xmltree) + foptmp = NamedTemporaryFile() + pdftmp = NamedTemporaryFile() + foptree.write(foptmp) + foptmp.flush() + fopproc = sub(['/usr/bin/fop', foptmp.name, pdftmp.name]) + fopproc.wait() + pdftmp.seek(0) + pdf = pdftmp.read() + return pdf + # page parts templates ######################################################## class HTMLHeader(View): @@ -489,4 +527,4 @@ ## vregistry registration callback ############################################ def registration_callback(vreg): - vreg.register_all(globals().values(), modname=__name__) + vreg.register_all(globals().values(), __name__) diff -r bc0a270622c2 -r 46a5a94287fa web/views/boxes.py --- a/web/views/boxes.py Mon Aug 24 20:27:05 2009 +0200 +++ b/web/views/boxes.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa web/views/forms.py --- a/web/views/forms.py Mon Aug 24 20:27:05 2009 +0200 +++ b/web/views/forms.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa web/views/primary.py --- a/web/views/primary.py Mon Aug 24 20:27:05 2009 +0200 +++ b/web/views/primary.py Tue Aug 25 11:45:16 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 bc0a270622c2 -r 46a5a94287fa web/views/workflow.py --- a/web/views/workflow.py Mon Aug 24 20:27:05 2009 +0200 +++ b/web/views/workflow.py Tue Aug 25 11:45:16 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') diff -r bc0a270622c2 -r 46a5a94287fa web/xhtml2fo.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/xhtml2fo.py Tue Aug 25 11:45:16 2009 +0200 @@ -0,0 +1,142 @@ +from cubicweb.utils import can_do_pdf_conversion +assert can_do_pdf_conversion() + +from xml.etree.ElementTree import QName, fromstring +from pysixt.standard.xhtml_xslfo.transformer import XHTML2FOTransformer +from pysixt.utils.xslfo.standard import cm +from pysixt.utils.xslfo import SimplePageMaster +from pysixt.standard.xhtml_xslfo.default_styling import default_styles +from pysixt.standard.xhtml_xslfo import XHTML_NS + + +class ReportTransformer(XHTML2FOTransformer): + """ + Class transforming an XHTML input tree into a FO document + displaying reports (one report for each
+ element in the input tree. + """ + + def __init__(self, section, + page_width=21.0, page_height=29.7, + margin_top=1.0, margin_bottom=1.0, + margin_left=1.0, margin_right=1.0, + header_footer_height=0.75, + standard_font_size=11.0, default_lang=u"fr" ): + """ + Initializes a transformer turning an XHTML input tree + containing
elements representing + main content sections into a FO output tree displaying the + reports. + + page_width: float - width of the page (in cm) + page_height: float - height of the page (in cm) + margin_top: float - top margin of the page (in cm) + margin_bottom: float - bottom margin of the page (in cm) + margin_left: float - left margin of the page (in cm) + margin_right: float - right margin of the page (in cm) + header_footer_height: float - height of the header or the footer of the + page that the page number (if any) will be + inserted in. + standard_font_size: float - standard size of the font (in pt) + default_lang: u"" - default language (used for hyphenation) + """ + self.section = section + self.page_width = page_width + self.page_height = page_height + + self.page_tmargin = margin_top + self.page_bmargin = margin_bottom + self.page_lmargin = margin_left + self.page_rmargin = margin_right + + self.hf_height = header_footer_height + + self.font_size = standard_font_size + self.lang = default_lang + + XHTML2FOTransformer.__init__(self) + + + def define_pagemasters(self): + """ + Defines the page masters for the FO output document. + """ + pm = SimplePageMaster(u"page-report") + pm.set_page_dims( self.page_width*cm, self.page_height*cm ) + pm.set_page_margins({u'top' : self.page_tmargin*cm, + u'bottom': self.page_bmargin*cm, + u'left' : self.page_lmargin*cm, + u'right' : self.page_rmargin*cm }) + pm.add_peripheral_region(u"end",self.hf_height) + dims = {} + dims[u"bottom"] = self.hf_height + 0.25 + pm.set_main_region_margins(dims) + return [pm] + + def _visit_report(self, in_elt, _out_elt, params): + """ + Specific visit function for the input
elements whose class is + "report". The _root_visit method of this class selects these input + elements and asks the process of these elements with this specific + visit function. + """ + + ps = self.create_pagesequence(u"page-report") + props = { u"force-page-count": u"no-force", + u"initial-page-number": u"1", + u"format": u"1", } + self._output_properties(ps,props) + + sc = self.create_staticcontent(ps, u"end") + sc_bl = self.create_block(sc) + attrs = { u"hyphenate": u"false", } + attrs[u"font-size"] = u"%.1fpt" %(self.font_size*0.7) + attrs[u"language"] = self.lang + attrs[u"text-align"] = u"center" + self._output_properties(sc_bl,attrs) + sc_bl.text = u"Page" + u" " # ### Should be localised! + pn = self.create_pagenumber(sc_bl) + pn.tail = u"/" + lpn = self.create_pagenumbercitation( sc_bl, + u"last-block-of-report-%d" % params[u"context_pos"] + ) + + + fl = self.create_flow(ps,u"body") + bl = self.create_block(fl) + + # Sets on the highest block element the properties of the XHTML body + # element. These properties (at the least the inheritable ones) will + # be inherited by all the future FO elements. + bodies = list(self.in_tree.getiterator(QName(XHTML_NS,u"body"))) + if len(bodies) > 0: + attrs = self._extract_properties([bodies[0]]) + else: + attrs = default_styles[u"body"].copy() + attrs[u"font-size"] = u"%.1fpt" %self.font_size + attrs[u"language"] = self.lang + self._output_properties(bl,attrs) + + # Processes the report content + self._copy_text(in_elt,bl) + self._process_nodes(in_elt.getchildren(),bl) + + # Inserts an empty block at the end of the report in order to be able + # to compute the last page number of this report. + last_bl = self.create_block(bl) + props = { u"keep-with-previous": u"always", } + props[u"id"] = u"last-block-of-report-%d" % params[u"context_pos"] + self._output_properties(last_bl,props) + + + def _root_visit(self): + """ + Visit function called when starting the process of the input tree. + """ + content = [ d for d in self.in_tree.getiterator(QName(XHTML_NS,u"div")) + if d.get(u"id") == self.section ] + # Asks the process of the report elements with a specific visit + # function + self._process_nodes(content, self.fo_root, + with_function=self._visit_report) +