somewhat painful backport of 3.5 branch, should mostly be ok
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 21 Aug 2009 16:26:20 +0200
changeset 2968 0e3460341023
parent 2902 dd9f2dd02f85 (current diff)
parent 2967 e7d348134006 (diff)
child 3023 7864fee8b4ec
somewhat painful backport of 3.5 branch, should mostly be ok
__init__.py
appobject.py
common/mixins.py
common/test/unittest_mixins.py
common/uilib.py
cwctl.py
cwvreg.py
debian.hardy/control
devtools/__init__.py
devtools/testlib.py
entities/test/unittest_base.py
entities/test/unittest_wfobjs.py
entity.py
hooks/integrity.py
hooks/metadata.py
hooks/security.py
hooks/syncschema.py
hooks/workflow.py
misc/migration/2.42.0_Any.py
misc/migration/2.42.1_Any.py
misc/migration/2.43.0_Any.py
misc/migration/2.44.0_Any.py
misc/migration/2.45.0_Any.py
misc/migration/2.46.0_Any.py
misc/migration/2.47.0_Any.py
misc/migration/2.48.8_Any.py
misc/migration/2.49.3_Any.py
misc/migration/2.50.0_Any.py
misc/migration/bootstrapmigration_repository.py
req.py
schema.py
server/hook.py
server/hookhelper.py
server/hooksmanager.py
server/migractions.py
server/repository.py
server/serverconfig.py
server/session.py
server/test/unittest_hookhelper.py
server/test/unittest_hooks.py
server/test/unittest_ldapuser.py
server/test/unittest_migractions.py
server/test/unittest_msplanner.py
server/test/unittest_multisources.py
server/test/unittest_querier.py
server/test/unittest_repository.py
server/test/unittest_security.py
server/test/unittest_ssplanner.py
sobjects/supervising.py
sobjects/test/unittest_notification.py
sobjects/test/unittest_supervising.py
test/unittest_entity.py
vregistry.py
web/__init__.py
web/test/unittest_form.py
web/test/unittest_views_editforms.py
web/uicfg.py
web/views/basecomponents.py
web/views/basetemplates.py
web/views/forms.py
web/views/primary.py
web/views/workflow.py
--- a/.hgtags	Tue Aug 18 09:25:44 2009 +0200
+++ b/.hgtags	Fri Aug 21 16:26:20 2009 +0200
@@ -56,3 +56,5 @@
 e6a8cd8cc910507e5f928ddba6de3cf5e5bdf3d4 cubicweb-debian-version-3.4.2-1
 b2add17d5bc437a807976cd13870f92d3a94f2a6 cubicweb-version-3.4.3
 a3828745e2cf0c4bf1cab6f5397c524ba0510df6 cubicweb-debian-version-3.4.3-1
+2aee4ea585cdba159bc9490741db3fcd25dcaaca cubicweb-version-3.4.4
+e244a0fd7d719c25f4267470342ff8334b2dc8b3 cubicweb-debian-version-3.4.4-1
--- a/__pkginfo__.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/__pkginfo__.py	Fri Aug 21 16:26:20 2009 +0200
@@ -7,7 +7,7 @@
 distname = "cubicweb"
 modname = "cubicweb"
 
-numversion = (3, 4, 3)
+numversion = (3, 5, 0)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL v2'
--- a/appobject.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/appobject.py	Fri Aug 21 16:26:20 2009 +0200
@@ -303,101 +303,101 @@
     # deprecated ###############################################################
 
     @property
-    @deprecated('[3.5] use self._cw.vreg')
+    @deprecated('[3.6] use self._cw.vreg')
     def vreg(self):
         return self._cw.vreg
 
     @property
-    @deprecated('[3.5] use self._cw.vreg.schema')
+    @deprecated('[3.6] use self._cw.vreg.schema')
     def schema(self):
         return self._cw.vreg.schema
 
     @property
-    @deprecated('[3.5] use self._cw.vreg.config')
+    @deprecated('[3.6] use self._cw.vreg.config')
     def config(self):
         return self._cw.vreg.config
 
     @property
-    @deprecated('[3.5] use self._cw')
+    @deprecated('[3.6] use self._cw')
     def req(self):
         return self._cw
 
-    @deprecated('[3.5] use self.cw_rset')
+    @deprecated('[3.6] use self.cw_rset')
     def get_rset(self):
         return self.cw_rset
-    @deprecated('[3.5] use self.cw_rset')
+    @deprecated('[3.6] use self.cw_rset')
     def set_rset(self, rset):
         self.cw_rset = rset
     rset = property(get_rset, set_rset)
 
     @property
-    @deprecated('[3.5] use self.cw_row')
+    @deprecated('[3.6] use self.cw_row')
     def row(self):
         return self.cw_row
 
     @property
-    @deprecated('[3.5] use self.cw_col')
+    @deprecated('[3.6] use self.cw_col')
     def col(self):
         return self.cw_col
 
     @property
-    @deprecated('[3.5] use self.cw_extra_kwargs')
+    @deprecated('[3.6] use self.cw_extra_kwargs')
     def extra_kwargs(self):
         return self.cw_extra_kwargs
 
-    @deprecated('[3.5] use self._cw.view')
+    @deprecated('[3.6] use self._cw.view')
     def view(self, *args, **kwargs):
         return self._cw.view(*args, **kwargs)
 
-    @deprecated('[3.5] use self._cw.varmaker')
+    @deprecated('[3.6] use self._cw.varmaker')
     def initialize_varmaker(self):
         self.varmaker = self._cw.varmaker
 
-    @deprecated('[3.5] use self._cw.get_cache')
+    @deprecated('[3.6] use self._cw.get_cache')
     def get_cache(self, cachename):
         return self._cw.get_cache(cachename)
 
-    @deprecated('[3.5] use self._cw.build_url')
+    @deprecated('[3.6] use self._cw.build_url')
     def build_url(self, *args, **kwargs):
         return self._cw.build_url(*args, **kwargs)
 
-    @deprecated('[3.5] use self.cw_rset.limited_rql')
+    @deprecated('[3.6] use self.cw_rset.limited_rql')
     def limited_rql(self):
         return self.rset.limited_rql()
 
-    @deprecated('[3.5] use self.rset.complete_entity(row,col) instead')
+    @deprecated('[3.6] use self.rset.complete_entity(row,col) instead')
     def complete_entity(self, row, col=0, skip_bytes=True):
         return self.rset.complete_entity(row, col, skip_bytes)
 
-    @deprecated('[3.5] use self.rset.get_entity(row,col) instead')
+    @deprecated('[3.6] use self.rset.get_entity(row,col) instead')
     def entity(self, row, col=0):
         return self.rset.get_entity(row, col)
 
-    @deprecated('[3.5] use self._cw.user_rql_callback')
+    @deprecated('[3.6] use self._cw.user_rql_callback')
     def user_rql_callback(self, args, msg=None):
         return self._cw.user_rql_callback(args, msg)
 
-    @deprecated('[3.5] use self._cw.user_callback')
+    @deprecated('[3.6] use self._cw.user_callback')
     def user_callback(self, cb, args, msg=None, nonify=False):
         return self._cw.user_callback(cb, args, msg, nonify)
 
-    @deprecated('[3.5] use self._cw.format_date')
+    @deprecated('[3.6] use self._cw.format_date')
     def format_date(self, date, date_format=None, time=False):
         return self._cw.format_date(date, date_format, time)
 
-    @deprecated('[3.5] use self._cw.format_timoe')
+    @deprecated('[3.6] use self._cw.format_timoe')
     def format_time(self, time):
         return self._cw.format_time(time)
 
-    @deprecated('[3.5] use self._cw.format_float')
+    @deprecated('[3.6] use self._cw.format_float')
     def format_float(self, num):
         return self._cw.format_float(num)
 
-    @deprecated('[3.5] use self._cw.parse_datetime')
+    @deprecated('[3.6] use self._cw.parse_datetime')
     def parse_datetime(self, value, etype='Datetime'):
         return self._cw.parse_datetime(value, etype)
 
-    @deprecated('[3.5] use self.cw_propval')
+    @deprecated('[3.6] use self.cw_propval')
     def propval(self, propid):
         return self._cw.property_value(self._cwpropkey(propid))
 
--- a/common/migration.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/common/migration.py	Fri Aug 21 16:26:20 2009 +0200
@@ -157,7 +157,7 @@
                     # take care to X.Y.Z_Any.py / X.Y.Z_common.py: we've to call
                     # cube_upgraded once all script of X.Y.Z have been executed
                     if prevversion is not None and version != prevversion:
-                        self.cube_upgraded(cube, version)
+                        self.cube_upgraded(cube, prevversion)
                     prevversion = version
                     self.process_script(script)
                 self.cube_upgraded(cube, toversion)
--- a/common/mixins.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/common/mixins.py	Fri Aug 21 16:26:20 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,
     }
--- a/common/test/unittest_mixins.py	Tue Aug 18 09:25:44 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.testlib import CubicWebTC
-
-class WorkfloableMixInTC(CubicWebTC):
-    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()
--- a/common/test/unittest_uilib.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/common/test/unittest_uilib.py	Fri Aug 21 16:26:20 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 = """\
-<table class="tree">
-<tr><td class="tree_cell" rowspan="2"><div class="tree_cell">root</div></td><td class="tree_cell_1_1">&nbsp;</td><td class="tree_cell_1_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_1_1</div></td><td class="tree_cell_1_1">&nbsp;</td><td class="tree_cell_1_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_2_1</div></td><td class="tree_cell_0_1">&nbsp;</td><td class="tree_cell_0_2">&nbsp;</td><td rowspan="2">&nbsp;</td></tr>
-<tr><td class="tree_cell_1_3">&nbsp;</td><td class="tree_cell_1_4">&nbsp;</td><td class="tree_cell_1_3">&nbsp;</td><td class="tree_cell_1_4">&nbsp;</td><td class="tree_cell_0_3">&nbsp;</td><td class="tree_cell_0_4">&nbsp;</td></tr>
-<tr><td rowspan="2">&nbsp;</td><td class="tree_cell_2_1">&nbsp;</td><td class="tree_cell_2_2">&nbsp;</td><td rowspan="2">&nbsp;</td><td class="tree_cell_4_1">&nbsp;</td><td class="tree_cell_4_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">child_2_2</div></td><td class="tree_cell_1_1">&nbsp;</td><td class="tree_cell_1_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_1</div></td></tr>
-<tr><td class="tree_cell_2_3">&nbsp;</td><td class="tree_cell_2_4">&nbsp;</td><td class="tree_cell_4_3">&nbsp;</td><td class="tree_cell_4_4">&nbsp;</td><td class="tree_cell_1_3">&nbsp;</td><td class="tree_cell_1_4">&nbsp;</td></tr>
-<tr><td rowspan="2">&nbsp;</td><td class="tree_cell_2_1">&nbsp;</td><td class="tree_cell_2_2">&nbsp;</td><td rowspan="2">&nbsp;</td><td class="tree_cell_0_1">&nbsp;</td><td class="tree_cell_0_2">&nbsp;</td><td rowspan="2">&nbsp;</td><td class="tree_cell_3_1">&nbsp;</td><td class="tree_cell_3_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_2</div></td></tr>
-<tr><td class="tree_cell_2_3">&nbsp;</td><td class="tree_cell_2_4">&nbsp;</td><td class="tree_cell_0_3">&nbsp;</td><td class="tree_cell_0_4">&nbsp;</td><td class="tree_cell_3_3">&nbsp;</td><td class="tree_cell_3_4">&nbsp;</td></tr>
-<tr><td rowspan="2">&nbsp;</td><td class="tree_cell_2_1">&nbsp;</td><td class="tree_cell_2_2">&nbsp;</td><td rowspan="2">&nbsp;</td><td class="tree_cell_0_1">&nbsp;</td><td class="tree_cell_0_2">&nbsp;</td><td rowspan="2">&nbsp;</td><td class="tree_cell_4_1">&nbsp;</td><td class="tree_cell_4_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_3</div></td></tr>
-<tr><td class="tree_cell_2_3">&nbsp;</td><td class="tree_cell_2_4">&nbsp;</td><td class="tree_cell_0_3">&nbsp;</td><td class="tree_cell_0_4">&nbsp;</td><td class="tree_cell_4_3">&nbsp;</td><td class="tree_cell_4_4">&nbsp;</td></tr>
-<tr><td rowspan="2">&nbsp;</td><td class="tree_cell_4_1">&nbsp;</td><td class="tree_cell_4_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_1_2</div></td><td class="tree_cell_5_1">&nbsp;</td><td class="tree_cell_5_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_2_3</div></td><td class="tree_cell_0_1">&nbsp;</td><td class="tree_cell_0_2">&nbsp;</td><td rowspan="2">&nbsp;</td></tr>
-<tr><td class="tree_cell_4_3">&nbsp;</td><td class="tree_cell_4_4">&nbsp;</td><td class="tree_cell_5_3">&nbsp;</td><td class="tree_cell_5_4">&nbsp;</td><td class="tree_cell_0_3">&nbsp;</td><td class="tree_cell_0_4">&nbsp;</td></tr>
-</table>\
-"""
-
-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__':
--- a/common/uilib.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/common/uilib.py	Fri Aug 21 16:26:20 2009 +0200
@@ -263,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 <div> 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 <table>
-    s = u'<table class="tree">\n'
-    if caption:
-        s += '<caption>%s</caption>\n' % caption
-
-    for i, link_line in enumerate(links):
-        line = matrix[i]
-
-        s += '<tr>'
-        for j, link_cell in enumerate(link_line):
-            cell = line[j]
-            if cell:
-                if cell.id == selected_node:
-                    s += '<td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">%s</div></td>' % (render_node(cell.id))
-                else:
-                    s += '<td class="tree_cell" rowspan="2"><div class="tree_cell">%s</div></td>' % (render_node(cell.id))
-            else:
-                s += '<td rowspan="2">&nbsp;</td>'
-            s += '<td class="tree_cell_%d_1">&nbsp;</td>' % link_cell
-            s += '<td class="tree_cell_%d_2">&nbsp;</td>' % link_cell
-
-        cell = line[-1]
-        if cell:
-            if cell.id == selected_node:
-                s += '<td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">%s</div></td>' % (render_node(cell.id))
-            else:
-                s += '<td class="tree_cell" rowspan="2"><div class="tree_cell">%s</div></td>' % (render_node(cell.id))
-        else:
-            s += '<td rowspan="2">&nbsp;</td>'
-
-        s += '</tr>\n'
-        if link_line:
-            s += '<tr>'
-            for j, link_cell in enumerate(link_line):
-                s += '<td class="tree_cell_%d_3">&nbsp;</td>' % link_cell
-                s += '<td class="tree_cell_%d_4">&nbsp;</td>' % link_cell
-            s += '</tr>\n'
-
-    s += '</table>'
-    return s
-
-
 
 # traceback formatting ########################################################
 
--- a/cwctl.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/cwctl.py	Fri Aug 21 16:26:20 2009 +0200
@@ -723,6 +723,7 @@
           'help': 'don\'t check instance is up to date.'}
          ),
         )
+
     def run(self, args):
         appid = pop_arg(args, 99, msg="No instance specified !")
         config = cwcfg.config_for(appid)
--- a/cwvreg.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/cwvreg.py	Fri Aug 21 16:26:20 2009 +0200
@@ -47,12 +47,15 @@
     def __init__(self, vreg):
         super(CWRegistry, self).__init__(vreg.config)
         self.vreg = vreg
-        self.schema = vreg.schema
+
+    @property
+    def schema(self):
+        return self.vreg.schema
 
     def initialization_completed(self):
         pass
 
-    @deprecated('[3.5] select object, then use obj.render()')
+    @deprecated('[3.6] select object, then use obj.render()')
     def render(self, __oid, req, __fallback_oid=None, rset=None, **kwargs):
         """select object, or fallback object if specified and the first one
         isn't selectable, then render it
@@ -62,10 +65,10 @@
         except NoSelectableObject:
             if __fallback_oid is None:
                 raise
-            obj = self.select(__fallback_oid, req, **kwargs)
+            obj = self.select(__fallback_oid, req, rset=rset, **kwargs)
         return obj.render(**kwargs)
 
-    @deprecated('[3.5] use select_or_none and test for obj.cw_propval("visible")')
+    @deprecated('[3.6] use select_or_none and test for obj.cw_propval("visible")')
     def select_vobject(self, oid, *args, **kwargs):
         selected = self.select_or_none(oid, *args, **kwargs)
         if selected and selected.cw_propval('visible'):
@@ -80,7 +83,7 @@
         return sorted([x for x in self.possible_objects(*args, **kwargs)
                        if x.cw_propval('visible')],
                       key=lambda x: x.cw_propval('order'))
-    possible_vobjects = deprecated('[3.5] use poss_visible_objects()')(poss_visible_objects)
+    possible_vobjects = deprecated('[3.6] use poss_visible_objects()')(poss_visible_objects)
 
 
 VRegistry.REGISTRY_FACTORY[None] = CWRegistry
@@ -109,7 +112,7 @@
         if etype == 'Any':
             return [self.etype_class('Any')]
         eschema = self.schema.eschema(etype)
-        parents = [cls.etype_class(e.type) for e in eschema.ancestors()]
+        parents = [self.etype_class(e.type) for e in eschema.ancestors()]
         parents.append(self.etype_class('Any'))
         return parents
 
@@ -236,8 +239,8 @@
             config.init_log(debug=debug)
         super(CubicWebVRegistry, self).__init__(config)
         self.schema = None
+        self.initialized = False
         self.reset()
-        self.initialized = False
 
     def setdefault(self, regid):
         try:
@@ -264,10 +267,11 @@
         # two special registries, propertydefs which care all the property
         # definitions, and propertyvals which contains values for those
         # properties
-        self['propertydefs'] = {}
-        self['propertyvalues'] = {}
-        for key, propdef in self.config.eproperty_definitions():
-            self.register_property(key, **propdef)
+        if not self.initialized:
+            self['propertydefs'] = {}
+            self['propertyvalues'] = self.eprop_values = {}
+            for key, propdef in self.config.eproperty_definitions():
+                self.register_property(key, **propdef)
         if path is not None and force_reload:
             cleanup_sys_modules(path)
             cubes = self.config.cubes()
--- a/debian.hardy/control	Tue Aug 18 09:25:44 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-Source: cubicweb
-Section: web
-Priority: optional
-Maintainer: Logilab S.A. <contact@logilab.fr>
-Uploaders: Sylvain Thenault <sylvain.thenault@logilab.fr>,
-           Julien Jehannet <julien.jehannet@logilab.fr>,
-           Aurélien Campéas <aurelien.campeas@logilab.fr>
-Build-Depends: debhelper (>= 5), python-dev (>=2.4), python-central (>= 0.5)
-Standards-Version: 3.8.0
-Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.4, << 2.6
-
-
-Package: cubicweb
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-twisted (= ${source:Version}), cubicweb-client (= ${source:Version})
-XB-Recommends: (postgresql, postgresql-plpython, postgresql-contrib) | mysql | sqlite3
-Recommends: postgresql | mysql | sqlite3
-Description: the complete CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package will install all the components you need to run cubicweb on
- a single machine. You can also deploy cubicweb by running the different
- process on different computers, in which case you need to install the
- corresponding packages on the different hosts.
-
-
-Package: cubicweb-server
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Conflicts: cubicweb-multisources
-Replaces: cubicweb-multisources
-Provides: cubicweb-multisources
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-indexer (>= 0.6.1), python-psycopg2 | python-mysqldb | python-pysqlite2
-Recommends: pyro, cubicweb-documentation (= ${source:Version})
-Description: server part of the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides the repository server part of the system.
- .
- This package provides the repository server part of the library and
- necessary shared data files such as the schema library.
-
-
-Package: cubicweb-twisted
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Provides: cubicweb-web-frontend
-Depends: ${python:Depends}, cubicweb-web (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-twisted-web2
-Recommends: pyro, cubicweb-documentation (= ${source:Version})
-Description: twisted-based web interface for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides a twisted based HTTP server to serve
- the adaptative web interface (see cubicweb-web package).
- .
- This package provides only the twisted server part of the library.
-
-
-Package: cubicweb-web
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), python-docutils, python-vobject, python-elementtree
-Recommends: fckeditor
-Description: web interface library for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides an adaptative web interface to the CubicWeb server.
- Install the cubicweb-twisted package to serve this interface via HTTP.
- .
- This package provides the web interface part of the library and
- necessary shared data files such as defaut views, images...
-
-
-Package: cubicweb-common
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, python-logilab-mtconverter (>= 0.6.0), python-simpletal (>= 4.0), graphviz, gettext, python-lxml, python-logilab-common (>= 0.38.1), python-yams (>= 0.20.2), python-rql (>= 0.20.2), python-simplejson (>= 1.3)
-Recommends: python-psyco
-Conflicts: cubicweb-core
-Replaces: cubicweb-core
-Description: common library for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides the common parts of the library used by both server
- code and web application code.
-
-
-Package: cubicweb-ctl
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version})
-Description: tool to manage the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides a control script to manage (create, upgrade, start,
- stop, etc) CubicWeb applications. It also include the init.d script
- to automatically start and stop CubicWeb applications on boot or shutdown.
-
-
-Package: cubicweb-client
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-ctl (= ${source:Version}), pyro
-Description: RQL command line client for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides a RQL (Relation Query Language) command line client using
- pyro to connect to a repository server.
-
-
-Package: cubicweb-dev
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-web (= ${source:Version}), python-pysqlite2
-Suggests: w3c-dtd-xhtml
-Description: tests suite and development tools for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides the CubicWeb tests suite and some development tools
- helping in the creation of application.
-
-
-Package: cubicweb-documentation
-Architecture: all
-Recommends: doc-base
-Description: documentation for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides the system's documentation.
--- a/debian.hardy/rules	Tue Aug 18 09:25:44 2009 +0200
+++ b/debian.hardy/rules	Fri Aug 21 16:26:20 2009 +0200
@@ -39,13 +39,20 @@
 	# Put all the python library and data in cubicweb-common
 	# and scripts in cubicweb-server
 	dh_install -vi
-	#dh_lintian XXX not before debhelper 7
+	# cwctl in the cubicweb-ctl package
+	rm -f debian/cubicweb-common/usr/share/pyshared/cubicweb/cwctl.py
+	# hercule in the cubicweb-client package
+	rm -f debian/cubicweb-common/usr/share/pyshared/cubicweb/hercule.py
+
 
 	# Remove unittests directory (should be available in cubicweb-dev only)
 	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/server/test
 	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/sobjects/test
 	rm -rf debian/cubicweb-web/usr/lib/${PY_VERSION}/site-packages/cubicweb/web/test
+	rm -rf debian/cubicweb-twisted/usr/lib/${PY_VERSION}/site-packages/cubicweb/etwist/test
+	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/ext/test
 	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/common/test
+	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/entities/test
 
 	# cubes directory must be managed as a valid python module
 	touch debian/cubicweb-common/usr/share/cubicweb/cubes/__init__.py
--- a/debian/changelog	Tue Aug 18 09:25:44 2009 +0200
+++ b/debian/changelog	Fri Aug 21 16:26:20 2009 +0200
@@ -1,3 +1,9 @@
+cubicweb (3.4.4-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Tue, 18 Aug 2009 14:27:08 +0200
+
 cubicweb (3.4.3-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Tue Aug 18 09:25:44 2009 +0200
+++ b/debian/control	Fri Aug 21 16:26:20 2009 +0200
@@ -6,7 +6,7 @@
            Julien Jehannet <julien.jehannet@logilab.fr>,
            Aurélien Campéas <aurelien.campeas@logilab.fr>,
            Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 7), python-dev (>=2.4), python-central (>= 0.5)
+Build-Depends: debhelper (>= 5), python-dev (>=2.4), python-central (>= 0.5)
 Standards-Version: 3.8.0
 Homepage: http://www.cubicweb.org
 XS-Python-Version: >= 2.4, << 2.6
@@ -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.
  .
--- a/devtools/__init__.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/devtools/__init__.py	Fri Aug 21 16:26:20 2009 +0200
@@ -24,14 +24,15 @@
 
 SYSTEM_ENTITIES = schema.SCHEMA_TYPES | set((
     'CWGroup', 'CWUser', 'CWProperty',
-    'State', 'Transition', 'TrInfo',
+    'Workflow', 'State', 'BaseTransition', 'Transition', 'WorkflowTransition',
+    'TrInfo', 'SubWorkflowExitPoint',
     ))
 
 SYSTEM_RELATIONS = schema.META_RTYPES | set((
     # 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',
     # cwproperty
     'for_user',
     # schema definition
--- a/devtools/testlib.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/devtools/testlib.py	Fri Aug 21 16:26:20 2009 +0200
@@ -282,8 +282,7 @@
         if password is None:
             password = login.encode('utf8')
         cursor = self._orig_cnx.cursor(req or self.request())
-        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': password})
         user = rset.get_entity(0, 0)
         cursor.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)'
--- a/doc/book/en/development/testing/index.rst	Tue Aug 18 09:25:44 2009 +0200
+++ b/doc/book/en/development/testing/index.rst	Fri Aug 21 16:26:20 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
 -------------------------
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/test/data/migration/postcreate.py	Fri Aug 21 16:26:20 2009 +0200
@@ -0,0 +1,2 @@
+wf = add_workflow(u'bmk wf', 'Bookmark')
+wf.add_state(u'hop', initial=True)
--- a/entities/test/data/schema.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/entities/test/data/schema.py	Fri Aug 21 16:26:20 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)
--- a/entities/test/unittest_base.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/entities/test/unittest_base.py	Fri Aug 21 16:26:20 2009 +0200
@@ -57,149 +57,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]
@@ -233,7 +90,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'))
@@ -241,23 +97,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(CubicWebTC):
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/test/unittest_wfobjs.py	Fri Aug 21 16:26:20 2009 +0200
@@ -0,0 +1,380 @@
+from cubicweb.devtools.testlib import CubicWebTC
+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(CubicWebTC):
+
+    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(CubicWebTC):
+
+    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(CubicWebTC):
+
+    def setup_database(self):
+        self.member = self.create_user('member')
+
+    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"'),])
+
+
+class WorkflowHooksTC(CubicWebTC):
+
+    def setUp(self):
+        CubicWebTC.setUp(self)
+        self.wf = self.session.user.current_workflow
+        self.session.set_pool()
+        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).eid
+        # 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()
+
+    # 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.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.session)
+        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.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()
--- a/entities/wfobjs.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/entities/wfobjs.py	Fri Aug 21 16:26:20 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,46 +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):
-        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()
 
 
@@ -94,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
@@ -110,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
--- a/entity.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/entity.py	Fri Aug 21 16:26:20 2009 +0200
@@ -66,7 +66,7 @@
     # class attributes that must be set in class definition
     rest_attr = None
     fetch_attrs = None
-    skip_copy_for = ()
+    skip_copy_for = ('in_state',)
     # class attributes set automatically at registration time
     e_schema = None
 
@@ -83,15 +83,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
@@ -152,8 +152,8 @@
                 desttype = rschema.objects(eschema.type)[0]
                 card = rschema.rproperty(eschema, desttype, 'cardinality')[0]
                 if card not in '?1':
-                    self.warning('bad relation %s specified in fetch attrs for %s',
-                                 attr, self.__class__)
+                    cls.warning('bad relation %s specified in fetch attrs for %s',
+                                 attr, cls)
                     selection.pop()
                     restrictions.pop()
                     continue
@@ -379,13 +379,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
@@ -516,14 +509,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 <name>, query the repository
@@ -712,6 +705,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):
--- a/hooks/integrity.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/hooks/integrity.py	Fri Aug 21 16:26:20 2009 +0200
@@ -254,4 +254,4 @@
     def __call__(self):
         user = self.entity
         if 'login' in user.edited_attributes and user.login:
-            entity.login = entity.login.strip()
+            user.login = user.login.strip()
--- a/hooks/metadata.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/hooks/metadata.py	Fri Aug 21 16:26:20 2009 +0200
@@ -22,7 +22,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
 
 
--- a/hooks/security.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/hooks/security.py	Fri Aug 21 16:26:20 2009 +0200
@@ -91,6 +91,9 @@
 
     def __call__(self):
         if self.rtype in BEFORE_ADD_RELATIONS:
+            nocheck = self._cw.transaction_data.get('skip-security', ())
+            if (self.eidfrom, self.rtype, self.eidto) in nocheck:
+                return
             rschema = self._cw.repo.schema[self.rtype]
             rschema.check_perm(self._cw, 'add', self.eidfrom, self.eidto)
 
@@ -101,6 +104,9 @@
 
     def __call__(self):
         if not self.rtype in BEFORE_ADD_RELATIONS:
+            nocheck = self._cw.transaction_data.get('skip-security', ())
+            if (self.eidfrom, self.rtype, self.eidto) in nocheck:
+                return
             rschema = self._cw.repo.schema[self.rtype]
             if self.rtype in ON_COMMIT_ADD_RELATIONS:
                 _CheckRelationPermissionOp(self._cw, action='add',
@@ -116,6 +122,9 @@
     events = ('before_delete_relation',)
 
     def __call__(self):
+        nocheck = self._cw.transaction_data.get('skip-security', ())
+        if (self.eidfrom, self.rtype, self.eidto) in nocheck:
+            return
         self._cw.repo.schema[self.rtype].check_perm(self._cw, 'delete',
                                                        self.eidfrom, self.eidto)
 
--- a/hooks/syncschema.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/hooks/syncschema.py	Fri Aug 21 16:26:20 2009 +0200
@@ -145,7 +145,8 @@
         hook.SingleLastOperation.__init__(self, session)
 
     def commit_event(self):
-        self.session.repo.set_schema(self.session.repo.schema)
+        rebuildinfered = self.session.data.get('rebuild-infered', True)
+        self.session.repo.set_schema(self.repo.schema, rebuildinfered=rebuildinfered)
 
 
 class MemSchemaOperation(hook.Operation):
@@ -727,10 +728,33 @@
         erschema.set_rqlexprs(self.perm, rqlexprs)
 
 
+class MemSchemaSpecializesAdd(MemSchemaOperation):
+
+    def commit_event(self):
+        eschema = self.session.schema.schema_by_eid(self.etypeeid)
+        parenteschema = self.session.schema.schema_by_eid(self.parentetypeeid)
+        eschema._specialized_type = parenteschema.type
+        parenteschema._specialized_by.append(eschema.type)
+
+
+class MemSchemaSpecializesDel(MemSchemaOperation):
+
+    def commit_event(self):
+        try:
+            eschema = self.session.schema.schema_by_eid(self.etypeeid)
+            parenteschema = self.session.schema.schema_by_eid(self.parentetypeeid)
+        except KeyError:
+            # etype removed, nothing to do
+            return
+        eschema._specialized_type = None
+        parenteschema._specialized_by.remove(eschema.type)
+
+
 class SyncSchemaHook(hook.Hook):
     __abstract__ = True
     category = 'syncschema'
 
+
 # CWEType hooks ################################################################
 
 class DelCWETypeHook(SyncSchemaHook):
@@ -760,8 +784,7 @@
 
     def __call__(self):
         # workflow cleanup
-        self._cw.execute('DELETE State X WHERE NOT X state_of Y')
-        self._cw.execute('DELETE Transition X WHERE NOT X transition_of Y')
+        self._cw.execute('DELETE Workflow X WHERE NOT X workflow_of Y')
 
 
 class AfterAddCWETypeHook(DelCWETypeHook):
@@ -1052,7 +1075,6 @@
 
 # permissions synchronization hooks ############################################
 
-
 class AfterAddPermissionHook(SyncSchemaHook):
     """added entity/relation *_permission, need to update schema"""
     __id__ = 'syncaddperm'
@@ -1089,14 +1111,24 @@
             MemSchemaPermRQLExpressionDel(self._cw, perm, self.eidfrom, expr)
 
 
+# specializes synchronization hooks ############################################
 
-class ModifySpecializesHook(SyncSchemaHook):
-    __id__ = 'syncspecializes'
+
+class AfterAddSpecializesHook(SyncSchemaHook):
+    __id__ = 'syncaddspecializes'
     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('specializes')
-    events = ('after_add_relation', 'after_delete_relation')
+    events = ('after_add_relation',)
 
     def __call__(self):
-        # registering a schema operation will trigger a call to
-        # repo.set_schema() on commit which will in turn rebuild
-        # infered relation definitions
-        MemSchemaNotifyChanges(self._cw)
+        MemSchemaSpecializesAdd(session, etypeeid=self.eidfrom,
+                                parentetypeeid=self.eidto)
+
+
+class AfterAddSpecializesHook(SyncSchemaHook):
+    __id__ = 'syncdelspecializes'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('specializes')
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        MemSchemaSpecializesDel(session, etypeeid=self.eidfrom,
+                                parentetypeeid=self.eidto)
--- a/hooks/workflow.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/hooks/workflow.py	Fri Aug 21 16:26:20 2009 +0200
@@ -15,31 +15,13 @@
 from cubicweb.server import hook
 
 
-
-def previous_state(session, eid):
-    """return the state of the entity with the given eid,
-    usually since it's changing in the current transaction. Due to internal
-    relation hooks, the relation may has been deleted at this point, so
-    we have handle that
-    """
-    # don't check eid has been added in the current transaction, we don't want
-    # to miss previous state of entity whose state change in the same
-    # transaction as it's being created
-    pending = session.transaction_data.get('pendingrelations', ())
-    for eidfrom, rtype, eidto in reversed(pending):
-        if rtype == 'in_state' and eidfrom == eid:
-            rset = session.execute('Any S,N WHERE S eid %(x)s, S name N',
-                                   {'x': eidto}, 'x')
-            return rset.get_entity(0, 0)
-    rset = session.execute('Any S,N WHERE X eid %(x)s, X in_state S, S name N',
-                           {'x': eid}, 'x')
-    if rset:
-        return rset.get_entity(0, 0)
-
-
-def relation_deleted(session, eidfrom, rtype, eidto):
-    session.transaction_data.setdefault('pendingrelations', []).append(
-        (eidfrom, rtype, eidto))
+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)
 
 
 class _SetInitialStateOp(hook.Operation):
@@ -50,11 +32,44 @@
         entity = self.entity
         # if there is an initial state and the entity's state is not set,
         # use the initial state as a default state
-        if not session.deleted_in_transaction(entity.eid) 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])
+        pendingeids = session.transaction_data.get('pendingeids', ())
+        if not (session.deleted_in_transaction(entity.eid) or 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)
+
+class _WorkflowChangedOp(hook.Operation):
+    """fix entity current state when changing its workflow"""
+
+    def precommit_event(self):
+        session = self.session
+        if session.deleted_in_transaction(self.eid):
+            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)
+
+
 
 class WorkflowHook(hook.Hook):
     __abstract__ = True
@@ -81,52 +96,82 @@
             (self.eidfrom, self.rtype, self.eidto))
 
 
-class FireTransitionHook(PrepareStateChangeHook):
-    """check the transition is allowed and record transition information"""
+class FireTransitionHook(WorkflowHook):
+    """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
+    """
     __id__ = 'wffiretransition'
-    events = ('before_add_relation',)
+    __select__ = WorkflowHook.__select__ & entity_implements('TrInfo')
+    events = ('before_add_entity',)
 
     def __call__(self):
         session = self._cw
-        eidfrom = self.eidfrom
-        eidto = self.eidto
-        state = previous_state(session, eidfrom)
-        etype = session.describe(eidfrom)[0]
-        if not (session.is_super_session or 'managers' in session.user.groups):
-            if not state is None:
-                entity = session.entity_from_eid(eidfrom)
-                # we should find at least one transition going to this state
-                try:
-                    iter(state.transitions(entity, eidto)).next()
-                except StopIteration:
-                    msg = session._('transition is not allowed')
-                    raise ValidationError(eidfrom, {'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 eidto == isrset[0][0]:
-                    msg = session._('not the initial state for this entity')
-                    raise ValidationError(eidfrom, {'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': eidfrom, 'ds': eidto}
-        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')
+        entity = self.entity
+        # 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))
+
+
+class FiredTransitionHook(WorkflowHook):
+    """change related entity state"""
+    __id__ = 'wffiretransition'
+    __select__ = WorkflowHook.__select__ & entity_implements('TrInfo')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        _change_state(self._cw, self.entity['wf_info_for'],
+                      self.entity['from_state'], self.entity['to_state'])
 
 
 class SetModificationDateOnStateChange(WorkflowHook):
@@ -147,3 +192,36 @@
             # usually occurs if entity is coming from a read-only source
             # (eg ldap user)
             self.warning('cant change modification date for %s: %s', entity, ex)
+
+
+class SetCustomWorkflow(WorkflowHook):
+    __id__ = 'wfsetcustom'
+    __select__ = WorkflowHook.__select__ & hook.match_rtype('custom_workflow')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=self.eidto)
+
+
+class DelCustomWorkflow(SetCustomWorkflow):
+    __id__ = 'wfdelcustom'
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        entity = self._cw.entity_from_eid(self.eidfrom)
+        typewf = entity.cwetype_workflow()
+        if typewf is not None:
+            _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid)
+
+
+
+class DelWorkflowHook(WorkflowHook):
+    __id__ = 'wfdel'
+    __select__ = WorkflowHook.__select__ & entity_implements('Workflow')
+    events = ('after_delete_entity',)
+
+    def __call__(self):
+        # cleanup unused state and transition
+        self._cw.execute('DELETE State X WHERE NOT X state_of Y')
+        self._cw.execute('DELETE Transition X WHERE NOT X transition_of Y')
+
--- a/i18n/en.po	Tue Aug 18 09:25:44 2009 +0200
+++ b/i18n/en.po	Fri Aug 21 16:26:20 2009 +0200
@@ -107,6 +107,10 @@
 msgstr ""
 
 #, python-format
+msgid "%s is not the initial state (%s) for this entity"
+msgstr ""
+
+#, python-format
 msgid "%s not estimated"
 msgstr ""
 
@@ -431,6 +435,9 @@
 msgid "Recipients:"
 msgstr ""
 
+msgid "Registry's content"
+msgstr ""
+
 msgid "Relations"
 msgstr ""
 
@@ -2298,9 +2305,6 @@
 msgid "not selected"
 msgstr ""
 
-msgid "not the initial state for this entity"
-msgstr ""
-
 msgid "nothing to edit"
 msgstr ""
 
@@ -2437,6 +2441,9 @@
 msgid "read_permission_object"
 msgstr "has permission to delete"
 
+msgid "registry"
+msgstr ""
+
 #, python-format
 msgid "relation %(relname)s of %(ent)s"
 msgstr ""
@@ -2830,7 +2837,8 @@
 msgid "toggle check boxes"
 msgstr ""
 
-msgid "transition is not allowed"
+#, python-format
+msgid "transition from %s to %s does not exist or is not allowed"
 msgstr ""
 
 msgid "transition_of"
--- a/i18n/es.po	Tue Aug 18 09:25:44 2009 +0200
+++ b/i18n/es.po	Fri Aug 21 16:26:20 2009 +0200
@@ -112,6 +112,10 @@
 msgstr "%s reporte de errores"
 
 #, python-format
+msgid "%s is not the initial state (%s) for this entity"
+msgstr ""
+
+#, python-format
 msgid "%s not estimated"
 msgstr "%s no estimado(s)"
 
@@ -439,6 +443,9 @@
 msgid "Recipients:"
 msgstr "Destinatarios"
 
+msgid "Registry's content"
+msgstr ""
+
 msgid "Relations"
 msgstr "Relaciones"
 
@@ -2389,9 +2396,6 @@
 msgid "not selected"
 msgstr "no seleccionado"
 
-msgid "not the initial state for this entity"
-msgstr "no el estado inicial para esta entidad"
-
 msgid "nothing to edit"
 msgstr "nada que editar"
 
@@ -2527,6 +2531,9 @@
 msgid "read_permission_object"
 msgstr "Objeto_permiso_lectura"
 
+msgid "registry"
+msgstr ""
+
 #, python-format
 msgid "relation %(relname)s of %(ent)s"
 msgstr "relación %(relname)s de %(ent)s"
@@ -2928,8 +2935,9 @@
 msgid "toggle check boxes"
 msgstr "cambiar valor"
 
-msgid "transition is not allowed"
-msgstr "transition no permitida"
+#, python-format
+msgid "transition from %s to %s does not exist or is not allowed"
+msgstr ""
 
 msgid "transition_of"
 msgstr "transicion de"
@@ -3376,6 +3384,9 @@
 #~ msgid "not specified"
 #~ msgstr "no especificado"
 
+#~ msgid "not the initial state for this entity"
+#~ msgstr "no el estado inicial para esta entidad"
+
 #~ msgid "owned by"
 #~ msgstr "appartient ‡"
 
@@ -3397,6 +3408,9 @@
 #~ msgid "synopsis"
 #~ msgstr "sinopsis"
 
+#~ msgid "transition is not allowed"
+#~ msgstr "transition no permitida"
+
 #~ msgid "wikiid"
 #~ msgstr "identificador wiki"
 
--- a/i18n/fr.po	Tue Aug 18 09:25:44 2009 +0200
+++ b/i18n/fr.po	Fri Aug 21 16:26:20 2009 +0200
@@ -112,6 +112,10 @@
 msgstr "%s rapport d'erreur"
 
 #, python-format
+msgid "%s is not the initial state (%s) for this entity"
+msgstr "%s n'est pas l'état initial (%s) de cette entité"
+
+#, python-format
 msgid "%s not estimated"
 msgstr "%s non estimé(s)"
 
@@ -438,6 +442,9 @@
 msgid "Recipients:"
 msgstr "Destinataires :"
 
+msgid "Registry's content"
+msgstr "Contenu du registre"
+
 msgid "Relations"
 msgstr "Relations"
 
@@ -2394,9 +2401,6 @@
 msgid "not selected"
 msgstr "non sélectionné"
 
-msgid "not the initial state for this entity"
-msgstr "n'est pas l'état initial pour cette entité"
-
 msgid "nothing to edit"
 msgstr "rien à éditer"
 
@@ -2534,6 +2538,9 @@
 msgid "read_permission_object"
 msgstr "a la permission de lire"
 
+msgid "registry"
+msgstr "registre"
+
 #, python-format
 msgid "relation %(relname)s of %(ent)s"
 msgstr "relation %(relname)s de %(ent)s"
@@ -2940,8 +2947,9 @@
 msgid "toggle check boxes"
 msgstr "inverser les cases à cocher"
 
-msgid "transition is not allowed"
-msgstr "transition non permise"
+#, python-format
+msgid "transition from %s to %s does not exist or is not allowed"
+msgstr "la transition de %s à %s n'existe pas ou n'est pas permise"
 
 msgid "transition_of"
 msgstr "transition de"
--- a/interfaces.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/interfaces.py	Fri Aug 21 16:26:20 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
 
--- a/misc/migration/2.42.0_Any.py	Tue Aug 18 09:25:44 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')
--- a/misc/migration/2.42.1_Any.py	Tue Aug 18 09:25:44 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')
-
--- a/misc/migration/2.43.0_Any.py	Tue Aug 18 09:25:44 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')
--- a/misc/migration/2.44.0_Any.py	Tue Aug 18 09:25:44 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)
--- a/misc/migration/2.45.0_Any.py	Tue Aug 18 09:25:44 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')
--- a/misc/migration/2.46.0_Any.py	Tue Aug 18 09:25:44 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')
--- a/misc/migration/2.47.0_Any.py	Tue Aug 18 09:25:44 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')
-
--- a/misc/migration/2.48.8_Any.py	Tue Aug 18 09:25:44 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)
--- a/misc/migration/2.49.3_Any.py	Tue Aug 18 09:25:44 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')
--- a/misc/migration/2.50.0_Any.py	Tue Aug 18 09:25:44 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')
--- a/misc/migration/bootstrapmigration_repository.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/misc/migration/bootstrapmigration_repository.py	Fri Aug 21 16:26:20 2009 +0200
@@ -27,6 +27,37 @@
     reactivate_verification_hooks()
     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'):
@@ -38,37 +69,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()
--- a/misc/migration/postcreate.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/misc/migration/postcreate.py	Fri Aug 21 16:26:20 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)
--- a/req.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/req.py	Fri Aug 21 16:26:20 2009 +0200
@@ -105,6 +105,28 @@
     def set_entity_cache(self, entity):
         pass
 
+    # XXX move to CWEntityManager or even better as factory method (unclear
+    # where yet...)
+    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)
+
     def ensure_ro_rql(self, rql):
         """raise an exception if the given rql is not a select query"""
         first = rql.split(' ', 1)[0].lower()
--- a/schema.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/schema.py	Fri Aug 21 16:26:20 2009 +0200
@@ -16,6 +16,7 @@
 from logilab.common.decorators import cached, clear_cache, monkeypatch
 from logilab.common.logging_ext import set_log_methods
 from logilab.common.deprecation import deprecated
+from logilab.common.graph import get_cycles
 from logilab.common.compat import any
 
 from yams import BadSchemaDefinition, buildobjs as ybo
@@ -59,6 +60,38 @@
 ybo.RTYPE_PROPERTIES += ('eid',)
 ybo.RDEF_PROPERTIES += ('eid',)
 
+
+# XXX same algorithm as in reorder_cubes and probably other place,
+# may probably extract a generic function
+def order_eschemas(eschemas):
+    """return entity schemas ordered such that entity types which specializes an
+    other one appears after that one
+    """
+    graph = {}
+    for eschema in eschemas:
+        if eschema.specializes():
+            graph[eschema] = set((eschema.specializes(),))
+        else:
+            graph[eschema] = set()
+    cycles = get_cycles(graph)
+    if cycles:
+        cycles = '\n'.join(' -> '.join(cycle) for cycle in cycles)
+        raise Exception('cycles in entity schema specialization: %s'
+                        % cycles)
+    eschemas = []
+    while graph:
+        # sorted to get predictable results
+        for eschema, deps in sorted(graph.items()):
+            if not deps:
+                eschemas.append(eschema)
+                del graph[eschema]
+                for deps in graph.itervalues():
+                    try:
+                        deps.remove(eschema)
+                    except KeyError:
+                        continue
+    return eschemas
+
 def bw_normalize_etype(etype):
     if etype in ETYPE_NAME_MAP:
         msg = '%s has been renamed to %s, please update your code' % (
@@ -414,6 +447,7 @@
     reading_from_database = False
     entity_class = CubicWebEntitySchema
     relation_class = CubicWebRelationSchema
+    no_specialization_inference = ('identity',)
 
     def __init__(self, *args, **kwargs):
         self._eid_index = {}
@@ -804,6 +838,7 @@
 PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression)
 
 # workflow extensions #########################################################
+
 from yams.buildobjs import _add_relation as yams_add_relation
 
 class workflowable_definition(ybo.metadefinition):
@@ -812,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
 
--- a/schemas/workflow.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/schemas/workflow.py	Fri Aug 21 16:26:20 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'
--- a/server/__init__.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/__init__.py	Fri Aug 21 16:26:20 2009 +0200
@@ -227,7 +227,8 @@
     for path in reversed(paths):
         mhandler.exec_event_script('pre%s' % event, path)
     # enter instance'schema into the database
-    serialize_schema(mhandler.rqlcursor, schema)
+    mhandler.session.set_pool()
+    serialize_schema(mhandler.session, schema)
     # execute cubicweb's post<event> script
     mhandler.exec_event_script('post%s' % event)
     # execute cubes'post<event> script if any
--- a/server/hook.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/hook.py	Fri Aug 21 16:26:20 2009 +0200
@@ -77,7 +77,7 @@
             if hook.enabled:
                 hook()
             else:
-                warn('[3.5] %s: enabled is deprecated' % cls)
+                warn('[3.6] %s: enabled is deprecated' % cls)
 
 VRegistry.REGISTRY_FACTORY['hooks'] = HooksRegistry
 
@@ -137,14 +137,14 @@
 
     @classproperty
     def __id__(cls):
-        warn('[3.5] %s: please specify an id for your hook' % cls)
+        warn('[3.6] %s: please specify an id for your hook' % cls)
         return str(id(cls))
 
     @classmethod
     def __registered__(cls, vreg):
         super(Hook, cls).__registered__(vreg)
         if getattr(cls, 'accepts', None):
-            warn('[3.5] %s: accepts is deprecated, define proper __select__' % cls)
+            warn('[3.6] %s: accepts is deprecated, define proper __select__' % cls)
             rtypes = []
             for ertype in cls.accepts:
                 if ertype.islower():
@@ -165,7 +165,7 @@
 
     def __call__(self):
         if hasattr(self, 'call'):
-            warn('[3.5] %s: call is deprecated, implements __call__' % self.__class__)
+            warn('[3.6] %s: call is deprecated, implements __call__' % self.__class__)
             if self.event.endswith('_relation'):
                 self.call(self._cw, self.eidfrom, self.rtype, self.eidto)
             elif 'delete' in self.event:
@@ -180,6 +180,85 @@
 set_log_methods(Hook, getLogger('cubicweb.hook'))
 
 
+# base classes for relation propagation ########################################
+
+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)
+
+
 # abstract classes for operation ###############################################
 
 class Operation(object):
@@ -265,22 +344,22 @@
         """
 
     @property
-    @deprecated('[3.5] use self.session.user')
+    @deprecated('[3.6] use self.session.user')
     def user(self):
         return self.session.user
 
     @property
-    @deprecated('[3.5] use self.session.repo')
+    @deprecated('[3.6] use self.session.repo')
     def repo(self):
         return self.session.repo
 
     @property
-    @deprecated('[3.5] use self.session.vreg.schema')
+    @deprecated('[3.6] use self.session.vreg.schema')
     def schema(self):
         return self.session.repo.schema
 
     @property
-    @deprecated('[3.5] use self.session.vreg.config')
+    @deprecated('[3.6] use self.session.vreg.config')
     def config(self):
         return self.session.repo.config
 
@@ -355,3 +434,10 @@
 
     def sendmails(self):
         self.config.sendmails(self.to_send)
+
+
+class RQLPrecommitOperation(Operation):
+    def precommit_event(self):
+        execute = self.session.unsafe_execute
+        for rql in self.rqls:
+            execute(*rql)
--- a/server/hookhelper.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/hookhelper.py	Fri Aug 21 16:26:20 2009 +0200
@@ -12,16 +12,14 @@
 from cubicweb import RepositoryError
 
 
-@deprecated('[3.5] entity_name is deprecated, use entity.name')
+@deprecated('[3.6] entity_name is deprecated, use entity.name')
 def entity_name(session, eid):
     """return the "name" attribute of the entity with the given eid"""
     return session.entity_from_eid(eid).name
 
-@deprecated('[3.5] rproperty is deprecated, use session.schema_rproperty')
+@deprecated('[3.6] rproperty is deprecated, use session.schema_rproperty')
 def rproperty(session, rtype, eidfrom, eidto, rprop):
     return session.rproperty(rtype, eidfrom, eidto, rprop)
 
 from cubicweb.server.hook import SendMailOp
-from cubicweb.hooks.workflow import previous_state
 SendMailOp = class_moved(SendMailOp)
-previous_state = deprecated('[3.5] use cubicweb.hooks.workflow.previous_state')(previous_state)
--- a/server/hooksmanager.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/hooksmanager.py	Fri Aug 21 16:26:20 2009 +0200
@@ -1,4 +1,7 @@
 from logilab.common.deprecation import class_renamed, class_moved
 from cubicweb.server.hook import Hook
 SystemHook = class_renamed('SystemHook', Hook)
+PropagateSubjectRelationHook = class_renamed('PropagateSubjectRelationHook', Hook)
+PropagateSubjectRelationAddHook = class_renamed('PropagateSubjectRelationAddHook', Hook)
+PropagateSubjectRelationDelHook = class_renamed('PropagateSubjectRelationDelHook', Hook)
 Hook = class_moved(Hook)
--- a/server/migractions.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/migractions.py	Fri Aug 21 16:26:20 2009 +0200
@@ -33,7 +33,8 @@
 from yams.schema2sql import eschema2sql, rschema2sql
 
 from cubicweb import AuthenticationError, ETYPE_NAME_MAP
-from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CubicWebRelationSchema
+from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES,
+                             CubicWebRelationSchema, order_eschemas)
 from cubicweb.dbapi import get_repository, repo_connect
 from cubicweb.common.migration import MigrationHelper, yes
 
@@ -60,6 +61,7 @@
             assert repo
             self._cnx = cnx
             self.repo = repo
+            self.session.data['rebuild-infered'] = False
         elif connect:
             self.repo_connect()
         if not schema:
@@ -132,33 +134,36 @@
         os.chmod(backupfile, 0600)
         # backup
         tmpdir = tempfile.mkdtemp(dir=instbkdir)
-        for source in repo.sources:
-            try:
-                source.backup(osp.join(tmpdir,source.uri))
-            except Exception, exc:
-                print '-> error trying to backup [%s]' % exc
-                if not self.confirm('Continue anyway?', default='n'):
-                    raise SystemExit(1)
-        bkup = tarfile.open(backupfile, 'w|gz')
-        for filename in os.listdir(tmpdir):
-            bkup.add(osp.join(tmpdir,filename), filename)
-        bkup.close()
-        shutil.rmtree(tmpdir)
-        # call hooks
-        repo.hm.call_hooks('server_backup', repo=repo, timestamp=timestamp)
-        # done
-        print '-> backup file',  backupfile
+        try:
+            for source in repo.sources:
+                try:
+                    source.backup(osp.join(tmpdir, source.uri))
+                except Exception, exc:
+                    print '-> error trying to backup [%s]' % exc
+                    if not self.confirm('Continue anyway?', default='n'):
+                        raise SystemExit(1)
+                    else:
+                        break
+            else:
+                bkup = tarfile.open(backupfile, 'w|gz')
+                for filename in os.listdir(tmpdir):
+                    bkup.add(osp.join(tmpdir,filename), filename)
+                bkup.close()
+                # call hooks
+                repo.hm.call_hooks('server_backup', repo=repo, timestamp=timestamp)
+                # done
+                print '-> backup file',  backupfile
+        finally:
+            shutil.rmtree(tmpdir)
 
     def restore_database(self, backupfile, drop=True, systemonly=True,
                          askconfirm=True):
-        config = self.config
-        repo = self.repo_connect()
         # check
         if not osp.exists(backupfile):
             raise Exception("Backup file %s doesn't exist" % backupfile)
             return
         if askconfirm and not self.confirm('Restore %s database from %s ?'
-                                           % (config.appid, backupfile)):
+                                           % (self.config.appid, backupfile)):
             return
         # unpack backup
         bkup = tarfile.open(backupfile, 'r|gz')
@@ -169,6 +174,9 @@
         bkup = tarfile.open(backupfile, 'r|gz')
         tmpdir = tempfile.mkdtemp()
         bkup.extractall(path=tmpdir)
+
+        self.config.open_connections_pools = False
+        repo = self.repo_connect()
         for source in repo.sources:
             if systemonly and source.uri != 'system':
                 continue
@@ -181,6 +189,7 @@
         bkup.close()
         shutil.rmtree(tmpdir)
         # call hooks
+        repo.open_connections_pools()
         repo.hm.call_hooks('server_restore', repo=repo, timestamp=backupfile)
         print '-> database restored.'
 
@@ -214,21 +223,13 @@
                     print 'aborting...'
                     sys.exit(0)
             self.session.keep_pool_mode('transaction')
+            self.session.data['rebuild-infered'] = False
             return self._cnx
 
     @property
     def session(self):
         return self.repo._get_session(self.cnx.sessionid)
 
-    @property
-    @cached
-    def rqlcursor(self):
-        """lazy rql cursor"""
-        # should not give session as cnx.cursor(), else we may try to execute
-        # some query while no pool is set on the session (eg on entity attribute
-        # access for instance)
-        return self.cnx.cursor()
-
     def commit(self):
         if hasattr(self, '_cnx'):
             self._cnx.commit()
@@ -262,7 +263,8 @@
     @cached
     def group_mapping(self):
         """cached group mapping"""
-        return ss.group_mapping(self.rqlcursor)
+        self.session.set_pool()
+        return ss.group_mapping(self.session)
 
     def exec_event_script(self, event, cubepath=None, funcname=None,
                           *args, **kwargs):
@@ -415,12 +417,12 @@
                          {'x': str(repoeschema), 'y': str(espschema)})
         self.rqlexecall(ss.updateeschema2rql(eschema),
                         ask_confirm=self.verbosity >= 2)
-        for rschema, targettypes, x in eschema.relation_definitions(True):
-            if x == 'subject':
+        for rschema, targettypes, role in eschema.relation_definitions(True):
+            if role == 'subject':
                 if not rschema in repoeschema.subject_relations():
                     continue
                 subjtypes, objtypes = [etype], targettypes
-            else: # x == 'object'
+            else: # role == 'object'
                 if not rschema in repoeschema.object_relations():
                     continue
                 subjtypes, objtypes = targettypes, [etype]
@@ -526,10 +528,11 @@
             if not rschema in self.repo.schema:
                 self.cmd_add_relation_type(rschema.type)
                 new.add(rschema.type)
-        for eschema in newcubes_schema.entities():
-            if not eschema in self.repo.schema:
-                self.cmd_add_entity_type(eschema.type)
-                new.add(eschema.type)
+        toadd = [eschema for eschema in newcubes_schema.entities()
+                 if not eschema in self.repo.schema]
+        for eschema in order_eschemas(toadd):
+            self.cmd_add_entity_type(eschema.type)
+            new.add(eschema.type)
         # check if attributes has been added to existing entities
         for rschema in newcubes_schema.relations():
             existingschema = self.repo.schema.rschema(rschema.type)
@@ -561,9 +564,11 @@
         for rschema in fsschema.relations():
             if not rschema in removedcubes_schema and rschema in reposchema:
                 self.cmd_drop_relation_type(rschema.type)
-        for eschema in fsschema.entities():
-            if not eschema in removedcubes_schema and eschema in reposchema:
-                self.cmd_drop_entity_type(eschema.type)
+        toremove = [eschema for eschema in fsschema.entities()
+                    if not eschema in removedcubes_schema
+                    and eschema in reposchema]
+        for eschema in reversed(order_eschemas(toremove)):
+            self.cmd_drop_entity_type(eschema.type)
         for rschema in fsschema.relations():
             if rschema in removedcubes_schema and rschema in reposchema:
                 # check if attributes/relations has been added to entities from
@@ -623,11 +628,13 @@
         in auto mode, automatically register entity's relation where the
         targeted type is known
         """
-        applschema = self.repo.schema
-        if etype in applschema:
-            eschema = applschema[etype]
+        instschema = self.repo.schema
+        if etype in instschema:
+            # XXX (syt) plz explain: if we're adding an entity type, it should
+            # not be there...
+            eschema = instschema[etype]
             if eschema.is_final():
-                applschema.del_entity_type(etype)
+                instschema.del_entity_type(etype)
         else:
             eschema = self.fs_schema.eschema(etype)
         confirm = self.verbosity >= 2
@@ -643,13 +650,46 @@
             # ignore those meta relations, they will be automatically added
             if rschema.type in META_RTYPES:
                 continue
-            if not rschema.type in applschema:
+            if not rschema.type in instschema:
                 # need to add the relation type and to commit to get it
                 # actually in the schema
                 self.cmd_add_relation_type(rschema.type, False, commit=True)
             # register relation definition
             self.rqlexecall(ss.rdef2rql(rschema, etype, attrschema.type),
                             ask_confirm=confirm)
+        # take care to newly introduced base class
+        # XXX some part of this should probably be under the "if auto" block
+        for spschema in eschema.specialized_by(recursive=False):
+            try:
+                instspschema = instschema[spschema]
+            except KeyError:
+                # specialized entity type not in schema, ignore
+                continue
+            if instspschema.specializes() != eschema:
+                self.rqlexec('SET D specializes P WHERE D eid %(d)s, P name %(pn)s',
+                              {'d': instspschema.eid,
+                               'pn': eschema.type}, ask_confirm=confirm)
+                for rschema, tschemas, role in spschema.relation_definitions(True):
+                    for tschema in tschemas:
+                        if not tschema in instschema:
+                            continue
+                        if role == 'subject':
+                            subjschema = spschema
+                            objschema = tschema
+                            if rschema.final and instspschema.has_subject_relation(rschema):
+                                # attribute already set, has_rdef would check if
+                                # it's of the same type, we don't want this so
+                                # simply skip here
+                                continue
+                        elif role == 'object':
+                            subjschema = tschema
+                            objschema = spschema
+                        if (rschema.rproperty(subjschema, objschema, 'infered')
+                            or (instschema.has_relation(rschema) and
+                                instschema[rschema].has_rdef(subjschema, objschema))):
+                                continue
+                        self.cmd_add_relation_definition(
+                            subjschema.type, rschema.type, objschema.type)
         if auto:
             # we have commit here to get relation types actually in the schema
             self.commit()
@@ -659,12 +699,12 @@
                 # 'owned_by'/'created_by' will be automatically added
                 if rschema.final or rschema.type in META_RTYPES:
                     continue
-                rtypeadded = rschema.type in applschema
+                rtypeadded = rschema.type in instschema
                 for targetschema in rschema.objects(etype):
                     # ignore relations where the targeted type is not in the
                     # current instance schema
                     targettype = targetschema.type
-                    if not targettype in applschema and targettype != etype:
+                    if not targettype in instschema and targettype != etype:
                         continue
                     if not rtypeadded:
                         # need to add the relation type and to commit to get it
@@ -679,14 +719,14 @@
                     self.rqlexecall(ss.rdef2rql(rschema, etype, targettype),
                                     ask_confirm=confirm)
             for rschema in eschema.object_relations():
-                rtypeadded = rschema.type in applschema or rschema.type in added
+                rtypeadded = rschema.type in instschema or rschema.type in added
                 for targetschema in rschema.subjects(etype):
                     # ignore relations where the targeted type is not in the
                     # current instance schema
                     targettype = targetschema.type
                     # don't check targettype != etype since in this case the
                     # relation has already been added as a subject relation
-                    if not targettype in applschema:
+                    if not targettype in instschema:
                         continue
                     if not rtypeadded:
                         # need to add the relation type and to commit to get it
@@ -914,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()
 
@@ -1002,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
@@ -1055,6 +1089,7 @@
         if not isinstance(rql, (tuple, list)):
             rql = ( (rql, kwargs), )
         res = None
+        self.session.set_pool()
         for rql, kwargs in rql:
             if kwargs:
                 msg = '%s (%s)' % (rql, kwargs)
@@ -1062,7 +1097,7 @@
                 msg = rql
             if not ask_confirm or self.confirm('execute rql: %s ?' % msg):
                 try:
-                    res = self.rqlcursor.execute(rql, kwargs, cachekey)
+                    res = self.session.execute(rql, kwargs, cachekey)
                 except Exception, ex:
                     if self.confirm('error: %s\nabort?' % ex):
                         raise
@@ -1151,8 +1186,9 @@
         if self.ask_confirm:
             if not self._h.confirm('execute rql: %s ?' % msg):
                 raise StopIteration
+        self._h.session.set_pool()
         try:
-            rset = self._h.rqlcursor.execute(rql, kwargs)
+            rset = self._h.session.execute(rql, kwargs)
         except Exception, ex:
             if self._h.confirm('error: %s\nabort?' % ex):
                 raise
--- a/server/repository.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/repository.py	Fri Aug 21 16:26:20 2009 +0200
@@ -167,6 +167,11 @@
         # cache (extid, source uri) -> eid
         self._extid_cache = {}
         # open some connections pools
+        if config.open_connections_pools:
+            self.open_connections_pools()
+
+    def open_connections_pools(self):
+        config = self.config
         self._available_pools = Queue.Queue()
         self._available_pools.put_nowait(pool.ConnectionsPool(self.sources))
         if config.read_instance_schema:
@@ -176,7 +181,7 @@
             # usually during repository creation
             self.warning("set fs instance'schema as bootstrap schema")
             config.bootstrap_cubes()
-            self.set_schema(self.config.load_schema(), resetvreg=False)
+            self.set_schema(config.load_schema(), resetvreg=False)
             # need to load the Any and CWUser entity types
             self.vreg.schema = self.schema
             etdirectory = join(CW_SOFTWARE_ROOT, 'entities')
@@ -185,11 +190,13 @@
                                 '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")
             config.bootstrap_cubes()
-            self.set_schema(self.config.load_schema())
+            self.set_schema(config.load_schema())
         if not config.creating:
             if 'CWProperty' in self.schema:
                 self.vreg.init_properties(self.properties())
@@ -213,13 +220,12 @@
             self.pools.append(pool.ConnectionsPool(self.sources))
             self._available_pools.put_nowait(self.pools[-1])
         self._shutting_down = False
-        self.hm = vreg['hooks']
+        self.hm = self.vreg['hooks']
         if not (config.creating or config.repairing):
             # call instance level initialisation hooks
             self.hm.call_hooks('server_startup', repo=self)
             # register a task to cleanup expired session
-            self.looping_task(self.config['session-time']/3.,
-                              self.clean_sessions)
+            self.looping_task(config['session-time']/3., self.clean_sessions)
 
     # internals ###############################################################
 
@@ -227,8 +233,9 @@
         source_config['uri'] = uri
         return sources.get_source(source_config, self.schema, self)
 
-    def set_schema(self, schema, resetvreg=True):
-        schema.rebuild_infered_relations()
+    def set_schema(self, schema, resetvreg=True, rebuildinfered=True):
+        if rebuildinfered:
+            schema.rebuild_infered_relations()
         self.info('set schema %s %#x', schema.name, id(schema))
         self.debug(', '.join(sorted(str(e) for e in schema.entities())))
         self.querier.set_schema(schema)
@@ -951,15 +958,16 @@
             print 'ADD entity', etype, entity.eid, dict(entity)
         entity._is_saved = False # entity has an eid but is not yet saved
         relations = []
-        # if inlined relations are specified, fill entity's related cache to
-        # avoid unnecessary queries
+        # init edited_attributes before calling before_add_entity hooks
         entity.edited_attributes = set(entity)
-        for attr in entity.edited_attributes:
+        if source.should_call_hooks:
+            self.hm.call_hooks('before_add_entity', session, entity=entity)
+        # XXX use entity.keys here since edited_attributes is not updated for
+        # inline relations
+        for attr in entity.keys():
             rschema = eschema.subject_relation(attr)
             if not rschema.is_final(): # inlined relation
                 relations.append((attr, entity[attr]))
-        if source.should_call_hooks:
-            self.hm.call_hooks('before_add_entity', session, entity=entity)
         entity.set_defaults()
         entity.check(creation=True)
         source.add_entity(session, entity)
--- a/server/schemaserial.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/schemaserial.py	Fri Aug 21 16:26:20 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
--- a/server/serverconfig.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/serverconfig.py	Fri Aug 21 16:26:20 2009 +0200
@@ -178,6 +178,10 @@
           }),
         ) + CubicWebConfiguration.options)
 
+    # should we open connections pools (eg connect to sources). This is usually
+    # necessary...
+    open_connections_pools = True
+
     # read the schema from the database
     read_instance_schema = True
     bootstrap_schema = True
--- a/server/session.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/session.py	Fri Aug 21 16:26:20 2009 +0200
@@ -75,16 +75,43 @@
         return '<%ssession %s (%s 0x%x)>' % (self.cnxtype, self.user.login,
                                              self.id, id(self))
 
-    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:
@@ -563,7 +590,7 @@
     # deprecated ###############################################################
 
     @property
-    @deprecated("[3.5] use session.vreg.schema")
+    @deprecated("[3.6] use session.vreg.schema")
     def schema(self):
         return self.repo.schema
 
--- a/server/sources/native.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/sources/native.py	Fri Aug 21 16:26:20 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"
@@ -216,23 +214,16 @@
 
     def restore(self, backupfile, drop):
         """method called to restore a backup of source's data"""
-        self.close_pool_connections()
+        if self.repo.config.open_connections_pools:
+            self.close_pool_connections()
         try:
             self.restore_from_file(backupfile, drop)
         finally:
-            self.open_pool_connections()
+            if self.repo.config.open_connections_pools:
+                self.open_pool_connections()
 
     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
@@ -547,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
--- a/server/sources/rql2sql.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/sources/rql2sql.py	Fri Aug 21 16:26:20 2009 +0200
@@ -1146,7 +1146,9 @@
             key = table
         if key in self._state.tables:
             return
-        self._state.tables[key] = (len(self._state.actual_tables) - 1, table)
+        if scope == -1:
+            scope = len(self._state.actual_tables) - 1
+        self._state.tables[key] = (scope, table)
         self._state.actual_tables[scope].append(table)
 
     def replace_tables_by_outer_join(self, substitute, lefttable, *tables):
--- a/server/ssplanner.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/ssplanner.py	Fri Aug 21 16:26:20 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
--- a/server/test/data/migratedapp/schema.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/data/migratedapp/schema.py	Fri Aug 21 16:26:20 2009 +0200
@@ -33,7 +33,15 @@
         'delete': ('managers', RRQLExpression('O owned_by U')),
         }
 
-class Note(EntityType):
+class Para(EntityType):
+    para = String(maxsize=512)
+    newattr = String()
+    newinlined = SubjectRelation('Affaire', cardinality='?*', inlined=True)
+    newnotinlined = SubjectRelation('Affaire', cardinality='?*')
+
+class Note(Para):
+    __specializes_schema__ = True
+
     permissions = {'read':   ('managers', 'users', 'guests',),
                    'update': ('managers', 'owners',),
                    'delete': ('managers', ),
@@ -46,11 +54,14 @@
     type = String(maxsize=1)
     whatever = Int()
     mydate = Date(default='TODAY')
-    para = String(maxsize=512)
     shortpara = String(maxsize=64)
     ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
     attachment = SubjectRelation(('File', 'Image'))
 
+class Text(Para):
+    __specializes_schema__ = True
+    summary = String(maxsize=512)
+
 class ecrit_par(RelationType):
     permissions = {'read':   ('managers', 'users', 'guests',),
                    'delete': ('managers', ),
@@ -93,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'),
@@ -112,7 +123,6 @@
     cp   = String(maxsize=12)
     ville= String(maxsize=32)
 
-    in_state = SubjectRelation('State', cardinality='?*')
 
 class evaluee(RelationDefinition):
     subject = ('Personne', 'CWUser', 'Societe')
--- a/server/test/data/schema.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/data/schema.py	Fri Aug 21 16:26:20 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'
--- a/server/test/unittest_hookhelper.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_hookhelper.py	Fri Aug 21 16:26:20 2009 +0200
@@ -60,38 +60,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()
-        self.assertEquals([], self.session.pending_operations)
-
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_hooks.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_hooks.py	Fri Aug 21 16:26:20 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').eid
-        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,41 @@
         self.assertEquals(entity.descr, u'R&amp;D<p>yo</p>')
 
 
+    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(CubicWebTC):
 
@@ -482,169 +505,5 @@
                      'RT name "prenom", E name "Personne"')
         self.commit()
 
-
-class WorkflowHooksTC(CubicWebTC):
-
-    def setup_database(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"')
-
-    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):
-        self.login('stduser')
-        self.assertRaises(ValidationError, self.execute,
-                          'INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, '
-                          'X in_state S WHERE S name "deactivated"', {'pwd': 'oops'})
-        # though managers can do whatever he want
-        self.restore_connection()
-        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.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.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.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.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').eid
-        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()
--- a/server/test/unittest_ldapuser.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_ldapuser.py	Fri Aug 21 16:26:20 2009 +0200
@@ -151,7 +151,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.sexecute('CWUser X WHERE X login "alf"').get_entity(0, 0)
@@ -167,7 +168,8 @@
         finally:
             # restore db state
             self.restore_connection()
-            self.sexecute('SET X in_state S WHERE X login "alf", S name "activated"')
+            alf = self.sexecute('Any X WHERE X login "alf"').get_entity(0, 0)
+            alf.fire_transition('activate')
             self.sexecute('DELETE X in_group G WHERE X login "syt", G name "managers"')
 
     def test_same_column_names(self):
--- a/server/test/unittest_migractions.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_migractions.py	Fri Aug 21 16:26:20 2009 +0200
@@ -113,23 +113,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)
@@ -167,8 +158,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)
@@ -265,7 +257,7 @@
                                               fulltextindexed=False)
 
     def test_sync_schema_props_perms(self):
-        cursor = self.mh.rqlcursor
+        cursor = self.mh.session
         nbrqlexpr_start = len(cursor.execute('RQLExpression X'))
         migrschema['titre']._rproperties[('Personne', 'String')]['order'] = 7
         migrschema['adel']._rproperties[('Personne', 'String')]['order'] = 6
@@ -344,14 +336,14 @@
 
     def _erqlexpr_rset(self, action, ertype):
         rql = 'RQLExpression X WHERE ET is CWEType, ET %s_permission X, ET name %%(name)s' % action
-        return self.mh.rqlcursor.execute(rql, {'name': ertype})
+        return self.mh.session.execute(rql, {'name': ertype})
     def _erqlexpr_entity(self, action, ertype):
         rset = self._erqlexpr_rset(action, ertype)
         self.assertEquals(len(rset), 1)
         return rset.get_entity(0, 0)
     def _rrqlexpr_rset(self, action, ertype):
         rql = 'RQLExpression X WHERE ET is CWRType, ET %s_permission X, ET name %%(name)s' % action
-        return self.mh.rqlcursor.execute(rql, {'name': ertype})
+        return self.mh.session.execute(rql, {'name': ertype})
     def _rrqlexpr_entity(self, action, ertype):
         rset = self._rrqlexpr_rset(action, ertype)
         self.assertEquals(len(rset), 1)
@@ -464,5 +456,47 @@
         user.clear_related_cache('in_state', 'subject')
         self.assertEquals(user.state, 'deactivated')
 
+    def test_introduce_base_class(self):
+        self.mh.cmd_add_entity_type('Para')
+        self.mh.repo.schema.rebuild_infered_relations()
+        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+                          ['Note'])
+        self.assertEquals(self.schema['Note'].specializes().type, 'Para')
+        self.mh.cmd_add_entity_type('Text')
+        self.mh.repo.schema.rebuild_infered_relations()
+        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+                          ['Note', 'Text'])
+        self.assertEquals(self.schema['Text'].specializes().type, 'Para')
+        # test columns have been actually added
+        text = self.execute('INSERT Text X: X para "hip", X summary "hop", X newattr "momo"').get_entity(0, 0)
+        note = self.execute('INSERT Note X: X para "hip", X shortpara "hop", X newattr "momo"').get_entity(0, 0)
+        aff = self.execute('INSERT Affaire X').get_entity(0, 0)
+        self.failUnless(self.execute('SET X newnotinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': text.eid, 'y': aff.eid}, 'x'))
+        self.failUnless(self.execute('SET X newnotinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': note.eid, 'y': aff.eid}, 'x'))
+        self.failUnless(self.execute('SET X newinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': text.eid, 'y': aff.eid}, 'x'))
+        self.failUnless(self.execute('SET X newinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': note.eid, 'y': aff.eid}, 'x'))
+        # XXX remove specializes by ourselves, else tearDown fails when removing
+        # Para because of Note inheritance. This could be fixed by putting the
+        # MemSchemaCWETypeDel(session, name) operation in the
+        # after_delete_entity(CWEType) hook, since in that case the MemSchemaSpecializesDel
+        # operation would be removed before, but I'm not sure this is a desired behaviour.
+        #
+        # also we need more tests about introducing/removing base classes or
+        # specialization relationship...
+        self.session.data['rebuild-infered'] = True
+        try:
+            self.execute('DELETE X specializes Y WHERE Y name "Para"')
+            self.commit()
+        finally:
+            self.session.data['rebuild-infered'] = False
+        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+                          [])
+        self.assertEquals(self.schema['Note'].specializes(), None)
+        self.assertEquals(self.schema['Text'].specializes(), None)
+
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_msplanner.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_msplanner.py	Fri Aug 21 16:26:20 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',
--- a/server/test/unittest_multisources.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_multisources.py	Fri Aug 21 16:26:20 2009 +0200
@@ -61,7 +61,7 @@
         cu = cnx2.cursor()
         self.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"')
-        self.aff1 = cu.execute('INSERT Affaire X: X ref "AFFREF", X in_state S WHERE S name "pitetre"')[0][0]
+        self.aff1 = cu.execute('INSERT Affaire X: X ref "AFFREF"')[0][0]
         cnx2.commit()
         # trigger discovery
         self.sexecute('Card X')
@@ -124,7 +124,7 @@
         cu = cnx2.cursor()
         assert cu.execute('Any X WHERE X eid %(x)s', {'x': self.aff1}, 'x')
         cu.execute('SET X ref "BLAH" WHERE X eid %(x)s', {'x': self.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
@@ -276,6 +276,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.sexecute('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D',
                             {'x': treid})
--- a/server/test/unittest_querier.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_querier.py	Fri Aug 21 16:26:20 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):
--- a/server/test/unittest_repository.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_repository.py	Fri Aug 21 16:26:20 2009 +0200
@@ -89,7 +89,7 @@
     def test_login_upassword_accent(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, self.admpassword)
-        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)
@@ -99,7 +99,7 @@
         repo = self.repo
         cnxid = repo.connect(self.admlogin, self.admpassword)
         # 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"')
@@ -177,16 +177,13 @@
         repo = self.repo
         cnxid = repo.connect(self.admlogin, self.admpassword)
         # 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')
@@ -329,6 +326,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(CubicWebTC):
 
@@ -471,11 +484,11 @@
 
     def test_after_add_inline(self):
         """make sure after_<event>_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_<event>_relation hooks are called directly"""
--- a/server/test/unittest_rql2sql.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_rql2sql.py	Fri Aug 21 16:26:20 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,14 +379,15 @@
 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'''),
 
-#    ('Any XN WHERE X name XN GROUPBY XN',
-#     ''''''),
-#    ('Any XN, COUNT(X) WHERE X name XN GROUPBY XN',
-#     ''''''),
-
-    # DISTINCT, can use relatin under exists scope as principal
+    # DISTINCT, can use relation under exists scope as principal
     ('DISTINCT Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), EXISTS(X read_permission Y)',
      '''SELECT DISTINCT X.cw_eid, rel_read_permission0.eid_to
 FROM cw_CWEType AS X, read_permission_relation AS rel_read_permission0
@@ -467,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
@@ -503,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;',
@@ -760,14 +773,10 @@
     ('Any X,S WHERE X travaille S?',
      '''SELECT X.cw_eid, rel_travaille0.eid_to
 FROM cw_Personne AS X LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_from=X.cw_eid)'''
-#SELECT X.cw_eid, S.cw_eid
-#FROM cw_Personne AS X LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_from=X.cw_eid) LEFT OUTER JOIN cw_Societe AS S ON (rel_travaille0.eid_to=S.cw_eid)'''
     ),
     ('Any S,X WHERE X? travaille S, S is Societe',
      '''SELECT S.cw_eid, rel_travaille0.eid_from
 FROM cw_Societe AS S LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_to=S.cw_eid)'''
-#SELECT S.cw_eid, X.cw_eid
-#FROM cw_Societe AS S LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_to=S.cw_eid) LEFT OUTER JOIN cw_Personne AS X ON (rel_travaille0.eid_from=X.cw_eid)'''
     ),
 
     ('Any N,A WHERE N inline1 A?',
@@ -803,8 +812,6 @@
     ('Any C,M WHERE C travaille G?, G evaluee M?, G is Societe',
      '''SELECT C.cw_eid, rel_evaluee1.eid_to
 FROM cw_Personne AS C LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_from=C.cw_eid) LEFT OUTER JOIN cw_Societe AS G ON (rel_travaille0.eid_to=G.cw_eid) LEFT OUTER JOIN evaluee_relation AS rel_evaluee1 ON (rel_evaluee1.eid_from=G.cw_eid)'''
-#SELECT C.cw_eid, M.cw_eid
-#FROM cw_Personne AS C LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_from=C.cw_eid) LEFT OUTER JOIN cw_Societe AS G ON (rel_travaille0.eid_to=G.cw_eid) LEFT OUTER JOIN evaluee_relation AS rel_evaluee1 ON (rel_evaluee1.eid_from=G.cw_eid) LEFT OUTER JOIN cw_Note AS M ON (rel_evaluee1.eid_to=M.cw_eid)'''
      ),
 
     ('Any A,C WHERE A documented_by C?, (C is NULL) OR (EXISTS(C require_permission F, '
@@ -817,9 +824,6 @@
      '''SELECT X.cw_eid
 FROM cw_Personne AS X LEFT OUTER JOIN connait_relation AS rel_connait0 ON (rel_connait0.eid_to=12)
 WHERE X.cw_eid=12'''
-#SELECT 12
-#FROM cw_Personne AS X LEFT OUTER JOIN connait_relation AS rel_connait0 ON (rel_connait0.eid_to=12) LEFT OUTER JOIN Personne AS P ON (rel_connait0.eid_from=P.cw_eid)
-#WHERE X.cw_eid=12'''
     ),
 
     ('Any GN, TN ORDERBY GN WHERE T tags G?, T name TN, G name GN',
@@ -880,14 +884,18 @@
      '''
 SELECT T.cw_eid, _T0.C0, _T0.C1
 FROM cw_Tag AS T LEFT OUTER JOIN tags_relation AS rel_tags0 ON (rel_tags0.eid_from=T.cw_eid) LEFT OUTER JOIN (SELECT G.cw_eid AS C0, S.cw_eid AS C1
-FROM cw_Affaire AS G LEFT OUTER JOIN cw_State AS S ON (G.cw_in_state=S.cw_eid AND S.cw_name=hop) 
+FROM cw_Affaire AS G LEFT OUTER JOIN cw_State AS S ON (G.cw_in_state=S.cw_eid AND S.cw_name=hop)
 UNION ALL
 SELECT G.cw_eid AS C0, S.cw_eid AS C1
-FROM cw_CWUser AS G LEFT OUTER JOIN cw_State AS S ON (G.cw_in_state=S.cw_eid AND S.cw_name=hop) 
+FROM cw_CWUser AS G LEFT OUTER JOIN cw_State AS S ON (G.cw_in_state=S.cw_eid AND S.cw_name=hop)
 UNION ALL
 SELECT G.cw_eid AS C0, S.cw_eid AS C1
 FROM cw_Note AS G LEFT OUTER JOIN cw_State AS S ON (G.cw_in_state=S.cw_eid AND S.cw_name=hop) ) AS _T0 ON (rel_tags0.eid_to=_T0.C0)'''),
 
+    ('Any O,AD  WHERE NOT S inline1 O, S eid 123, O todo_by AD?',
+     '''SELECT O.cw_eid, rel_todo_by0.eid_to
+FROM cw_Affaire AS O LEFT OUTER JOIN todo_by_relation AS rel_todo_by0 ON (rel_todo_by0.eid_from=O.cw_eid), cw_Note AS S
+WHERE NOT EXISTS(SELECT 1 WHERE S.cw_inline1=O.cw_eid) AND S.cw_eid=123''')
     ]
 
 VIRTUAL_VARS = [
@@ -949,10 +957,6 @@
     ("Any COUNT(P) WHERE P is Personne",
      '''SELECT COUNT(P.cw_eid)
 FROM cw_Personne AS P'''),
-##     ("Personne X where X nom upper('TOTO')",
-##      '''SELECT X.cw_eid\nFROM cw_Personne AS X\nWHERE UPPER(X.cw_nom) = TOTO'''),
-##     ("Personne X where X nom Y, UPPER(X) prenom upper(Y)",
-##      '''SELECT X.cw_eid\nFROM cw_Personne AS X\nWHERE UPPER(X.cw_prenom) = UPPER(X.cw_nom)'''),
     ]
 
 SYMETRIC = [
@@ -960,13 +964,6 @@
      '''SELECT DISTINCT P.cw_eid
 FROM connait_relation AS rel_connait0, cw_Personne AS P
 WHERE (rel_connait0.eid_from=0 AND rel_connait0.eid_to=P.cw_eid OR rel_connait0.eid_to=0 AND rel_connait0.eid_from=P.cw_eid)'''
-#      '''SELECT rel_connait0.eid_to
-# FROM connait_relation AS rel_connait0
-# WHERE rel_connait0.eid_from=0
-# UNION
-# SELECT rel_connait0.eid_from
-# FROM connait_relation AS rel_connait0
-# WHERE rel_connait0.eid_to=0'''
      ),
 
     ('Any P WHERE X connait P',
@@ -1050,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
@@ -1127,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):
@@ -1171,7 +1169,7 @@
             r, nargs = self.o.generate(union, args,
                                       varmap=varmap)
             args.update(nargs)
-            self.assertLinesEquals((r % args).strip(), self._norm_sql(sql))
+            self.assertLinesEquals((r % args).strip(), self._norm_sql(sql), striplines=True)
         except Exception, ex:
             if 'r' in locals():
                 try:
@@ -1200,14 +1198,6 @@
                 print sql[0].strip()
             raise
         return
-#         rqlst, solutions = self._prepare(rql)
-#         for i, sol in enumerate(solutions):
-#             try:
-#                 r, args = self.o.generate([(rqlst, sol)])
-#                 self.assertEqual((r.strip(), args), sqls[i])
-#             except Exception, ex:
-#                 print rql
-#                 raise
 
     def test1(self):
         self._checkall('Any count(RDEF) WHERE RDEF relation_type X, X eid %(x)s',
@@ -1408,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
@@ -1420,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
@@ -1572,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
@@ -1584,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
 
@@ -1648,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
@@ -1660,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
--- a/server/test/unittest_rqlrewrite.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_rqlrewrite.py	Fri Aug 21 16:26:20 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,'
--- a/server/test/unittest_schemaserial.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_schemaserial.py	Fri Aug 21 16:26:20 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'))),
--- a/server/test/unittest_security.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_security.py	Fri Aug 21 16:26:20 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.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')
--- a/server/test/unittest_ssplanner.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/server/test/unittest_ssplanner.py	Fri Aug 21 16:26:20 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()
 
 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, [])])
 
--- a/sobjects/supervising.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/sobjects/supervising.py	Fri Aug 21 16:26:20 2009 +0200
@@ -34,27 +34,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].rtype == 'in_state':
+                index[key].remove(change)
     # filter changes
     for eid in added:
         try:
@@ -63,15 +54,10 @@
                 # skip meta-relations which are set automatically
                 # XXX generate list below using rtags (category = 'generated')
                 if changedescr.rtype 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.eidfrom == 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.rtype == 'in_state' and changedescr.eidfrom in added:
-                    index['add_relation'].remove(change)
-
         except KeyError:
             break
     for eid in deleted:
--- a/sobjects/test/unittest_notification.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/sobjects/test/unittest_notification.py	Fri Aug 21 16:26:20 2009 +0200
@@ -9,9 +9,9 @@
 from socket import gethostname
 
 from logilab.common.testlib import unittest_main, TestCase
-from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.devtools.testlib import CubicWebTC, 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):
@@ -70,17 +70,14 @@
 class StatusChangeViewsTC(CubicWebTC):
 
     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,
+        req = self.session()
+        u = self.create_user('toto', req=req, commit=False)
+        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 <activated> to <deactivated> for entity
 'toto'
@@ -89,7 +86,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()
--- a/sobjects/test/unittest_supervising.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/sobjects/test/unittest_supervising.py	Fri Aug 21 16:26:20 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
--- a/test/unittest_entity.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/test/unittest_entity.py	Fri Aug 21 16:26:20 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,22 +143,26 @@
             # 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')
-            self.assertEquals(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA ASC WHERE X is Personne, X nom AA, X connait AB')
+            self.assertEquals(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA ASC '
+                              'WHERE X is Personne, X nom AA, X connait AB?')
             # testing optional relation
             peschema.subject_relation('travaille').set_rproperty(peschema, seschema, 'cardinality', '?*')
             Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
@@ -321,33 +326,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()
--- a/test/unittest_schema.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/test/unittest_schema.py	Fri Aug 21 16:26:20 2009 +0200
@@ -20,7 +20,7 @@
 
 from cubicweb.schema import CubicWebSchema, CubicWebEntitySchema, \
      RQLConstraint, CubicWebSchemaLoader, ERQLExpression, RRQLExpression, \
-     normalize_expression
+     normalize_expression, order_eschemas
 from cubicweb.devtools import TestServerConfiguration as TestConfiguration
 
 DATADIR = join(dirname(__file__), 'data')
@@ -126,12 +126,18 @@
         expr = RRQLExpression('U has_update_permission O')
         self.assertEquals(str(expr), 'Any O WHERE U has_update_permission O, O eid %(o)s, U eid %(u)s')
 
-
 loader = CubicWebSchemaLoader()
 config = TestConfiguration('data')
 config.bootstrap_cubes()
 
-class SQLSchemaReaderClassTest(TestCase):
+class SchemaReaderClassTest(TestCase):
+
+    def test_order_eschemas(self):
+        schema = loader.load(config)
+        self.assertEquals(order_eschemas([schema['Note'], schema['SubNote']]),
+                                         [schema['Note'], schema['SubNote']])
+        self.assertEquals(order_eschemas([schema['SubNote'], schema['Note']]),
+                                         [schema['Note'], schema['SubNote']])
 
     def test_knownValues_load_schema(self):
         schema = loader.load(config)
@@ -139,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',
@@ -147,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',
@@ -183,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',
 
@@ -191,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',
--- a/utils.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/utils.py	Fri Aug 21 16:26:20 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
--- a/vregistry.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/vregistry.py	Fri Aug 21 16:26:20 2009 +0200
@@ -138,7 +138,7 @@
 
     # dynamic selection methods ################################################
 
-    @deprecated('[3.5] use select instead of object_by_id')
+    @deprecated('[3.6] use select instead of object_by_id')
     def object_by_id(self, oid, *args, **kwargs):
         """return object with the given oid. Only one object is expected to be
         found.
@@ -167,7 +167,7 @@
             return self.select(oid, *args, **kwargs)
         except (NoSelectableObject, ObjectNotFound):
             return None
-    select_object = deprecated('[3.5] use select_or_none instead of select_object'
+    select_object = deprecated('[3.6] use select_or_none instead of select_object'
                                )(select_or_none)
 
     def possible_objects(self, *args, **kwargs):
@@ -210,7 +210,7 @@
                                    [repr(v) for v in winners]))
         # return the result of calling the appobject
         return winners[0](*args, **kwargs)
-    select_best = deprecated('[3.5] select_best is now private')(_select_best)
+    select_best = deprecated('[3.6] select_best is now private')(_select_best)
 
 class VRegistry(dict):
     """class responsible to register, propose and select the various
@@ -223,7 +223,9 @@
         self.config = config
 
     def reset(self, path=None, force_reload=None):
-        self.clear()
+        # don't use self.clear, we want to keep existing subdictionaries
+        for subdict in self.itervalues():
+            subdict.clear()
         self._lastmodifs = {}
 
     def __getitem__(self, name):
--- a/web/data/cubicweb.css	Tue Aug 18 09:25:44 2009 +0200
+++ b/web/data/cubicweb.css	Fri Aug 21 16:26:20 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;
+}
Binary file web/data/pdf_icon.gif has changed
Binary file web/test/data/sample1.pdf has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/sample1.xml	Fri Aug 21 16:26:20 2009 +0200
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" [
+  <!ATTLIST html xmlns:cubicweb CDATA  #FIXED 'http://www.logilab.org/2008/cubicweb'  >
+
+<!ENTITY % coreattrs
+ "id          ID            #IMPLIED
+  class       CDATA         #IMPLIED
+  style       CDATA         #IMPLIED
+  title       CDATA         #IMPLIED
+
+ cubicweb:sortvalue         CDATA   #IMPLIED
+ cubicweb:target            CDATA   #IMPLIED
+ cubicweb:limit             CDATA   #IMPLIED
+ cubicweb:type              CDATA   #IMPLIED
+ cubicweb:loadtype          CDATA   #IMPLIED
+ cubicweb:wdgtype           CDATA   #IMPLIED
+ cubicweb:initfunc          CDATA   #IMPLIED
+ cubicweb:inputid           CDATA   #IMPLIED
+ cubicweb:tindex            CDATA   #IMPLIED
+ cubicweb:inputname         CDATA   #IMPLIED
+ cubicweb:value             CDATA   #IMPLIED
+ cubicweb:required          CDATA   #IMPLIED
+ cubicweb:accesskey         CDATA   #IMPLIED
+ cubicweb:maxlength         CDATA   #IMPLIED
+ cubicweb:variables         CDATA   #IMPLIED
+ cubicweb:displayactions    CDATA   #IMPLIED
+ cubicweb:fallbackvid       CDATA   #IMPLIED
+ cubicweb:fname             CDATA   #IMPLIED
+ cubicweb:vid               CDATA   #IMPLIED
+ cubicweb:rql               CDATA   #IMPLIED
+ cubicweb:actualrql         CDATA   #IMPLIED
+ cubicweb:rooteid           CDATA   #IMPLIED
+ cubicweb:dataurl           CDATA   #IMPLIED
+ cubicweb:size              CDATA   #IMPLIED
+ cubicweb:tlunit            CDATA   #IMPLIED
+ cubicweb:loadurl           CDATA   #IMPLIED
+ cubicweb:uselabel          CDATA   #IMPLIED
+ cubicweb:facetargs         CDATA   #IMPLIED
+ cubicweb:facetName         CDATA   #IMPLIED
+  "> ] >
+
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb" xml:lang="fr" lang="fr">
+<head>
+<base href="http://crater:8888/"></base><meta http-equiv="content-type" content="application/xhtml+xml; charset=UTF-8"/>
+<meta name="ROBOTS" content="NOINDEX" />
+<link rel="shortcut icon" href="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/favicon.ico"/>
+<link rel="alternate" type="application/rss+xml" title="RSS feed" href="http://crater:8888/project/Comet/0.2.0?vid=rss"/>
+<title>Comet 0.2.0 (unset title)</title>
+<script type="text/javascript"><!--//--><![CDATA[//><!--
+pageid = "0499a5d7add13919a458db30006d9832";
+//--><!]]></script>
+<link rel="stylesheet" type="text/css" media="all" href="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubes.tracker.css"/>
+<link rel="stylesheet" type="text/css" media="print" href="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.print.css"/>
+<link rel="stylesheet" type="text/css" media="all" href="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.login.css"/>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/jquery.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/jquery.corner.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/jquery.json.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.compat.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.python.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.htmlhelpers.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.ajax.js"></script>
+<script type="text/javascript" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/cubicweb.bookmarks.js"></script>
+<script type="text/javascript">
+jQuery(document).ready(function () {
+ jQuery("#__login:visible").focus()
+ });
+</script>
+</head>
+
+<body>
+<table id="header"><tr>
+<td id="firstcolumn"><a href="http://crater:8888/"><img class="logo" src="http://crater:8888/data0ea37b3fea72bf4b2fde96c64e51f626/logo.png" alt="logo"/></a></td>
+<td id="headtext"><span id="appliName"><a href="http://crater:8888/">unset title</a></span><span class="pathbar">&#160;&gt;&#160;<a href="http://crater:8888/Project">projets</a>&#160;&gt;&#160;<a href="http://crater:8888/project/Comet" title="">Comet</a>&#160;&gt;&#160;
+0.2.0</span></td><td>
+anonyme&#160;[<a class="logout" href="javascript: popupLoginBox();">s'authentifier</a>]</td><td><a href="http://crater:8888/doc/main" class="help" title="aide">&#160;</a></td><td id="lastcolumn"></td>
+</tr></table>
+<div id="popupLoginBox" class="hidden"><div id="loginContent">
+<form method="post" action="http://crater:8888/project/Comet/0.2.0?vid=statussheet" id="login_form">
+<table>
+<tr>
+<td><label for="__login">identifiant</label></td><td><input name="__login" id="__login" class="data" type="text" /></td></tr><tr>
+<td><label for="__password" >mot de passe</label></td><td><input name="__password" id="__password" class="data" type="password" /></td>
+</tr><tr>
+<td>&#160;</td><td><input type="submit" class="loginButton right" value="s'identifier" />
+</td></tr>
+</table>
+</form>
+</div></div>
+
+  <div id="stateheader">
+  </div>
+  <div id="page"><table width="100%" border="0" id="mainLayout"><tr>
+<td class="navcol"><div class="navboxes">
+<div class="searchBoxFrame" id="search_box"><div class="boxTitle"><span><span onclick="javascript: toggleVisibility('rqlinput')">rechercher</span></span></div><div class="boxContent">
+<form action="http://crater:8888/view">
+<table id="tsearch"><tr><td>
+<input id="norql" type="text" accesskey="q" tabindex="1" title="search text" value="" name="rql" />
+<input type="hidden" name="__fromsearchbox" value="1" />
+<input type="hidden" name="subvid" value="tsearch" />
+</td><td>
+<input tabindex="2" type="submit" id="rqlboxsubmit" class="rqlsubmit" value="" />
+</td></tr></table>
+</form></div>
+<div class="shadow">&#160;</div></div><div class="greyBoxFrame" id="edit_box"><div class="boxTitle"><span>actions - version</span></div><div class="boxContent">
+<ul class="boxListing"><li class="boxMainactions"><a href="http://crater:8888/project/Comet/0.2.0" title="keyword: view">voir</a></li>
+<li class="boxMainactions"><a href="http://crater:8888/project/Comet/0.2.0?vid=edition" title="keyword: edit">modifier</a></li>
+<li class="boxMainactions"><a title="aucune transition possible">état: <i>en cours</i></a></li><li><a href="javascript: toggleVisibility('boxmenu_ajouter')" class="boxMenu">ajouter</a><ul id="boxmenu_ajouter" class="hidden"><li class="boxItem"><a href="http://crater:8888/project/Comet/0.2.0?etype=Ticket&amp;__linkto=done_in%3A789%3Asubject&amp;__redirectvid=statussheet&amp;__redirectpath=project%2FComet%2F0.2.0&amp;vid=creation" title="">ticket</a></li>
+<li class="boxItem"><a href="http://crater:8888/project/Comet/0.2.0?etype=Ticket&amp;__linkto=appeared_in%3A789%3Asubject&amp;__redirectvid=statussheet&amp;__redirectpath=project%2FComet%2F0.2.0&amp;vid=creation" title="">signaler une anomalie</a></li>
+</ul></li><li><a href="javascript: toggleVisibility('boxmenu_plus_dactions')" class="boxMenu">plus d'actions</a><ul id="boxmenu_plus_dactions" class="hidden"><li class="boxMoreactions"><a href="http://crater:8888/project/Comet/0.2.0?vid=security" title="keyword: managepermission">gestion des permissions</a></li>
+<li class="boxMoreactions"><a href="http://crater:8888/project/Comet/0.2.0?vid=deleteconf" title="keyword: delete">supprimer</a></li>
+<li class="boxMoreactions"><a href="http://crater:8888/project/Comet/0.2.0?vid=copy" title="keyword: copy">copier</a></li>
+<li class="boxMoreactions"><a href="http://crater:8888/view?rql=Any%20X%20WHERE%20X%20version_of%20P%2C%20P%20name%20%22Comet%22%2C%20X%20num%20%220.2.0%22%2C%20X%20is%20Version&amp;template=pdf-main-template" title="keyword: pdfexport">export pdf</a></li>
+<li class="boxMoreactions"><a href="http://crater:8888/project/Comet/0.2.0?vid=document" title="keyword: pvrestexport">export ReST</a></li>
+</ul></li></ul>
+</div>
+<div class="shadow">&#160;</div></div><div class="boxFrame" id="bookmarks_box"><div class="boxTitle"><span>signets</span></div><div class="boxContent">
+<ul class="sideBox"><li><a href="javascript: toggleVisibility('boxmenu_gérer_les_signets')" class="boxMenu">gérer les signets</a><ul id="boxmenu_gérer_les_signets" class="hidden"><li class="boxManage"><a href="http://crater:8888/add/Bookmark?__linkto=bookmarked_by%3A5%3Asubject&amp;path=project%2FComet%2F0.2.0%3Fvid%3Dstatussheet" title="keyword: bookmark">poser un signet ici</a></li>
+<li class="boxManage"><a href="http://crater:8888/cwuser/admin?target=subject&amp;vid=xaddrelation&amp;rtype=bookmarked_by" title="">récupérer des signets existants</a></li>
+</ul></li></ul>
+</div>
+<div class="shadow">&#160;</div></div></div></td>
+<td id="contentcol">
+<div id="rqlinput" class="hidden">
+          <form action="http://crater:8888/view">
+<fieldset>
+<input type="text" id="rql" name="rql" value="Any X WHERE X version_of P, P name &quot;Comet&quot;, X num &quot;0.2.0&quot;, X is Version"  title="texte à rechercher ou requête RQL" tabindex="3" accesskey="q" class="searchField" />
+<input type="submit" value="" class="rqlsubmit" tabindex="4" />
+</fieldset>
+</form></div><div id="appMsg" onclick="javascript: toggleVisibility('appMsg')" class="hidden">
+</div><div id="pageContent">
+<div id="contentmain">
+<h2>Fiche de statut</h2><table class="listing"><tr><th rowspan="2">Projets</th><th colspan="2">Version</th><th rowspan="2">Parent</th><th rowspan="2">Tickets ouverts</th><th rowspan="2">Tickets implémentés</th><th rowspan="2">Statut</th></tr><tr><th>actuelle</th><th>ciblée</th></tr><tr><td title=""><a href="http://crater:8888/project/Developper%20manual" title="">Developper manual</a></td><td><a href="http://crater:8888/project/Developper%20manual/0.1.0">0.1.0</a></td><td><a href="http://crater:8888/project/Developper%20manual/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Comet%20documentation" title="">Comet documentation</a></td><td><div title="detail a bit configuration steps"><a href="http://crater:8888/ticket/803">T 803</a></div></td><td></td><td>en cours</td></tr><tr><td title=""><a href="http://crater:8888/project/User%20manual" title="">User manual</a></td><td><a href="http://crater:8888/project/User%20manual/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/User%20manual/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Comet%20documentation" title="">Comet documentation</a></td><td></td><td><div title="write a tutorial"><a href="http://crater:8888/ticket/801">T 801</a></div></td><td>livrée</td></tr><tr><td title=""><a href="http://crater:8888/project/Comet%20documentation" title="">Comet documentation</a></td><td><a href="http://crater:8888/project/Comet%20documentation/0.1.0">0.1.0</a></td><td><a href="http://crater:8888/project/Comet%20documentation/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Comet" title="">Comet</a></td><td></td><td></td><td>en cours</td></tr><tr><td title=""><a href="http://crater:8888/project/Lgc" title="">Lgc</a></td><td><a href="http://crater:8888/project/Lgc/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Lgc/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Tracker" title="">Tracker</a></td><td></td><td><div title="add support for xhtml -&gt; pdf conversion"><a href="http://crater:8888/ticket/793">T 793</a></div></td><td>livrée</td></tr><tr><td title=""><a href="http://crater:8888/project/Tracker" title="">Tracker</a></td><td><a href="http://crater:8888/project/Tracker/0.1.0">0.1.0</a></td><td><a href="http://crater:8888/project/Tracker/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Confman" title="">Confman</a></td><td><div title="extract core from forge cube"><a href="http://crater:8888/ticket/795">T 795</a></div></td><td></td><td>en cours</td></tr><tr><td title=""><a href="http://crater:8888/project/Confman" title="">Confman</a></td><td><a href="http://crater:8888/project/Confman/0.1.0">0.1.0</a></td><td><a href="http://crater:8888/project/Confman/0.2.0">0.2.0</a></td><td><a href="http://crater:8888/project/Comet" title="">Comet</a></td><td><div title="have a version status sheet"><a href="http://crater:8888/ticket/797">T 797</a></div></td><td></td><td>en cours</td></tr></table></div>
+</div>
+</td>
+</tr></table></div>
+<div class="footer"><a href="http://crater:8888/changelog">nouveautés</a> | <a href="http://crater:8888/doc/about">à propos de ce site</a> | © 2001-2009 <a href="http://www.logilab.fr">Logilab S.A.</a></div></body>
+</html>
\ No newline at end of file
--- a/web/test/unittest_form.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/web/test/unittest_form.py	Fri Aug 21 16:26:20 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.vreg['etypes'].etype_class('CWUser')(self.request())
-        form = EntityFieldsForm(e.req, 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(e.req, 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.vreg['etypes'].etype_class('CWUser')(self.request())
@@ -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'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_pdf.py	Fri Aug 21 16:26:20 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)
--- a/web/test/unittest_views_editforms.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/web/test/unittest_views_editforms.py	Fri Aug 21 16:26:20 2009 +0200
@@ -52,6 +52,7 @@
                                ])
         self.assertListEquals(rbc(e, 'generic'),
                               [('primary_email', 'subject'),
+                               ('custom_workflow', 'subject'),
                                ('connait', 'subject'),
                                ('checked_by', 'object'),
                                ])
--- a/web/uicfg.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/web/uicfg.py	Fri Aug 21 16:26:20 2009 +0200
@@ -149,6 +149,7 @@
 # * 'application'
 # * 'system'
 # * 'schema'
+# * 'hidden'
 # * 'subobject' (not displayed by default)
 
 class InitializableDict(dict):
@@ -169,6 +170,7 @@
                                             CWUser='system',
                                             CWGroup='system',
                                             CWPermission='system',
+                                            BaseTransition='hidden',
                                             )
 
 # autoform.AutomaticEntityForm configuration ##################################
--- a/web/views/autoform.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/web/views/autoform.py	Fri Aug 21 16:26:20 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 <attr>_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)
--- a/web/views/basecomponents.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/web/views/basecomponents.py	Fri Aug 21 16:26:20 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'&nbsp;|&nbsp;'.join(html))
         self.w(u'</div>')
 
+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'<a href="%s" class="otherView"><img src="data/pdf_icon.gif"/></a>' %
+               (xml_escape(entity.absolute_url() + '?vid=%s&__template=pdf-main-template' % vid)))
+
+
 
 def registration_callback(vreg):
     vreg.register_all(globals().values(), __name__, (SeeAlsoVComponent,))
--- a/web/views/basetemplates.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/web/views/basetemplates.py	Fri Aug 21 16:26:20 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 ##############################################################
@@ -265,6 +265,44 @@
             self.w(u'</td>\n')
             self.w(u'</tr></table>\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('&nbsp;', '&#160;').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):
@@ -488,4 +526,4 @@
 
 ## vregistry registration callback ############################################
 def registration_callback(vreg):
-    vreg.register_all(globals().values(), modname=__name__)
+    vreg.register_all(globals().values(), __name__)
--- a/web/views/boxes.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/web/views/boxes.py	Fri Aug 21 16:26:20 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')
--- a/web/views/forms.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/web/views/forms.py	Fri Aug 21 16:26:20 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)
--- a/web/views/primary.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/web/views/primary.py	Fri Aug 21 16:26:20 2009 +0200
@@ -230,9 +230,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')
--- a/web/views/workflow.py	Tue Aug 18 09:25:44 2009 +0200
+++ b/web/views/workflow.py	Fri Aug 21 16:26:20 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.rset.get_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'<h4>%s %s</h4>\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'<p>%s</p>\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')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/xhtml2fo.py	Fri Aug 21 16:26:20 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 <div class="contentmain">
+    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 <div class="contentmain"> 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 <div> 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)
+