STABLE IS NOW 3.5 stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 18 Sep 2009 11:24:37 +0200
branchstable
changeset 3315 59220b704562
parent 3298 caef98aa4a98 (current diff)
parent 3314 cfa77453e742 (diff)
child 3317 42ca81d2e805
child 3318 5b47b9f09bca
STABLE IS NOW 3.5
common/test/unittest_mixins.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
server/rqlrewrite.py
server/test/unittest_extlite.py
server/test/unittest_rqlrewrite.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Fri Sep 18 11:24:37 2009 +0200
@@ -0,0 +1,10 @@
+\.svn
+^build$
+^dist$
+\.pyc$
+\.pyo$
+\.bak$
+\.old$
+\~$
+\#.*?\#$
+\.swp$
--- a/.hgtags	Thu Sep 17 19:38:04 2009 +0200
+++ b/.hgtags	Fri Sep 18 11:24:37 2009 +0200
@@ -70,3 +70,7 @@
 24ea70f19a48cce60248ab18695925755009bcb8 cubicweb-debian-version-3.4.9-1
 f3d2adf483320d7726136433a41c57b130cbdc15 cubicweb-version-3.4.11
 635a25031f4abdd89c44d17f5d2b0d0d43914511 cubicweb-debian-version-3.4.11-1
+70c0dd1c3b7d747c3a268396a7f79d9a7a3340e6 cubicweb-version-3.5.0
+7e5d0ae8d2026c77f12ab512a4cde9911dcd8896 cubicweb-debian-version-3.5.0-1
+77ed72f3c2602bf300929f8863447653ce1beb0c cubicweb-version-3.5.1
+f476cecd46904f215bd29249ded8508d8f5634d7 cubicweb-debian-version-3.5.1-1
--- a/__init__.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/__init__.py	Fri Sep 18 11:24:37 2009 +0200
@@ -121,6 +121,27 @@
         raise KeyError
     def set_entity_cache(self, entity):
         pass
+
+    def create_entity(self, etype, *args, **kwargs):
+        """add a new entity of the given type"""
+        rql = 'INSERT %s X' % etype
+        relations = []
+        restrictions = []
+        cachekey = []
+        for rtype, rvar in args:
+            relations.append('X %s %s' % (rtype, rvar))
+            restrictions.append('%s eid %%(%s)s' % (rvar, rvar))
+            cachekey.append(rvar)
+        for attr in kwargs:
+            if attr in cachekey:
+                continue
+            relations.append('X %s %%(%s)s' % (attr, attr))
+        if relations:
+            rql = '%s: %s' % (rql, ', '.join(relations))
+        if restrictions:
+            rql = '%s WHERE %s' % (rql, ', '.join(restrictions))
+        return self.execute(rql, kwargs, cachekey).get_entity(0, 0)
+
     # url generation methods ##################################################
 
     def build_url(self, *args, **kwargs):
@@ -191,18 +212,9 @@
             userinfo['email'] = ""
             return userinfo
         user = self.actual_session().user
-        rql = "Any F,S,A where U eid %(x)s, U firstname F, U surname S, U primary_email E, E address A"
-        try:
-            firstname, lastname, email = self.execute(rql, {'x': user.eid}, 'x')[0]
-            if firstname is None and lastname is None:
-                userinfo['name'] = ''
-            else:
-                userinfo['name'] = ("%s %s" % (firstname, lastname))
-            userinfo['email'] = email
-        except IndexError:
-            userinfo['name'] = None
-            userinfo['email'] = None
         userinfo['login'] = user.login
+        userinfo['name'] = user.name()
+        userinfo['email'] = user.get_email()
         return userinfo
 
     def is_internal_session(self):
--- a/__pkginfo__.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/__pkginfo__.py	Fri Sep 18 11:24:37 2009 +0200
@@ -7,7 +7,7 @@
 distname = "cubicweb"
 modname = "cubicweb"
 
-numversion = (3, 4, 11)
+numversion = (3, 5, 1)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL v2'
--- a/_exceptions.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/_exceptions.py	Fri Sep 18 11:24:37 2009 +0200
@@ -65,9 +65,8 @@
     """no source support an entity type"""
     msg = 'No source supports %r entity\'s type'
 
-class RTypeNotSupportedBySources(RepositoryError, InternalError):
-    """no source support a relation type"""
-    msg = 'No source supports %r relation\'s type'
+class MultiSourcesError(RepositoryError, InternalError):
+    """usually due to bad multisources configuration or rql query"""
 
 
 # security exceptions #########################################################
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/cubicweb-ctl.bat	Fri Sep 18 11:24:37 2009 +0200
@@ -0,0 +1,18 @@
+@echo off
+rem = """-*-Python-*- script
+rem -------------------- DOS section --------------------
+rem You could set PYTHONPATH or TK environment variables here
+python -x "%~f0" %*
+goto exit
+ 
+"""
+# -------------------- Python section --------------------
+from cubicweb.cwctl import run
+import sys
+run(sys.argv[1:])
+
+DosExitLabel = """
+:exit
+rem """
+
+
--- a/common/i18n.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/common/i18n.py	Fri Sep 18 11:24:37 2009 +0200
@@ -25,10 +25,14 @@
     output.close()
 
 
-def add_msg(w, msgid):
+def add_msg(w, msgid, msgctx=None):
     """write an empty pot msgid definition"""
     if isinstance(msgid, unicode):
         msgid = msgid.encode('utf-8')
+    if msgctx:
+        if isinstance(msgctx, unicode):
+            msgctx = msgctx.encode('utf-8')
+        w('msgctxt "%s"\n' % msgctx)
     msgid = msgid.replace('"', r'\"').splitlines()
     if len(msgid) > 1:
         w('msgid ""\n')
@@ -44,9 +48,10 @@
     status != 0
     """
     print cmd.replace(os.getcwd() + os.sep, '')
-    status = os.system(cmd)
+    from subprocess import call
+    status = call(cmd, shell=True)
     if status != 0:
-        raise Exception()
+        raise Exception('status = %s' % status)
 
 
 def available_catalogs(i18ndir=None):
@@ -74,15 +79,15 @@
         mergedpo = join(destdir, '%s_merged.po' % lang)
         try:
             # merge instance/cubes messages catalogs with the stdlib's one
-            execute('msgcat --use-first --sort-output --strict %s > %s'
-                    % (' '.join(pofiles), mergedpo))
-            # make sure the .mo file is writeable and compile with *msgfmt*
+            execute('msgcat --use-first --sort-output --strict -o "%s" %s'
+                    % (mergedpo, ' '.join('"%s"' % f for f in pofiles)))
+            # make sure the .mo file is writeable and compiles with *msgfmt*
             applmo = join(destdir, lang, 'LC_MESSAGES', 'cubicweb.mo')
             try:
                 ensure_fs_mode(applmo)
             except OSError:
                 pass # suppose not exists
-            execute('msgfmt %s -o %s' % (mergedpo, applmo))
+            execute('msgfmt "%s" -o "%s"' % (mergedpo, applmo))
         except Exception, ex:
             errors.append('while handling language %s: %s' % (lang, ex))
         try:
--- a/common/mail.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/common/mail.py	Fri Sep 18 11:24:37 2009 +0200
@@ -142,39 +142,21 @@
 
     msgid_timestamp = True
 
-    def recipients(self):
-        finder = self.vreg['components'].select('recipients_finder', self.req,
-                                                rset=self.rset,
-                                                row=self.row or 0,
-                                                col=self.col or 0)
-        return finder.recipients()
-
-    def subject(self):
-        entity = self.entity(self.row or 0, self.col or 0)
-        subject = self.req._(self.message)
-        etype = entity.dc_type()
-        eid = entity.eid
-        login = self.user_data['login']
-        return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals()
-
-    def context(self, **kwargs):
-        entity = self.entity(self.row or 0, self.col or 0)
-        for key, val in kwargs.iteritems():
-            if val and isinstance(val, unicode) and val.strip():
-               kwargs[key] = self.req._(val)
-        kwargs.update({'user': self.user_data['login'],
-                       'eid': entity.eid,
-                       'etype': entity.dc_type(),
-                       'url': entity.absolute_url(),
-                       'title': entity.dc_long_title(),})
-        return kwargs
+    # this is usually the method to call
+    def render_and_send(self, **kwargs):
+        """generate and send an email message for this view"""
+        delayed = kwargs.pop('delay_to_commit', None)
+        for recipients, msg in self.render_emails(**kwargs):
+            if delayed is None:
+                self.send(recipients, msg)
+            elif delayed:
+                self.send_on_commit(recipients, msg)
+            else:
+                self.send_now(recipients, msg)
 
     def cell_call(self, row, col=0, **kwargs):
         self.w(self.req._(self.content) % self.context(**kwargs))
 
-    def construct_message_id(self, eid):
-        return construct_message_id(self.config.appid, eid, self.msgid_timestamp)
-
     def render_emails(self, **kwargs):
         """generate and send emails for this view (one per recipient)"""
         self._kwargs = kwargs
@@ -222,16 +204,17 @@
         # restore language
         req.set_language(origlang)
 
-    def render_and_send(self, **kwargs):
-        """generate and send an email message for this view"""
-        delayed = kwargs.pop('delay_to_commit', None)
-        for recipients, msg in self.render_emails(**kwargs):
-            if delayed is None:
-                self.send(recipients, msg)
-            elif delayed:
-                self.send_on_commit(recipients, msg)
-            else:
-                self.send_now(recipients, msg)
+    # recipients / email sending ###############################################
+
+    def recipients(self):
+        """return a list of either 2-uple (email, language) or user entity to
+        who this email should be sent
+        """
+        finder = self.vreg['components'].select('recipients_finder', self.req,
+                                                rset=self.rset,
+                                                row=self.row or 0,
+                                                col=self.col or 0)
+        return finder.recipients()
 
     def send_now(self, recipients, msg):
         self.config.sendmails([(msg, recipients)])
@@ -241,6 +224,38 @@
 
     send = send_now
 
+    # email generation helpers #################################################
+
+    def construct_message_id(self, eid):
+        return construct_message_id(self.config.appid, eid, self.msgid_timestamp)
+
+    def format_field(self, attr, value):
+        return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value}
+
+    def format_section(self, attr, value):
+        return '%(attr)s\n%(ul)s\n%(value)s\n' % {
+            'attr': attr, 'ul': '-'*len(attr), 'value': value}
+
+    def subject(self):
+        entity = self.entity(self.row or 0, self.col or 0)
+        subject = self.req._(self.message)
+        etype = entity.dc_type()
+        eid = entity.eid
+        login = self.user_data['login']
+        return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals()
+
+    def context(self, **kwargs):
+        entity = self.entity(self.row or 0, self.col or 0)
+        for key, val in kwargs.iteritems():
+            if val and isinstance(val, unicode) and val.strip():
+               kwargs[key] = self.req._(val)
+        kwargs.update({'user': self.user_data['login'],
+                       'eid': entity.eid,
+                       'etype': entity.dc_type(),
+                       'url': entity.absolute_url(),
+                       'title': entity.dc_long_title(),})
+        return kwargs
+
 
 class SkipEmail(Exception):
     """raise this if you decide to skip an email during its generation"""
--- a/common/migration.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/common/migration.py	Fri Sep 18 11:24:37 2009 +0200
@@ -228,7 +228,10 @@
         else:
             readline.set_completer(Completer(local_ctx).complete)
             readline.parse_and_bind('tab: complete')
-            histfile = os.path.join(os.environ["HOME"], ".eshellhist")
+            home_key = 'HOME'
+            if sys.platform == 'win32':
+                home_key = 'USERPROFILE'
+            histfile = os.path.join(os.environ[home_key], ".eshellhist")
             try:
                 readline.read_history_file(histfile)
             except IOError:
--- a/common/mixins.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/common/mixins.py	Fri Sep 18 11:24:37 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	Thu Sep 17 19:38:04 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-"""
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-from logilab.common.testlib import unittest_main
-from cubicweb.devtools.apptest import EnvBasedTC
-
-class WorkfloableMixInTC(EnvBasedTC):
-    def test_wf_state(self):
-        s = self.add_entity('State', name=u'activated')
-        self.execute('SET X state_of ET WHERE ET name "Bookmark", X eid %(x)s',
-                     {'x': s.eid})
-        es = self.user().wf_state('activated')
-        self.assertEquals(es.state_of[0].name, 'CWUser')
-
-    def test_wf_transition(self):
-        t = self.add_entity('Transition', name=u'deactivate')
-        self.execute('SET X transition_of ET WHERE ET name "Bookmark", X eid %(x)s',
-                     {'x': t.eid})
-        et = self.user().wf_transition('deactivate')
-        self.assertEquals(et.transition_of[0].name, 'CWUser')
-
-    def test_change_state(self):
-        user = self.user()
-        user.change_state(user.wf_state('deactivated').eid)
-        self.assertEquals(user.state, 'deactivated')
-
-if __name__ == '__main__':
-    unittest_main()
--- a/common/test/unittest_uilib.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/common/test/unittest_uilib.py	Fri Sep 18 11:24:37 2009 +0200
@@ -81,47 +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">&#160;</td><td class="tree_cell_1_2">&#160;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_1_1</div></td><td class="tree_cell_1_1">&#160;</td><td class="tree_cell_1_2">&#160;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_2_1</div></td><td class="tree_cell_0_1">&#160;</td><td class="tree_cell_0_2">&#160;</td><td rowspan="2">&#160;</td></tr>
-<tr><td class="tree_cell_1_3">&#160;</td><td class="tree_cell_1_4">&#160;</td><td class="tree_cell_1_3">&#160;</td><td class="tree_cell_1_4">&#160;</td><td class="tree_cell_0_3">&#160;</td><td class="tree_cell_0_4">&#160;</td></tr>
-<tr><td rowspan="2">&#160;</td><td class="tree_cell_2_1">&#160;</td><td class="tree_cell_2_2">&#160;</td><td rowspan="2">&#160;</td><td class="tree_cell_4_1">&#160;</td><td class="tree_cell_4_2">&#160;</td><td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">child_2_2</div></td><td class="tree_cell_1_1">&#160;</td><td class="tree_cell_1_2">&#160;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_1</div></td></tr>
-<tr><td class="tree_cell_2_3">&#160;</td><td class="tree_cell_2_4">&#160;</td><td class="tree_cell_4_3">&#160;</td><td class="tree_cell_4_4">&#160;</td><td class="tree_cell_1_3">&#160;</td><td class="tree_cell_1_4">&#160;</td></tr>
-<tr><td rowspan="2">&#160;</td><td class="tree_cell_2_1">&#160;</td><td class="tree_cell_2_2">&#160;</td><td rowspan="2">&#160;</td><td class="tree_cell_0_1">&#160;</td><td class="tree_cell_0_2">&#160;</td><td rowspan="2">&#160;</td><td class="tree_cell_3_1">&#160;</td><td class="tree_cell_3_2">&#160;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_2</div></td></tr>
-<tr><td class="tree_cell_2_3">&#160;</td><td class="tree_cell_2_4">&#160;</td><td class="tree_cell_0_3">&#160;</td><td class="tree_cell_0_4">&#160;</td><td class="tree_cell_3_3">&#160;</td><td class="tree_cell_3_4">&#160;</td></tr>
-<tr><td rowspan="2">&#160;</td><td class="tree_cell_2_1">&#160;</td><td class="tree_cell_2_2">&#160;</td><td rowspan="2">&#160;</td><td class="tree_cell_0_1">&#160;</td><td class="tree_cell_0_2">&#160;</td><td rowspan="2">&#160;</td><td class="tree_cell_4_1">&#160;</td><td class="tree_cell_4_2">&#160;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_3</div></td></tr>
-<tr><td class="tree_cell_2_3">&#160;</td><td class="tree_cell_2_4">&#160;</td><td class="tree_cell_0_3">&#160;</td><td class="tree_cell_0_4">&#160;</td><td class="tree_cell_4_3">&#160;</td><td class="tree_cell_4_4">&#160;</td></tr>
-<tr><td rowspan="2">&#160;</td><td class="tree_cell_4_1">&#160;</td><td class="tree_cell_4_2">&#160;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_1_2</div></td><td class="tree_cell_5_1">&#160;</td><td class="tree_cell_5_2">&#160;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_2_3</div></td><td class="tree_cell_0_1">&#160;</td><td class="tree_cell_0_2">&#160;</td><td rowspan="2">&#160;</td></tr>
-<tr><td class="tree_cell_4_3">&#160;</td><td class="tree_cell_4_4">&#160;</td><td class="tree_cell_5_3">&#160;</td><td class="tree_cell_5_4">&#160;</td><td class="tree_cell_0_3">&#160;</td><td class="tree_cell_0_4">&#160;</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__':
     unittest_main()
 
--- a/common/uilib.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/common/uilib.py	Fri Sep 18 11:24:37 2009 +0200
@@ -12,7 +12,6 @@
 
 import csv
 import re
-from urllib import quote as urlquote
 from StringIO import StringIO
 
 from logilab.mtconverter import xml_escape, html_unescape
@@ -47,9 +46,9 @@
     if attrtype == 'Time':
         return ustrftime(value, req.property_value('ui.time-format'))
     if attrtype == 'Datetime':
-        if not displaytime:
-            return ustrftime(value, req.property_value('ui.date-format'))
-        return ustrftime(value, req.property_value('ui.datetime-format'))
+        if displaytime:
+            return ustrftime(value, req.property_value('ui.datetime-format'))
+        return ustrftime(value, req.property_value('ui.date-format'))
     if attrtype == 'Boolean':
         if value:
             return req._('yes')
@@ -264,125 +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">&#160;</td>'
-            s += '<td class="tree_cell_%d_1">&#160;</td>' % link_cell
-            s += '<td class="tree_cell_%d_2">&#160;</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">&#160;</td>'
-
-        s += '</tr>\n'
-        if link_line:
-            s += '<tr>'
-            for j, link_cell in enumerate(link_line):
-                s += '<td class="tree_cell_%d_3">&#160;</td>' % link_cell
-                s += '<td class="tree_cell_%d_4">&#160;</td>' % link_cell
-            s += '</tr>\n'
-
-    s += '</table>'
-    return s
-
-
-
 # traceback formatting ########################################################
 
 import traceback
--- a/cwconfig.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/cwconfig.py	Fri Sep 18 11:24:37 2009 +0200
@@ -20,6 +20,7 @@
 from smtplib import SMTP
 from threading import Lock
 from os.path import exists, join, expanduser, abspath, normpath, basename, isdir
+import tempfile
 
 from logilab.common.decorators import cached
 from logilab.common.deprecation import deprecated
@@ -391,7 +392,11 @@
                         'server/serverctl.py', 'hercule.py',
                         'devtools/devctl.py', 'goa/goactl.py'):
             if exists(join(CW_SOFTWARE_ROOT, ctlfile)):
-                load_module_from_file(join(CW_SOFTWARE_ROOT, ctlfile))
+                try:
+                    load_module_from_file(join(CW_SOFTWARE_ROOT, ctlfile))
+                except ImportError, err:
+                    cls.critical('could not import the command provider %s (cause : %s)' %
+                                (ctlfile, err))
                 cls.info('loaded cubicweb-ctl plugin %s', ctlfile)
         for cube in cls.available_cubes():
             pluginfile = join(cls.cube_dir(cube), 'ecplugin.py')
@@ -526,13 +531,13 @@
     if CubicWebNoAppConfiguration.mode == 'test':
         root = os.environ['APYCOT_ROOT']
         REGISTRY_DIR = '%s/etc/cubicweb.d/' % root
-        RUNTIME_DIR = '/tmp/'
+        RUNTIME_DIR = tempfile.gettempdir()
         MIGRATION_DIR = '%s/local/share/cubicweb/migration/' % root
         if not exists(REGISTRY_DIR):
             os.makedirs(REGISTRY_DIR)
     elif CubicWebNoAppConfiguration.mode == 'dev':
         REGISTRY_DIR = expanduser('~/etc/cubicweb.d/')
-        RUNTIME_DIR = '/tmp/'
+        RUNTIME_DIR = tempfile.gettempdir()
         MIGRATION_DIR = join(CW_SOFTWARE_ROOT, 'misc', 'migration')
     else: #mode = 'installed'
         REGISTRY_DIR = '/etc/cubicweb.d/'
@@ -651,7 +656,7 @@
     def default_log_file(self):
         """return default path to the log file of the instance'server"""
         if self.mode == 'dev':
-            basepath = '/tmp/%s-%s' % (basename(self.appid), self.name)
+            basepath = join(tempfile.gettempdir(), '%s-%s' % (basename(self.appid), self.name))
             path = basepath + '.log'
             i = 1
             while exists(path) and i < 100: # arbitrary limit to avoid infinite loop
@@ -794,8 +799,8 @@
         from glob import glob
         yield 'en' # ensure 'en' is yielded even if no .mo found
         for path in glob(join(self.apphome, 'i18n',
-                              '*', 'LC_MESSAGES', 'cubicweb.mo')):
-            lang = path.split(os.sep)[-3]
+                              '*', 'LC_MESSAGES')):
+            lang = path.split(os.sep)[-2]
             if lang != 'en':
                 yield lang
 
@@ -807,7 +812,7 @@
             self.info("loading language %s", language)
             try:
                 tr = translation('cubicweb', path, languages=[language])
-                self.translations[language] = tr.ugettext
+                self.translations[language] = (tr.ugettext, tr.upgettext)
             except (ImportError, AttributeError, IOError):
                 self.exception('localisation support error for language %s',
                                language)
--- a/cwctl.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/cwctl.py	Fri Sep 18 11:24:37 2009 +0200
@@ -5,7 +5,13 @@
 %s"""
 
 import sys
-from os import remove, listdir, system, kill, getpgid, pathsep
+from os import remove, listdir, system, pathsep
+try:
+    from os import kill, getpgid
+except ImportError:
+    def kill(*args): pass
+    def getpgid(): pass
+
 from os.path import exists, join, isfile, isdir
 
 from logilab.common.clcommands import register_commands, pop_arg
@@ -23,7 +29,7 @@
     while nbtry < maxtry:
         try:
             kill(pid, signal.SIGUSR1)
-        except OSError:
+        except (OSError, AttributeError): # XXX win32
             break
         nbtry += 1
         sleep(waittime)
--- a/cwvreg.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/cwvreg.py	Fri Sep 18 11:24:37 2009 +0200
@@ -140,9 +140,12 @@
                 try:
                     objects = self[btype]
                     assert len(objects) == 1, objects
-                    cls = objects[0]
+                    if btype == etype:
+                        cls = objects[0]
+                    else:
+                        cls = self.etype_class(btype)
                 except ObjectNotFound:
-                    pass
+                    continue
             else:
                 # ensure parent classes are built first
                 self.etype_class(btype)
@@ -152,9 +155,9 @@
             objects = self['Any']
             assert len(objects) == 1, objects
             cls = objects[0]
-        if cls.id == etype:
-            cls.__initialize__()
-            return cls
+        # make a copy event if cls.id == etype, else we may have pb for client
+        # application using multiple connections to different repositories (eg
+        # shingouz)
         cls = dump_class(cls, etype)
         cls.id = etype
         cls.__initialize__()
@@ -288,8 +291,7 @@
 
     def set_schema(self, schema):
         """set instance'schema and load application objects"""
-        self.schema = schema
-        clear_cache(self, 'rqlhelper')
+        self._set_schema(schema)
         # now we can load application's web objects
         searchpath = self.config.vregistry_path()
         self.reset(searchpath, force_reload=False)
@@ -300,6 +302,11 @@
             etype = str(etype)
             self.case_insensitive_etypes[etype.lower()] = etype
 
+    def _set_schema(self, schema):
+        """set instance'schema"""
+        self.schema = schema
+        clear_cache(self, 'rqlhelper')
+
     def update_schema(self, schema):
         """update .schema attribute on registered objects, necessary for some
         tests
@@ -383,16 +390,7 @@
         # objects on automatic reloading
         self._needs_iface.clear()
 
-    def parse(self, session, rql, args=None):
-        rqlst = self.rqlhelper.parse(rql)
-        def type_from_eid(eid, session=session):
-            return session.describe(eid)[0]
-        try:
-            self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
-        except UnknownEid:
-            for select in rqlst.children:
-                select.solutions = []
-        return rqlst
+    # rql parsing utilities ####################################################
 
     @property
     @cached
@@ -400,38 +398,19 @@
         return RQLHelper(self.schema,
                          special_relations={'eid': 'uid', 'has_text': 'fti'})
 
-
-    @deprecated('use vreg["etypes"].etype_class(etype)')
-    def etype_class(self, etype):
-        return self["etypes"].etype_class(etype)
-
-    @deprecated('use vreg["views"].main_template(*args, **kwargs)')
-    def main_template(self, req, oid='main-template', **context):
-        return self["views"].main_template(req, oid, **context)
-
-    @deprecated('use vreg[registry].possible_vobjects(*args, **kwargs)')
-    def possible_vobjects(self, registry, *args, **kwargs):
-        return self[registry].possible_vobjects(*args, **kwargs)
+    def solutions(self, req, rqlst, args):
+        def type_from_eid(eid, req=req):
+            return req.describe(eid)[0]
+        self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
 
-    @deprecated('use vreg["actions"].possible_actions(*args, **kwargs)')
-    def possible_actions(self, req, rset=None, **kwargs):
-        return self["actions"].possible_actions(req, rest=rset, **kwargs)
-
-    @deprecated("use vreg['boxes'].select_object(...)")
-    def select_box(self, oid, *args, **kwargs):
-        return self['boxes'].select_object(oid, *args, **kwargs)
-
-    @deprecated("use vreg['components'].select_object(...)")
-    def select_component(self, cid, *args, **kwargs):
-        return self['components'].select_object(cid, *args, **kwargs)
-
-    @deprecated("use vreg['actions'].select_object(...)")
-    def select_action(self, oid, *args, **kwargs):
-        return self['actions'].select_object(oid, *args, **kwargs)
-
-    @deprecated("use vreg['views'].select(...)")
-    def select_view(self, __vid, req, rset=None, **kwargs):
-        return self['views'].select(__vid, req, rset=rset, **kwargs)
+    def parse(self, req, rql, args=None):
+        rqlst = self.rqlhelper.parse(rql)
+        try:
+            self.solutions(req, rqlst, args)
+        except UnknownEid:
+            for select in rqlst.children:
+                select.solutions = []
+        return rqlst
 
     # properties handling #####################################################
 
@@ -504,6 +483,40 @@
                 self.warning('%s (you should probably delete that property '
                              'from the database)', ex)
 
+    # deprecated code ####################################################
+
+    @deprecated('[3.4] use vreg["etypes"].etype_class(etype)')
+    def etype_class(self, etype):
+        return self["etypes"].etype_class(etype)
+
+    @deprecated('[3.4] use vreg["views"].main_template(*args, **kwargs)')
+    def main_template(self, req, oid='main-template', **context):
+        return self["views"].main_template(req, oid, **context)
+
+    @deprecated('[3.4] use vreg[registry].possible_vobjects(*args, **kwargs)')
+    def possible_vobjects(self, registry, *args, **kwargs):
+        return self[registry].possible_vobjects(*args, **kwargs)
+
+    @deprecated('[3.4] use vreg["actions"].possible_actions(*args, **kwargs)')
+    def possible_actions(self, req, rset=None, **kwargs):
+        return self["actions"].possible_actions(req, rest=rset, **kwargs)
+
+    @deprecated('[3.4] use vreg["boxes"].select_object(...)')
+    def select_box(self, oid, *args, **kwargs):
+        return self['boxes'].select_object(oid, *args, **kwargs)
+
+    @deprecated('[3.4] use vreg["components"].select_object(...)')
+    def select_component(self, cid, *args, **kwargs):
+        return self['components'].select_object(cid, *args, **kwargs)
+
+    @deprecated('[3.4] use vreg["actions"].select_object(...)')
+    def select_action(self, oid, *args, **kwargs):
+        return self['actions'].select_object(oid, *args, **kwargs)
+
+    @deprecated('[3.4] use vreg["views"].select(...)')
+    def select_view(self, __vid, req, rset=None, **kwargs):
+        return self['views'].select(__vid, req, rset=rset, **kwargs)
+
 
 from datetime import datetime, date, time, timedelta
 
--- a/dbapi.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/dbapi.py	Fri Sep 18 11:24:37 2009 +0200
@@ -214,10 +214,13 @@
             self.lang = 'en'
         # use req.__ to translate a message without registering it to the catalog
         try:
-            self._ = self.__ = self.translations[self.lang]
+            gettext, pgettext = self.translations[self.lang]
+            self._ = self.__ = gettext
+            self.pgettext = pgettext
         except KeyError:
             # this occurs usually during test execution
             self._ = self.__ = unicode
+            self.pgettext = lambda x,y: y
         self.debug('request default language: %s', self.lang)
 
     def decorate_rset(self, rset):
--- a/debian/changelog	Thu Sep 17 19:38:04 2009 +0200
+++ b/debian/changelog	Fri Sep 18 11:24:37 2009 +0200
@@ -1,3 +1,15 @@
+cubicweb (3.5.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 18 Sep 2009 10:35:00 +0200
+
+cubicweb (3.5.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 16 Sep 2009 17:51:13 +0200
+
 cubicweb (3.4.11-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Thu Sep 17 19:38:04 2009 +0200
+++ b/debian/control	Fri Sep 18 11:24:37 2009 +0200
@@ -62,7 +62,7 @@
 Architecture: all
 XB-Python-Version: ${python:Versions}
 Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3), python-elementtree
-Recommends: python-docutils, python-vobject, fckeditor, python-fyzz
+Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-pysixt, fop
 Description: web interface library for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
--- a/devtools/_apptest.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/devtools/_apptest.py	Fri Sep 18 11:24:37 2009 +0200
@@ -26,7 +26,7 @@
                    'CWAttribute', 'CWRelation',
                    'CWConstraint', 'CWConstraintType', 'CWProperty',
                    'CWEType', 'CWRType',
-                   'State', 'Transition', 'TrInfo',
+                   'Workflow', 'State', 'BaseTransition', 'Transition', 'WorkflowTransition', 'TrInfo', 'SubWorkflowExitPoint',
                    'RQLExpression',
                    )
 SYSTEM_RELATIONS = (
@@ -35,9 +35,9 @@
     # metadata
     'is', 'is_instance_of', 'owned_by', 'created_by', 'specializes',
     # workflow related
-    'state_of', 'transition_of', 'initial_state', 'allowed_transition',
+    'workflow_of', 'state_of', 'transition_of', 'initial_state', 'allowed_transition',
     'destination_state', 'in_state', 'wf_info_for', 'from_state', 'to_state',
-    'condition',
+    'condition', 'subworkflow', 'subworkflow_state', 'subworkflow_exit',
     # permission
     'in_group', 'require_group', 'require_permission',
     'read_permission', 'update_permission', 'delete_permission', 'add_permission',
@@ -121,8 +121,7 @@
     def create_user(self, login, groups=('users',), req=None):
         req = req or self.create_request()
         cursor = self._orig_cnx.cursor(req)
-        rset = cursor.execute('INSERT CWUser X: X login %(login)s, X upassword %(passwd)s,'
-                              'X in_state S WHERE S name "activated"',
+        rset = cursor.execute('INSERT CWUser X: X login %(login)s, X upassword %(passwd)s',
                               {'login': unicode(login), 'passwd': login.encode('utf8')})
         user = rset.get_entity(0, 0)
         cursor.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)'
--- a/devtools/apptest.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/devtools/apptest.py	Fri Sep 18 11:24:37 2009 +0200
@@ -230,6 +230,23 @@
         return [(a.id, a.__class__) for a in self.vreg['actions'].possible_vobjects(req, rset=rset)
                 if a.category not in skipcategories]
 
+    def action_submenu(self, req, rset, id):
+        return self._test_action(self.vreg['actions'].select(id, req, rset=rset))
+
+    def _test_action(self, action):
+        class fake_menu(list):
+            @property
+            def items(self):
+                return self
+        class fake_box(object):
+            def mk_action(self, label, url, **kwargs):
+                return (label, url)
+            def box_action(self, action, **kwargs):
+                return (action.title, action.url())
+        submenu = fake_menu()
+        action.fill_menu(fake_box(), submenu)
+        return submenu
+
     def pactions_by_cats(self, req, rset, categories=('addrelated',)):
         return [(a.id, a.__class__) for a in self.vreg['actions'].possible_vobjects(req, rset=rset)
                 if a.category in categories]
@@ -371,8 +388,7 @@
     def create_user(self, user, groups=('users',), password=None, commit=True):
         if password is None:
             password = user
-        eid = self.execute('INSERT CWUser X: X login %(x)s, X upassword %(p)s,'
-                            'X in_state S WHERE S name "activated"',
+        eid = self.execute('INSERT CWUser X: X login %(x)s, X upassword %(p)s',
                             {'x': unicode(user), 'p': password})[0][0]
         groups = ','.join(repr(group) for group in groups)
         self.execute('SET X in_group Y WHERE X eid %%(x)s, Y name IN (%s)' % groups,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/dataimport.py	Fri Sep 18 11:24:37 2009 +0200
@@ -0,0 +1,277 @@
+# -*- coding: utf-8 -*-
+"""This module provides tools to import tabular data.
+
+: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
+
+
+Example of use (run this with `cubicweb-ctl shell instance import-script.py`):
+
+.. sourcecode:: python
+
+  # define data generators
+  GENERATORS = []
+
+  USERS = [('Prenom', 'firstname', ()),
+           ('Nom', 'surname', ()),
+           ('Identifiant', 'login', ()),
+           ]
+
+  def gen_users(ctl):
+      for row in ctl.get_data('utilisateurs'):
+          entity = mk_entity(row, USERS)
+          entity['upassword'] = u'motdepasse'
+          ctl.check('login', entity['login'], None)
+          ctl.store.add('CWUser', entity)
+          email = {'address': row['email']}
+          ctl.store.add('EmailAddress', email)
+          ctl.store.relate(entity['eid'], 'use_email', email['eid'])
+          ctl.store.rql('SET U in_group G WHERE G name "users", U eid %(x)s', {'x':entity['eid']})
+
+  CHK = [('login', check_doubles, 'Utilisateurs Login',
+          'Deux utilisateurs ne devraient pas avoir le même login.'),
+         ]
+
+  GENERATORS.append( (gen_users, CHK) )
+
+  # create controller
+  ctl = CWImportController(RQLObjectStore())
+  ctl.askerror = True
+  ctl.generators = GENERATORS
+  ctl.store._checkpoint = checkpoint
+  ctl.store._rql = rql
+  ctl.data['utilisateurs'] = lazytable(utf8csvreader(open('users.csv')))
+  # run
+  ctl.run()
+  sys.exit(0)
+
+"""
+__docformat__ = "restructuredtext en"
+
+import sys, csv, traceback
+
+from logilab.common import shellutils
+
+def utf8csvreader(file, encoding='utf-8', separator=',', quote='"'):
+    """A csv reader that accepts files with any encoding and outputs
+    unicode strings."""
+    for row in csv.reader(file, delimiter=separator, quotechar=quote):
+        yield [item.decode(encoding) for item in row]
+
+def lazytable(reader):
+    """The first row is taken to be the header of the table and
+    used to output a dict for each row of data.
+
+    >>> data = lazytable(utf8csvreader(open(filename)))
+    """
+    header = reader.next()
+    for row in reader:
+        yield dict(zip(header, row))
+
+def tell(msg):
+    print msg
+
+# base sanitizing functions #####
+
+def capitalize_if_unicase(txt):
+    if txt.isupper() or txt.islower():
+        return txt.capitalize()
+    return txt
+
+def no_space(txt):
+    return txt.replace(' ','')
+
+def no_uspace(txt):
+    return txt.replace(u'\xa0','')
+
+def no_dash(txt):
+    return txt.replace('-','')
+
+def alldigits(txt):
+    if txt.isdigit():
+        return txt
+    else:
+        return u''
+
+def strip(txt):
+    return txt.strip()
+
+# base checks #####
+
+def check_doubles(buckets):
+    """Extract the keys that have more than one item in their bucket."""
+    return [(key, len(value)) for key,value in buckets.items() if len(value) > 1]
+
+# make entity helper #####
+
+def mk_entity(row, map):
+    """Return a dict made from sanitized mapped values.
+
+    >>> row = {'myname': u'dupont'}
+    >>> map = [('myname', u'name', (capitalize_if_unicase,))]
+    >>> mk_entity(row, map)
+    {'name': u'Dupont'}
+    """
+    res = {}
+    for src, dest, funcs in map:
+        res[dest] = row[src]
+        for func in funcs:
+            res[dest] = func(res[dest])
+    return res
+
+# object stores
+
+class ObjectStore(object):
+    """Store objects in memory for faster testing. Will not
+    enforce the constraints of the schema and hence will miss
+    some problems.
+
+    >>> store = ObjectStore()
+    >>> user = {'login': 'johndoe'}
+    >>> store.add('CWUser', user)
+    >>> group = {'name': 'unknown'}
+    >>> store.add('CWUser', group)
+    >>> store.relate(user['eid'], 'in_group', group['eid'])
+    """
+
+    def __init__(self):
+        self.items = []
+        self.eids = {}
+        self.types = {}
+        self.relations = set()
+        self.indexes = {}
+        self._rql = None
+        self._checkpoint = None
+
+    def _put(self, type, item):
+        self.items.append(item)
+        return len(self.items) - 1
+
+    def add(self, type, item):
+        assert isinstance(item, dict), item
+        eid = item['eid'] = self._put(type, item)
+        self.eids[eid] = item
+        self.types.setdefault(type, []).append(eid)
+
+    def relate(self, eid_from, rtype, eid_to):
+        eids_valid = (eid_from < len(self.items) and eid_to <= len(self.items))
+        assert eids_valid, 'eid error %s %s' % (eid_from, eid_to)
+        self.relations.add( (eid_from, rtype, eid_to) )
+
+    def build_index(self, name, type, func):
+        index = {}
+        for eid in self.types[type]:
+            index.setdefault(func(self.eids[eid]), []).append(eid)
+        self.indexes[name] = index
+
+    def get_many(self, name, key):
+        return self.indexes[name].get(key, [])
+
+    def get_one(self, name, key):
+        eids = self.indexes[name].get(key, [])
+        assert len(eids) == 1
+        return eids[0]
+
+    def find(self, type, key, value):
+        for idx in self.types[type]:
+            item = self.items[idx]
+            if item[key] == value:
+                yield item
+
+    def rql(self, query, args):
+        if self._rql:
+            return self._rql(query, args)
+
+    def checkpoint(self):
+        if self._checkpoint:
+            self._checkpoint()
+
+class RQLObjectStore(ObjectStore):
+    """ObjectStore that works with an actual RQL repository."""
+
+    def _put(self, type, item):
+        query = ('INSERT %s X: ' % type) + ', '.join(['X %s %%(%s)s' % (key,key) for key in item])
+        return self.rql(query, item)[0][0]
+
+    def relate(self, eid_from, rtype, eid_to):
+        query = 'SET X %s Y WHERE X eid %%(from)s, Y eid %%(to)s' % rtype
+        self.rql(query, {'from': int(eid_from), 'to': int(eid_to)})
+        self.relations.add( (eid_from, rtype, eid_to) )
+
+# import controller #####
+
+class CWImportController(object):
+    """Controller of the data import process.
+
+    >>> ctl = CWImportController(store)
+    >>> ctl.generators = list_of_data_generators
+    >>> ctl.data = dict_of_data_tables
+    >>> ctl.run()
+    """
+
+    def __init__(self, store):
+        self.store = store
+        self.generators = None
+        self.data = {}
+        self.errors = None
+        self.askerror = False
+        self._tell = tell
+
+    def check(self, type, key, value):
+        self._checks.setdefault(type, {}).setdefault(key, []).append(value)
+
+    def check_map(self, entity, key, map, default):
+        try:
+            entity[key] = map[entity[key]]
+        except KeyError:
+            self.check(key, entity[key], None)
+            entity[key] = default
+
+    def run(self):
+        self.errors = {}
+        for func, checks in self.generators:
+            self._checks = {}
+            func_name = func.__name__[4:]
+            question = 'Importation de %s' % func_name
+            self.tell(question)
+            try:
+                func(self)
+            except:
+                import StringIO
+                tmp = StringIO.StringIO()
+                traceback.print_exc(file=tmp)
+                print tmp.getvalue()
+                self.errors[func_name] = ('Erreur lors de la transformation',
+                                          tmp.getvalue().splitlines())
+            for key, func, title, help in checks:
+                buckets = self._checks.get(key)
+                if buckets:
+                    err = func(buckets)
+                    if err:
+                        self.errors[title] = (help, err)
+            self.store.checkpoint()
+        errors = sum(len(err[1]) for err in self.errors.values())
+        self.tell('Importation terminée. (%i objets, %i types, %i relations et %i erreurs).'
+                  % (len(self.store.eids), len(self.store.types),
+                     len(self.store.relations), errors))
+        if self.errors and self.askerror and confirm('Afficher les erreurs ?'):
+            import pprint
+            pprint.pprint(self.errors)
+
+    def get_data(self, key):
+        return self.data.get(key)
+
+    def index(self, name, key, value):
+        self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value)
+
+    def tell(self, msg):
+        self._tell(msg)
+
+def confirm(question):
+    """A confirm function that asks for yes/no/abort and exits on abort."""
+    answer = shellutils.ASK.ask(question, ('Y','n','abort'), 'Y')
+    if answer == 'abort':
+        sys.exit(1)
+    return answer == 'Y'
--- a/devtools/devctl.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/devtools/devctl.py	Fri Sep 18 11:24:37 2009 +0200
@@ -10,8 +10,9 @@
 
 import sys
 from datetime import datetime
-from os import mkdir, chdir
+from os import mkdir, chdir, getcwd
 from os.path import join, exists, abspath, basename, normpath, split, isdir
+from copy import deepcopy
 from warnings import warn
 
 from logilab.common import STD_BLACKLIST
@@ -20,7 +21,8 @@
 from logilab.common.shellutils import ASK
 from logilab.common.clcommands import register_commands
 
-from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage, underline_title
+from cubicweb import (CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage,
+                      underline_title)
 from cubicweb.__pkginfo__ import version as cubicwebversion
 from cubicweb.toolsutils import Command, copy_skeleton
 from cubicweb.web.webconfig import WebConfiguration
@@ -111,88 +113,100 @@
 
 def _generate_schema_pot(w, vreg, schema, libconfig=None, cube=None):
     from cubicweb.common.i18n import add_msg
+    from cubicweb.web import uicfg
+    from cubicweb.schema import META_RTYPES, SYSTEM_RTYPES
+    no_context_rtypes = META_RTYPES | SYSTEM_RTYPES
     w('# schema pot file, generated on %s\n' % datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
     w('# \n')
     w('# singular and plural forms for each entity type\n')
     w('\n')
+    vregdone = set()
     if libconfig is not None:
+        from cubicweb.cwvreg import CubicWebVRegistry, clear_rtag_objects
         libschema = libconfig.load_schema(remove_unused_rtypes=False)
-        entities = [e for e in schema.entities() if not e in libschema]
+        rinlined = deepcopy(uicfg.autoform_is_inlined)
+        appearsin_addmenu = deepcopy(uicfg.actionbox_appearsin_addmenu)
+        clear_rtag_objects()
+        cleanup_sys_modules(libconfig)
+        libvreg = CubicWebVRegistry(libconfig)
+        libvreg.set_schema(libschema) # trigger objects registration
+        librinlined = uicfg.autoform_is_inlined
+        libappearsin_addmenu = uicfg.actionbox_appearsin_addmenu
+        # prefill vregdone set
+        list(_iter_vreg_objids(libvreg, vregdone))
     else:
-        entities = schema.entities()
+        libschema = {}
+        rinlined = uicfg.autoform_is_inlined
+        appearsin_addmenu = uicfg.actionbox_appearsin_addmenu
     done = set()
-    for eschema in sorted(entities):
+    for eschema in sorted(schema.entities()):
         etype = eschema.type
-        add_msg(w, etype)
-        add_msg(w, '%s_plural' % etype)
-        if not eschema.is_final():
-            add_msg(w, 'This %s' % etype)
-            add_msg(w, 'New %s' % etype)
-            add_msg(w, 'add a %s' % etype)
-            add_msg(w, 'remove this %s' % etype)
-        if eschema.description and not eschema.description in done:
-            done.add(eschema.description)
-            add_msg(w, eschema.description)
-    w('# subject and object forms for each relation type\n')
-    w('# (no object form for final relation types)\n')
-    w('\n')
-    if libconfig is not None:
-        relations = [r for r in schema.relations() if not r in libschema]
-    else:
-        relations = schema.relations()
-    for rschema in sorted(set(relations)):
-        rtype = rschema.type
-        add_msg(w, rtype)
-        done.add(rtype)
-        if not (schema.rschema(rtype).is_final() or rschema.symetric):
-            add_msg(w, '%s_object' % rtype)
-        if rschema.description and rschema.description not in done:
-            done.add(rschema.description)
-            add_msg(w, rschema.description)
-    w('# add related box generated message\n')
-    w('\n')
-    actionbox = vreg['boxes']['edit_box'][0]
-    for eschema in schema.entities():
+        if etype not in libschema:
+            add_msg(w, etype)
+            add_msg(w, '%s_plural' % etype)
+            if not eschema.is_final():
+                add_msg(w, 'This %s' % etype)
+                add_msg(w, 'New %s' % etype)
+            if eschema.description and not eschema.description in done:
+                done.add(eschema.description)
+                add_msg(w, eschema.description)
         if eschema.is_final():
             continue
-        for role, rschemas in (('subject', eschema.subject_relations()),
-                            ('object', eschema.object_relations())):
-            for rschema in rschemas:
-                if rschema.is_final():
-                    continue
-                if libconfig is not None:
-                    librschema = libschema.get(rschema)
-                for teschema in rschema.targets(eschema, role):
-                    if libconfig is not None and librschema is not None:
-                        if role == 'subject':
-                            subjtype, objtype = eschema, teschema
-                        else:
-                            subjtype, objtype = teschema, eschema
-                        if librschema.has_rdef(subjtype, objtype):
-                            continue
-                    if actionbox.appearsin_addmenu.etype_get(eschema, rschema,
-                                                             role, teschema):
-                        if role == 'subject':
-                            label = 'add %s %s %s %s' % (eschema, rschema,
-                                                         teschema, role)
-                            label2 = "creating %s (%s %%(linkto)s %s %s)" % (
-                                teschema, eschema, rschema, teschema)
-                        else:
-                            label = 'add %s %s %s %s' % (teschema, rschema,
-                                                         eschema, role)
-                            label2 = "creating %s (%s %s %s %%(linkto)s)" % (
-                                teschema, teschema, rschema, eschema)
-                        add_msg(w, label)
-                        add_msg(w, label2)
-    #cube = (cube and 'cubes.%s.' % cube or 'cubicweb.')
-    done = set()
-    if libconfig is not None:
-        from cubicweb.cwvreg import CubicWebVRegistry
-        libvreg = CubicWebVRegistry(libconfig)
-        libvreg.set_schema(libschema) # trigger objects registration
-        # prefill done set
-        list(_iter_vreg_objids(libvreg, done))
-    for objid in _iter_vreg_objids(vreg, done):
+        for rschema, targetschemas, role in eschema.relation_definitions(True):
+            for tschema in targetschemas:
+                if rinlined.etype_get(eschema, rschema, role, tschema) and \
+                       (libconfig is None or not
+                        librinlined.etype_get(eschema, rschema, role, tschema)):
+                    add_msg(w, 'add a %s' % tschema,
+                            'inlined:%s.%s.%s' % (etype, rschema, role))
+                    add_msg(w, 'remove this %s' % tschema,
+                            'inlined:%s:%s:%s' % (etype, rschema, role))
+                if appearsin_addmenu.etype_get(eschema, rschema, role, tschema) and \
+                       (libconfig is None or not
+                        libappearsin_addmenu.etype_get(eschema, rschema, role, tschema)):
+                    if role == 'subject':
+                        label = 'add %s %s %s %s' % (eschema, rschema,
+                                                     tschema, role)
+                        label2 = "creating %s (%s %%(linkto)s %s %s)" % (
+                            tschema, eschema, rschema, tschema)
+                    else:
+                        label = 'add %s %s %s %s' % (tschema, rschema,
+                                                     eschema, role)
+                        label2 = "creating %s (%s %s %s %%(linkto)s)" % (
+                            tschema, tschema, rschema, eschema)
+                    add_msg(w, label)
+                    add_msg(w, label2)
+    w('# subject and object forms for each relation type\n')
+    w('# (no object form for final or symetric relation types)\n')
+    w('\n')
+    for rschema in sorted(schema.relations()):
+        rtype = rschema.type
+        if rtype not in libschema:
+            # bw compat, necessary until all translation of relation are done properly...
+            add_msg(w, rtype)
+            if rschema.description and rschema.description not in done:
+                done.add(rschema.description)
+                add_msg(w, rschema.description)
+            done.add(rtype)
+            librschema = None
+        else:
+            librschema = libschema.rschema(rtype)
+        # add context information only for non-metadata rtypes
+        if rschema not in no_context_rtypes:
+            libsubjects = librschema and librschema.subjects() or ()
+            for subjschema in rschema.subjects():
+                if not subjschema in libsubjects:
+                    add_msg(w, rtype, subjschema.type)
+        if not (schema.rschema(rtype).is_final() or rschema.symetric):
+            if rschema not in no_context_rtypes:
+                libobjects = librschema and librschema.objects() or ()
+                for objschema in rschema.objects():
+                    if not objschema in libobjects:
+                        add_msg(w, '%s_object' % rtype, objschema.type)
+            if rtype not in libschema:
+                # bw compat, necessary until all translation of relation are done properly...
+                add_msg(w, '%s_object' % rtype)
+    for objid in _iter_vreg_objids(vreg, vregdone):
         add_msg(w, '%s_description' % objid)
         add_msg(w, objid)
 
@@ -283,20 +297,20 @@
             if lang is not None:
                 cmd += ' -L %s' % lang
             potfile = join(tempdir, '%s.pot' % id)
-            execute(cmd % (potfile, ' '.join(files)))
+            execute(cmd % (potfile, ' '.join('"%s"' % f for f in files)))
             if exists(potfile):
                 potfiles.append(potfile)
             else:
                 print '-> WARNING: %s file was not generated' % potfile
         print '-> merging %i .pot files' % len(potfiles)
         cubicwebpot = join(tempdir, 'cubicweb.pot')
-        execute('msgcat %s > %s' % (' '.join(potfiles), cubicwebpot))
+        execute('msgcat -o %s %s' % (cubicwebpot, ' '.join('"%s"' % f for f in potfiles)))
         print '-> merging main pot file with existing translations.'
         chdir(I18NDIR)
         toedit = []
         for lang in LANGS:
             target = '%s.po' % lang
-            execute('msgmerge -N --sort-output  %s %s > %snew' % (target, cubicwebpot, target))
+            execute('msgmerge -N --sort-output -o "%snew" "%s" "%s"' % (target, target, cubicwebpot))
             ensure_fs_mode(target)
             shutil.move('%snew' % target, target)
             toedit.append(abspath(target))
@@ -390,12 +404,13 @@
     cubefiles = find('.', '.py', blacklist=STD_BLACKLIST+('test',))
     cubefiles.append(tali18nfile)
     execute('xgettext --no-location --omit-header -k_ -o %s %s'
-            % (tmppotfile, ' '.join(cubefiles)))
+            % (tmppotfile, ' '.join('"%s"' % f for f in cubefiles)))
     if exists(tmppotfile): # doesn't exists of no translation string found
         potfiles.append(tmppotfile)
     potfile = join(tempdir, 'cube.pot')
     print '-> merging %i .pot files:' % len(potfiles)
-    execute('msgcat %s > %s' % (' '.join(potfiles), potfile))
+    execute('msgcat -o %s %s' % (potfile,
+                                 ' '.join('"%s"' % f for f in potfiles)))
     print '-> merging main pot file with existing translations:'
     chdir('i18n')
     for lang in LANGS:
@@ -404,7 +419,7 @@
         if not exists(cubepo):
             shutil.copy(potfile, cubepo)
         else:
-            execute('msgmerge -N -s %s %s > %snew' % (cubepo, potfile, cubepo))
+            execute('msgmerge -N -s -o %snew %s %s' % (cubepo, cubepo, potfile))
             ensure_fs_mode(cubepo)
             shutil.move('%snew' % cubepo, cubepo)
         toedit.append(abspath(cubepo))
--- a/devtools/fake.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/devtools/fake.py	Fri Sep 18 11:24:37 2009 +0200
@@ -13,6 +13,7 @@
 from indexer import get_indexer
 
 from cubicweb import RequestSessionMixIn
+from cubicweb.cwvreg import CubicWebVRegistry
 from cubicweb.web.request import CubicWebRequestBase
 from cubicweb.devtools import BASE_URL, BaseApptestConfiguration
 
@@ -35,39 +36,13 @@
     def sources(self):
         return {}
 
-class FakeVReg(dict):
-    def __init__(self, schema=None, config=None):
-        self.schema = schema
-        self.config = config or FakeConfig()
-        self.properties = {'ui.encoding': 'UTF8',
-                           'ui.language': 'en',
-                           }
-        self.update({
-            'controllers' : {'login': []},
-            'views' : {},
-            })
-
-    def property_value(self, key):
-        return self.properties[key]
-
-    def etype_class(self, etype):
-        class Entity(dict):
-            e_schema = self.schema[etype]
-            def __init__(self, session, eid, row=0, col=0):
-                self.req = session
-                self.eid = eid
-                self.row, self.col = row, col
-            def set_eid(self, eid):
-                self.eid = self['eid'] = eid
-        return Entity
-
 
 class FakeRequest(CubicWebRequestBase):
     """test implementation of an cubicweb request object"""
 
     def __init__(self, *args, **kwargs):
         if not (args or 'vreg' in kwargs):
-            kwargs['vreg'] = FakeVReg()
+            kwargs['vreg'] = CubicWebVRegistry(FakeConfig(), initlog=False)
         kwargs['https'] = False
         self._url = kwargs.pop('url', 'view?rql=Blop&vid=blop')
         super(FakeRequest, self).__init__(*args, **kwargs)
@@ -148,25 +123,6 @@
         return self.execute(*args, **kwargs)
 
 
-# class FakeRequestNoCnx(FakeRequest):
-#     def get_session_data(self, key, default=None, pop=False):
-#         """return value associated to `key` in session data"""
-#         if pop:
-#             return self._session_data.pop(key, default)
-#         else:
-#             return self._session_data.get(key, default)
-
-#     def set_session_data(self, key, value):
-#         """set value associated to `key` in session data"""
-#         self._session_data[key] = value
-
-#     def del_session_data(self, key):
-#         try:
-#             del self._session_data[key]
-#         except KeyError:
-#             pass
-
-
 class FakeUser(object):
     login = 'toto'
     eid = 0
@@ -177,7 +133,7 @@
 class FakeSession(RequestSessionMixIn):
     def __init__(self, repo=None, user=None):
         self.repo = repo
-        self.vreg = getattr(self.repo, 'vreg', FakeVReg())
+        self.vreg = getattr(self.repo, 'vreg', CubicWebVRegistry(FakeConfig(), initlog=False))
         self.pool = FakePool()
         self.user = user or FakeUser()
         self.is_internal_session = False
@@ -210,8 +166,9 @@
         self.eids = {}
         self._count = 0
         self.schema = schema
-        self.vreg = vreg or FakeVReg()
         self.config = config or FakeConfig()
+        self.vreg = vreg or CubicWebVRegistry(self.config, initlog=False)
+        self.vreg.schema = schema
 
     def internal_session(self):
         return FakeSession(self)
@@ -249,10 +206,3 @@
 class FakePool(object):
     def source(self, uri):
         return FakeSource(uri)
-
-# commented until proven to be useful
-## from logging import getLogger
-## from cubicweb import set_log_methods
-## for cls in (FakeConfig, FakeVReg, FakeRequest, FakeSession, FakeRepo,
-##             FakeSource, FakePool):
-##     set_log_methods(cls, getLogger('fake'))
--- a/devtools/repotest.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/devtools/repotest.py	Fri Sep 18 11:24:37 2009 +0200
@@ -108,9 +108,10 @@
     schema = None # set this in concret test
 
     def setUp(self):
+        self.repo = FakeRepo(self.schema)
         self.rqlhelper = RQLHelper(self.schema, special_relations={'eid': 'uid',
                                                                    'has_text': 'fti'})
-        self.qhelper = QuerierHelper(FakeRepo(self.schema), self.schema)
+        self.qhelper = QuerierHelper(self.repo, self.schema)
         ExecutionPlan._check_permissions = _dummy_check_permissions
         rqlannotation._select_principal = _select_principal
 
@@ -129,7 +130,7 @@
         #print '********* solutions', solutions
         self.rqlhelper.simplify(union)
         #print '********* simplified', union.as_string()
-        plan = self.qhelper.plan_factory(union, {}, FakeSession())
+        plan = self.qhelper.plan_factory(union, {}, FakeSession(self.repo))
         plan.preprocess(union)
         for select in union.children:
             select.solutions.sort()
@@ -167,7 +168,7 @@
         set_debug(debug)
 
     def _rqlhelper(self):
-        rqlhelper = self.o._rqlhelper
+        rqlhelper = self.repo.vreg.rqlhelper
         # reset uid_func so it don't try to get type from eids
         rqlhelper._analyser.uid_func = None
         rqlhelper._analyser.uid_func_mapping = {}
@@ -241,7 +242,7 @@
         rqlst = self.o.parse(rql, annotate=True)
         self.o.solutions(self.session, rqlst, kwargs)
         if rqlst.TYPE == 'select':
-            self.o._rqlhelper.annotate(rqlst)
+            self.repo.vreg.rqlhelper.annotate(rqlst)
             for select in rqlst.children:
                 select.solutions.sort()
         else:
@@ -251,7 +252,7 @@
 
 # monkey patch some methods to get predicatable results #######################
 
-from cubicweb.server.rqlrewrite import RQLRewriter
+from cubicweb.rqlrewrite import RQLRewriter
 _orig_insert_snippets = RQLRewriter.insert_snippets
 _orig_build_variantes = RQLRewriter.build_variantes
 
--- a/devtools/testlib.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/devtools/testlib.py	Fri Sep 18 11:24:37 2009 +0200
@@ -333,7 +333,7 @@
             # resultset's syntax tree
             rset = backup_rset
         for action in self.list_actions_for(rset):
-            yield InnerTest(self._testname(rset, action.id, 'action'), action.url)
+            yield InnerTest(self._testname(rset, action.id, 'action'), self._test_action, action)
         for box in self.list_boxes_for(rset):
             yield InnerTest(self._testname(rset, box.id, 'box'), box.render)
 
--- a/doc/book/en/admin/setup.rst	Thu Sep 17 19:38:04 2009 +0200
+++ b/doc/book/en/admin/setup.rst	Fri Sep 18 11:24:37 2009 +0200
@@ -77,6 +77,133 @@
 In both cases, make sure you have installed the dependencies (see appendixes for
 the list).
 
+Windows installation
+````````````````````
+
+Base elements
+_____________
+
+Setting up a windows development environment is not too complicated
+but requires a series of small steps. What is proposed there is only
+an example of what can be done. We assume everything goes into C:\ in
+this document. Adjusting the installation drive should be
+straightforward.
+
+You should start by downloading and installing the Python(x,y)
+distribution. It contains python 2.5 plus numerous useful third-party
+modules and applications::
+
+  http://www.pythonxy.com/download_fr.php
+
+At the time of this writting, one gets version 2.1.15. Among the many
+things provided, one finds Eclipse + pydev (an arguably good IDE for
+python under windows).
+
+Then you must grab Twisted. There is a windows installer directly
+available from this page::
+
+  http://twistedmatrix.com/trac/
+
+A windows installer for lxml will be found there::
+
+  http://pypi.python.org/pypi/lxml/2.2.1
+
+Check out the lxml-2.2.1-win32-py2.5.exe file. More recent bugfix
+releases should probably work, too.
+
+You should find postgresql 8.4 there::
+
+  http://www.enterprisedb.com/products/pgdownload.do#windows
+
+The python drivers for posgtresql are to be found there::
+
+  http://www.stickpeople.com/projects/python/win-psycopg/#Version2
+
+Please be careful to select the right python (2.5) and postgres (8.4)
+versions.
+
+Having graphviz will allow schema drawings, which is quite recommended
+(albeit not mandatory). You should get an msi installer there::
+
+  http://www.graphviz.org/Download_windows.php
+
+Tools
+_____
+
+Get mercurial + its standard windows GUI (TortoiseHG) there (the
+latest is the greatest)::
+
+  http://bitbucket.org/tortoisehg/stable/wiki/download
+
+If you need to peruse mercurial over ssh, it can be helpful to get an
+ssh client like Putty::
+
+  http://www.putty.org/
+
+Integration of mercurial and Eclipse is convenient enough that we want
+it. Instructions are set there, in the `Download & Install` section::
+
+  http://www.vectrace.com/mercurialeclipse/
+
+Setting up the sources
+______________________
+
+You need to enable the mercurial forest extension. To do this, edit
+the file::
+
+  C:\Program Files\TortoiseHg\Mercurial.ini
+
+In the [extensions] section, add the following line::
+
+  forest=C:\Program Files\TortoiseHg\ext\forest\forest.py
+
+Now, you need to clone the cubicweb repository. We assume that you use
+Eclipse. From the IDE, choose File -> Import. In the box, select
+`Mercurial/Clone repository using MercurialEclipse`.
+
+In the import main panel you just have to:
+
+* fill the URL field with http://www.logilab.org/hg/forests/cubicwin32
+
+* check the 'Repository is a forest' box.
+
+Then, click on 'Finish'. It might take some time to get it all. Note
+that the `cubicwin32` forest contains additional python packages such
+as yapps, vobject, simplejson and twisted-web2 which are not provided
+with Python(x,y). This is provided for convenience, as we do not
+ensure the up-to-dateness of these packages, especially with respect
+to security fixes.
+
+Environment variables
+_____________________
+
+You will need some convenience environment variables once all is set
+up. These variables are settable through the GUI by getting at the
+'System properties' window (by righ-clicking on 'My Computer' ->
+properties).
+
+In the 'advanced' tab, there is an 'Environment variables'
+button. Click on it. That opens a small window allowing edition of
+user-related and system-wide variables.
+
+We will consider only user variables. First, the PATH variable. You
+should ensure it contains, separated by semi-colons, and assuming you
+are logged in as user Jane::
+
+  C:\Documents and Settings\Jane\My Documents\Python\cubicweb\cubicweb\bin
+  C:\Program Files\Graphviz2.24\bin
+
+The PYTHONPATH variable should also contain::
+
+  C:\Documents and Settings\Jane\My Documents\Python\cubicweb\
+
+From now, on a fresh `cmd` shell, you should be able to type::
+
+  cubicweb-ctl list
+
+... and get a meaningful output.
+
+
 PostgreSQL installation
 ```````````````````````
 
@@ -135,8 +262,6 @@
 Databases configuration
 -----------------------
 
-
-
 .. _ConfigurationPostgresql:
 
 PostgreSQL configuration
--- a/doc/book/en/development/entityclasses/load-sort.rst	Thu Sep 17 19:38:04 2009 +0200
+++ b/doc/book/en/development/entityclasses/load-sort.rst	Fri Sep 18 11:24:37 2009 +0200
@@ -16,18 +16,30 @@
   `None` if we do not want to sort on the attribute given in the parameter.
   By default, the entities are sorted according to their creation date.
 
-* The class method `fetch_unrelated_order(attr, var)` is similar to the
-  method `fetch_order` except that it is essentially used to control
-  the sorting of drop-down lists enabling relations creation in
-  the editing view of an entity.
+* The class method `fetch_unrelated_order(attr, var)` is similar to
+  the method `fetch_order` except that it is essentially used to
+  control the sorting of drop-down lists enabling relations creation
+  in the editing view of an entity. The default implementation uses
+  the modification date. Here's how to adapt it for one entity (sort
+  on the name attribute): ::
+
+   class MyEntity(AnyEntity):
+       fetch_attrs = ('modification_date', 'name')
+
+       @classmethod
+       def fetch_unrelated_order(cls, attr, var):
+           if attr == 'name':
+              return '%s ASC' % var
+           return None
+
 
 The function `fetch_config(fetchattrs, mainattr=None)` simplifies the
 definition of the attributes to load and the sorting by returning a
-list of attributes to pre-load (considering automatically the attributes
-of `AnyEntity`) and a sorting function based on the main attribute
-(the second parameter if specified otherwisethe first attribute from
-the list `fetchattrs`).
-This function is defined in `cubicweb.entities`.
+list of attributes to pre-load (considering automatically the
+attributes of `AnyEntity`) and a sorting function based on the main
+attribute (the second parameter if specified, otherwise the first
+attribute from the list `fetchattrs`). This function is defined in
+`cubicweb.entities`.
 
 For example: ::
 
--- a/doc/book/en/development/testing/index.rst	Thu Sep 17 19:38:04 2009 +0200
+++ b/doc/book/en/development/testing/index.rst	Fri Sep 18 11:24:37 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
 -------------------------
--- a/entities/lib.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/entities/lib.py	Fri Sep 18 11:24:37 2009 +0200
@@ -10,7 +10,7 @@
 from urlparse import urlsplit, urlunsplit
 from datetime import datetime
 
-from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated
 
 from cubicweb import UnknownProperty
 from cubicweb.entity import _marker
@@ -25,7 +25,7 @@
 
 class EmailAddress(AnyEntity):
     id = 'EmailAddress'
-    fetch_attrs, fetch_order = fetch_config(['address', 'alias', 'canonical'])
+    fetch_attrs, fetch_order = fetch_config(['address', 'alias'])
 
     def dc_title(self):
         if self.alias:
@@ -36,15 +36,13 @@
     def email_of(self):
         return self.reverse_use_email and self.reverse_use_email[0]
 
-    @cached
+    @property
+    def prefered(self):
+        return self.prefered_form and self.prefered_form[0] or self
+
+    @deprecated('use .prefered')
     def canonical_form(self):
-        if self.canonical:
-            return self
-        rql = 'EmailAddress X WHERE X identical_to Y, X canonical TRUE, Y eid %(y)s'
-        cnrset = self.req.execute(rql, {'y': self.eid}, 'y')
-        if cnrset:
-            return cnrset.get_entity(0, 0)
-        return None
+        return self.prefered_form and self.prefered_form[0] or self
 
     def related_emails(self, skipeids=None):
         # XXX move to eemail
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/test/data/migration/postcreate.py	Fri Sep 18 11:24:37 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	Thu Sep 17 19:38:04 2009 +0200
+++ b/entities/test/data/schema.py	Fri Sep 18 11:24:37 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	Thu Sep 17 19:38:04 2009 +0200
+++ b/entities/test/unittest_base.py	Fri Sep 18 11:24:37 2009 +0200
@@ -16,7 +16,6 @@
 from cubicweb import ValidationError
 from cubicweb.interfaces import IMileStone, IWorkflowable
 from cubicweb.entities import AnyEntity
-from cubicweb.entities.authobjs import CWUser
 from cubicweb.web.widgets import AutoCompletionWidget
 
 
@@ -58,161 +57,15 @@
         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]
-        eid2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com", X canonical TRUE')[0][0]
-        self.execute('SET X identical_to Y WHERE X eid %s, Y eid %s' % (eid1, eid2))
-        email1 = self.entity('Any X WHERE X eid %(x)s', {'x':eid1}, 'x')
-        email2 = self.entity('Any X WHERE X eid %(x)s', {'x':eid2}, 'x')
-        self.assertEquals(email1.canonical_form().eid, eid2)
-        self.assertEquals(email2.canonical_form(), email2)
-        eid3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"')[0][0]
-        email3 = self.entity('Any X WHERE X eid %s'%eid3)
-        self.assertEquals(email3.canonical_form(), None)
+        email1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"').get_entity(0, 0)
+        email2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com"').get_entity(0, 0)
+        email3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"').get_entity(0, 0)
+        self.execute('SET X prefered_form Y WHERE X eid %s, Y eid %s' % (email1.eid, email2.eid))
+        self.assertEquals(email1.prefered.eid, email2.eid)
+        self.assertEquals(email2.prefered.eid, email2.eid)
+        self.assertEquals(email3.prefered.eid, email3.eid)
 
     def test_mangling(self):
         eid = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"')[0][0]
@@ -234,7 +87,6 @@
         e = self.entity('CWUser X WHERE X login "admin"')
         e.complete()
 
-
     def test_matching_groups(self):
         e = self.entity('CWUser X WHERE X login "admin"')
         self.failUnless(e.matching_groups('managers'))
@@ -242,27 +94,11 @@
         self.failUnless(e.matching_groups(('xyz', 'managers')))
         self.failIf(e.matching_groups(('xyz', 'abcd')))
 
-    def test_workflow_base(self):
-        e = self.create_user('toto')
-        self.assertEquals(e.state, 'activated')
-        activatedeid = self.execute('State X WHERE X name "activated"')[0][0]
-        deactivatedeid = self.execute('State X WHERE X name "deactivated"')[0][0]
-        e.change_state(deactivatedeid, u'deactivate 1')
-        self.commit()
-        e.change_state(activatedeid, u'activate 1')
-        self.commit()
-        e.change_state(deactivatedeid, u'deactivate 2')
-        self.commit()
-        # get a fresh user to avoid potential cache issues
-        e = self.entity('CWUser X WHERE X eid %s' % e.eid)
-        self.assertEquals([tr.comment for tr in e.reverse_wf_info_for],
-                          [None, 'deactivate 1', 'activate 1', 'deactivate 2'])
-        self.assertEquals(e.latest_trinfo().comment, 'deactivate 2')
-
 
 class InterfaceTC(EnvBasedTC):
 
     def test_nonregr_subclasses_and_mixins_interfaces(self):
+        CWUser = self.vreg['etypes'].etype_class('CWUser')
         self.failUnless(implements(CWUser, IWorkflowable))
         class MyUser(CWUser):
             __implements__ = (IMileStone,)
@@ -270,9 +106,13 @@
         self.vreg.register_appobject_class(MyUser)
         self.vreg['etypes'].initialization_completed()
         MyUser_ = self.vreg['etypes'].etype_class('CWUser')
-        self.failUnless(MyUser is MyUser_)
+        # a copy is done systematically
+        self.failUnless(issubclass(MyUser_, MyUser))
         self.failUnless(implements(MyUser_, IMileStone))
         self.failUnless(implements(MyUser_, IWorkflowable))
+        # original class should not have beed modified, only the copy
+        self.failUnless(implements(MyUser, IMileStone))
+        self.failIf(implements(MyUser, IWorkflowable))
 
 
 class SpecializedEntityClassesTC(EnvBasedTC):
@@ -295,11 +135,11 @@
                 id = etype
             self.vreg.register_appobject_class(Foo)
             eclass = self.select_eclass('SubDivision')
+            self.failUnless(eclass.__autogenerated__)
             if etype == 'SubDivision':
-                self.failUnless(eclass is Foo)
+                self.assertEquals(eclass.__bases__, (Foo,))
             else:
-                self.failUnless(eclass.__autogenerated__)
-                self.assertEquals(eclass.__bases__, (Foo,))
+                self.assertEquals(eclass.__bases__[0].__bases__, (Foo,))
         # check Division eclass is still selected for plain Division entities
         eclass = self.select_eclass('Division')
         self.assertEquals(eclass.id, 'Division')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/test/unittest_wfobjs.py	Fri Sep 18 11:24:37 2009 +0200
@@ -0,0 +1,430 @@
+from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb import ValidationError
+
+def add_wf(self, etype, name=None, default=False):
+    if name is None:
+        name = etype
+    wf = self.execute('INSERT Workflow X: X name %(n)s', {'n': unicode(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})
+    if default:
+        self.execute('SET ET default_workflow WF WHERE WF eid %(wf)s, ET name %(et)s',
+                     {'wf': wf.eid, 'et': etype})
+    return wf
+
+def parse_hist(wfhist):
+    return [(ti.previous_state.name, ti.new_state.name,
+             ti.transition and ti.transition.name, ti.comment)
+            for ti in wfhist]
+
+
+class WorkflowBuildingTC(EnvBasedTC):
+
+    def test_wf_construction(self):
+        wf = add_wf(self, 'Company')
+        foo = wf.add_state(u'foo', initial=True)
+        bar = wf.add_state(u'bar')
+        self.assertEquals(wf.state_by_name('bar').eid, bar.eid)
+        self.assertEquals(wf.state_by_name('barrr'), None)
+        baz = wf.add_transition(u'baz', (foo,), bar, ('managers',))
+        self.assertEquals(wf.transition_by_name('baz').eid, baz.eid)
+        self.assertEquals(len(baz.require_group), 1)
+        self.assertEquals(baz.require_group[0].name, 'managers')
+
+    def test_duplicated_state(self):
+        wf = add_wf(self, 'Company')
+        wf.add_state(u'foo', initial=True)
+        self.commit()
+        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'})
+        # no pb if not in the same workflow
+        wf2 = add_wf(self, 'Company')
+        foo = wf2.add_state(u'foo', initial=True)
+        self.commit()
+
+    def test_duplicated_transition(self):
+        wf = add_wf(self, 'Company')
+        foo = wf.add_state(u'foo', initial=True)
+        bar = wf.add_state(u'bar')
+        wf.add_transition(u'baz', (foo,), bar, ('managers',))
+        wf.add_transition(u'baz', (bar,), foo)
+        ex = self.assertRaises(ValidationError, self.commit)
+        # XXX enhance message
+        self.assertEquals(ex.errors, {'transition_of': 'unique constraint S name N, Y transition_of O, Y name N failed'})
+
+
+class WorkflowTC(EnvBasedTC):
+
+    def setup_database(self):
+        rschema = self.schema['in_state']
+        for x, y in rschema.iter_rdefs():
+            self.assertEquals(rschema.rproperty(x, y, 'cardinality'), '1*')
+        self.member = self.create_user('member')
+
+    def test_workflow_base(self):
+        e = self.create_user('toto')
+        self.assertEquals(e.state, 'activated')
+        e.change_state('deactivated', u'deactivate 1')
+        self.commit()
+        e.change_state('activated', u'activate 1')
+        self.commit()
+        e.change_state('deactivated', u'deactivate 2')
+        self.commit()
+        e.clear_related_cache('wf_info_for', 'object')
+        self.assertEquals([tr.comment for tr in e.reverse_wf_info_for],
+                          ['deactivate 1', 'activate 1', 'deactivate 2'])
+        self.assertEquals(e.latest_trinfo().comment, 'deactivate 2')
+
+    def test_possible_transitions(self):
+        user = self.entity('CWUser X')
+        trs = list(user.possible_transitions())
+        self.assertEquals(len(trs), 1)
+        self.assertEquals(trs[0].name, u'deactivate')
+        self.assertEquals(trs[0].destination().name, u'deactivated')
+        # test a std user get no possible transition
+        cnx = self.login('member')
+        # fetch the entity using the new session
+        trs = list(cnx.user().possible_transitions())
+        self.assertEquals(len(trs), 0)
+
+    def _test_manager_deactivate(self, user):
+        user.clear_related_cache('in_state', 'subject')
+        self.assertEquals(len(user.in_state), 1)
+        self.assertEquals(user.state, 'deactivated')
+        trinfo = user.latest_trinfo()
+        self.assertEquals(trinfo.previous_state.name, 'activated')
+        self.assertEquals(trinfo.new_state.name, 'deactivated')
+        self.assertEquals(trinfo.comment, 'deactivate user')
+        self.assertEquals(trinfo.comment_format, 'text/plain')
+        return trinfo
+
+    def test_change_state(self):
+        user = self.user()
+        user.change_state('deactivated', comment=u'deactivate user')
+        trinfo = self._test_manager_deactivate(user)
+        self.assertEquals(trinfo.transition, None)
+
+    def test_set_in_state_bad_wf(self):
+        wf = add_wf(self, 'CWUser')
+        s = wf.add_state(u'foo', initial=True)
+        self.commit()
+        ex = self.assertRaises(ValidationError, self.session().unsafe_execute,
+                               'SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
+                               {'x': self.user().eid, 's': s.eid}, 'x')
+        self.assertEquals(ex.errors, {'in_state': "state doesn't belong to entity's workflow. "
+                                      "You may want to set a custom workflow for this entity first."})
+
+    def test_fire_transition(self):
+        user = self.user()
+        user.fire_transition('deactivate', comment=u'deactivate user')
+        user.clear_all_caches()
+        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 test_subworkflow_base(self):
+        """subworkflow
+
+        +-----------+  tr1   +-----------+
+        | swfstate1 | ------>| swfstate2 |
+        +-----------+        +-----------+
+                  |  tr2  +-----------+
+                  `------>| swfstate3 |
+                          +-----------+
+
+        main workflow
+
+        +--------+  swftr1             +--------+
+        | state1 | -------[swfstate2]->| state2 |
+        +--------+     |               +--------+
+                       |               +--------+
+                       `-[swfstate3]-->| state3 |
+                                       +--------+
+        """
+        # sub-workflow
+        swf = add_wf(self, 'CWGroup', name='subworkflow')
+        swfstate1 = swf.add_state(u'swfstate1', initial=True)
+        swfstate2 = swf.add_state(u'swfstate2')
+        swfstate3 = swf.add_state(u'swfstate3')
+        tr1 = swf.add_transition(u'tr1', (swfstate1,), swfstate2)
+        tr2 = swf.add_transition(u'tr2', (swfstate1,), swfstate3)
+        # main workflow
+        mwf = add_wf(self, 'CWGroup', name='main workflow', default=True)
+        state1 = mwf.add_state(u'state1', initial=True)
+        state2 = mwf.add_state(u'state2')
+        state3 = mwf.add_state(u'state3')
+        swftr1 = mwf.add_wftransition(u'swftr1', swf, state1,
+                                      [(swfstate2, state2), (swfstate3, state3)])
+        self.assertEquals(swftr1.destination().eid, swfstate1.eid)
+        # workflows built, begin test
+        self.group = self.add_entity('CWGroup', name=u'grp1')
+        self.commit()
+        self.assertEquals(self.group.current_state.eid, state1.eid)
+        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.subworkflow_input_transition(), None)
+        self.group.fire_transition('swftr1', u'go')
+        self.commit()
+        self.group.clear_all_caches()
+        self.assertEquals(self.group.current_state.eid, swfstate1.eid)
+        self.assertEquals(self.group.current_workflow.eid, swf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.subworkflow_input_transition().eid, swftr1.eid)
+        self.group.fire_transition('tr1', u'go')
+        self.commit()
+        self.group.clear_all_caches()
+        self.assertEquals(self.group.current_state.eid, state2.eid)
+        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.subworkflow_input_transition(), None)
+        # force back to swfstate1 is impossible since we can't any more find
+        # subworkflow input transition
+        ex = self.assertRaises(ValidationError,
+                               self.group.change_state, swfstate1, u'gadget')
+        self.assertEquals(ex.errors, {'to_state': "state doesn't belong to entity's current workflow"})
+        self.rollback()
+        # force back to state1
+        self.group.change_state('state1', u'gadget')
+        self.group.fire_transition('swftr1', u'au')
+        self.group.clear_all_caches()
+        self.group.fire_transition('tr2', u'chapeau')
+        self.commit()
+        self.group.clear_all_caches()
+        self.assertEquals(self.group.current_state.eid, state3.eid)
+        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertListEquals(parse_hist(self.group.workflow_history),
+                              [('state1', 'swfstate1', 'swftr1', 'go'),
+                               ('swfstate1', 'swfstate2', 'tr1', 'go'),
+                               ('swfstate2', 'state2', 'swftr1', 'exiting from subworkflow subworkflow'),
+                               ('state2', 'state1', None, 'gadget'),
+                               ('state1', 'swfstate1', 'swftr1', 'au'),
+                               ('swfstate1', 'swfstate3', 'tr2', 'chapeau'),
+                               ('swfstate3', 'state3', 'swftr1', 'exiting from subworkflow subworkflow'),
+                               ])
+
+    def test_subworkflow_exit_consistency(self):
+        # sub-workflow
+        swf = add_wf(self, 'CWGroup', name='subworkflow')
+        swfstate1 = swf.add_state(u'swfstate1', initial=True)
+        swfstate2 = swf.add_state(u'swfstate2')
+        tr1 = swf.add_transition(u'tr1', (swfstate1,), swfstate2)
+        # main workflow
+        mwf = add_wf(self, 'CWGroup', name='main workflow', default=True)
+        state1 = mwf.add_state(u'state1', initial=True)
+        state2 = mwf.add_state(u'state2')
+        state3 = mwf.add_state(u'state3')
+        mwf.add_wftransition(u'swftr1', swf, state1,
+                             [(swfstate2, state2), (swfstate2, state3)])
+        ex = self.assertRaises(ValidationError, self.commit)
+        self.assertEquals(ex.errors, {'subworkflow_exit': u"can't have multiple exits on the same state"})
+
+
+class CustomWorkflowTC(EnvBasedTC):
+
+    def setup_database(self):
+        self.member = self.create_user('member')
+
+    def tearDown(self):
+        super(CustomWorkflowTC, self).tearDown()
+        self.execute('DELETE X custom_workflow WF')
+
+    def test_custom_wf_replace_state_no_history(self):
+        """member in inital state with no previous history, state is simply
+        redirected when changing workflow
+        """
+        wf = add_wf(self, 'CWUser')
+        wf.add_state('asleep', initial=True)
+        self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
+                     {'wf': wf.eid, 'x': self.member.eid})
+        self.member.clear_all_caches()
+        self.assertEquals(self.member.state, 'activated')# no change before commit
+        self.commit()
+        self.member.clear_all_caches()
+        self.assertEquals(self.member.current_workflow.eid, wf.eid)
+        self.assertEquals(self.member.state, 'asleep')
+        self.assertEquals(self.member.workflow_history, [])
+
+    def test_custom_wf_replace_state_keep_history(self):
+        """member in inital state with some history, state is redirected and
+        state change is recorded to history
+        """
+        self.member.fire_transition('deactivate')
+        self.member.fire_transition('activate')
+        wf = add_wf(self, 'CWUser')
+        wf.add_state('asleep', initial=True)
+        self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
+                     {'wf': wf.eid, 'x': self.member.eid})
+        self.commit()
+        self.member.clear_all_caches()
+        self.assertEquals(self.member.current_workflow.eid, wf.eid)
+        self.assertEquals(self.member.state, 'asleep')
+        self.assertEquals(parse_hist(self.member.workflow_history),
+                          [('activated', 'deactivated', 'deactivate', None),
+                           ('deactivated', 'activated', 'activate', None),
+                           ('activated', 'asleep', None, 'workflow changed to "CWUser"')])
+
+    def test_custom_wf_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 doesn't apply to entity type"""
+        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, "default user 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 "default user workflow"'),])
+
+
+from cubicweb.devtools.apptest import RepositoryBasedTC
+
+class WorkflowHooksTC(RepositoryBasedTC):
+
+    def setUp(self):
+        RepositoryBasedTC.setUp(self)
+        self.wf = self.session.user.current_workflow
+        self.s_activated = self.wf.state_by_name('activated').eid
+        self.s_deactivated = self.wf.state_by_name('deactivated').eid
+        self.s_dummy = self.wf.add_state(u'dummy').eid
+        self.wf.add_transition(u'dummy', (self.s_deactivated,), self.s_dummy)
+        ueid = self.create_user('stduser', commit=False)
+        # test initial state is set
+        rset = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s',
+                            {'x' : ueid})
+        self.failIf(rset, rset.rows)
+        self.commit()
+        initialstate = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s',
+                                    {'x' : ueid})[0][0]
+        self.assertEquals(initialstate, u'activated')
+        # give access to users group on the user's wf transitions
+        # so we can test wf enforcing on euser (managers don't have anymore this
+        # enforcement
+        self.execute('SET X require_group G '
+                     'WHERE G name "users", X transition_of WF, WF eid %(wf)s',
+                     {'wf': self.wf.eid})
+        self.commit()
+
+    def tearDown(self):
+        self.execute('DELETE X require_group G '
+                     'WHERE G name "users", X transition_of WF, WF eid %(wf)s',
+                     {'wf': self.wf.eid})
+        self.commit()
+        RepositoryBasedTC.tearDown(self)
+
+    # XXX currently, we've to rely on hooks to set initial state, or to use unsafe_execute
+    # def test_initial_state(self):
+    #     cnx = self.login('stduser')
+    #     cu = cnx.cursor()
+    #     self.assertRaises(ValidationError, cu.execute,
+    #                       'INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, '
+    #                       'X in_state S WHERE S name "deactivated"', {'pwd': 'oops'})
+    #     cnx.close()
+    #     # though managers can do whatever he want
+    #     self.execute('INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, '
+    #                  'X in_state S, X in_group G WHERE S name "deactivated", G name "users"', {'pwd': 'oops'})
+    #     self.commit()
+
+    # test that the workflow is correctly enforced
+    def test_transition_checking1(self):
+        cnx = self.login('stduser')
+        user = cnx.user(self.current_session())
+        ex = self.assertRaises(ValidationError,
+                               user.fire_transition, 'activate')
+        self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"})
+        cnx.close()
+
+    def test_transition_checking2(self):
+        cnx = self.login('stduser')
+        user = cnx.user(self.current_session())
+        assert user.state == 'activated'
+        ex = self.assertRaises(ValidationError,
+                               user.fire_transition, 'dummy')
+        self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"})
+        cnx.close()
+
+    def test_transition_checking3(self):
+        cnx = self.login('stduser')
+        session = self.current_session()
+        user = cnx.user(session)
+        user.fire_transition('deactivate')
+        cnx.commit()
+        session.set_pool()
+        ex = self.assertRaises(ValidationError,
+                               user.fire_transition, 'deactivate')
+        self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"})
+        # get back now
+        user.fire_transition('activate')
+        cnx.commit()
+        cnx.close()
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/entities/wfobjs.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/entities/wfobjs.py	Fri Sep 18 11:24:37 2009 +0200
@@ -7,23 +7,176 @@
 """
 __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 WorkflowException(Exception): pass
+
+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()
+
+    def iter_workflows(self, _done=None):
+        """return an iterator on actual workflows, eg this workflow and its
+        subworkflows
+        """
+        # infinite loop safety belt
+        if _done is None:
+            _done = set()
+        yield self
+        _done.add(self.eid)
+        for tr in self.req.execute('Any T WHERE T is WorkflowTransition, '
+                                   'T transition_of WF, WF eid %(wf)s',
+                                   {'wf': self.eid}).entities():
+            if tr.subwf.eid in _done:
+                continue
+            for subwf in tr.subwf.iter_workflows(_done):
+                yield subwf
+
+    # 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
+
+    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):
+        """add a state to this workflow"""
+        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, trtype, name, fromstates,
+                        requiredgroups=(), conditions=(), **kwargs):
+        tr = self.req.create_entity(trtype, 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'))
+        assert fromstates, fromstates
+        if not isinstance(fromstates, (tuple, list)):
+            fromstates = (fromstates,)
+        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'))
+        tr.set_transition_permissions(requiredgroups, conditions, reset=False)
+        return tr
+
+    def add_transition(self, name, fromstates, tostate,
+                       requiredgroups=(), conditions=(), **kwargs):
+        """add a transition to this workflow from some state(s) to another"""
+        tr = self._add_transition('Transition', name, fromstates,
+                                  requiredgroups, conditions, **kwargs)
+        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'))
+        return tr
+
+    def add_wftransition(self, name, subworkflow, fromstates, exitpoints,
+                       requiredgroups=(), conditions=(), **kwargs):
+        """add a workflow transition to this workflow"""
+        tr = self._add_transition('WorkflowTransition', name, fromstates,
+                                  requiredgroups, conditions, **kwargs)
+        if hasattr(subworkflow, 'eid'):
+            subworkflow = subworkflow.eid
+        self.req.execute('SET T subworkflow WF WHERE WF eid %(wf)s,T eid %(t)s',
+                         {'t': tr.eid, 'wf': subworkflow}, ('wf', 't'))
+        for fromstate, tostate in exitpoints:
+            tr.add_exit_point(fromstate, tostate)
+        return tr
 
 
-class Transition(AnyEntity):
-    """customized class for Transition entities
+class BaseTransition(AnyEntity):
+    """customized class for abstract transition
 
-    provides a specific may_be_passed method to check if the relation may be
-    passed by the logged user
+    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 WorkflowException('should not be instantiated')
+        super(BaseTransition, self).__init__(*args, **kwargs)
+
+    @property
+    def workflow(self):
+        return self.transition_of[0]
 
-        `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 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)
+
+    def may_be_fired(self, eid):
+        """return true if the logged user may fire this transition
+
+        `eid` is the eid of the object on which we may fire the transition
         """
         user = self.req.user
         # check user is at least in one of the required groups if any
@@ -43,47 +196,122 @@
             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:
+            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]
+
+
+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
+
+    def add_exit_point(self, fromstate, tostate):
+        if hasattr(fromstate, 'eid'):
+            fromstate = fromstate.eid
+        if hasattr(tostate, 'eid'):
+            tostate = tostate.eid
+        self.req.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, '
+                         'X subworkflow_state FS, X destination_state TS '
+                         'WHERE T eid %(t)s, FS eid %(fs)s, TS eid %(ts)s',
+                         {'t': self.eid, 'fs': fromstate, 'ts': tostate},
+                         ('t', 'fs', 'ts'))
+
+    def get_exit_point(self, state):
+        """if state is an exit point, return its associated destination state"""
+        if hasattr(state, 'eid'):
+            state = state.eid
+        stateeid = self.exit_points().get(state)
+        if stateeid is not None:
+            return self.req.entity_from_eid(stateeid)
+        return None
+
+    @cached
+    def exit_points(self):
+        result = {}
+        for ep in self.subworkflow_exit:
+            result[ep.subwf_state.eid] = ep.destination.eid
+        return result
+
+    def clear_all_caches(self):
+        super(WorkflowableMixIn, self).clear_all_caches()
+        clear_cache(self, 'exit_points')
+
+
+class SubWorkflowExitPoint(AnyEntity):
+    """customized class for SubWorkflowExitPoint entities"""
+    id = 'SubWorkflowExitPoint'
+
+    @property
+    def subwf_state(self):
+        return self.subworkflow_state[0]
+
+    @property
+    def destination(self):
+        return self.destination_state[0]
+
 
 class State(AnyEntity):
-    """customized class for State entities
-
-    provides a specific transitions method returning transitions that may be
-    passed by the current user for the given entity
-    """
+    """customized class for State entities"""
     id = 'State'
     fetch_attrs, fetch_order = fetch_config(['name'])
     rest_attr = 'eid'
 
-    def transitions(self, entity, desteid=None):
-        """generates transition that MAY be passed"""
-        rql = ('Any T,N,DS where S allowed_transition T, S eid %(x)s, '
-               'T name N, T destination_state DS, '
-               'T transition_of ET, ET name %(et)s')
-        if desteid is not None:
-            rql += ', DS eid %(ds)s'
-        rset = self.req.execute(rql, {'x': self.eid, 'et': str(entity.e_schema),
-                                         'ds': desteid}, 'x')
-        for tr in rset.entities():
-            if tr.may_be_passed(entity.eid, self.eid):
-                yield tr
+    @property
+    def workflow(self):
+        # take care, may be missing in multi-sources configuration
+        return self.state_of and self.state_of[0]
 
     def after_deletion_path(self):
         """return (path, parameters) which should be used as redirect
         information when this entity is being deleted
         """
         if self.state_of:
-            return self.state_of[0].rest_path(), {'vid': 'workflow'}
+            return self.state_of[0].rest_path(), {}
         return super(State, self).after_deletion_path()
 
 
@@ -95,15 +323,20 @@
                                             pclass=None) # don't want modification_date
     @property
     def for_entity(self):
-        return self.wf_info_for and self.wf_info_for[0]
+        return self.wf_info_for[0]
+
     @property
     def previous_state(self):
-        return self.from_state and self.from_state[0]
+        return self.from_state[0]
 
     @property
     def new_state(self):
         return self.to_state[0]
 
+    @property
+    def transition(self):
+        return self.by_transition and self.by_transition[0] or None
+
     def after_deletion_path(self):
         """return (path, parameters) which should be used as redirect
         information when this entity is being deleted
@@ -111,3 +344,176 @@
         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 main_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_workflow(self):
+        """return current workflow applied to this entity"""
+        return self.current_state and self.current_state.workflow or self.main_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 _add_trinfo(self, comment, commentformat, treid=None, tseid=None):
+        kwargs = {}
+        if comment is not None:
+            kwargs['comment'] = comment
+            if commentformat is not None:
+                kwargs['comment_format'] = commentformat
+        args = [('wf_info_for', 'E')]
+        kwargs['E'] = self.eid
+        if treid is not None:
+            args.append( ('by_transition', 'T') )
+            kwargs['T'] = treid
+        if tseid is not None:
+            args.append( ('to_state', 'S') )
+            kwargs['S'] = tseid
+        return self.req.create_entity('TrInfo', *args, **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, trname)
+        return self._add_trinfo(comment, commentformat, tr.eid)
+
+    def change_state(self, statename, comment=None, commentformat=None, tr=None):
+        """change the entity's state to the given state (name or entity) 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 hasattr(statename, 'eid'):
+            stateeid = statename.eid
+        else:
+            if not isinstance(statename, basestring):
+                warn('give a state name')
+                state = self.current_workflow.state_by_eid(statename)
+            else:
+                state = self.current_workflow.state_by_name(statename)
+            if state is None:
+                raise WorkflowException('not a %s state: %s' % (self.id,
+                                                                statename))
+            stateeid = state.eid
+        # XXX try to find matching transition?
+        return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid)
+
+    def subworkflow_input_transition(self):
+        """return the transition which has went through the current sub-workflow
+        """
+        if self.main_workflow.eid == self.current_workflow.eid:
+            return # doesn't make sense
+        subwfentries = []
+        for trinfo in reversed(self.workflow_history):
+            if (trinfo.transition and
+                trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid):
+                # entering or leaving a subworkflow
+                if (subwfentries and
+                    subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid):
+                    # leave
+                    del subwfentries[-1]
+                else:
+                    # enter
+                    subwfentries.append(trinfo)
+        if not subwfentries:
+            return None
+        return subwfentries[-1].transition
+
+    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	Thu Sep 17 19:38:04 2009 +0200
+++ b/entity.py	Fri Sep 18 11:24:37 2009 +0200
@@ -15,12 +15,14 @@
 from logilab.common.deprecation import deprecated
 from logilab.mtconverter import TransformData, TransformError, xml_escape
 
+from rql import parse
 from rql.utils import rqlvar_maker
 
 from cubicweb import Unauthorized
 from cubicweb.rset import ResultSet
 from cubicweb.selectors import yes
 from cubicweb.appobject import AppObject
+from cubicweb.rqlrewrite import RQLRewriter
 from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint, bw_normalize_etype
 
 from cubicweb.common.uilib import printable_value, soup2xhtml
@@ -82,6 +84,7 @@
             except AttributeError:
                 continue
 
+
 class _metaentity(type):
     """this metaclass sets the relation tags on the entity class
     and deals with the `widgets` attribute
@@ -163,7 +166,7 @@
     id = None
     rest_attr = None
     fetch_attrs = None
-    skip_copy_for = ()
+    skip_copy_for = ('in_state',)
     # class attributes set automatically at registration time
     e_schema = None
 
@@ -182,15 +185,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
@@ -457,7 +460,8 @@
                 return self.mtc_transform(value.getvalue(), attrformat, format,
                                           encoding)
             return u''
-        value = printable_value(self.req, attrtype, value, props, displaytime)
+        value = printable_value(self.req, attrtype, value, props,
+                                displaytime=displaytime)
         if format == 'text/html':
             value = xml_escape(value)
         return value
@@ -488,13 +492,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
@@ -625,14 +622,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
@@ -725,24 +722,13 @@
 
     # generic vocabulary methods ##############################################
 
-    @deprecated('see new form api')
-    def vocabulary(self, rtype, role='subject', limit=None):
-        """vocabulary functions must return a list of couples
-        (label, eid) that will typically be used to fill the
-        edition view's combobox.
-
-        If `eid` is None in one of these couples, it should be
-        interpreted as a separator in case vocabulary results are grouped
-        """
-        from logilab.common.testlib import mock_object
-        form = self.vreg.select('forms', 'edition', self.req, entity=self)
-        field = mock_object(name=rtype, role=role)
-        return form.form_field_vocabulary(field, limit)
-
     def unrelated_rql(self, rtype, targettype, role, ordermethod=None,
                       vocabconstraints=True):
         """build a rql to fetch `targettype` entities unrelated to this entity
-        using (rtype, role) relation
+        using (rtype, role) relation.
+
+        Consider relation permissions so that returned entities may be actually
+        linked by `rtype`.
         """
         ordermethod = ordermethod or 'fetch_unrelated_order'
         if isinstance(rtype, basestring):
@@ -755,8 +741,17 @@
             objtype, subjtype = self.e_schema, targettype
         if self.has_eid():
             restriction = ['NOT S %s O' % rtype, '%s eid %%(x)s' % evar]
+            args = {'x': self.eid}
+            if role == 'subject':
+                securitycheck_args = {'fromeid': self.eid}
+            else:
+                securitycheck_args = {'toeid': self.eid}
         else:
             restriction = []
+            args = {}
+            securitycheck_args = {}
+        insertsecurity = (rtype.has_local_role('add') and not
+                          rtype.has_perm(self.req, 'add', **securitycheck_args))
         constraints = rtype.rproperty(subjtype, objtype, 'constraints')
         if vocabconstraints:
             # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
@@ -773,20 +768,29 @@
         if not ' ORDERBY ' in rql:
             before, after = rql.split(' WHERE ', 1)
             rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after)
-        return rql
+        if insertsecurity:
+            rqlexprs = rtype.get_rqlexprs('add')
+            rewriter = RQLRewriter(self.req)
+            rqlst = self.req.vreg.parse(self.req, rql, args)
+            for select in rqlst.children:
+                rewriter.rewrite(select, [((searchedvar, searchedvar), rqlexprs)],
+                                 select.solutions, args)
+            rql = rqlst.as_string()
+        return rql, args
 
     def unrelated(self, rtype, targettype, role='subject', limit=None,
                   ordermethod=None):
         """return a result set of target type objects that may be related
         by a given relation, with self as subject or object
         """
-        rql = self.unrelated_rql(rtype, targettype, role, ordermethod)
+        try:
+            rql, args = self.unrelated_rql(rtype, targettype, role, ordermethod)
+        except Unauthorized:
+            return self.req.empty_rset()
         if limit is not None:
             before, after = rql.split(' WHERE ', 1)
             rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
-        if self.has_eid():
-            return self.req.execute(rql, {'x': self.eid})
-        return self.req.execute(rql)
+        return self.req.execute(rql, args, tuple(args))
 
     # relations cache handling ################################################
 
@@ -837,6 +841,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):
@@ -937,6 +946,20 @@
                     words += entity.get_words()
         return words
 
+    @deprecated('[3.2] see new form api')
+    def vocabulary(self, rtype, role='subject', limit=None):
+        """vocabulary functions must return a list of couples
+        (label, eid) that will typically be used to fill the
+        edition view's combobox.
+
+        If `eid` is None in one of these couples, it should be
+        interpreted as a separator in case vocabulary results are grouped
+        """
+        from logilab.common.testlib import mock_object
+        form = self.vreg.select('forms', 'edition', self.req, entity=self)
+        field = mock_object(name=rtype, role=role)
+        return form.form_field_vocabulary(field, limit)
+
 
 # attribute and relation descriptors ##########################################
 
--- a/etwist/server.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/etwist/server.py	Fri Sep 18 11:24:37 2009 +0200
@@ -15,7 +15,7 @@
 from urlparse import urlsplit, urlunsplit
 import hotshot
 
-from twisted.application import service, strports
+from twisted.application import strports
 from twisted.internet import reactor, task, threads
 from twisted.internet.defer import maybeDeferred
 from twisted.web2 import channel, http, server, iweb
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/xhtml2fo.py	Fri Sep 18 11:24:37 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)
+
--- a/gettext.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/gettext.py	Fri Sep 18 11:24:37 2009 +0200
@@ -8,7 +8,6 @@
 languages.  L10N refers to the adaptation of your program, once
 internationalized, to the local language and cultural habits.
 
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 
 # This module represents the integration of work, contributions, feedback, and
@@ -47,7 +46,7 @@
 #   find this format documented anywhere.
 
 
-import copy, os, re, struct, sys
+import locale, copy, os, re, struct, sys
 from errno import ENOENT
 
 
@@ -78,7 +77,10 @@
     Python lambda function that implements an equivalent expression.
     """
     # Security check, allow only the "n" identifier
-    from StringIO import StringIO
+    try:
+        from cStringIO import StringIO
+    except ImportError:
+        from StringIO import StringIO
     import token, tokenize
     tokens = tokenize.generate_tokens(StringIO(plural).readline)
     try:
@@ -172,6 +174,7 @@
     def __init__(self, fp=None):
         self._info = {}
         self._charset = None
+        self._output_charset = None
         self._fallback = None
         if fp is not None:
             self._parse(fp)
@@ -190,6 +193,21 @@
             return self._fallback.gettext(message)
         return message
 
+    def pgettext(self, context, message):
+        if self._fallback:
+            return self._fallback.pgettext(context, message)
+        return message
+
+    def lgettext(self, message):
+        if self._fallback:
+            return self._fallback.lgettext(message)
+        return message
+
+    def lpgettext(self, context, message):
+        if self._fallback:
+            return self._fallback.lpgettext(context, message)
+        return message
+
     def ngettext(self, msgid1, msgid2, n):
         if self._fallback:
             return self._fallback.ngettext(msgid1, msgid2, n)
@@ -198,11 +216,40 @@
         else:
             return msgid2
 
+    def npgettext(self, context, msgid1, msgid2, n):
+        if self._fallback:
+            return self._fallback.npgettext(context, msgid1, msgid2, n)
+        if n == 1:
+            return msgid1
+        else:
+            return msgid2
+
+    def lngettext(self, msgid1, msgid2, n):
+        if self._fallback:
+            return self._fallback.lngettext(msgid1, msgid2, n)
+        if n == 1:
+            return msgid1
+        else:
+            return msgid2
+
+    def lnpgettext(self, context, msgid1, msgid2, n):
+        if self._fallback:
+            return self._fallback.lnpgettext(context, msgid1, msgid2, n)
+        if n == 1:
+            return msgid1
+        else:
+            return msgid2
+
     def ugettext(self, message):
         if self._fallback:
             return self._fallback.ugettext(message)
         return unicode(message)
 
+    def upgettext(self, context, message):
+        if self._fallback:
+            return self._fallback.upgettext(context, message)
+        return unicode(message)
+
     def ungettext(self, msgid1, msgid2, n):
         if self._fallback:
             return self._fallback.ungettext(msgid1, msgid2, n)
@@ -211,15 +258,49 @@
         else:
             return unicode(msgid2)
 
+    def unpgettext(self, context, msgid1, msgid2, n):
+        if self._fallback:
+            return self._fallback.unpgettext(context, msgid1, msgid2, n)
+        if n == 1:
+            return unicode(msgid1)
+        else:
+            return unicode(msgid2)
+
     def info(self):
         return self._info
 
     def charset(self):
         return self._charset
 
-    def install(self, unicode=False):
+    def output_charset(self):
+        return self._output_charset
+
+    def set_output_charset(self, charset):
+        self._output_charset = charset
+
+    def install(self, unicode=False, names=None):
         import __builtin__
         __builtin__.__dict__['_'] = unicode and self.ugettext or self.gettext
+        if hasattr(names, "__contains__"):
+            if "gettext" in names:
+                __builtin__.__dict__['gettext'] = __builtin__.__dict__['_']
+            if "pgettext" in names:
+                __builtin__.__dict__['pgettext'] = (unicode and self.upgettext
+                                                    or self.pgettext)
+            if "ngettext" in names:
+                __builtin__.__dict__['ngettext'] = (unicode and self.ungettext
+                                                             or self.ngettext)
+            if "npgettext" in names:
+                __builtin__.__dict__['npgettext'] = \
+                    (unicode and self.unpgettext or self.npgettext)
+            if "lgettext" in names:
+                __builtin__.__dict__['lgettext'] = self.lgettext
+            if "lpgettext" in names:
+                __builtin__.__dict__['lpgettext'] = self.lpgettext
+            if "lngettext" in names:
+                __builtin__.__dict__['lngettext'] = self.lngettext
+            if "lnpgettext" in names:
+                __builtin__.__dict__['lnpgettext'] = self.lnpgettext
 
 
 class GNUTranslations(NullTranslations):
@@ -227,6 +308,10 @@
     LE_MAGIC = 0x950412deL
     BE_MAGIC = 0xde120495L
 
+    # The encoding of a msgctxt and a msgid in a .mo file is
+    # msgctxt + "\x04" + msgid (gettext version >= 0.15)
+    CONTEXT_ENCODING = "%s\x04%s"
+
     def _parse(self, fp):
         """Override this method to support alternative .mo formats."""
         unpack = struct.unpack
@@ -262,18 +347,19 @@
             # See if we're looking at GNU .mo conventions for metadata
             if mlen == 0:
                 # Catalog description
-                # don't handle multi-lines fields here, and skip
-                # lines which don't look like a header description
-                # (e.g. "header: value")
                 lastk = k = None
                 for item in tmsg.splitlines():
                     item = item.strip()
-                    if not item or not ':' in item:
+                    if not item:
                         continue
-                    k, v = item.split(':', 1)
-                    k = k.strip().lower()
-                    v = v.strip()
-                    self._info[k] = v
+                    if ':' in item:
+                        k, v = item.split(':', 1)
+                        k = k.strip().lower()
+                        v = v.strip()
+                        self._info[k] = v
+                        lastk = k
+                    elif lastk:
+                        self._info[lastk] += '\n' + item
                     if k == 'content-type':
                         self._charset = v.split('charset=')[1]
                     elif k == 'plural-forms':
@@ -289,7 +375,7 @@
             # cause no problems since us-ascii should always be a subset of
             # the charset encoding.  We may want to fall back to 8-bit msgids
             # if the Unicode conversion fails.
-            if msg.find('\x00') >= 0:
+            if '\x00' in msg:
                 # Plural forms
                 msgid1, msgid2 = msg.split('\x00')
                 tmsg = tmsg.split('\x00')
@@ -315,14 +401,56 @@
                 return self._fallback.gettext(message)
             return message
         # Encode the Unicode tmsg back to an 8-bit string, if possible
-        if self._charset:
+        if self._output_charset:
+            return tmsg.encode(self._output_charset)
+        elif self._charset:
+            return tmsg.encode(self._charset)
+        return tmsg
+
+    def pgettext(self, context, message):
+        ctxt_msg_id = self.CONTEXT_ENCODING % (context, message)
+        missing = object()
+        tmsg = self._catalog.get(ctxt_msg_id, missing)
+        if tmsg is missing:
+            if self._fallback:
+                return self._fallback.pgettext(context, message)
+            return message
+        # Encode the Unicode tmsg back to an 8-bit string, if possible
+        if self._output_charset:
+            return tmsg.encode(self._output_charset)
+        elif self._charset:
             return tmsg.encode(self._charset)
         return tmsg
 
+    def lgettext(self, message):
+        missing = object()
+        tmsg = self._catalog.get(message, missing)
+        if tmsg is missing:
+            if self._fallback:
+                return self._fallback.lgettext(message)
+            return message
+        if self._output_charset:
+            return tmsg.encode(self._output_charset)
+        return tmsg.encode(locale.getpreferredencoding())
+
+    def lpgettext(self, context, message):
+        ctxt_msg_id = self.CONTEXT_ENCODING % (context, message)
+        missing = object()
+        tmsg = self._catalog.get(ctxt_msg_id, missing)
+        if tmsg is missing:
+            if self._fallback:
+                return self._fallback.lpgettext(context, message)
+            return message
+        if self._output_charset:
+            return tmsg.encode(self._output_charset)
+        return tmsg.encode(locale.getpreferredencoding())
+
     def ngettext(self, msgid1, msgid2, n):
         try:
             tmsg = self._catalog[(msgid1, self.plural(n))]
-            if self._charset:
+            if self._output_charset:
+                return tmsg.encode(self._output_charset)
+            elif self._charset:
                 return tmsg.encode(self._charset)
             return tmsg
         except KeyError:
@@ -333,6 +461,52 @@
             else:
                 return msgid2
 
+    def npgettext(self, context, msgid1, msgid2, n):
+        ctxt_msg_id = self.CONTEXT_ENCODING % (context, msgid1)
+        try:
+            tmsg = self._catalog[(ctxt_msg_id, self.plural(n))]
+            if self._output_charset:
+                return tmsg.encode(self._output_charset)
+            elif self._charset:
+                return tmsg.encode(self._charset)
+            return tmsg
+        except KeyError:
+            if self._fallback:
+                return self._fallback.npgettext(context, msgid1, msgid2, n)
+            if n == 1:
+                return msgid1
+            else:
+                return msgid2        
+
+    def lngettext(self, msgid1, msgid2, n):
+        try:
+            tmsg = self._catalog[(msgid1, self.plural(n))]
+            if self._output_charset:
+                return tmsg.encode(self._output_charset)
+            return tmsg.encode(locale.getpreferredencoding())
+        except KeyError:
+            if self._fallback:
+                return self._fallback.lngettext(msgid1, msgid2, n)
+            if n == 1:
+                return msgid1
+            else:
+                return msgid2
+
+    def lnpgettext(self, context, msgid1, msgid2, n):
+        ctxt_msg_id = self.CONTEXT_ENCODING % (context, msgid1)
+        try:
+            tmsg = self._catalog[(ctxt_msg_id, self.plural(n))]
+            if self._output_charset:
+                return tmsg.encode(self._output_charset)
+            return tmsg.encode(locale.getpreferredencoding())
+        except KeyError:
+            if self._fallback:
+                return self._fallback.lnpgettext(context, msgid1, msgid2, n)
+            if n == 1:
+                return msgid1
+            else:
+                return msgid2
+
     def ugettext(self, message):
         missing = object()
         tmsg = self._catalog.get(message, missing)
@@ -342,6 +516,18 @@
             return unicode(message)
         return tmsg
 
+    def upgettext(self, context, message):
+        ctxt_message_id = self.CONTEXT_ENCODING % (context, message)
+        missing = object()
+        tmsg = self._catalog.get(ctxt_message_id, missing)
+        if tmsg is missing:
+            # XXX logilab patch for compat w/ catalog generated by cw < 3.5
+            return self.ugettext(message)
+            if self._fallback:
+                return self._fallback.upgettext(context, message)
+            return unicode(message)
+        return tmsg
+
     def ungettext(self, msgid1, msgid2, n):
         try:
             tmsg = self._catalog[(msgid1, self.plural(n))]
@@ -354,6 +540,19 @@
                 tmsg = unicode(msgid2)
         return tmsg
 
+    def unpgettext(self, context, msgid1, msgid2, n):
+        ctxt_message_id = self.CONTEXT_ENCODING % (context, msgid1)
+        try:
+            tmsg = self._catalog[(ctxt_message_id, self.plural(n))]
+        except KeyError:
+            if self._fallback:
+                return self._fallback.unpgettext(context, msgid1, msgid2, n)
+            if n == 1:
+                tmsg = unicode(msgid1)
+            else:
+                tmsg = unicode(msgid2)
+        return tmsg
+
 
 # Locate a .mo file using the gettext strategy
 def find(domain, localedir=None, languages=None, all=0):
@@ -397,7 +596,7 @@
 _translations = {}
 
 def translation(domain, localedir=None, languages=None,
-                class_=None, fallback=False):
+                class_=None, fallback=False, codeset=None):
     if class_ is None:
         class_ = GNUTranslations
     mofiles = find(domain, localedir, languages, all=1)
@@ -414,9 +613,12 @@
         t = _translations.get(key)
         if t is None:
             t = _translations.setdefault(key, class_(open(mofile, 'rb')))
-        # Copy the translation object to allow setting fallbacks.
-        # All other instance data is shared with the cached object.
+        # Copy the translation object to allow setting fallbacks and
+        # output charset. All other instance data is shared with the
+        # cached object.
         t = copy.copy(t)
+        if codeset:
+            t.set_output_charset(codeset)
         if result is None:
             result = t
         else:
@@ -424,13 +626,16 @@
     return result
 
 
-def install(domain, localedir=None, unicode=False):
-    translation(domain, localedir, fallback=True).install(unicode)
+def install(domain, localedir=None, unicode=False, codeset=None, names=None):
+    t = translation(domain, localedir, fallback=True, codeset=codeset)
+    t.install(unicode, names)
 
 
 
 # a mapping b/w domains and locale directories
 _localedirs = {}
+# a mapping b/w domains and codesets
+_localecodesets = {}
 # current global domain, `messages' used for compatibility w/ GNU gettext
 _current_domain = 'messages'
 
@@ -443,22 +648,55 @@
 
 
 def bindtextdomain(domain, localedir=None):
+    global _localedirs
     if localedir is not None:
         _localedirs[domain] = localedir
     return _localedirs.get(domain, _default_localedir)
 
 
+def bind_textdomain_codeset(domain, codeset=None):
+    global _localecodesets
+    if codeset is not None:
+        _localecodesets[domain] = codeset
+    return _localecodesets.get(domain)
+
+
 def dgettext(domain, message):
     try:
-        t = translation(domain, _localedirs.get(domain, None))
+        t = translation(domain, _localedirs.get(domain, None),
+                        codeset=_localecodesets.get(domain))
     except IOError:
         return message
     return t.gettext(message)
 
+def dpgettext(domain, context, message):
+    try:
+        t = translation(domain, _localedirs.get(domain, None),
+                        codeset=_localecodesets.get(domain))
+    except IOError:
+        return message
+    return t.pgettext(context, message)
+
+def ldgettext(domain, message):
+    try:
+        t = translation(domain, _localedirs.get(domain, None),
+                        codeset=_localecodesets.get(domain))
+    except IOError:
+        return message
+    return t.lgettext(message)
+
+def ldpgettext(domain, context, message):
+    try:
+        t = translation(domain, _localedirs.get(domain, None),
+                        codeset=_localecodesets.get(domain))
+    except IOError:
+        return message
+    return t.lpgettext(context, message)
 
 def dngettext(domain, msgid1, msgid2, n):
     try:
-        t = translation(domain, _localedirs.get(domain, None))
+        t = translation(domain, _localedirs.get(domain, None),
+                        codeset=_localecodesets.get(domain))
     except IOError:
         if n == 1:
             return msgid1
@@ -466,14 +704,62 @@
             return msgid2
     return t.ngettext(msgid1, msgid2, n)
 
+def dnpgettext(domain, context, msgid1, msgid2, n):
+    try:
+        t = translation(domain, _localedirs.get(domain, None),
+                        codeset=_localecodesets.get(domain))
+    except IOError:
+        if n == 1:
+            return msgid1
+        else:
+            return msgid2
+    return t.npgettext(context, msgid1, msgid2, n)
+
+def ldngettext(domain, msgid1, msgid2, n):
+    try:
+        t = translation(domain, _localedirs.get(domain, None),
+                        codeset=_localecodesets.get(domain))
+    except IOError:
+        if n == 1:
+            return msgid1
+        else:
+            return msgid2
+    return t.lngettext(msgid1, msgid2, n)
+
+def ldnpgettext(domain, context, msgid1, msgid2, n):
+    try:
+        t = translation(domain, _localedirs.get(domain, None),
+                        codeset=_localecodesets.get(domain))
+    except IOError:
+        if n == 1:
+            return msgid1
+        else:
+            return msgid2
+    return t.lnpgettext(context, msgid1, msgid2, n)
 
 def gettext(message):
     return dgettext(_current_domain, message)
 
+def pgettext(context, message):
+    return dpgettext(_current_domain, context, message)
+
+def lgettext(message):
+    return ldgettext(_current_domain, message)
+
+def lpgettext(context, message):
+    return ldpgettext(_current_domain, context, message)
 
 def ngettext(msgid1, msgid2, n):
     return dngettext(_current_domain, msgid1, msgid2, n)
 
+def npgettext(context, msgid1, msgid2, n):
+    return dnpgettext(_current_domain, context, msgid1, msgid2, n)
+
+def lngettext(msgid1, msgid2, n):
+    return ldngettext(_current_domain, msgid1, msgid2, n)
+
+def lnpgettext(context, msgid1, msgid2, n):
+    return ldnpgettext(_current_domain, context, msgid1, msgid2, n)
 
 # dcgettext() has been deemed unnecessary and is not implemented.
 
--- a/goa/gaesource.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/goa/gaesource.py	Fri Sep 18 11:24:37 2009 +0200
@@ -149,7 +149,7 @@
     # ISource interface #######################################################
 
     def compile_rql(self, rql):
-        rqlst = self.repo.querier._rqlhelper.parse(rql)
+        rqlst = self.repo.vreg.parse(rql)
         rqlst.restricted_vars = ()
         rqlst.children[0].solutions = self._sols
         return rqlst
--- a/goa/goactl.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/goa/goactl.py	Fri Sep 18 11:24:37 2009 +0200
@@ -8,7 +8,6 @@
 __docformat__ = "restructuredtext en"
 
 from os.path import exists, join, split, basename, normpath, abspath
-
 from logilab.common.clcommands import register_commands
 
 from cubicweb import CW_SOFTWARE_ROOT, BadCommandUsage
@@ -19,9 +18,9 @@
 from logilab import common as lgc
 from logilab import constraint as lgcstr
 from logilab import mtconverter as lgmtc
-import rql, yams, yapps, simplejson, dateutil, vobject, docutils, roman
+import rql, yams, yapps, simplejson, docutils, roman
 
-SLINK_DIRECTORIES = (
+SLINK_DIRECTORIES = [
     (lgc.__path__[0], 'logilab/common'),
     (lgmtc.__path__[0], 'logilab/mtconverter'),
     (lgcstr.__path__[0], 'logilab/constraint'),
@@ -29,8 +28,6 @@
     (simplejson.__path__[0], 'simplejson'),
     (yams.__path__[0], 'yams'),
     (yapps.__path__[0], 'yapps'),
-    (dateutil.__path__[0], 'dateutil'),
-    (vobject.__path__[0], 'vobject'),
     (docutils.__path__[0], 'docutils'),
     (roman.__file__.replace('.pyc', '.py'), 'roman.py'),
 
@@ -42,7 +39,15 @@
     (join(CW_SOFTWARE_ROOT, 'i18n'), join('cubes', 'shared', 'i18n')),
     (join(CW_SOFTWARE_ROOT, 'goa', 'tools'), 'tools'),
     (join(CW_SOFTWARE_ROOT, 'goa', 'bin'), 'bin'),
-    )
+    ]
+
+try:
+    import dateutil
+    import vobject
+    SLINK_DIRECTORIES.extend([ (dateutil.__path__[0], 'dateutil'),
+                               (vobject.__path__[0], 'vobject') ] )
+except ImportError:
+    pass
 
 COPY_CW_FILES = (
     '__init__.py',
@@ -54,6 +59,7 @@
     'cwconfig.py',
     'entity.py',
     'interfaces.py',
+    'rqlrewrite.py',
     'rset.py',
     'schema.py',
     'schemaviewer.py',
@@ -78,7 +84,6 @@
     'server/pool.py',
     'server/querier.py',
     'server/repository.py',
-    'server/rqlrewrite.py',
     'server/securityhooks.py',
     'server/session.py',
     'server/serverconfig.py',
--- a/i18n/en.po	Thu Sep 17 19:38:04 2009 +0200
+++ b/i18n/en.po	Fri Sep 18 11:24:37 2009 +0200
@@ -5,7 +5,7 @@
 msgstr ""
 "Project-Id-Version: 2.0\n"
 "POT-Creation-Date: 2006-01-12 17:35+CET\n"
-"PO-Revision-Date: 2009-08-05 08:39+0200\n"
+"PO-Revision-Date: 2009-09-17 11:53+0200\n"
 "Last-Translator: Sylvain Thenault <sylvain.thenault@logilab.fr>\n"
 "Language-Team: English <devel@logilab.fr.org>\n"
 "MIME-Version: 1.0\n"
@@ -107,10 +107,6 @@
 msgstr ""
 
 #, python-format
-msgid "%s is not the initial state (%s) for this entity"
-msgstr ""
-
-#, python-format
 msgid "%s not estimated"
 msgstr ""
 
@@ -197,6 +193,15 @@
 msgid "Attributes"
 msgstr ""
 
+# schema pot file, generated on 2009-09-16 16:46:55
+#
+# singular and plural forms for each entity type
+msgid "BaseTransition"
+msgstr "Transition (abstract)"
+
+msgid "BaseTransition_plural"
+msgstr "Transitions (abstract)"
+
 msgid "Bookmark"
 msgstr "Bookmark"
 
@@ -351,6 +356,9 @@
 msgid "Interval_plural"
 msgstr "Intervals"
 
+msgid "New BaseTransition"
+msgstr "XXX"
+
 msgid "New Bookmark"
 msgstr "New bookmark"
 
@@ -358,7 +366,7 @@
 msgstr "New attribute"
 
 msgid "New CWCache"
-msgstr ""
+msgstr "New cache"
 
 msgid "New CWConstraint"
 msgstr "New constraint"
@@ -399,12 +407,21 @@
 msgid "New State"
 msgstr "New state"
 
+msgid "New SubWorkflowExitPoint"
+msgstr "New subworkflow exit-point"
+
 msgid "New TrInfo"
 msgstr "New transition information"
 
 msgid "New Transition"
 msgstr "New transition"
 
+msgid "New Workflow"
+msgstr "New workflow"
+
+msgid "New WorkflowTransition"
+msgstr "New workflow-transition"
+
 msgid "No query has been executed"
 msgstr ""
 
@@ -472,6 +489,12 @@
 msgid "String_plural"
 msgstr "Strings"
 
+msgid "SubWorkflowExitPoint"
+msgstr "Subworkflow exit-point"
+
+msgid "SubWorkflowExitPoint_plural"
+msgstr "subworkflow exit-points"
+
 msgid "Subject:"
 msgstr ""
 
@@ -492,12 +515,8 @@
 msgid "The view %s could not be found"
 msgstr ""
 
-msgid "There is no workflow defined for this entity."
-msgstr ""
-
-#, python-format
-msgid "This %s"
-msgstr ""
+msgid "This BaseTransition"
+msgstr "This abstract transition"
 
 msgid "This Bookmark"
 msgstr "This bookmark"
@@ -506,7 +525,7 @@
 msgstr "This attribute"
 
 msgid "This CWCache"
-msgstr ""
+msgstr "This cache"
 
 msgid "This CWConstraint"
 msgstr "This constraint"
@@ -547,12 +566,21 @@
 msgid "This State"
 msgstr "This state"
 
+msgid "This SubWorkflowExitPoint"
+msgstr "This subworkflow exit-point"
+
 msgid "This TrInfo"
 msgstr "This transition information"
 
 msgid "This Transition"
 msgstr "This transition"
 
+msgid "This Workflow"
+msgstr "This workflow"
+
+msgid "This WorkflowTransition"
+msgstr "This workflow-transition"
+
 msgid "Time"
 msgstr "Time"
 
@@ -584,9 +612,21 @@
 msgid "What's new?"
 msgstr ""
 
+msgid "Workflow"
+msgstr "Workflow"
+
 msgid "Workflow history"
 msgstr ""
 
+msgid "WorkflowTransition"
+msgstr "Workflow-transition"
+
+msgid "WorkflowTransition_plural"
+msgstr "Workflow-transitions"
+
+msgid "Workflow_plural"
+msgstr "Workflows"
+
 msgid "You are not connected to an instance !"
 msgstr ""
 
@@ -622,9 +662,6 @@
 msgid "[%s supervision] changes summary"
 msgstr ""
 
-msgid "__msg state changed"
-msgstr "state changed"
-
 msgid ""
 "a RQL expression which should return some results, else the transition won't "
 "be available. This query may use X and U variables that will respectivly "
@@ -644,12 +681,12 @@
 msgid "about this site"
 msgstr ""
 
+msgid "abstract base class for transitions"
+msgstr ""
+
 msgid "access type"
 msgstr ""
 
-msgid "account state"
-msgstr ""
-
 msgid "action(s) on this selection"
 msgstr ""
 
@@ -662,6 +699,12 @@
 msgid "actions_addentity_description"
 msgstr ""
 
+msgid "actions_addrelated"
+msgstr ""
+
+msgid "actions_addrelated_description"
+msgstr ""
+
 msgid "actions_cancel"
 msgstr "cancel the selection"
 
@@ -848,7 +891,10 @@
 msgid "add State allowed_transition Transition subject"
 msgstr "allowed transition"
 
-msgid "add State state_of CWEType object"
+msgid "add State allowed_transition WorkflowTransition subject"
+msgstr "workflow-transition"
+
+msgid "add State state_of Workflow object"
 msgstr "state"
 
 msgid "add Transition condition RQLExpression subject"
@@ -860,63 +906,34 @@
 msgid "add Transition destination_state State subject"
 msgstr "destination state"
 
-msgid "add Transition transition_of CWEType object"
+msgid "add Transition transition_of Workflow object"
 msgstr "transition"
 
-msgid "add a Bookmark"
-msgstr "add a bookmark"
-
-msgid "add a CWAttribute"
-msgstr "add an attribute"
-
-msgid "add a CWCache"
-msgstr "add a cubicweb cache"
-
-msgid "add a CWConstraint"
-msgstr "add a constraint"
-
-msgid "add a CWConstraintType"
-msgstr "add a constraint type"
-
+msgid "add WorkflowTransition condition RQLExpression subject"
+msgstr "workflow-transition"
+
+msgid "add WorkflowTransition subworkflow_exit SubWorkflowExitPoint subject"
+msgstr "subworkflow exit-point"
+
+msgid "add WorkflowTransition transition_of Workflow object"
+msgstr "workflow-transition"
+
+msgctxt "inlined:CWRelation.from_entity.subject"
 msgid "add a CWEType"
 msgstr "add an entity type"
 
-msgid "add a CWGroup"
-msgstr "add a group"
-
-msgid "add a CWPermission"
-msgstr "add a permission"
-
-msgid "add a CWProperty"
-msgstr "add a property"
-
+msgctxt "inlined:CWRelation.to_entity.subject"
+msgid "add a CWEType"
+msgstr "add an entity type"
+
+msgctxt "inlined:CWRelation.relation_type.subject"
 msgid "add a CWRType"
 msgstr "add a relation type"
 
-msgid "add a CWRelation"
-msgstr "add a relation"
-
-msgid "add a CWUser"
-msgstr "add a user"
-
+msgctxt "inlined:CWUser.use_email.subject"
 msgid "add a EmailAddress"
 msgstr "add an email address"
 
-msgid "add a ExternalUri"
-msgstr "and an external uri"
-
-msgid "add a RQLExpression"
-msgstr "add a rql expression"
-
-msgid "add a State"
-msgstr "add a state"
-
-msgid "add a TrInfo"
-msgstr "add a transition information"
-
-msgid "add a Transition"
-msgstr "add a transition"
-
 msgid "add a new permission"
 msgstr ""
 
@@ -931,6 +948,24 @@
 msgid "add_permission"
 msgstr "can be added by"
 
+# subject and object forms for each relation type
+# (no object form for final relation types)
+msgctxt "CWEType"
+msgid "add_permission"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "add_permission"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "add_permission_object"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "add_permission_object"
+msgstr ""
+
 msgid "add_permission_object"
 msgstr "has permission to add"
 
@@ -944,12 +979,26 @@
 "(toeid)s"
 msgstr ""
 
+msgid "addrelated"
+msgstr ""
+
+msgid "address"
+msgstr ""
+
+msgctxt "EmailAddress"
 msgid "address"
 msgstr ""
 
 msgid "alias"
 msgstr ""
 
+msgctxt "EmailAddress"
+msgid "alias"
+msgstr ""
+
+msgid "allow to set a specific workflow for an entity"
+msgstr ""
+
 msgid "allowed transition from this state"
 msgstr ""
 
@@ -959,6 +1008,22 @@
 msgid "allowed_transition"
 msgstr "allowed transition"
 
+msgctxt "State"
+msgid "allowed_transition"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "allowed_transition_object"
+msgstr ""
+
+msgctxt "Transition"
+msgid "allowed_transition_object"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "allowed_transition_object"
+msgstr ""
+
 msgid "allowed_transition_object"
 msgstr "incoming states"
 
@@ -1038,6 +1103,14 @@
 msgid "bookmarked_by"
 msgstr "bookmarked by"
 
+msgctxt "Bookmark"
+msgid "bookmarked_by"
+msgstr "bookmarked by"
+
+msgctxt "CWUser"
+msgid "bookmarked_by_object"
+msgstr ""
+
 msgid "bookmarked_by_object"
 msgstr "has bookmarks"
 
@@ -1122,6 +1195,28 @@
 msgid "by relation"
 msgstr ""
 
+msgid "by_transition"
+msgstr ""
+
+msgctxt "TrInfo"
+msgid "by_transition"
+msgstr "by transition"
+
+msgctxt "BaseTransition"
+msgid "by_transition_object"
+msgstr ""
+
+msgctxt "Transition"
+msgid "by_transition_object"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "by_transition_object"
+msgstr ""
+
+msgid "by_transition_object"
+msgstr "transition information"
+
 msgid "calendar"
 msgstr ""
 
@@ -1152,6 +1247,9 @@
 msgid "can't display data, unexpected error: %s"
 msgstr ""
 
+msgid "can't have multiple exits on the same state"
+msgstr ""
+
 #, python-format
 msgid ""
 "can't set inlined=%(inlined)s, %(stype)s %(rtype)s %(otype)s has cardinality="
@@ -1164,9 +1262,14 @@
 msgid "cancel this insert"
 msgstr ""
 
-msgid "canonical"
-msgstr ""
-
+msgid "cardinality"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "cardinality"
+msgstr "cardinality"
+
+msgctxt "CWRelation"
 msgid "cardinality"
 msgstr ""
 
@@ -1192,9 +1295,14 @@
 msgid "comment"
 msgstr ""
 
-msgid "comment:"
-msgstr ""
-
+msgctxt "TrInfo"
+msgid "comment"
+msgstr ""
+
+msgid "comment_format"
+msgstr "format"
+
+msgctxt "TrInfo"
 msgid "comment_format"
 msgstr "format"
 
@@ -1246,6 +1354,12 @@
 msgid "components_navigation_description"
 msgstr "pagination component for large resultsets"
 
+msgid "components_pdfview"
+msgstr ""
+
+msgid "components_pdfview_description"
+msgstr ""
+
 msgid "components_rqlinput"
 msgstr "rql input box"
 
@@ -1255,12 +1369,32 @@
 msgid "composite"
 msgstr ""
 
+msgctxt "CWRelation"
+msgid "composite"
+msgstr ""
+
+msgid "condition"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "condition"
+msgstr ""
+
+msgctxt "Transition"
+msgid "condition"
+msgstr ""
+
+msgctxt "WorkflowTransition"
 msgid "condition"
 msgstr ""
 
 msgid "condition:"
 msgstr ""
 
+msgctxt "RQLExpression"
+msgid "condition_object"
+msgstr ""
+
 msgid "condition_object"
 msgstr "condition of"
 
@@ -1270,6 +1404,18 @@
 msgid "constrained_by"
 msgstr "constrained by"
 
+msgctxt "CWAttribute"
+msgid "constrained_by"
+msgstr "constrained by"
+
+msgctxt "CWRelation"
+msgid "constrained_by"
+msgstr "constrained by"
+
+msgctxt "CWConstraint"
+msgid "constrained_by_object"
+msgstr ""
+
 msgid "constrained_by_object"
 msgstr "constraints"
 
@@ -1445,23 +1591,43 @@
 msgid "creating RQLExpression (Transition %(linkto)s condition RQLExpression)"
 msgstr "creating rql expression for transition %(linkto)s"
 
+msgid ""
+"creating RQLExpression (WorkflowTransition %(linkto)s condition "
+"RQLExpression)"
+msgstr "creating rql expression for workflow-transition %(linkto)s"
+
 msgid "creating State (State allowed_transition Transition %(linkto)s)"
 msgstr "creating a state able to trigger transition %(linkto)s"
 
-msgid "creating State (State state_of CWEType %(linkto)s)"
-msgstr "creating state for the %(linkto)s entity type"
+msgid "creating State (State state_of Workflow %(linkto)s)"
+msgstr "creating state of workflow %(linkto)s"
 
 msgid "creating State (Transition %(linkto)s destination_state State)"
 msgstr "creating destination state for transition %(linkto)s"
 
+msgid ""
+"creating SubWorkflowExitPoint (WorkflowTransition %(linkto)s "
+"subworkflow_exit SubWorkflowExitPoint)"
+msgstr "creating subworkflow exit-point for workflow-transition %(linkto)s"
+
 msgid "creating Transition (State %(linkto)s allowed_transition Transition)"
 msgstr "creating triggerable transition for state %(linkto)s"
 
 msgid "creating Transition (Transition destination_state State %(linkto)s)"
 msgstr "creating transition leading to state %(linkto)s"
 
-msgid "creating Transition (Transition transition_of CWEType %(linkto)s)"
-msgstr "creating transition for the %(linkto)s entity type"
+msgid "creating Transition (Transition transition_of Workflow %(linkto)s)"
+msgstr "creating transition of workflow %(linkto)s"
+
+msgid ""
+"creating WorkflowTransition (State %(linkto)s allowed_transition "
+"WorkflowTransition)"
+msgstr "creating workflow-transition leading to state %(linkto)s"
+
+msgid ""
+"creating WorkflowTransition (WorkflowTransition transition_of Workflow %"
+"(linkto)s)"
+msgstr "creating workflow-transition of workflow %(linkto)s"
 
 msgid "creation"
 msgstr ""
@@ -1475,6 +1641,14 @@
 msgid "cstrtype"
 msgstr "constraint's type"
 
+msgctxt "CWConstraint"
+msgid "cstrtype"
+msgstr ""
+
+msgctxt "CWConstraintType"
+msgid "cstrtype_object"
+msgstr ""
+
 msgid "cstrtype_object"
 msgstr "used by"
 
@@ -1488,6 +1662,12 @@
 msgid "currently attached file: %s"
 msgstr ""
 
+msgid "custom_workflow"
+msgstr "custom workflow"
+
+msgid "custom_workflow_object"
+msgstr "custom workflow of"
+
 msgid "cwetype-schema-image"
 msgstr "schema"
 
@@ -1524,6 +1704,30 @@
 msgid "default text format for rich text fields."
 msgstr ""
 
+msgid "default user workflow"
+msgstr ""
+
+msgid "default workflow for an entity type"
+msgstr ""
+
+msgid "default_workflow"
+msgstr "default workflow"
+
+msgctxt "CWEType"
+msgid "default_workflow"
+msgstr "default workflow"
+
+msgctxt "Workflow"
+msgid "default_workflow_object"
+msgstr ""
+
+msgid "default_workflow_object"
+msgstr "default workflow of"
+
+msgid "defaultval"
+msgstr "default value"
+
+msgctxt "CWAttribute"
 msgid "defaultval"
 msgstr "default value"
 
@@ -1558,6 +1762,9 @@
 msgid "define an entity type, used to build the instance schema"
 msgstr ""
 
+msgid "define how we get out from a sub-workflow"
+msgstr ""
+
 msgid ""
 "defines what's the property is applied for. You must select this first to be "
 "able to set value"
@@ -1581,6 +1788,22 @@
 msgid "delete_permission"
 msgstr "can be deleted by"
 
+msgctxt "CWEType"
+msgid "delete_permission"
+msgstr "delete permission"
+
+msgctxt "CWRType"
+msgid "delete_permission"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "delete_permission_object"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "delete_permission_object"
+msgstr ""
+
 msgid "delete_permission_object"
 msgstr "has permission to delete"
 
@@ -1600,9 +1823,84 @@
 msgid "description"
 msgstr ""
 
+msgctxt "CWEType"
+msgid "description"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "description"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "description"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "description"
+msgstr ""
+
+msgctxt "Transition"
+msgid "description"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "description"
+msgstr ""
+
+msgctxt "State"
+msgid "description"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "description"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "description"
+msgstr ""
+
 msgid "description_format"
 msgstr "format"
 
+msgctxt "CWEType"
+msgid "description_format"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "description_format"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "CWAttribute"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "Transition"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "WorkflowTransition"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "State"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "CWRType"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "BaseTransition"
+msgid "description_format"
+msgstr "format"
+
+msgid "destination state"
+msgstr ""
+
 msgid "destination state for this transition"
 msgstr ""
 
@@ -1612,6 +1910,18 @@
 msgid "destination_state"
 msgstr "destination state"
 
+msgctxt "Transition"
+msgid "destination_state"
+msgstr ""
+
+msgctxt "SubWorkflowExitPoint"
+msgid "destination_state"
+msgstr "destination state"
+
+msgctxt "State"
+msgid "destination_state_object"
+msgstr ""
+
 msgid "destination_state_object"
 msgstr "destination of"
 
@@ -1640,6 +1950,9 @@
 msgid "display the component or not"
 msgstr ""
 
+msgid "display the pdf icon or not"
+msgstr ""
+
 msgid ""
 "distinct label to distinguate between other permission entity of the same "
 "name"
@@ -1655,6 +1968,9 @@
 msgid "download icon"
 msgstr ""
 
+msgid "download page as pdf"
+msgstr ""
+
 msgid "download schema as owl"
 msgstr ""
 
@@ -1712,6 +2028,9 @@
 msgid "entity edited"
 msgstr ""
 
+msgid "entity has no workflow set"
+msgstr ""
+
 msgid "entity linked"
 msgstr ""
 
@@ -1723,10 +2042,7 @@
 "configuration"
 msgstr ""
 
-msgid "entity types which may use this state"
-msgstr ""
-
-msgid "entity types which may use this transition"
+msgid "entity types which may use this workflow"
 msgstr ""
 
 msgid "error while embedding page"
@@ -1746,15 +2062,33 @@
 msgid "eta_date"
 msgstr ""
 
+msgid "exit_point"
+msgstr ""
+
+msgid "exit_point_object"
+msgstr ""
+
+#, python-format
+msgid "exiting from subworkflow %s"
+msgstr ""
+
 msgid "expected:"
 msgstr ""
 
 msgid "expression"
 msgstr ""
 
+msgctxt "RQLExpression"
+msgid "expression"
+msgstr ""
+
 msgid "exprtype"
 msgstr "expression's type"
 
+msgctxt "RQLExpression"
+msgid "exprtype"
+msgstr ""
+
 msgid "external page"
 msgstr ""
 
@@ -1806,6 +2140,18 @@
 msgid "final"
 msgstr ""
 
+msgctxt "CWEType"
+msgid "final"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "final"
+msgstr ""
+
+msgid "firstname"
+msgstr ""
+
+msgctxt "CWUser"
 msgid "firstname"
 msgstr ""
 
@@ -1818,6 +2164,14 @@
 msgid "for_user"
 msgstr "for user"
 
+msgctxt "CWProperty"
+msgid "for_user"
+msgstr ""
+
+msgctxt "CWUser"
+msgid "for_user_object"
+msgstr ""
+
 msgid "for_user_object"
 msgstr "use properties"
 
@@ -1834,6 +2188,18 @@
 msgid "from_entity"
 msgstr "from entity"
 
+msgctxt "CWAttribute"
+msgid "from_entity"
+msgstr "from entity"
+
+msgctxt "CWRelation"
+msgid "from_entity"
+msgstr "from entity"
+
+msgctxt "CWEType"
+msgid "from_entity_object"
+msgstr ""
+
 msgid "from_entity_object"
 msgstr "subjet relation"
 
@@ -1843,6 +2209,14 @@
 msgid "from_state"
 msgstr "from state"
 
+msgctxt "TrInfo"
+msgid "from_state"
+msgstr "from state"
+
+msgctxt "State"
+msgid "from_state_object"
+msgstr ""
+
 msgid "from_state_object"
 msgstr "transitions from this state"
 
@@ -1852,9 +2226,17 @@
 msgid "fulltext_container"
 msgstr ""
 
+msgctxt "CWRType"
+msgid "fulltext_container"
+msgstr "fulltext container"
+
 msgid "fulltextindexed"
 msgstr "fulltext indexed"
 
+msgctxt "CWAttribute"
+msgid "fulltextindexed"
+msgstr ""
+
 msgid "generic plot"
 msgstr ""
 
@@ -1873,6 +2255,10 @@
 msgid "granted to groups"
 msgstr ""
 
+#, python-format
+msgid "graphical representation of %s"
+msgstr ""
+
 msgid "graphical representation of the instance'schema"
 msgstr ""
 
@@ -1961,6 +2347,9 @@
 msgid "id of main template used to render pages"
 msgstr ""
 
+msgid "identical to"
+msgstr ""
+
 msgid "identical_to"
 msgstr "identical to"
 
@@ -1987,6 +2376,14 @@
 msgid "in_group"
 msgstr "in group"
 
+msgctxt "CWUser"
+msgid "in_group"
+msgstr "in group"
+
+msgctxt "CWGroup"
+msgid "in_group_object"
+msgstr ""
+
 msgid "in_group_object"
 msgstr "contains"
 
@@ -2012,6 +2409,10 @@
 msgid "indexed"
 msgstr ""
 
+msgctxt "CWAttribute"
+msgid "indexed"
+msgstr ""
+
 msgid "indicate the current state of an entity"
 msgstr ""
 
@@ -2027,18 +2428,30 @@
 msgid "initial estimation %s"
 msgstr ""
 
-msgid "initial state for entities of this type"
+msgid "initial state for this workflow"
 msgstr ""
 
 msgid "initial_state"
 msgstr "initial state"
 
+msgctxt "Workflow"
+msgid "initial_state"
+msgstr "initial state"
+
+msgctxt "State"
+msgid "initial_state_object"
+msgstr ""
+
 msgid "initial_state_object"
 msgstr "initial state of"
 
 msgid "inlined"
 msgstr ""
 
+msgctxt "CWRType"
+msgid "inlined"
+msgstr ""
+
 msgid "instance schema"
 msgstr ""
 
@@ -2048,6 +2461,10 @@
 msgid "internationalizable"
 msgstr ""
 
+msgctxt "CWAttribute"
+msgid "internationalizable"
+msgstr ""
+
 #, python-format
 msgid "invalid action %r"
 msgstr ""
@@ -2084,7 +2501,7 @@
 msgstr ""
 
 msgid "is_instance_of_object"
-msgstr ""
+msgstr "is instance of"
 
 msgid "is_object"
 msgstr "has instances"
@@ -2101,6 +2518,10 @@
 msgid "label"
 msgstr ""
 
+msgctxt "CWPermission"
+msgid "label"
+msgstr ""
+
 msgid "language of the user interface"
 msgstr ""
 
@@ -2110,6 +2531,10 @@
 msgid "last_login_time"
 msgstr "last login time"
 
+msgctxt "CWUser"
+msgid "last_login_time"
+msgstr "last login time"
+
 msgid "latest modification time of an entity"
 msgstr ""
 
@@ -2138,13 +2563,16 @@
 msgid "link a relation definition to its subject entity type"
 msgstr ""
 
-msgid "link a state to one or more entity type"
+msgid "link a state to one or more workflow"
 msgstr ""
 
 msgid "link a transition information to its object"
 msgstr ""
 
-msgid "link a transition to one or more entity type"
+msgid "link a transition to one or more workflow"
+msgstr ""
+
+msgid "link a workflow to one or more entity type"
 msgstr ""
 
 msgid "link to each item in"
@@ -2162,6 +2590,10 @@
 msgid "login"
 msgstr ""
 
+msgctxt "CWUser"
+msgid "login"
+msgstr ""
+
 msgid "login or email"
 msgstr ""
 
@@ -2181,6 +2613,10 @@
 msgid "mainvars"
 msgstr ""
 
+msgctxt "RQLExpression"
+msgid "mainvars"
+msgstr ""
+
 msgid "manage"
 msgstr ""
 
@@ -2196,6 +2632,9 @@
 msgid "managers"
 msgstr ""
 
+msgid "mandatory relation"
+msgstr ""
+
 msgid "march"
 msgstr ""
 
@@ -2242,6 +2681,50 @@
 msgid "name"
 msgstr ""
 
+msgctxt "CWEType"
+msgid "name"
+msgstr ""
+
+msgctxt "Transition"
+msgid "name"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "name"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "name"
+msgstr ""
+
+msgctxt "CWConstraintType"
+msgid "name"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "name"
+msgstr ""
+
+msgctxt "State"
+msgid "name"
+msgstr ""
+
+msgctxt "CWPermission"
+msgid "name"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "name"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "name"
+msgstr ""
+
+msgctxt "CWCache"
+msgid "name"
+msgstr ""
+
 msgid "name of the cache"
 msgstr ""
 
@@ -2347,6 +2830,14 @@
 msgid "ordernum"
 msgstr "order"
 
+msgctxt "CWAttribute"
+msgid "ordernum"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "ordernum"
+msgstr ""
+
 msgid "owl"
 msgstr ""
 
@@ -2381,6 +2872,10 @@
 msgid "path"
 msgstr ""
 
+msgctxt "Bookmark"
+msgid "path"
+msgstr ""
+
 msgid "permission"
 msgstr ""
 
@@ -2402,6 +2897,10 @@
 msgid "pkey"
 msgstr "key"
 
+msgctxt "CWProperty"
+msgid "pkey"
+msgstr ""
+
 msgid "please correct errors below"
 msgstr ""
 
@@ -2414,6 +2913,20 @@
 msgid "powered by CubicWeb"
 msgstr ""
 
+msgid "prefered_form"
+msgstr ""
+
+msgctxt "EmailAddress"
+msgid "prefered_form"
+msgstr ""
+
+msgctxt "EmailAddress"
+msgid "prefered_form_object"
+msgstr ""
+
+msgid "prefered_form_object"
+msgstr ""
+
 msgid "preferences"
 msgstr ""
 
@@ -2426,6 +2939,14 @@
 msgid "primary_email"
 msgstr "primary email"
 
+msgctxt "CWUser"
+msgid "primary_email"
+msgstr "primary email"
+
+msgctxt "EmailAddress"
+msgid "primary_email_object"
+msgstr ""
+
 msgid "primary_email_object"
 msgstr "primary email of"
 
@@ -2442,17 +2963,39 @@
 msgstr ""
 
 msgid "read_perm"
-msgstr ""
+msgstr "read perm"
 
 msgid "read_permission"
 msgstr "can be read by"
 
+msgctxt "CWEType"
+msgid "read_permission"
+msgstr "read permission"
+
+msgctxt "CWRType"
+msgid "read_permission"
+msgstr "read permission"
+
+msgctxt "CWGroup"
+msgid "read_permission_object"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "read_permission_object"
+msgstr ""
+
 msgid "read_permission_object"
 msgstr "has permission to delete"
 
 msgid "registry"
 msgstr ""
 
+msgid "related entity has no state"
+msgstr ""
+
+msgid "related entity has no workflow set"
+msgstr ""
+
 #, python-format
 msgid "relation %(relname)s of %(ent)s"
 msgstr ""
@@ -2460,6 +3003,18 @@
 msgid "relation_type"
 msgstr "relation type"
 
+msgctxt "CWAttribute"
+msgid "relation_type"
+msgstr "relation type"
+
+msgctxt "CWRelation"
+msgid "relation_type"
+msgstr "relation type"
+
+msgctxt "CWRType"
+msgid "relation_type_object"
+msgstr ""
+
 msgid "relation_type_object"
 msgstr "relation definitions"
 
@@ -2472,71 +3027,53 @@
 msgid "relative url of the bookmarked page"
 msgstr ""
 
-msgid "remove this Bookmark"
-msgstr "remove this bookmark"
-
-msgid "remove this CWAttribute"
-msgstr "remove this attribute"
-
-msgid "remove this CWCache"
-msgstr "remove this cubicweb cache"
-
-msgid "remove this CWConstraint"
-msgstr "remove this constraint"
-
-msgid "remove this CWConstraintType"
-msgstr "remove this constraint type"
-
+msgctxt "inlined:CWRelation:from_entity:subject"
+msgid "remove this CWEType"
+msgstr "remove this entity type"
+
+msgctxt "inlined:CWRelation:to_entity:subject"
 msgid "remove this CWEType"
 msgstr "remove this entity type"
 
-msgid "remove this CWGroup"
-msgstr "remove this group"
-
-msgid "remove this CWPermission"
-msgstr "remove this permission"
-
-msgid "remove this CWProperty"
-msgstr "remove this property"
-
+msgctxt "inlined:CWRelation:relation_type:subject"
 msgid "remove this CWRType"
 msgstr "remove this relation type"
 
-msgid "remove this CWRelation"
-msgstr "remove this relation"
-
-msgid "remove this CWUser"
-msgstr "remove this user"
-
+msgctxt "inlined:CWUser:use_email:subject"
 msgid "remove this EmailAddress"
 msgstr "remove this email address"
 
-msgid "remove this ExternalUri"
-msgstr ""
-
-msgid "remove this RQLExpression"
-msgstr "remove this RQL expression"
-
-msgid "remove this State"
-msgstr "remove this state"
-
-msgid "remove this TrInfo"
-msgstr "remove this transition information"
-
-msgid "remove this Transition"
-msgstr "remove this transition"
-
 msgid "require_group"
 msgstr "require the group"
 
+msgctxt "BaseTransition"
+msgid "require_group"
+msgstr "require group"
+
+msgctxt "Transition"
+msgid "require_group"
+msgstr "require group"
+
+msgctxt "CWPermission"
+msgid "require_group"
+msgstr "require group"
+
+msgctxt "WorkflowTransition"
+msgid "require_group"
+msgstr "require group"
+
+msgctxt "CWGroup"
+msgid "require_group_object"
+msgstr ""
+
 msgid "require_group_object"
 msgstr "required by"
 
 msgid "require_permission"
-msgstr ""
+msgstr "require permission"
 
 msgid "require_permission_object"
-msgstr ""
+msgstr "required by"
 
 msgid "required attribute"
 msgstr ""
@@ -2653,6 +3190,9 @@
 msgid "semantic description of this transition"
 msgstr ""
 
+msgid "semantic description of this workflow"
+msgstr ""
+
 msgid "send email"
 msgstr ""
 
@@ -2705,11 +3245,22 @@
 msgid "sparql xml"
 msgstr ""
 
+msgid "special transition allowing to go through a sub-workflow"
+msgstr ""
+
 msgid "specializes"
 msgstr ""
 
+msgctxt "CWEType"
+msgid "specializes"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "specializes_object"
+msgstr ""
+
 msgid "specializes_object"
-msgstr ""
+msgstr "specialized by"
 
 msgid "startup views"
 msgstr ""
@@ -2717,9 +3268,28 @@
 msgid "state"
 msgstr ""
 
+msgid "state doesn't belong to entity's current workflow"
+msgstr ""
+
+msgid "state doesn't belong to entity's workflow"
+msgstr ""
+
+msgid ""
+"state doesn't belong to entity's workflow. You may want to set a custom "
+"workflow for this entity first."
+msgstr ""
+
 msgid "state_of"
 msgstr "state of"
 
+msgctxt "State"
+msgid "state_of"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "state_of_object"
+msgstr ""
+
 msgid "state_of_object"
 msgstr "use states"
 
@@ -2742,12 +3312,65 @@
 msgid "subject_plural:"
 msgstr "subjects:"
 
+msgid "subworkflow"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "subworkflow"
+msgstr ""
+
+msgid "subworkflow state"
+msgstr ""
+
+msgid "subworkflow_exit"
+msgstr "subworkflow exit"
+
+msgctxt "WorkflowTransition"
+msgid "subworkflow_exit"
+msgstr ""
+
+msgctxt "SubWorkflowExitPoint"
+msgid "subworkflow_exit_object"
+msgstr ""
+
+msgid "subworkflow_exit_object"
+msgstr "subworkflow exit of"
+
+msgctxt "Workflow"
+msgid "subworkflow_object"
+msgstr ""
+
+msgid "subworkflow_object"
+msgstr "subworkflow of"
+
+msgid "subworkflow_state"
+msgstr "subworkflow state"
+
+msgctxt "SubWorkflowExitPoint"
+msgid "subworkflow_state"
+msgstr "subworkflow state"
+
+msgctxt "State"
+msgid "subworkflow_state_object"
+msgstr ""
+
+msgid "subworkflow_state_object"
+msgstr ""
+
 msgid "sunday"
 msgstr ""
 
 msgid "surname"
 msgstr ""
 
+msgctxt "CWUser"
+msgid "surname"
+msgstr ""
+
+msgid "symetric"
+msgstr ""
+
+msgctxt "CWRType"
 msgid "symetric"
 msgstr ""
 
@@ -2806,6 +3429,10 @@
 msgid "timestamp"
 msgstr ""
 
+msgctxt "CWCache"
+msgid "timestamp"
+msgstr ""
+
 msgid "timestamp of the latest source synchronization."
 msgstr ""
 
@@ -2815,6 +3442,10 @@
 msgid "title"
 msgstr ""
 
+msgctxt "Bookmark"
+msgid "title"
+msgstr ""
+
 msgid "to"
 msgstr ""
 
@@ -2828,6 +3459,18 @@
 msgid "to_entity"
 msgstr "to entity"
 
+msgctxt "CWAttribute"
+msgid "to_entity"
+msgstr "to entity"
+
+msgctxt "CWRelation"
+msgid "to_entity"
+msgstr "to entity"
+
+msgctxt "CWEType"
+msgid "to_entity_object"
+msgstr ""
+
 msgid "to_entity_object"
 msgstr "object relations"
 
@@ -2837,6 +3480,14 @@
 msgid "to_state"
 msgstr "to state"
 
+msgctxt "TrInfo"
+msgid "to_state"
+msgstr ""
+
+msgctxt "State"
+msgid "to_state_object"
+msgstr ""
+
 msgid "to_state_object"
 msgstr "transitions to this state"
 
@@ -2846,13 +3497,34 @@
 msgid "toggle check boxes"
 msgstr ""
 
-#, python-format
-msgid "transition from %s to %s does not exist or is not allowed"
+msgid "transition doesn't belong to entity's workflow"
+msgstr ""
+
+msgid "transition isn't allowed"
+msgstr ""
+
+msgid "transition may not be fired"
 msgstr ""
 
 msgid "transition_of"
 msgstr "transition of"
 
+msgctxt "BaseTransition"
+msgid "transition_of"
+msgstr "transition of"
+
+msgctxt "Transition"
+msgid "transition_of"
+msgstr "transition of"
+
+msgctxt "WorkflowTransition"
+msgid "transition_of"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "transition_of_object"
+msgstr ""
+
 msgid "transition_of_object"
 msgstr "use transitions"
 
@@ -2925,6 +3597,10 @@
 msgid "upassword"
 msgstr "password"
 
+msgctxt "CWUser"
+msgid "upassword"
+msgstr ""
+
 msgid "update"
 msgstr ""
 
@@ -2934,6 +3610,18 @@
 msgid "update_permission"
 msgstr "can be updated by"
 
+msgctxt "CWEType"
+msgid "update_permission"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "update_permission_object"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "update_permission_object"
+msgstr ""
+
 msgid "update_permission_object"
 msgstr "has permission to update"
 
@@ -2944,6 +3632,10 @@
 msgid "uri"
 msgstr ""
 
+msgctxt "ExternalUri"
+msgid "uri"
+msgstr ""
+
 msgid "use template languages"
 msgstr ""
 
@@ -2955,6 +3647,14 @@
 msgid "use_email"
 msgstr "use email"
 
+msgctxt "CWUser"
+msgid "use_email"
+msgstr ""
+
+msgctxt "EmailAddress"
+msgid "use_email_object"
+msgstr ""
+
 msgid "use_email_object"
 msgstr "used by"
 
@@ -3005,6 +3705,14 @@
 msgid "value"
 msgstr ""
 
+msgctxt "CWConstraint"
+msgid "value"
+msgstr ""
+
+msgctxt "CWProperty"
+msgid "value"
+msgstr ""
+
 msgid "value associated to this key is not editable manually"
 msgstr ""
 
@@ -3056,12 +3764,41 @@
 
 msgid ""
 "when multiple addresses are equivalent (such as python-projects@logilab.org "
-"and python-projects@lists.logilab.org), set this to true on one of them "
-"which is the preferred form."
+"and python-projects@lists.logilab.org), set this to indicate which is the "
+"preferred form."
+msgstr ""
+
+msgid "workflow"
 msgstr ""
 
 #, python-format
-msgid "workflow for %s"
+msgid "workflow changed to \"%s\""
+msgstr ""
+
+msgid "workflow has no initial state"
+msgstr ""
+
+msgid "workflow history item"
+msgstr ""
+
+msgid "workflow to which this state belongs"
+msgstr ""
+
+msgid "workflow to which this transition belongs"
+msgstr ""
+
+msgid "workflow_of"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "workflow_of"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "workflow_of_object"
+msgstr ""
+
+msgid "workflow_of_object"
 msgstr ""
 
 msgid "xbel"
--- a/i18n/es.po	Thu Sep 17 19:38:04 2009 +0200
+++ b/i18n/es.po	Fri Sep 18 11:24:37 2009 +0200
@@ -112,10 +112,6 @@
 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)"
 
@@ -205,6 +201,15 @@
 msgid "Attributes"
 msgstr "Atributos"
 
+# schema pot file, generated on 2009-09-16 16:46:55
+#
+# singular and plural forms for each entity type
+msgid "BaseTransition"
+msgstr ""
+
+msgid "BaseTransition_plural"
+msgstr ""
+
 msgid "Bookmark"
 msgstr "Favorito"
 
@@ -359,6 +364,9 @@
 msgid "Interval_plural"
 msgstr "Duraciones"
 
+msgid "New BaseTransition"
+msgstr ""
+
 msgid "New Bookmark"
 msgstr "Agregar a Favoritos"
 
@@ -407,12 +415,21 @@
 msgid "New State"
 msgstr "Agregar Estado"
 
+msgid "New SubWorkflowExitPoint"
+msgstr ""
+
 msgid "New TrInfo"
 msgstr "Agregar Información de Transición"
 
 msgid "New Transition"
 msgstr "Agregar transición"
 
+msgid "New Workflow"
+msgstr ""
+
+msgid "New WorkflowTransition"
+msgstr ""
+
 msgid "No query has been executed"
 msgstr "Ninguna búsqueda ha sido ejecutada"
 
@@ -480,6 +497,12 @@
 msgid "String_plural"
 msgstr "Cadenas de caracteres"
 
+msgid "SubWorkflowExitPoint"
+msgstr ""
+
+msgid "SubWorkflowExitPoint_plural"
+msgstr ""
+
 msgid "Subject:"
 msgstr "Sujeto:"
 
@@ -500,12 +523,8 @@
 msgid "The view %s could not be found"
 msgstr "La vista %s no ha podido ser encontrada"
 
-msgid "There is no workflow defined for this entity."
-msgstr "No hay workflow para este entidad"
-
-#, python-format
-msgid "This %s"
-msgstr "Este %s"
+msgid "This BaseTransition"
+msgstr ""
 
 msgid "This Bookmark"
 msgstr "Este favorito"
@@ -555,12 +574,21 @@
 msgid "This State"
 msgstr "Este estado"
 
+msgid "This SubWorkflowExitPoint"
+msgstr ""
+
 msgid "This TrInfo"
 msgstr "Esta información de transición"
 
 msgid "This Transition"
 msgstr "Esta transición"
 
+msgid "This Workflow"
+msgstr ""
+
+msgid "This WorkflowTransition"
+msgstr ""
+
 msgid "Time"
 msgstr "Hora"
 
@@ -592,9 +620,21 @@
 msgid "What's new?"
 msgstr "Lo último en el sitio"
 
+msgid "Workflow"
+msgstr ""
+
 msgid "Workflow history"
 msgstr "Histórico del Workflow"
 
+msgid "WorkflowTransition"
+msgstr ""
+
+msgid "WorkflowTransition_plural"
+msgstr ""
+
+msgid "Workflow_plural"
+msgstr ""
+
 msgid "You are not connected to an instance !"
 msgstr ""
 
@@ -628,7 +668,8 @@
 msgid ""
 "You have no access to this view or it can not be used to display the current "
 "data."
-msgstr "No tiene acceso a esta vista o No se puede utilizare para los datos actuales."
+msgstr ""
+"No tiene acceso a esta vista o No se puede utilizare para los datos actuales."
 
 msgid ""
 "You're not authorized to access this page. If you think you should, please "
@@ -641,9 +682,6 @@
 msgid "[%s supervision] changes summary"
 msgstr "[%s supervision] descripción de cambios"
 
-msgid "__msg state changed"
-msgstr "El estado a cambiado"
-
 msgid ""
 "a RQL expression which should return some results, else the transition won't "
 "be available. This query may use X and U variables that will respectivly "
@@ -666,12 +704,12 @@
 msgid "about this site"
 msgstr "Sobre este Espacio"
 
+msgid "abstract base class for transitions"
+msgstr ""
+
 msgid "access type"
 msgstr "Tipo de Acceso"
 
-msgid "account state"
-msgstr "Estado de la Cuenta"
-
 msgid "action(s) on this selection"
 msgstr "acción(es) en esta selección"
 
@@ -684,6 +722,12 @@
 msgid "actions_addentity_description"
 msgstr ""
 
+msgid "actions_addrelated"
+msgstr ""
+
+msgid "actions_addrelated_description"
+msgstr ""
+
 msgid "actions_cancel"
 msgstr "Anular"
 
@@ -829,28 +873,28 @@
 msgstr "Definición de atributo"
 
 msgid "add CWEType add_permission RQLExpression subject"
-msgstr "Agregar una autorización"
+msgstr "Expresión RQL de agregación"
 
 msgid "add CWEType delete_permission RQLExpression subject"
-msgstr "Eliminar una autorización"
+msgstr "Expresión RQL de eliminación"
 
 msgid "add CWEType read_permission RQLExpression subject"
-msgstr "Definir una expresión RQL de lectura"
+msgstr "Expresión RQL de lectura"
 
 msgid "add CWEType update_permission RQLExpression subject"
 msgstr "Definir una expresión RQL de actualización"
 
 msgid "add CWProperty for_user CWUser object"
-msgstr "Agregar Propiedad"
+msgstr "Propiedad"
 
 msgid "add CWRType add_permission RQLExpression subject"
-msgstr "Agregar expresión RQL de agregación"
+msgstr "Expresión RQL de agregación"
 
 msgid "add CWRType delete_permission RQLExpression subject"
-msgstr "Agregar expresión RQL de eliminación"
+msgstr "Expresión RQL de eliminación"
 
 msgid "add CWRType read_permission RQLExpression subject"
-msgstr "Agregar expresión RQL de lectura"
+msgstr "Expresión RQL de lectura"
 
 msgid "add CWRelation constrained_by CWConstraint subject"
 msgstr "Restricción"
@@ -859,85 +903,59 @@
 msgstr "Definición de relación"
 
 msgid "add CWUser in_group CWGroup object"
-msgstr "Agregar usuario"
+msgstr "Usuario"
 
 msgid "add CWUser use_email EmailAddress subject"
-msgstr "Agregar email"
+msgstr "Email"
 
 msgid "add State allowed_transition Transition object"
-msgstr "Agregar un estado en entrada"
+msgstr "Estado en entrada"
 
 msgid "add State allowed_transition Transition subject"
-msgstr "Agregar una transición en salida"
-
-msgid "add State state_of CWEType object"
-msgstr "Agregar un estado"
+msgstr "Transición en salida"
+
+msgid "add State allowed_transition WorkflowTransition subject"
+msgstr ""
+
+msgid "add State state_of Workflow object"
+msgstr ""
 
 msgid "add Transition condition RQLExpression subject"
-msgstr "Agregar una Restricción"
+msgstr "Restricción"
 
 msgid "add Transition destination_state State object"
-msgstr "Agregar una transición de entrada"
+msgstr "Transición de entrada"
 
 msgid "add Transition destination_state State subject"
-msgstr "Agregar el estado de salida"
-
-msgid "add Transition transition_of CWEType object"
-msgstr "Agregar una transición"
-
-msgid "add a Bookmark"
-msgstr "Agregar un Favorito"
-
-msgid "add a CWAttribute"
-msgstr "Agregar un tipo de relación"
-
-msgid "add a CWCache"
-msgstr "Agregar un cache"
-
-msgid "add a CWConstraint"
-msgstr "Agregar una Restricción"
-
-msgid "add a CWConstraintType"
-msgstr "Agregar un tipo de Restricción"
-
+msgstr "Estado de salida"
+
+msgid "add Transition transition_of Workflow object"
+msgstr ""
+
+msgid "add WorkflowTransition condition RQLExpression subject"
+msgstr ""
+
+msgid "add WorkflowTransition subworkflow_exit SubWorkflowExitPoint subject"
+msgstr ""
+
+msgid "add WorkflowTransition transition_of Workflow object"
+msgstr ""
+
+msgctxt "inlined:CWRelation.from_entity.subject"
 msgid "add a CWEType"
-msgstr "Agregar un tipo de entidad"
-
-msgid "add a CWGroup"
-msgstr "Agregar un grupo de usuarios"
-
-msgid "add a CWPermission"
-msgstr "Agregar una autorización"
-
-msgid "add a CWProperty"
-msgstr "Agregar una propiedad"
-
+msgstr ""
+
+msgctxt "inlined:CWRelation.to_entity.subject"
+msgid "add a CWEType"
+msgstr ""
+
+msgctxt "inlined:CWRelation.relation_type.subject"
 msgid "add a CWRType"
-msgstr "Agregar un tipo de relación"
-
-msgid "add a CWRelation"
-msgstr "Agregar una relación"
-
-msgid "add a CWUser"
-msgstr "Agregar un usuario"
-
+msgstr ""
+
+msgctxt "inlined:CWUser.use_email.subject"
 msgid "add a EmailAddress"
-msgstr "Agregar un email"
-
-msgid "add a ExternalUri"
-msgstr ""
-
-msgid "add a RQLExpression"
-msgstr "Agregar una expresión rql"
-
-msgid "add a State"
-msgstr "Agregar un estado"
-
-msgid "add a TrInfo"
-msgstr "Agregar una información de transición"
-
-msgid "add a Transition"
-msgstr "Agregar una transición"
+msgstr ""
 
 msgid "add a new permission"
 msgstr "Agregar una autorización"
@@ -953,6 +971,24 @@
 msgid "add_permission"
 msgstr "Autorización para agregar"
 
+# subject and object forms for each relation type
+# (no object form for final relation types)
+msgctxt "CWEType"
+msgid "add_permission"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "add_permission"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "add_permission_object"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "add_permission_object"
+msgstr ""
+
 msgid "add_permission_object"
 msgstr "tiene la autorización para agregar"
 
@@ -968,12 +1004,26 @@
 "Relación agregada %(rtype)s de %(frometype)s #%(fromeid)s hacia %(toetype)s #"
 "%(toeid)s"
 
+msgid "addrelated"
+msgstr ""
+
 msgid "address"
 msgstr "dirección"
 
+msgctxt "EmailAddress"
+msgid "address"
+msgstr ""
+
 msgid "alias"
 msgstr "alias"
 
+msgctxt "EmailAddress"
+msgid "alias"
+msgstr ""
+
+msgid "allow to set a specific workflow for an entity"
+msgstr ""
+
 msgid "allowed transition from this state"
 msgstr "transición autorizada desde este estado"
 
@@ -983,6 +1033,22 @@
 msgid "allowed_transition"
 msgstr "transición autorizada"
 
+msgctxt "State"
+msgid "allowed_transition"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "allowed_transition_object"
+msgstr ""
+
+msgctxt "Transition"
+msgid "allowed_transition_object"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "allowed_transition_object"
+msgstr ""
+
 msgid "allowed_transition_object"
 msgstr "Estados de entrada"
 
@@ -1064,6 +1130,14 @@
 msgid "bookmarked_by"
 msgstr "está en los favoritos de"
 
+msgctxt "Bookmark"
+msgid "bookmarked_by"
+msgstr ""
+
+msgctxt "CWUser"
+msgid "bookmarked_by_object"
+msgstr ""
+
 msgid "bookmarked_by_object"
 msgstr "selecciona en sus favoritos a"
 
@@ -1149,6 +1223,28 @@
 msgid "by relation"
 msgstr "por relación"
 
+msgid "by_transition"
+msgstr ""
+
+msgctxt "TrInfo"
+msgid "by_transition"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "by_transition_object"
+msgstr ""
+
+msgctxt "Transition"
+msgid "by_transition_object"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "by_transition_object"
+msgstr ""
+
+msgid "by_transition_object"
+msgstr ""
+
 msgid "calendar"
 msgstr "mostrar un calendario"
 
@@ -1179,6 +1275,9 @@
 msgid "can't display data, unexpected error: %s"
 msgstr "imposible de mostrar los datos, a causa del siguiente error: %s"
 
+msgid "can't have multiple exits on the same state"
+msgstr ""
+
 #, python-format
 msgid ""
 "can't set inlined=%(inlined)s, %(stype)s %(rtype)s %(otype)s has cardinality="
@@ -1193,12 +1292,17 @@
 msgid "cancel this insert"
 msgstr "Cancelar esta inserción"
 
-msgid "canonical"
-msgstr "canónico"
-
 msgid "cardinality"
 msgstr "cardinalidad"
 
+msgctxt "CWAttribute"
+msgid "cardinality"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "cardinality"
+msgstr ""
+
 msgid "category"
 msgstr "categoria"
 
@@ -1221,12 +1325,17 @@
 msgid "comment"
 msgstr "Comentario"
 
-msgid "comment:"
-msgstr "Comentario:"
+msgctxt "TrInfo"
+msgid "comment"
+msgstr ""
 
 msgid "comment_format"
 msgstr "Formato"
 
+msgctxt "TrInfo"
+msgid "comment_format"
+msgstr ""
+
 msgid "components"
 msgstr "Componentes"
 
@@ -1278,6 +1387,12 @@
 "Componente que permite distribuir sobre varias páginas las búsquedas que "
 "arrojan mayores resultados que un número previamente elegido"
 
+msgid "components_pdfview"
+msgstr ""
+
+msgid "components_pdfview_description"
+msgstr ""
+
 msgid "components_rqlinput"
 msgstr "Barra rql"
 
@@ -1287,12 +1402,32 @@
 msgid "composite"
 msgstr "composite"
 
+msgctxt "CWRelation"
+msgid "composite"
+msgstr ""
+
 msgid "condition"
 msgstr "condición"
 
+msgctxt "BaseTransition"
+msgid "condition"
+msgstr ""
+
+msgctxt "Transition"
+msgid "condition"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "condition"
+msgstr ""
+
 msgid "condition:"
 msgstr "condición:"
 
+msgctxt "RQLExpression"
+msgid "condition_object"
+msgstr ""
+
 msgid "condition_object"
 msgstr "condición de"
 
@@ -1302,6 +1437,18 @@
 msgid "constrained_by"
 msgstr "Restricción hecha por"
 
+msgctxt "CWAttribute"
+msgid "constrained_by"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "constrained_by"
+msgstr ""
+
+msgctxt "CWConstraint"
+msgid "constrained_by_object"
+msgstr ""
+
 msgid "constrained_by_object"
 msgstr "ha restringido"
 
@@ -1501,23 +1648,43 @@
 msgid "creating RQLExpression (Transition %(linkto)s condition RQLExpression)"
 msgstr "Creación de una expresión RQL para la transición %(linkto)s"
 
+msgid ""
+"creating RQLExpression (WorkflowTransition %(linkto)s condition "
+"RQLExpression)"
+msgstr ""
+
 msgid "creating State (State allowed_transition Transition %(linkto)s)"
 msgstr "Creación de un estado que pueda ir hacia la transición %(linkto)s"
 
-msgid "creating State (State state_of CWEType %(linkto)s)"
-msgstr "Creación de un estado por el tipo %(linkto)s"
+msgid "creating State (State state_of Workflow %(linkto)s)"
+msgstr ""
 
 msgid "creating State (Transition %(linkto)s destination_state State)"
 msgstr "Creación de un estado destinación de la transición %(linkto)s"
 
+msgid ""
+"creating SubWorkflowExitPoint (WorkflowTransition %(linkto)s "
+"subworkflow_exit SubWorkflowExitPoint)"
+msgstr ""
+
 msgid "creating Transition (State %(linkto)s allowed_transition Transition)"
 msgstr "Creación de una transición autorizada desde el estado %(linkto)s"
 
 msgid "creating Transition (Transition destination_state State %(linkto)s)"
 msgstr "Creación de un transición hacia el estado %(linkto)s"
 
-msgid "creating Transition (Transition transition_of CWEType %(linkto)s)"
-msgstr "Creación de una transición para el tipo %(linkto)s"
+msgid "creating Transition (Transition transition_of Workflow %(linkto)s)"
+msgstr ""
+
+msgid ""
+"creating WorkflowTransition (State %(linkto)s allowed_transition "
+"WorkflowTransition)"
+msgstr ""
+
+msgid ""
+"creating WorkflowTransition (WorkflowTransition transition_of Workflow %"
+"(linkto)s)"
+msgstr ""
 
 msgid "creation"
 msgstr "Creación"
@@ -1531,6 +1698,14 @@
 msgid "cstrtype"
 msgstr "Tipo de condición"
 
+msgctxt "CWConstraint"
+msgid "cstrtype"
+msgstr ""
+
+msgctxt "CWConstraintType"
+msgid "cstrtype_object"
+msgstr ""
+
 msgid "cstrtype_object"
 msgstr "utilizado por"
 
@@ -1544,6 +1719,12 @@
 msgid "currently attached file: %s"
 msgstr "archivo adjunto: %s"
 
+msgid "custom_workflow"
+msgstr ""
+
+msgid "custom_workflow_object"
+msgstr ""
+
 msgid "cwetype-schema-image"
 msgstr "Esquema"
 
@@ -1580,9 +1761,33 @@
 msgid "default text format for rich text fields."
 msgstr "Formato de texto como opción por defecto para los campos texto"
 
+msgid "default user workflow"
+msgstr ""
+
+msgid "default workflow for an entity type"
+msgstr ""
+
+msgid "default_workflow"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "default_workflow"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "default_workflow_object"
+msgstr ""
+
+msgid "default_workflow_object"
+msgstr ""
+
 msgid "defaultval"
 msgstr "Valor por defecto"
 
+msgctxt "CWAttribute"
+msgid "defaultval"
+msgstr ""
+
 msgid "define a CubicWeb user"
 msgstr "Define un usuario CubicWeb"
 
@@ -1614,6 +1819,9 @@
 msgid "define an entity type, used to build the instance schema"
 msgstr ""
 
+msgid "define how we get out from a sub-workflow"
+msgstr ""
+
 msgid ""
 "defines what's the property is applied for. You must select this first to be "
 "able to set value"
@@ -1639,6 +1847,22 @@
 msgid "delete_permission"
 msgstr "Autorización de eliminar"
 
+msgctxt "CWEType"
+msgid "delete_permission"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "delete_permission"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "delete_permission_object"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "delete_permission_object"
+msgstr ""
+
 msgid "delete_permission_object"
 msgstr "posee la autorización de eliminar"
 
@@ -1660,9 +1884,84 @@
 msgid "description"
 msgstr "Descripción"
 
+msgctxt "CWEType"
+msgid "description"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "description"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "description"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "description"
+msgstr ""
+
+msgctxt "Transition"
+msgid "description"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "description"
+msgstr ""
+
+msgctxt "State"
+msgid "description"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "description"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "description"
+msgstr ""
+
 msgid "description_format"
 msgstr "Formato"
 
+msgctxt "CWEType"
+msgid "description_format"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "description_format"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "description_format"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "description_format"
+msgstr ""
+
+msgctxt "Transition"
+msgid "description_format"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "description_format"
+msgstr ""
+
+msgctxt "State"
+msgid "description_format"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "description_format"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "description_format"
+msgstr ""
+
+msgid "destination state"
+msgstr ""
+
 msgid "destination state for this transition"
 msgstr "Estado destino para esta transición"
 
@@ -1672,6 +1971,18 @@
 msgid "destination_state"
 msgstr "Estado destino"
 
+msgctxt "Transition"
+msgid "destination_state"
+msgstr ""
+
+msgctxt "SubWorkflowExitPoint"
+msgid "destination_state"
+msgstr ""
+
+msgctxt "State"
+msgid "destination_state_object"
+msgstr ""
+
 msgid "destination_state_object"
 msgstr "Destino de"
 
@@ -1700,6 +2011,9 @@
 msgid "display the component or not"
 msgstr "Mostrar el componente o no"
 
+msgid "display the pdf icon or not"
+msgstr ""
+
 msgid ""
 "distinct label to distinguate between other permission entity of the same "
 "name"
@@ -1717,6 +2031,9 @@
 msgid "download icon"
 msgstr "ícono de descarga"
 
+msgid "download page as pdf"
+msgstr ""
+
 msgid "download schema as owl"
 msgstr "Descargar esquema en OWL"
 
@@ -1774,6 +2091,9 @@
 msgid "entity edited"
 msgstr "entidad modificada"
 
+msgid "entity has no workflow set"
+msgstr ""
+
 msgid "entity linked"
 msgstr "entidad asociada"
 
@@ -1787,11 +2107,8 @@
 "Tipo de entidad utilizada para definir una configuración de seguridad "
 "avanzada"
 
-msgid "entity types which may use this state"
-msgstr "Tipo de entidades que pueden utilizar este estado"
-
-msgid "entity types which may use this transition"
-msgstr "Entidades que pueden utilizar esta transición"
+msgid "entity types which may use this workflow"
+msgstr ""
 
 msgid "error while embedding page"
 msgstr "Error durante la inclusión de la página"
@@ -1813,15 +2130,33 @@
 msgid "eta_date"
 msgstr "fecha de fin"
 
+msgid "exit_point"
+msgstr ""
+
+msgid "exit_point_object"
+msgstr ""
+
+#, python-format
+msgid "exiting from subworkflow %s"
+msgstr ""
+
 msgid "expected:"
 msgstr "Previsto :"
 
 msgid "expression"
 msgstr "Expresión"
 
+msgctxt "RQLExpression"
+msgid "expression"
+msgstr ""
+
 msgid "exprtype"
 msgstr "Tipo de la expresión"
 
+msgctxt "RQLExpression"
+msgid "exprtype"
+msgstr ""
+
 msgid "external page"
 msgstr "Página externa"
 
@@ -1873,9 +2208,21 @@
 msgid "final"
 msgstr "Final"
 
+msgctxt "CWEType"
+msgid "final"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "final"
+msgstr ""
+
 msgid "firstname"
 msgstr "Nombre"
 
+msgctxt "CWUser"
+msgid "firstname"
+msgstr ""
+
 msgid "foaf"
 msgstr "Amigo de un Amigo, FOAF"
 
@@ -1885,6 +2232,14 @@
 msgid "for_user"
 msgstr "Para el usuario"
 
+msgctxt "CWProperty"
+msgid "for_user"
+msgstr ""
+
+msgctxt "CWUser"
+msgid "for_user_object"
+msgstr ""
+
 msgid "for_user_object"
 msgstr "Utiliza las propiedades"
 
@@ -1901,6 +2256,18 @@
 msgid "from_entity"
 msgstr "De la entidad"
 
+msgctxt "CWAttribute"
+msgid "from_entity"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "from_entity"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "from_entity_object"
+msgstr ""
+
 msgid "from_entity_object"
 msgstr "Relación sujeto"
 
@@ -1910,6 +2277,14 @@
 msgid "from_state"
 msgstr "De el estado"
 
+msgctxt "TrInfo"
+msgid "from_state"
+msgstr ""
+
+msgctxt "State"
+msgid "from_state_object"
+msgstr ""
+
 msgid "from_state_object"
 msgstr "Transiciones desde este estado"
 
@@ -1919,9 +2294,17 @@
 msgid "fulltext_container"
 msgstr "Contenedor de texto indexado"
 
+msgctxt "CWRType"
+msgid "fulltext_container"
+msgstr ""
+
 msgid "fulltextindexed"
 msgstr "Indexación de texto"
 
+msgctxt "CWAttribute"
+msgid "fulltextindexed"
+msgstr ""
+
 msgid "generic plot"
 msgstr "Trazado de curbas estándares"
 
@@ -1940,6 +2323,10 @@
 msgid "granted to groups"
 msgstr "Otorgado a los grupos"
 
+#, python-format
+msgid "graphical representation of %s"
+msgstr ""
+
 msgid "graphical representation of the instance'schema"
 msgstr ""
 
@@ -2034,6 +2421,9 @@
 msgid "id of main template used to render pages"
 msgstr "ID del template principal"
 
+msgid "identical to"
+msgstr ""
+
 msgid "identical_to"
 msgstr "idéntico a"
 
@@ -2062,6 +2452,14 @@
 msgid "in_group"
 msgstr "En el grupo"
 
+msgctxt "CWUser"
+msgid "in_group"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "in_group_object"
+msgstr ""
+
 msgid "in_group_object"
 msgstr "Miembros"
 
@@ -2087,6 +2485,10 @@
 msgid "indexed"
 msgstr "Indexado"
 
+msgctxt "CWAttribute"
+msgid "indexed"
+msgstr ""
+
 msgid "indicate the current state of an entity"
 msgstr "Indica el estado actual de una entidad"
 
@@ -2103,18 +2505,30 @@
 msgid "initial estimation %s"
 msgstr "Estimación inicial %s"
 
-msgid "initial state for entities of this type"
-msgstr "Estado inicial para las entidades de este tipo"
+msgid "initial state for this workflow"
+msgstr ""
 
 msgid "initial_state"
 msgstr "estado inicial"
 
+msgctxt "Workflow"
+msgid "initial_state"
+msgstr ""
+
+msgctxt "State"
+msgid "initial_state_object"
+msgstr ""
+
 msgid "initial_state_object"
 msgstr "es el estado inicial de"
 
 msgid "inlined"
 msgstr "Puesto en línea"
 
+msgctxt "CWRType"
+msgid "inlined"
+msgstr ""
+
 msgid "instance schema"
 msgstr ""
 
@@ -2124,6 +2538,10 @@
 msgid "internationalizable"
 msgstr "Internacionalizable"
 
+msgctxt "CWAttribute"
+msgid "internationalizable"
+msgstr ""
+
 #, python-format
 msgid "invalid action %r"
 msgstr "Acción %r invalida"
@@ -2181,6 +2599,10 @@
 msgid "label"
 msgstr "Etiqueta"
 
+msgctxt "CWPermission"
+msgid "label"
+msgstr ""
+
 msgid "language of the user interface"
 msgstr "Idioma para la interface del usuario"
 
@@ -2190,6 +2612,10 @@
 msgid "last_login_time"
 msgstr "Ultima fecha de conexión"
 
+msgctxt "CWUser"
+msgid "last_login_time"
+msgstr ""
+
 msgid "latest modification time of an entity"
 msgstr "Fecha de la última modificación de una entidad "
 
@@ -2223,14 +2649,17 @@
 msgid "link a relation definition to its subject entity type"
 msgstr "liga una definición de relación a su tipo de entidad"
 
-msgid "link a state to one or more entity type"
-msgstr "liga un estado a una o mas entidades"
+msgid "link a state to one or more workflow"
+msgstr ""
 
 msgid "link a transition information to its object"
 msgstr "liga una transcion de informacion a los objetos asociados"
 
-msgid "link a transition to one or more entity type"
-msgstr "liga una transición a una o mas tipos de entidad"
+msgid "link a transition to one or more workflow"
+msgstr ""
+
+msgid "link a workflow to one or more entity type"
+msgstr ""
 
 msgid "link to each item in"
 msgstr "ligar hacia cada elemento en"
@@ -2247,6 +2676,10 @@
 msgid "login"
 msgstr "Clave de acesso"
 
+msgctxt "CWUser"
+msgid "login"
+msgstr ""
+
 msgid "login or email"
 msgstr "Clave de acesso o dirección de correo"
 
@@ -2266,6 +2699,10 @@
 msgid "mainvars"
 msgstr "Principales variables"
 
+msgctxt "RQLExpression"
+msgid "mainvars"
+msgstr ""
+
 msgid "manage"
 msgstr "Administracion del Sitio"
 
@@ -2281,6 +2718,9 @@
 msgid "managers"
 msgstr "editores"
 
+msgid "mandatory relation"
+msgstr ""
+
 msgid "march"
 msgstr "Marzo"
 
@@ -2327,6 +2767,50 @@
 msgid "name"
 msgstr "Nombre"
 
+msgctxt "CWEType"
+msgid "name"
+msgstr ""
+
+msgctxt "Transition"
+msgid "name"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "name"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "name"
+msgstr ""
+
+msgctxt "CWConstraintType"
+msgid "name"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "name"
+msgstr ""
+
+msgctxt "State"
+msgid "name"
+msgstr ""
+
+msgctxt "CWPermission"
+msgid "name"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "name"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "name"
+msgstr ""
+
+msgctxt "CWCache"
+msgid "name"
+msgstr ""
+
 msgid "name of the cache"
 msgstr "Nombre del Cache"
 
@@ -2438,6 +2922,14 @@
 msgid "ordernum"
 msgstr "orden"
 
+msgctxt "CWAttribute"
+msgid "ordernum"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "ordernum"
+msgstr ""
+
 msgid "owl"
 msgstr "owl"
 
@@ -2471,6 +2963,10 @@
 msgid "path"
 msgstr "Ruta"
 
+msgctxt "Bookmark"
+msgid "path"
+msgstr ""
+
 msgid "permission"
 msgstr "Permiso"
 
@@ -2492,6 +2988,10 @@
 msgid "pkey"
 msgstr "pkey"
 
+msgctxt "CWProperty"
+msgid "pkey"
+msgstr ""
+
 msgid "please correct errors below"
 msgstr "Favor de corregir errores"
 
@@ -2504,6 +3004,20 @@
 msgid "powered by CubicWeb"
 msgstr ""
 
+msgid "prefered_form"
+msgstr ""
+
+msgctxt "EmailAddress"
+msgid "prefered_form"
+msgstr ""
+
+msgctxt "EmailAddress"
+msgid "prefered_form_object"
+msgstr ""
+
+msgid "prefered_form_object"
+msgstr ""
+
 msgid "preferences"
 msgstr "Preferencias"
 
@@ -2516,6 +3030,14 @@
 msgid "primary_email"
 msgstr "Dirección de email principal"
 
+msgctxt "CWUser"
+msgid "primary_email"
+msgstr ""
+
+msgctxt "EmailAddress"
+msgid "primary_email_object"
+msgstr ""
+
 msgid "primary_email_object"
 msgstr "Dirección de email principal (objeto)"
 
@@ -2537,12 +3059,34 @@
 msgid "read_permission"
 msgstr "Permiso de lectura"
 
+msgctxt "CWEType"
+msgid "read_permission"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "read_permission"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "read_permission_object"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "read_permission_object"
+msgstr ""
+
 msgid "read_permission_object"
 msgstr "Objeto_permiso_lectura"
 
 msgid "registry"
 msgstr ""
 
+msgid "related entity has no state"
+msgstr ""
+
+msgid "related entity has no workflow set"
+msgstr ""
+
 #, python-format
 msgid "relation %(relname)s of %(ent)s"
 msgstr "relación %(relname)s de %(ent)s"
@@ -2550,6 +3094,18 @@
 msgid "relation_type"
 msgstr "tipo de relación"
 
+msgctxt "CWAttribute"
+msgid "relation_type"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "relation_type"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "relation_type_object"
+msgstr ""
+
 msgid "relation_type_object"
 msgstr "Definición"
 
@@ -2562,63 +3118,45 @@
 msgid "relative url of the bookmarked page"
 msgstr "Url relativa de la pagina"
 
-msgid "remove this Bookmark"
-msgstr "Eliminar este Favorito"
-
-msgid "remove this CWAttribute"
-msgstr "Eliminar este atributo"
-
-msgid "remove this CWCache"
-msgstr "Eliminar esta cache de aplicación"
-
-msgid "remove this CWConstraint"
-msgstr "Eliminar esta restricción"
-
-msgid "remove this CWConstraintType"
-msgstr "Eliminar este tipo de restricción"
-
+msgctxt "inlined:CWRelation:from_entity:subject"
+msgid "remove this CWEType"
+msgstr ""
+
+msgctxt "inlined:CWRelation:to_entity:subject"
 msgid "remove this CWEType"
-msgstr "Eliminar este tipo de entidad"
-
-msgid "remove this CWGroup"
-msgstr "Eliminar este grupo"
-
-msgid "remove this CWPermission"
-msgstr "Eliminar este permiso"
-
-msgid "remove this CWProperty"
-msgstr "Eliminar esta propiedad"
-
+msgstr ""
+
+msgctxt "inlined:CWRelation:relation_type:subject"
 msgid "remove this CWRType"
-msgstr "Eliminar esta definición de relación"
-
-msgid "remove this CWRelation"
-msgstr "Eliminar esta relación"
-
-msgid "remove this CWUser"
-msgstr "Eliminar este usuario"
-
+msgstr ""
+
+msgctxt "inlined:CWUser:use_email:subject"
 msgid "remove this EmailAddress"
-msgstr "Eliminar este correo electronico"
-
-msgid "remove this ExternalUri"
-msgstr ""
-
-msgid "remove this RQLExpression"
-msgstr "Eliminar esta expresión RQL"
-
-msgid "remove this State"
-msgstr "Eliminar este estado"
-
-msgid "remove this TrInfo"
-msgstr "Eliminar información de esta transición"
-
-msgid "remove this Transition"
-msgstr "Eliminar esta transición"
+msgstr ""
 
 msgid "require_group"
 msgstr "Requiere grupo"
 
+msgctxt "BaseTransition"
+msgid "require_group"
+msgstr ""
+
+msgctxt "Transition"
+msgid "require_group"
+msgstr ""
+
+msgctxt "CWPermission"
+msgid "require_group"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "require_group"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "require_group_object"
+msgstr ""
+
 msgid "require_group_object"
 msgstr "Requerido por grupo"
 
@@ -2747,6 +3285,9 @@
 msgid "semantic description of this transition"
 msgstr "descripcion semantica de esta transición"
 
+msgid "semantic description of this workflow"
+msgstr ""
+
 msgid "send email"
 msgstr "enviar email"
 
@@ -2803,9 +3344,20 @@
 msgid "sparql xml"
 msgstr ""
 
+msgid "special transition allowing to go through a sub-workflow"
+msgstr ""
+
 msgid "specializes"
 msgstr "derivado de"
 
+msgctxt "CWEType"
+msgid "specializes"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "specializes_object"
+msgstr ""
+
 msgid "specializes_object"
 msgstr "objeto_derivado"
 
@@ -2815,9 +3367,28 @@
 msgid "state"
 msgstr "estado"
 
+msgid "state doesn't belong to entity's current workflow"
+msgstr ""
+
+msgid "state doesn't belong to entity's workflow"
+msgstr ""
+
+msgid ""
+"state doesn't belong to entity's workflow. You may want to set a custom "
+"workflow for this entity first."
+msgstr ""
+
 msgid "state_of"
 msgstr "estado_de"
 
+msgctxt "State"
+msgid "state_of"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "state_of_object"
+msgstr ""
+
 msgid "state_of_object"
 msgstr "objeto_estado_de"
 
@@ -2840,15 +3411,68 @@
 msgid "subject_plural:"
 msgstr "sujetos:"
 
+msgid "subworkflow"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "subworkflow"
+msgstr ""
+
+msgid "subworkflow state"
+msgstr ""
+
+msgid "subworkflow_exit"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "subworkflow_exit"
+msgstr ""
+
+msgctxt "SubWorkflowExitPoint"
+msgid "subworkflow_exit_object"
+msgstr ""
+
+msgid "subworkflow_exit_object"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "subworkflow_object"
+msgstr ""
+
+msgid "subworkflow_object"
+msgstr ""
+
+msgid "subworkflow_state"
+msgstr ""
+
+msgctxt "SubWorkflowExitPoint"
+msgid "subworkflow_state"
+msgstr ""
+
+msgctxt "State"
+msgid "subworkflow_state_object"
+msgstr ""
+
+msgid "subworkflow_state_object"
+msgstr ""
+
 msgid "sunday"
 msgstr "domingo"
 
 msgid "surname"
 msgstr "apellido"
 
+msgctxt "CWUser"
+msgid "surname"
+msgstr ""
+
 msgid "symetric"
 msgstr "simetrico"
 
+msgctxt "CWRType"
+msgid "symetric"
+msgstr ""
+
 msgid "system entities"
 msgstr "entidades de sistema"
 
@@ -2904,6 +3528,10 @@
 msgid "timestamp"
 msgstr "fecha"
 
+msgctxt "CWCache"
+msgid "timestamp"
+msgstr ""
+
 msgid "timestamp of the latest source synchronization."
 msgstr "fecha de la ultima sincronización de la fuente."
 
@@ -2913,6 +3541,10 @@
 msgid "title"
 msgstr "titulo"
 
+msgctxt "Bookmark"
+msgid "title"
+msgstr ""
+
 msgid "to"
 msgstr "a"
 
@@ -2926,6 +3558,18 @@
 msgid "to_entity"
 msgstr "hacia entidad"
 
+msgctxt "CWAttribute"
+msgid "to_entity"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "to_entity"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "to_entity_object"
+msgstr ""
+
 msgid "to_entity_object"
 msgstr "hacia entidad objeto"
 
@@ -2935,6 +3579,14 @@
 msgid "to_state"
 msgstr "hacia el estado"
 
+msgctxt "TrInfo"
+msgid "to_state"
+msgstr ""
+
+msgctxt "State"
+msgid "to_state_object"
+msgstr ""
+
 msgid "to_state_object"
 msgstr "hacia objeto estado"
 
@@ -2944,13 +3596,34 @@
 msgid "toggle check boxes"
 msgstr "cambiar valor"
 
-#, python-format
-msgid "transition from %s to %s does not exist or is not allowed"
+msgid "transition doesn't belong to entity's workflow"
+msgstr ""
+
+msgid "transition isn't allowed"
+msgstr ""
+
+msgid "transition may not be fired"
 msgstr ""
 
 msgid "transition_of"
 msgstr "transicion de"
 
+msgctxt "BaseTransition"
+msgid "transition_of"
+msgstr ""
+
+msgctxt "Transition"
+msgid "transition_of"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "transition_of"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "transition_of_object"
+msgstr ""
+
 msgid "transition_of_object"
 msgstr "objeto de transición"
 
@@ -3023,6 +3696,10 @@
 msgid "upassword"
 msgstr "clave de acceso"
 
+msgctxt "CWUser"
+msgid "upassword"
+msgstr ""
+
 msgid "update"
 msgstr "modificación"
 
@@ -3032,6 +3709,18 @@
 msgid "update_permission"
 msgstr "Permiso de modificación"
 
+msgctxt "CWEType"
+msgid "update_permission"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "update_permission_object"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "update_permission_object"
+msgstr ""
+
 msgid "update_permission_object"
 msgstr "objeto de autorización de modificaciones"
 
@@ -3042,6 +3731,10 @@
 msgid "uri"
 msgstr ""
 
+msgctxt "ExternalUri"
+msgid "uri"
+msgstr ""
+
 msgid "use template languages"
 msgstr "utilizar plantillas de lenguaje"
 
@@ -3055,6 +3748,14 @@
 msgid "use_email"
 msgstr "correo electrónico"
 
+msgctxt "CWUser"
+msgid "use_email"
+msgstr ""
+
+msgctxt "EmailAddress"
+msgid "use_email_object"
+msgstr ""
+
 msgid "use_email_object"
 msgstr "objeto email utilizado"
 
@@ -3113,6 +3814,14 @@
 msgid "value"
 msgstr "valor"
 
+msgctxt "CWConstraint"
+msgid "value"
+msgstr ""
+
+msgctxt "CWProperty"
+msgid "value"
+msgstr ""
+
 msgid "value associated to this key is not editable manually"
 msgstr "el valor asociado a este elemento no es editable manualmente"
 
@@ -3164,16 +3873,42 @@
 
 msgid ""
 "when multiple addresses are equivalent (such as python-projects@logilab.org "
-"and python-projects@lists.logilab.org), set this to true on one of them "
-"which is the preferred form."
-msgstr ""
-"cuando multiples direcciones de correo son equivalentes (como python-"
-"projects@logilab.org y python-projects@lists.logilab.org), establecer esto "
-"como verdadero en una de ellas es la forma preferida "
+"and python-projects@lists.logilab.org), set this to indicate which is the "
+"preferred form."
+msgstr ""
+
+msgid "workflow"
+msgstr ""
 
 #, python-format
-msgid "workflow for %s"
-msgstr "workflow para %s"
+msgid "workflow changed to \"%s\""
+msgstr ""
+
+msgid "workflow has no initial state"
+msgstr ""
+
+msgid "workflow history item"
+msgstr ""
+
+msgid "workflow to which this state belongs"
+msgstr ""
+
+msgid "workflow to which this transition belongs"
+msgstr ""
+
+msgid "workflow_of"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "workflow_of"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "workflow_of_object"
+msgstr ""
+
+msgid "workflow_of_object"
+msgstr ""
 
 msgid "xbel"
 msgstr "xbel"
@@ -3193,6 +3928,162 @@
 msgid "you should probably delete that property"
 msgstr "deberia probablamente suprimir esta propriedad"
 
+#~ msgid "There is no workflow defined for this entity."
+#~ msgstr "No hay workflow para este entidad"
+
 #~ msgid ""
 #~ "You have no access to this view or it's not applyable to current data"
 #~ msgstr "No tiene acceso a esta vista o No es aplicable a los datos actuales"
+
+#~ msgid "__msg state changed"
+#~ msgstr "El estado a cambiado"
+
+#~ msgid "account state"
+#~ msgstr "Estado de la Cuenta"
+
+#~ msgid "add State state_of CWEType object"
+#~ msgstr "Estado"
+
+#~ msgid "add Transition transition_of CWEType object"
+#~ msgstr "Transición"
+
+#~ msgid "add a Bookmark"
+#~ msgstr "Agregar un Favorito"
+
+#~ msgid "add a CWAttribute"
+#~ msgstr "Agregar un tipo de relación"
+
+#~ msgid "add a CWCache"
+#~ msgstr "Agregar un cache"
+
+#~ msgid "add a CWConstraint"
+#~ msgstr "Agregar una Restricción"
+
+#~ msgid "add a CWConstraintType"
+#~ msgstr "Agregar un tipo de Restricción"
+
+#~ msgid "add a CWEType"
+#~ msgstr "Agregar un tipo de entidad"
+
+#~ msgid "add a CWGroup"
+#~ msgstr "Agregar un grupo de usuarios"
+
+#~ msgid "add a CWPermission"
+#~ msgstr "Agregar una autorización"
+
+#~ msgid "add a CWProperty"
+#~ msgstr "Agregar una propiedad"
+
+#~ msgid "add a CWRType"
+#~ msgstr "Agregar un tipo de relación"
+
+#~ msgid "add a CWRelation"
+#~ msgstr "Agregar una relación"
+
+#~ msgid "add a CWUser"
+#~ msgstr "Agregar un usuario"
+
+#~ msgid "add a EmailAddress"
+#~ msgstr "Agregar un email"
+
+#~ msgid "add a RQLExpression"
+#~ msgstr "Agregar una expresión rql"
+
+#~ msgid "add a State"
+#~ msgstr "Agregar un estado"
+
+#~ msgid "add a TrInfo"
+#~ msgstr "Agregar una información de transición"
+
+#~ msgid "add a Transition"
+#~ msgstr "Agregar una transición"
+
+#~ msgid "canonical"
+#~ msgstr "canónico"
+
+#~ msgid "comment:"
+#~ msgstr "Comentario:"
+
+#~ msgid "creating State (State state_of CWEType %(linkto)s)"
+#~ msgstr "Creación de un estado por el tipo %(linkto)s"
+
+#~ msgid "creating Transition (Transition transition_of CWEType %(linkto)s)"
+#~ msgstr "Creación de una transición para el tipo %(linkto)s"
+
+#~ msgid "entity types which may use this state"
+#~ msgstr "Tipo de entidades que pueden utilizar este estado"
+
+#~ msgid "entity types which may use this transition"
+#~ msgstr "Entidades que pueden utilizar esta transición"
+
+#~ msgid "initial state for entities of this type"
+#~ msgstr "Estado inicial para las entidades de este tipo"
+
+#~ msgid "link a state to one or more entity type"
+#~ msgstr "liga un estado a una o mas entidades"
+
+#~ msgid "link a transition to one or more entity type"
+#~ msgstr "liga una transición a una o mas tipos de entidad"
+
+#~ msgid "remove this Bookmark"
+#~ msgstr "Eliminar este Favorito"
+
+#~ msgid "remove this CWAttribute"
+#~ msgstr "Eliminar este atributo"
+
+#~ msgid "remove this CWCache"
+#~ msgstr "Eliminar esta cache de aplicación"
+
+#~ msgid "remove this CWConstraint"
+#~ msgstr "Eliminar esta restricción"
+
+#~ msgid "remove this CWConstraintType"
+#~ msgstr "Eliminar este tipo de restricción"
+
+#~ msgid "remove this CWEType"
+#~ msgstr "Eliminar este tipo de entidad"
+
+#~ msgid "remove this CWGroup"
+#~ msgstr "Eliminar este grupo"
+
+#~ msgid "remove this CWPermission"
+#~ msgstr "Eliminar este permiso"
+
+#~ msgid "remove this CWProperty"
+#~ msgstr "Eliminar esta propiedad"
+
+#~ msgid "remove this CWRType"
+#~ msgstr "Eliminar esta definición de relación"
+
+#~ msgid "remove this CWRelation"
+#~ msgstr "Eliminar esta relación"
+
+#~ msgid "remove this CWUser"
+#~ msgstr "Eliminar este usuario"
+
+#~ msgid "remove this EmailAddress"
+#~ msgstr "Eliminar este correo electronico"
+
+#~ msgid "remove this RQLExpression"
+#~ msgstr "Eliminar esta expresión RQL"
+
+#~ msgid "remove this State"
+#~ msgstr "Eliminar este estado"
+
+#~ msgid "remove this TrInfo"
+#~ msgstr "Eliminar información de esta transición"
+
+#~ msgid "remove this Transition"
+#~ msgstr "Eliminar esta transición"
+
+#~ msgid ""
+#~ "when multiple addresses are equivalent (such as python-projects@logilab."
+#~ "org and python-projects@lists.logilab.org), set this to true on one of "
+#~ "them which is the preferred form."
+#~ msgstr ""
+#~ "cuando multiples direcciones de correo son equivalentes (como python-"
+#~ "projects@logilab.org y python-projects@lists.logilab.org), establecer "
+#~ "esto como verdadero en una de ellas es la forma preferida "
+
+#~ msgid "workflow for %s"
+#~ msgstr "workflow para %s"
--- a/i18n/fr.po	Thu Sep 17 19:38:04 2009 +0200
+++ b/i18n/fr.po	Fri Sep 18 11:24:37 2009 +0200
@@ -4,7 +4,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: cubicweb 2.46.0\n"
-"PO-Revision-Date: 2009-08-05 08:37+0200\n"
+"PO-Revision-Date: 2009-09-17 12:03+0200\n"
 "Last-Translator: Logilab Team <contact@logilab.fr>\n"
 "Language-Team: fr <contact@logilab.fr>\n"
 "MIME-Version: 1.0\n"
@@ -112,10 +112,6 @@
 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)"
 
@@ -204,6 +200,15 @@
 msgid "Attributes"
 msgstr "Attributs"
 
+# schema pot file, generated on 2009-09-16 16:46:55
+#
+# singular and plural forms for each entity type
+msgid "BaseTransition"
+msgstr "Transition (abstraite)"
+
+msgid "BaseTransition_plural"
+msgstr "Transitions (abstraites)"
+
 msgid "Bookmark"
 msgstr "Signet"
 
@@ -358,6 +363,9 @@
 msgid "Interval_plural"
 msgstr "Durées"
 
+msgid "New BaseTransition"
+msgstr "XXX"
+
 msgid "New Bookmark"
 msgstr "Nouveau signet"
 
@@ -406,12 +414,21 @@
 msgid "New State"
 msgstr "Nouvel état"
 
+msgid "New SubWorkflowExitPoint"
+msgstr "Nouvelle sortie de sous-workflow"
+
 msgid "New TrInfo"
 msgstr "Nouvelle information de transition"
 
 msgid "New Transition"
 msgstr "Nouvelle transition"
 
+msgid "New Workflow"
+msgstr "Nouveau workflow"
+
+msgid "New WorkflowTransition"
+msgstr "Nouvelle transition workflow"
+
 msgid "No query has been executed"
 msgstr "Aucune requête n'a été éxécuté"
 
@@ -479,6 +496,12 @@
 msgid "String_plural"
 msgstr "Chaînes de caractères"
 
+msgid "SubWorkflowExitPoint"
+msgstr "Sortie de sous-workflow"
+
+msgid "SubWorkflowExitPoint_plural"
+msgstr "Sorties de sous-workflow"
+
 msgid "Subject:"
 msgstr "Sujet :"
 
@@ -499,12 +522,8 @@
 msgid "The view %s could not be found"
 msgstr "La vue %s est introuvable"
 
-msgid "There is no workflow defined for this entity."
-msgstr "Il n'y a pas de workflow défini pour ce type d'entité"
-
-#, python-format
-msgid "This %s"
-msgstr "Ce %s"
+msgid "This BaseTransition"
+msgstr "Cette transition abstraite"
 
 msgid "This Bookmark"
 msgstr "Ce signet"
@@ -554,12 +573,21 @@
 msgid "This State"
 msgstr "Cet état"
 
+msgid "This SubWorkflowExitPoint"
+msgstr "Cette sortie de sous-workflow"
+
 msgid "This TrInfo"
 msgstr "Cette information de transition"
 
 msgid "This Transition"
 msgstr "Cette transition"
 
+msgid "This Workflow"
+msgstr "Ce workflow"
+
+msgid "This WorkflowTransition"
+msgstr "Cette transition workflow"
+
 msgid "Time"
 msgstr "Heure"
 
@@ -591,9 +619,21 @@
 msgid "What's new?"
 msgstr "Nouveautés"
 
+msgid "Workflow"
+msgstr "Workflow"
+
 msgid "Workflow history"
 msgstr "Historique des changements d'état"
 
+msgid "WorkflowTransition"
+msgstr "Transition workflow"
+
+msgid "WorkflowTransition_plural"
+msgstr "Transitions workflow"
+
+msgid "Workflow_plural"
+msgstr "Workflows"
+
 msgid "You are not connected to an instance !"
 msgstr "Vous n'êtes pas connecté à une instance"
 
@@ -627,7 +667,8 @@
 msgid ""
 "You have no access to this view or it can not be used to display the current "
 "data."
-msgstr "Vous n'avez pas accès à cette vue ou elle ne peut pas afficher ces données."
+msgstr ""
+"Vous n'avez pas accès à cette vue ou elle ne peut pas afficher ces données."
 
 msgid ""
 "You're not authorized to access this page. If you think you should, please "
@@ -640,9 +681,6 @@
 msgid "[%s supervision] changes summary"
 msgstr "[%s supervision] description des changements"
 
-msgid "__msg state changed"
-msgstr "l'état a été changé"
-
 msgid ""
 "a RQL expression which should return some results, else the transition won't "
 "be available. This query may use X and U variables that will respectivly "
@@ -671,12 +709,12 @@
 msgid "about this site"
 msgstr "à propos de ce site"
 
+msgid "abstract base class for transitions"
+msgstr "classe de base abstraite pour les transitions"
+
 msgid "access type"
 msgstr "type d'accès"
 
-msgid "account state"
-msgstr "état du compte"
-
 msgid "action(s) on this selection"
 msgstr "action(s) sur cette sélection"
 
@@ -689,6 +727,12 @@
 msgid "actions_addentity_description"
 msgstr ""
 
+msgid "actions_addrelated"
+msgstr "menu ajouter"
+
+msgid "actions_addrelated_description"
+msgstr ""
+
 msgid "actions_cancel"
 msgstr "annuler la sélection"
 
@@ -867,82 +911,56 @@
 msgstr "utilisateur"
 
 msgid "add CWUser use_email EmailAddress subject"
-msgstr "ajouter une addresse email"
+msgstr "addresse email"
 
 msgid "add State allowed_transition Transition object"
-msgstr "ajouter un état en entrée"
+msgstr "état en entrée"
 
 msgid "add State allowed_transition Transition subject"
-msgstr "ajouter une transition en sortie"
-
-msgid "add State state_of CWEType object"
-msgstr "ajouter un état"
+msgstr "transition en sortie"
+
+msgid "add State allowed_transition WorkflowTransition subject"
+msgstr "transition workflow en sortie"
+
+msgid "add State state_of Workflow object"
+msgstr "état"
 
 msgid "add Transition condition RQLExpression subject"
-msgstr "ajouter une condition"
+msgstr "condition"
 
 msgid "add Transition destination_state State object"
-msgstr "ajouter une transition en entrée"
+msgstr "transition en entrée"
 
 msgid "add Transition destination_state State subject"
-msgstr "ajouter l'état de sortie"
-
-msgid "add Transition transition_of CWEType object"
-msgstr "ajouter une transition"
-
-msgid "add a Bookmark"
-msgstr "ajouter un signet"
-
-msgid "add a CWAttribute"
-msgstr "ajouter un type de relation"
-
-msgid "add a CWCache"
-msgstr "ajouter un cache applicatif"
-
-msgid "add a CWConstraint"
-msgstr "ajouter une contrainte"
-
-msgid "add a CWConstraintType"
-msgstr "ajouter un type de contrainte"
-
+msgstr "état de sortie"
+
+msgid "add Transition transition_of Workflow object"
+msgstr "transition"
+
+msgid "add WorkflowTransition condition RQLExpression subject"
+msgstr "condition"
+
+msgid "add WorkflowTransition subworkflow_exit SubWorkflowExitPoint subject"
+msgstr "sortie de sous-workflow"
+
+msgid "add WorkflowTransition transition_of Workflow object"
+msgstr "transition workflow"
+
+msgctxt "inlined:CWRelation.from_entity.subject"
 msgid "add a CWEType"
-msgstr "ajouter un type d'entité"
-
-msgid "add a CWGroup"
-msgstr "ajouter un groupe d'utilisateurs"
-
-msgid "add a CWPermission"
-msgstr "ajouter une permission"
-
-msgid "add a CWProperty"
-msgstr "ajouter une propriété"
-
+msgstr "ajouter un type d'entité sujet"
+
+msgctxt "inlined:CWRelation.to_entity.subject"
+msgid "add a CWEType"
+msgstr "ajouter un type d'entité objet"
+
+msgctxt "inlined:CWRelation.relation_type.subject"
 msgid "add a CWRType"
 msgstr "ajouter un type de relation"
 
-msgid "add a CWRelation"
-msgstr "ajouter une relation"
-
-msgid "add a CWUser"
-msgstr "ajouter un utilisateur"
-
+msgctxt "inlined:CWUser.use_email.subject"
 msgid "add a EmailAddress"
-msgstr "ajouter une adresse email"
-
-msgid "add a ExternalUri"
-msgstr "ajouter une Uri externe"
-
-msgid "add a RQLExpression"
-msgstr "ajouter une expression rql"
-
-msgid "add a State"
-msgstr "ajouter un état"
-
-msgid "add a TrInfo"
-msgstr "ajouter une information de transition"
-
-msgid "add a Transition"
-msgstr "ajouter une transition"
+msgstr "ajouter une adresse électronique"
 
 msgid "add a new permission"
 msgstr "ajouter une permission"
@@ -956,7 +974,25 @@
 # subject and object forms for each relation type
 # (no object form for final relation types)
 msgid "add_permission"
-msgstr "permission d'ajouter"
+msgstr "peut ajouter"
+
+# subject and object forms for each relation type
+# (no object form for final relation types)
+msgctxt "CWEType"
+msgid "add_permission"
+msgstr "permission d'ajout"
+
+msgctxt "CWRType"
+msgid "add_permission"
+msgstr "permission d'ajout"
+
+msgctxt "CWGroup"
+msgid "add_permission_object"
+msgstr "a la permission d'ajouter"
+
+msgctxt "RQLExpression"
+msgid "add_permission_object"
+msgstr "a la permission d'ajouter"
 
 msgid "add_permission_object"
 msgstr "a la permission d'ajouter"
@@ -973,12 +1009,26 @@
 "ajout de la relation %(rtype)s de %(frometype)s #%(fromeid)s vers %(toetype)"
 "s #%(toeid)s"
 
+msgid "addrelated"
+msgstr "ajouter"
+
+msgid "address"
+msgstr "adresse électronique"
+
+msgctxt "EmailAddress"
 msgid "address"
 msgstr "adresse électronique"
 
 msgid "alias"
 msgstr "alias"
 
+msgctxt "EmailAddress"
+msgid "alias"
+msgstr "alias"
+
+msgid "allow to set a specific workflow for an entity"
+msgstr "permet de spécifier un workflow donné pour une entité"
+
 msgid "allowed transition from this state"
 msgstr "transition autorisée depuis cet état"
 
@@ -986,7 +1036,23 @@
 msgstr "transitions autorisées depuis cet état"
 
 msgid "allowed_transition"
-msgstr "transition autorisée"
+msgstr "transitions autorisées"
+
+msgctxt "State"
+msgid "allowed_transition"
+msgstr "transitions autorisées"
+
+msgctxt "BaseTransition"
+msgid "allowed_transition_object"
+msgstr "transition autorisée de"
+
+msgctxt "Transition"
+msgid "allowed_transition_object"
+msgstr "transition autorisée de"
+
+msgctxt "WorkflowTransition"
+msgid "allowed_transition_object"
+msgstr "transition autorisée de"
 
 msgid "allowed_transition_object"
 msgstr "états en entrée"
@@ -1069,6 +1135,14 @@
 msgid "bookmarked_by"
 msgstr "utilisé par"
 
+msgctxt "Bookmark"
+msgid "bookmarked_by"
+msgstr "utilisé par"
+
+msgctxt "CWUser"
+msgid "bookmarked_by_object"
+msgstr "utilise le(s) signet(s)"
+
 msgid "bookmarked_by_object"
 msgstr "a pour signets"
 
@@ -1155,6 +1229,28 @@
 msgid "by relation"
 msgstr "via la relation"
 
+msgid "by_transition"
+msgstr "transition"
+
+msgctxt "TrInfo"
+msgid "by_transition"
+msgstr "transition"
+
+msgctxt "BaseTransition"
+msgid "by_transition_object"
+msgstr "a pour information"
+
+msgctxt "Transition"
+msgid "by_transition_object"
+msgstr "a pour information"
+
+msgctxt "WorkflowTransition"
+msgid "by_transition_object"
+msgstr "a pour information"
+
+msgid "by_transition_object"
+msgstr "changement d'états"
+
 msgid "calendar"
 msgstr "afficher un calendrier"
 
@@ -1185,6 +1281,9 @@
 msgid "can't display data, unexpected error: %s"
 msgstr "impossible d'afficher les données à cause de l'erreur suivante: %s"
 
+msgid "can't have multiple exits on the same state"
+msgstr "ne peut avoir plusieurs sorties sur le même état"
+
 #, python-format
 msgid ""
 "can't set inlined=%(inlined)s, %(stype)s %(rtype)s %(otype)s has cardinality="
@@ -1199,9 +1298,14 @@
 msgid "cancel this insert"
 msgstr "annuler cette insertion"
 
-msgid "canonical"
-msgstr "canonique"
-
+msgid "cardinality"
+msgstr "cardinalité"
+
+msgctxt "CWAttribute"
+msgid "cardinality"
+msgstr "cardinalité"
+
+msgctxt "CWRelation"
 msgid "cardinality"
 msgstr "cardinalité"
 
@@ -1227,9 +1331,14 @@
 msgid "comment"
 msgstr "commentaire"
 
-msgid "comment:"
-msgstr "commentaire :"
-
+msgctxt "TrInfo"
+msgid "comment"
+msgstr "commentaire"
+
+msgid "comment_format"
+msgstr "format"
+
+msgctxt "TrInfo"
 msgid "comment_format"
 msgstr "format"
 
@@ -1284,6 +1393,12 @@
 "composant permettant de présenter sur plusieurs pages les requêtes renvoyant "
 "plus d'un certain nombre de résultat"
 
+msgid "components_pdfview"
+msgstr "icône pdf"
+
+msgid "components_pdfview_description"
+msgstr "l'icône pdf pour obtenir la page courant au format PDF"
+
 msgid "components_rqlinput"
 msgstr "barre rql"
 
@@ -1293,12 +1408,32 @@
 msgid "composite"
 msgstr "composite"
 
+msgctxt "CWRelation"
+msgid "composite"
+msgstr "composite"
+
+msgid "condition"
+msgstr "condition"
+
+msgctxt "BaseTransition"
+msgid "condition"
+msgstr "condition"
+
+msgctxt "Transition"
+msgid "condition"
+msgstr "condition"
+
+msgctxt "WorkflowTransition"
 msgid "condition"
 msgstr "condition"
 
 msgid "condition:"
 msgstr "condition :"
 
+msgctxt "RQLExpression"
+msgid "condition_object"
+msgstr "condition de"
+
 msgid "condition_object"
 msgstr "condition de"
 
@@ -1308,6 +1443,18 @@
 msgid "constrained_by"
 msgstr "contraint par"
 
+msgctxt "CWAttribute"
+msgid "constrained_by"
+msgstr "contraint par"
+
+msgctxt "CWRelation"
+msgid "constrained_by"
+msgstr "contraint par"
+
+msgctxt "CWConstraint"
+msgid "constrained_by_object"
+msgstr "contrainte de"
+
 msgid "constrained_by_object"
 msgstr "contrainte de"
 
@@ -1508,23 +1655,43 @@
 msgid "creating RQLExpression (Transition %(linkto)s condition RQLExpression)"
 msgstr "création d'une expression RQL pour la transition %(linkto)s"
 
+msgid ""
+"creating RQLExpression (WorkflowTransition %(linkto)s condition "
+"RQLExpression)"
+msgstr "création d'une expression RQL pour la transition workflow %(linkto)s"
+
 msgid "creating State (State allowed_transition Transition %(linkto)s)"
 msgstr "création d'un état pouvant aller vers la transition %(linkto)s"
 
-msgid "creating State (State state_of CWEType %(linkto)s)"
-msgstr "création d'un état pour le type %(linkto)s"
+msgid "creating State (State state_of Workflow %(linkto)s)"
+msgstr "création d'un état du workflow  %(linkto)s"
 
 msgid "creating State (Transition %(linkto)s destination_state State)"
 msgstr "création d'un état destination de la transition %(linkto)s"
 
+msgid ""
+"creating SubWorkflowExitPoint (WorkflowTransition %(linkto)s "
+"subworkflow_exit SubWorkflowExitPoint)"
+msgstr "création d'un point de sortie de la transition workflow %(linkto)s"
+
 msgid "creating Transition (State %(linkto)s allowed_transition Transition)"
 msgstr "création d'une transition autorisée depuis l'état %(linkto)s"
 
 msgid "creating Transition (Transition destination_state State %(linkto)s)"
 msgstr "création d'une transition vers l'état %(linkto)s"
 
-msgid "creating Transition (Transition transition_of CWEType %(linkto)s)"
-msgstr "création d'une transition pour le type %(linkto)s"
+msgid "creating Transition (Transition transition_of Workflow %(linkto)s)"
+msgstr "création d'une transition du workflow %(linkto)s"
+
+msgid ""
+"creating WorkflowTransition (State %(linkto)s allowed_transition "
+"WorkflowTransition)"
+msgstr "création d'une transition workflow autorisée depuis l'état %(linkto)s"
+
+msgid ""
+"creating WorkflowTransition (WorkflowTransition transition_of Workflow %"
+"(linkto)s)"
+msgstr "création d'une transition workflow du workflow %(linkto)s"
 
 msgid "creation"
 msgstr "création"
@@ -1538,6 +1705,14 @@
 msgid "cstrtype"
 msgstr "type de constrainte"
 
+msgctxt "CWConstraint"
+msgid "cstrtype"
+msgstr "type"
+
+msgctxt "CWConstraintType"
+msgid "cstrtype_object"
+msgstr "type des contraintes"
+
 msgid "cstrtype_object"
 msgstr "utilisé par"
 
@@ -1551,6 +1726,12 @@
 msgid "currently attached file: %s"
 msgstr "fichie actuellement attaché %s"
 
+msgid "custom_workflow"
+msgstr "workflow spécifique"
+
+msgid "custom_workflow_object"
+msgstr "workflow de"
+
 msgid "cwetype-schema-image"
 msgstr "schéma"
 
@@ -1587,6 +1768,30 @@
 msgid "default text format for rich text fields."
 msgstr "format de texte par défaut pour les champs textes"
 
+msgid "default user workflow"
+msgstr "workflow par défaut des utilisateurs"
+
+msgid "default workflow for an entity type"
+msgstr "workflow par défaut pour un type d'entité"
+
+msgid "default_workflow"
+msgstr "workflow par défaut"
+
+msgctxt "CWEType"
+msgid "default_workflow"
+msgstr "workflow par défaut"
+
+msgctxt "Workflow"
+msgid "default_workflow_object"
+msgstr "workflow par défaut de"
+
+msgid "default_workflow_object"
+msgstr "workflow par défaut de"
+
+msgid "defaultval"
+msgstr "valeur par défaut"
+
+msgctxt "CWAttribute"
 msgid "defaultval"
 msgstr "valeur par défaut"
 
@@ -1626,6 +1831,9 @@
 msgid "define an entity type, used to build the instance schema"
 msgstr "définit un type d'entité"
 
+msgid "define how we get out from a sub-workflow"
+msgstr "définit comment sortir d'un sous-workflow"
+
 msgid ""
 "defines what's the property is applied for. You must select this first to be "
 "able to set value"
@@ -1651,6 +1859,22 @@
 msgid "delete_permission"
 msgstr "permission de supprimer"
 
+msgctxt "CWEType"
+msgid "delete_permission"
+msgstr "permission de supprimer"
+
+msgctxt "CWRType"
+msgid "delete_permission"
+msgstr "permission de supprimer"
+
+msgctxt "CWGroup"
+msgid "delete_permission_object"
+msgstr "peut supprimer"
+
+msgctxt "RQLExpression"
+msgid "delete_permission_object"
+msgstr "peut supprimer"
+
 msgid "delete_permission_object"
 msgstr "a la permission de supprimer"
 
@@ -1672,9 +1896,84 @@
 msgid "description"
 msgstr "description"
 
+msgctxt "CWEType"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWRelation"
+msgid "description"
+msgstr "description"
+
+msgctxt "Workflow"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWAttribute"
+msgid "description"
+msgstr "description"
+
+msgctxt "Transition"
+msgid "description"
+msgstr "description"
+
+msgctxt "WorkflowTransition"
+msgid "description"
+msgstr "description"
+
+msgctxt "State"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWRType"
+msgid "description"
+msgstr "description"
+
+msgctxt "BaseTransition"
+msgid "description"
+msgstr "description"
+
 msgid "description_format"
 msgstr "format"
 
+msgctxt "CWEType"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "CWRelation"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "Workflow"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "CWAttribute"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "Transition"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "WorkflowTransition"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "State"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "CWRType"
+msgid "description_format"
+msgstr "format"
+
+msgctxt "BaseTransition"
+msgid "description_format"
+msgstr "format"
+
+msgid "destination state"
+msgstr "état de destination"
+
 msgid "destination state for this transition"
 msgstr "états accessibles par cette transition"
 
@@ -1684,6 +1983,18 @@
 msgid "destination_state"
 msgstr "état de destination"
 
+msgctxt "Transition"
+msgid "destination_state"
+msgstr "état de destination"
+
+msgctxt "SubWorkflowExitPoint"
+msgid "destination_state"
+msgstr "état de destination"
+
+msgctxt "State"
+msgid "destination_state_object"
+msgstr "état final de"
+
 msgid "destination_state_object"
 msgstr "destination de"
 
@@ -1712,6 +2023,9 @@
 msgid "display the component or not"
 msgstr "afficher le composant ou non"
 
+msgid "display the pdf icon or not"
+msgstr "afficher l'icône pdf ou non"
+
 msgid ""
 "distinct label to distinguate between other permission entity of the same "
 "name"
@@ -1729,6 +2043,9 @@
 msgid "download icon"
 msgstr "icône de téléchargement"
 
+msgid "download page as pdf"
+msgstr "télécharger la page au format PDF"
+
 msgid "download schema as owl"
 msgstr "télécharger le schéma OWL"
 
@@ -1786,6 +2103,9 @@
 msgid "entity edited"
 msgstr "entité éditée"
 
+msgid "entity has no workflow set"
+msgstr "l'entité n'a pas de workflow"
+
 msgid "entity linked"
 msgstr "entité liée"
 
@@ -1798,11 +2118,8 @@
 msgstr ""
 "type d'entité à utiliser pour définir une configuration de sécurité avancée"
 
-msgid "entity types which may use this state"
-msgstr "type d'entités opuvant utiliser cet état"
-
-msgid "entity types which may use this transition"
-msgstr "entités qui peuvent utiliser cette transition"
+msgid "entity types which may use this workflow"
+msgstr "types d'entité pouvant utiliser ce workflow"
 
 msgid "error while embedding page"
 msgstr "erreur pendant l'inclusion de la page"
@@ -1824,15 +2141,33 @@
 msgid "eta_date"
 msgstr "date de fin"
 
+msgid "exit_point"
+msgstr "état de sortie"
+
+msgid "exit_point_object"
+msgstr "état de sortie de"
+
+#, python-format
+msgid "exiting from subworkflow %s"
+msgstr "sortie du sous-workflow %s"
+
 msgid "expected:"
 msgstr "attendu :"
 
 msgid "expression"
 msgstr "expression"
 
+msgctxt "RQLExpression"
+msgid "expression"
+msgstr "rql de l'expression"
+
 msgid "exprtype"
 msgstr "type de l'expression"
 
+msgctxt "RQLExpression"
+msgid "exprtype"
+msgstr "type"
+
 msgid "external page"
 msgstr "page externe"
 
@@ -1884,6 +2219,18 @@
 msgid "final"
 msgstr "final"
 
+msgctxt "CWEType"
+msgid "final"
+msgstr "final"
+
+msgctxt "CWRType"
+msgid "final"
+msgstr "final"
+
+msgid "firstname"
+msgstr "prénom"
+
+msgctxt "CWUser"
 msgid "firstname"
 msgstr "prénom"
 
@@ -1896,6 +2243,14 @@
 msgid "for_user"
 msgstr "pour l'utilisateur"
 
+msgctxt "CWProperty"
+msgid "for_user"
+msgstr "propriété de l'utilisateur"
+
+msgctxt "CWUser"
+msgid "for_user_object"
+msgstr "a pour préférence"
+
 msgid "for_user_object"
 msgstr "utilise les propriétés"
 
@@ -1912,6 +2267,18 @@
 msgid "from_entity"
 msgstr "de l'entité"
 
+msgctxt "CWAttribute"
+msgid "from_entity"
+msgstr "attribut de l'entité"
+
+msgctxt "CWRelation"
+msgid "from_entity"
+msgstr "relation de l'entité"
+
+msgctxt "CWEType"
+msgid "from_entity_object"
+msgstr "entité de"
+
 msgid "from_entity_object"
 msgstr "relation sujet"
 
@@ -1921,6 +2288,14 @@
 msgid "from_state"
 msgstr "de l'état"
 
+msgctxt "TrInfo"
+msgid "from_state"
+msgstr "état de départ"
+
+msgctxt "State"
+msgid "from_state_object"
+msgstr "état de départ de"
+
 msgid "from_state_object"
 msgstr "transitions depuis cet état"
 
@@ -1930,9 +2305,17 @@
 msgid "fulltext_container"
 msgstr "conteneur du texte indexé"
 
+msgctxt "CWRType"
+msgid "fulltext_container"
+msgstr "objet à indexer"
+
 msgid "fulltextindexed"
 msgstr "indexation du texte"
 
+msgctxt "CWAttribute"
+msgid "fulltextindexed"
+msgstr "texte indexé"
+
 msgid "generic plot"
 msgstr "tracé de courbes standard"
 
@@ -1951,6 +2334,10 @@
 msgid "granted to groups"
 msgstr "accordée aux groupes"
 
+#, python-format
+msgid "graphical representation of %s"
+msgstr "représentation graphique de %s"
+
 msgid "graphical representation of the instance'schema"
 msgstr "représentation graphique du schéma de l'instance"
 
@@ -2046,6 +2433,9 @@
 msgid "id of main template used to render pages"
 msgstr "id du template principal"
 
+msgid "identical to"
+msgstr "identique à"
+
 msgid "identical_to"
 msgstr "identique à"
 
@@ -2074,6 +2464,14 @@
 msgid "in_group"
 msgstr "dans le groupe"
 
+msgctxt "CWUser"
+msgid "in_group"
+msgstr "fait partie du groupe"
+
+msgctxt "CWGroup"
+msgid "in_group_object"
+msgstr "contient les utilisateurs"
+
 msgid "in_group_object"
 msgstr "membres"
 
@@ -2099,6 +2497,10 @@
 msgid "indexed"
 msgstr "index"
 
+msgctxt "CWAttribute"
+msgid "indexed"
+msgstr "indexé"
+
 msgid "indicate the current state of an entity"
 msgstr "indique l'état courant d'une entité"
 
@@ -2115,15 +2517,27 @@
 msgid "initial estimation %s"
 msgstr "estimation initiale %s"
 
-msgid "initial state for entities of this type"
-msgstr "état initial pour les entités de ce type"
+msgid "initial state for this workflow"
+msgstr "état initial pour ce workflow"
 
 msgid "initial_state"
 msgstr "état initial"
 
+msgctxt "Workflow"
+msgid "initial_state"
+msgstr "état initial"
+
+msgctxt "State"
 msgid "initial_state_object"
 msgstr "état initial de"
 
+msgid "initial_state_object"
+msgstr "état initial de"
+
+msgid "inlined"
+msgstr "mise en ligne"
+
+msgctxt "CWRType"
 msgid "inlined"
 msgstr "mise en ligne"
 
@@ -2136,6 +2550,10 @@
 msgid "internationalizable"
 msgstr "internationalisable"
 
+msgctxt "CWAttribute"
+msgid "internationalizable"
+msgstr "internationalisable"
+
 #, python-format
 msgid "invalid action %r"
 msgstr "action %r invalide"
@@ -2194,6 +2612,10 @@
 msgid "label"
 msgstr "libellé"
 
+msgctxt "CWPermission"
+msgid "label"
+msgstr "libellé"
+
 msgid "language of the user interface"
 msgstr "langue pour l'interface utilisateur"
 
@@ -2203,6 +2625,10 @@
 msgid "last_login_time"
 msgstr "dernière date de connexion"
 
+msgctxt "CWUser"
+msgid "last_login_time"
+msgstr "dernière date de connexion"
+
 msgid "latest modification time of an entity"
 msgstr "date de dernière modification d'une entité"
 
@@ -2236,14 +2662,17 @@
 msgid "link a relation definition to its subject entity type"
 msgstr "lie une définition de relation à son type d'entité sujet"
 
-msgid "link a state to one or more entity type"
-msgstr "lier un état à une ou plusieurs entités"
+msgid "link a state to one or more workflow"
+msgstr "lie un état à un ou plusieurs workflow"
 
 msgid "link a transition information to its object"
 msgstr "lié une enregistrement de transition vers l'objet associé"
 
-msgid "link a transition to one or more entity type"
-msgstr "lie une transition à un ou plusieurs types d'entités"
+msgid "link a transition to one or more workflow"
+msgstr "lie une transition à un ou plusieurs workflow"
+
+msgid "link a workflow to one or more entity type"
+msgstr "lie un workflow à un ou plusieurs types d'entité"
 
 msgid "link to each item in"
 msgstr "lier vers chaque élément dans"
@@ -2260,6 +2689,10 @@
 msgid "login"
 msgstr "identifiant"
 
+msgctxt "CWUser"
+msgid "login"
+msgstr "identifiant"
+
 msgid "login or email"
 msgstr "identifiant ou email"
 
@@ -2279,6 +2712,10 @@
 msgid "mainvars"
 msgstr "variables principales"
 
+msgctxt "RQLExpression"
+msgid "mainvars"
+msgstr "variables principales"
+
 msgid "manage"
 msgstr "gestion du site"
 
@@ -2294,6 +2731,9 @@
 msgid "managers"
 msgstr "administrateurs"
 
+msgid "mandatory relation"
+msgstr "relation obligatoire"
+
 msgid "march"
 msgstr "mars"
 
@@ -2340,6 +2780,50 @@
 msgid "name"
 msgstr "nom"
 
+msgctxt "CWEType"
+msgid "name"
+msgstr "nom"
+
+msgctxt "Transition"
+msgid "name"
+msgstr "nom"
+
+msgctxt "Workflow"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWGroup"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWConstraintType"
+msgid "name"
+msgstr "nom"
+
+msgctxt "WorkflowTransition"
+msgid "name"
+msgstr "nom"
+
+msgctxt "State"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWPermission"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWRType"
+msgid "name"
+msgstr "nom"
+
+msgctxt "BaseTransition"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWCache"
+msgid "name"
+msgstr "nom"
+
 msgid "name of the cache"
 msgstr "nom du cache applicatif"
 
@@ -2447,6 +2931,14 @@
 msgid "ordernum"
 msgstr "ordre"
 
+msgctxt "CWAttribute"
+msgid "ordernum"
+msgstr "numéro d'ordre"
+
+msgctxt "CWRelation"
+msgid "ordernum"
+msgstr "numéro d'ordre"
+
 msgid "owl"
 msgstr "owl"
 
@@ -2482,6 +2974,10 @@
 msgid "path"
 msgstr "chemin"
 
+msgctxt "Bookmark"
+msgid "path"
+msgstr "chemin"
+
 msgid "permission"
 msgstr "permission"
 
@@ -2503,6 +2999,10 @@
 msgid "pkey"
 msgstr "clé"
 
+msgctxt "CWProperty"
+msgid "pkey"
+msgstr "code de la propriété"
+
 msgid "please correct errors below"
 msgstr "veuillez corriger les erreurs ci-dessous"
 
@@ -2513,7 +3013,21 @@
 msgstr "vues possibles"
 
 msgid "powered by CubicWeb"
-msgstr ""
+msgstr "utilise la technologie CubicWeb"
+
+msgid "prefered_form"
+msgstr "forme préférée"
+
+msgctxt "EmailAddress"
+msgid "prefered_form"
+msgstr "forme préférée"
+
+msgctxt "EmailAddress"
+msgid "prefered_form_object"
+msgstr "forme préférée de"
+
+msgid "prefered_form_object"
+msgstr "forme préférée à"
 
 msgid "preferences"
 msgstr "préférences"
@@ -2527,6 +3041,14 @@
 msgid "primary_email"
 msgstr "adresse email principale"
 
+msgctxt "CWUser"
+msgid "primary_email"
+msgstr "email principal"
+
+msgctxt "EmailAddress"
+msgid "primary_email_object"
+msgstr "adresse principale de"
+
 msgid "primary_email_object"
 msgstr "adresse email principale (object)"
 
@@ -2548,12 +3070,34 @@
 msgid "read_permission"
 msgstr "permission de lire"
 
+msgctxt "CWEType"
+msgid "read_permission"
+msgstr "permission d'ajouter"
+
+msgctxt "CWRType"
+msgid "read_permission"
+msgstr "permission d'ajouter"
+
+msgctxt "CWGroup"
+msgid "read_permission_object"
+msgstr "peut lire"
+
+msgctxt "RQLExpression"
+msgid "read_permission_object"
+msgstr "peut lire"
+
 msgid "read_permission_object"
 msgstr "a la permission de lire"
 
 msgid "registry"
 msgstr "registre"
 
+msgid "related entity has no state"
+msgstr "l'entité lié n'a pas d'état"
+
+msgid "related entity has no workflow set"
+msgstr "l'entité lié n'a pas de workflow"
+
 #, python-format
 msgid "relation %(relname)s of %(ent)s"
 msgstr "relation %(relname)s de %(ent)s"
@@ -2561,6 +3105,18 @@
 msgid "relation_type"
 msgstr "type de relation"
 
+msgctxt "CWAttribute"
+msgid "relation_type"
+msgstr "type de relation"
+
+msgctxt "CWRelation"
+msgid "relation_type"
+msgstr "type de relation"
+
+msgctxt "CWRType"
+msgid "relation_type_object"
+msgstr "définition"
+
 msgid "relation_type_object"
 msgstr "définition"
 
@@ -2573,63 +3129,45 @@
 msgid "relative url of the bookmarked page"
 msgstr "url relative de la page"
 
-msgid "remove this Bookmark"
-msgstr "supprimer ce signet"
-
-msgid "remove this CWAttribute"
-msgstr "supprimer cet attribut"
-
-msgid "remove this CWCache"
-msgstr "supprimer ce cache applicatif"
-
-msgid "remove this CWConstraint"
-msgstr "supprimer cette contrainte"
-
-msgid "remove this CWConstraintType"
-msgstr "supprimer ce type de contrainte"
-
+msgctxt "inlined:CWRelation:from_entity:subject"
+msgid "remove this CWEType"
+msgstr "supprimer ce type d'entité"
+
+msgctxt "inlined:CWRelation:to_entity:subject"
 msgid "remove this CWEType"
 msgstr "supprimer ce type d'entité"
 
-msgid "remove this CWGroup"
-msgstr "supprimer ce groupe"
-
-msgid "remove this CWPermission"
-msgstr "supprimer cette permission"
-
-msgid "remove this CWProperty"
-msgstr "supprimer cette propriété"
-
+msgctxt "inlined:CWRelation:relation_type:subject"
 msgid "remove this CWRType"
-msgstr "supprimer cette définition de relation"
-
-msgid "remove this CWRelation"
-msgstr "supprimer cette relation"
-
-msgid "remove this CWUser"
-msgstr "supprimer cet utilisateur"
-
+msgstr "supprimer ce type de relation"
+
+msgctxt "inlined:CWUser:use_email:subject"
 msgid "remove this EmailAddress"
-msgstr "supprimer cette adresse email"
-
-msgid "remove this ExternalUri"
-msgstr "supprimer cette Uri externe"
-
-msgid "remove this RQLExpression"
-msgstr "supprimer cette expression rql"
-
-msgid "remove this State"
-msgstr "supprimer cet état"
-
-msgid "remove this TrInfo"
-msgstr "retirer cette information de transition"
-
-msgid "remove this Transition"
-msgstr "supprimer cette transition"
+msgstr "supprimer cette adresse électronique"
 
 msgid "require_group"
 msgstr "nécessite le groupe"
 
+msgctxt "BaseTransition"
+msgid "require_group"
+msgstr "restreinte au groupe"
+
+msgctxt "Transition"
+msgid "require_group"
+msgstr "restreinte au groupe"
+
+msgctxt "CWPermission"
+msgid "require_group"
+msgstr "restreinte au groupe"
+
+msgctxt "WorkflowTransition"
+msgid "require_group"
+msgstr "restreinte au groupe"
+
+msgctxt "CWGroup"
+msgid "require_group_object"
+msgstr "dé"
+
 msgid "require_group_object"
 msgstr "à les droits"
 
@@ -2763,6 +3301,9 @@
 msgid "semantic description of this transition"
 msgstr "description sémantique de cette transition"
 
+msgid "semantic description of this workflow"
+msgstr "description sémantique de ce workflow"
+
 msgid "send email"
 msgstr "envoyer un courriel"
 
@@ -2818,9 +3359,20 @@
 msgid "sparql xml"
 msgstr "XML Sparql"
 
+msgid "special transition allowing to go through a sub-workflow"
+msgstr "transition spécial permettant d'aller dans un sous-workfow"
+
 msgid "specializes"
 msgstr "dérive de"
 
+msgctxt "CWEType"
+msgid "specializes"
+msgstr "spécialise"
+
+msgctxt "CWEType"
+msgid "specializes_object"
+msgstr "parent de"
+
 msgid "specializes_object"
 msgstr "parent de"
 
@@ -2830,9 +3382,30 @@
 msgid "state"
 msgstr "état"
 
+msgid "state doesn't belong to entity's current workflow"
+msgstr "l'état n'appartient pas au workflow courant de l'entité"
+
+msgid "state doesn't belong to entity's workflow"
+msgstr "l'état n'appartient pas au workflow de l'entité"
+
+msgid ""
+"state doesn't belong to entity's workflow. You may want to set a custom "
+"workflow for this entity first."
+msgstr ""
+"l'état n'appartient pas au workflow courant de l'entité. Vous désirez peut-"
+"être spécifier que cette entité doit utiliser ce workflow."
+
 msgid "state_of"
 msgstr "état de"
 
+msgctxt "State"
+msgid "state_of"
+msgstr "état de"
+
+msgctxt "Workflow"
+msgid "state_of_object"
+msgstr "contient les états"
+
 msgid "state_of_object"
 msgstr "a pour état"
 
@@ -2855,12 +3428,65 @@
 msgid "subject_plural:"
 msgstr "sujets :"
 
+msgid "subworkflow"
+msgstr "sous-workflow"
+
+msgctxt "WorkflowTransition"
+msgid "subworkflow"
+msgstr "sous-workflow"
+
+msgid "subworkflow state"
+msgstr "état de sous-workflow"
+
+msgid "subworkflow_exit"
+msgstr "sortie de sous-workflow"
+
+msgctxt "WorkflowTransition"
+msgid "subworkflow_exit"
+msgstr "sortie du sous-workflow"
+
+msgctxt "SubWorkflowExitPoint"
+msgid "subworkflow_exit_object"
+msgstr "états de sortie"
+
+msgid "subworkflow_exit_object"
+msgstr "états de sortie"
+
+msgctxt "Workflow"
+msgid "subworkflow_object"
+msgstr ""
+
+msgid "subworkflow_object"
+msgstr "utilisé par la transition"
+
+msgid "subworkflow_state"
+msgstr "état du sous-workflow"
+
+msgctxt "SubWorkflowExitPoint"
+msgid "subworkflow_state"
+msgstr "état"
+
+msgctxt "State"
+msgid "subworkflow_state_object"
+msgstr ""
+
+msgid "subworkflow_state_object"
+msgstr "état de sortie de"
+
 msgid "sunday"
 msgstr "dimanche"
 
 msgid "surname"
 msgstr "nom"
 
+msgctxt "CWUser"
+msgid "surname"
+msgstr "nom de famille"
+
+msgid "symetric"
+msgstr "symétrique"
+
+msgctxt "CWRType"
 msgid "symetric"
 msgstr "symétrique"
 
@@ -2920,6 +3546,10 @@
 msgid "timestamp"
 msgstr "date"
 
+msgctxt "CWCache"
+msgid "timestamp"
+msgstr "valide depuis"
+
 msgid "timestamp of the latest source synchronization."
 msgstr "date de la dernière synchronisation avec la source."
 
@@ -2929,6 +3559,10 @@
 msgid "title"
 msgstr "titre"
 
+msgctxt "Bookmark"
+msgid "title"
+msgstr "libellé"
+
 msgid "to"
 msgstr "à"
 
@@ -2942,6 +3576,18 @@
 msgid "to_entity"
 msgstr "vers l'entité"
 
+msgctxt "CWAttribute"
+msgid "to_entity"
+msgstr "pour l'entité"
+
+msgctxt "CWRelation"
+msgid "to_entity"
+msgstr "pour l'entité"
+
+msgctxt "CWEType"
+msgid "to_entity_object"
+msgstr "relation objet"
+
 msgid "to_entity_object"
 msgstr "relation objet"
 
@@ -2951,8 +3597,16 @@
 msgid "to_state"
 msgstr "vers l'état"
 
+msgctxt "TrInfo"
+msgid "to_state"
+msgstr "état de destination"
+
+msgctxt "State"
 msgid "to_state_object"
-msgstr "transitions vers cette état"
+msgstr "transition vers cet état"
+
+msgid "to_state_object"
+msgstr "transitions vers cet état"
 
 msgid "todo_by"
 msgstr "à faire par"
@@ -2960,13 +3614,34 @@
 msgid "toggle check boxes"
 msgstr "inverser les cases à cocher"
 
-#, 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 doesn't belong to entity's workflow"
+msgstr "la transition n'appartient pas au workflow de l'entité"
+
+msgid "transition isn't allowed"
+msgstr "la transition n'est pas autorisée"
+
+msgid "transition may not be fired"
+msgstr "la transition ne peut-être déclenchée"
 
 msgid "transition_of"
 msgstr "transition de"
 
+msgctxt "BaseTransition"
+msgid "transition_of"
+msgstr "transition de"
+
+msgctxt "Transition"
+msgid "transition_of"
+msgstr "transition de"
+
+msgctxt "WorkflowTransition"
+msgid "transition_of"
+msgstr "transition de"
+
+msgctxt "Workflow"
+msgid "transition_of_object"
+msgstr "a pour transition"
+
 msgid "transition_of_object"
 msgstr "a pour transition"
 
@@ -3039,6 +3714,10 @@
 msgid "upassword"
 msgstr "mot de passe"
 
+msgctxt "CWUser"
+msgid "upassword"
+msgstr "mot de passe"
+
 msgid "update"
 msgstr "modification"
 
@@ -3048,6 +3727,18 @@
 msgid "update_permission"
 msgstr "permission de modification"
 
+msgctxt "CWEType"
+msgid "update_permission"
+msgstr "permission de modifier"
+
+msgctxt "CWGroup"
+msgid "update_permission_object"
+msgstr "peut modifier"
+
+msgctxt "RQLExpression"
+msgid "update_permission_object"
+msgstr "peut modifier"
+
 msgid "update_permission_object"
 msgstr "à la permission de modifier"
 
@@ -3058,6 +3749,10 @@
 msgid "uri"
 msgstr "uri"
 
+msgctxt "ExternalUri"
+msgid "uri"
+msgstr "uri"
+
 msgid "use template languages"
 msgstr "utiliser les langages de template"
 
@@ -3071,6 +3766,14 @@
 msgid "use_email"
 msgstr "adresse électronique"
 
+msgctxt "CWUser"
+msgid "use_email"
+msgstr "utilise l'adresse électronique"
+
+msgctxt "EmailAddress"
+msgid "use_email_object"
+msgstr "utilisée par"
+
 msgid "use_email_object"
 msgstr "adresse utilisée par"
 
@@ -3127,6 +3830,14 @@
 msgid "value"
 msgstr "valeur"
 
+msgctxt "CWConstraint"
+msgid "value"
+msgstr "contrainte"
+
+msgctxt "CWProperty"
+msgid "value"
+msgstr "valeur"
+
 msgid "value associated to this key is not editable manually"
 msgstr "la valeur associée à cette clé n'est pas éditable manuellement"
 
@@ -3179,16 +3890,42 @@
 
 msgid ""
 "when multiple addresses are equivalent (such as python-projects@logilab.org "
-"and python-projects@lists.logilab.org), set this to true on one of them "
-"which is the preferred form."
+"and python-projects@lists.logilab.org), set this to indicate which is the "
+"preferred form."
 msgstr ""
-"quand plusieurs adresses sont équivalentes (comme python-projects@logilab."
-"org et python-projects@lists.logilab.org), mettez cette propriété à vrai sur "
-"l'une d'entre-elle qui sera la forme canonique"
+
+msgid "workflow"
+msgstr "workflow"
 
 #, python-format
-msgid "workflow for %s"
-msgstr "workflow pour %s"
+msgid "workflow changed to \"%s\""
+msgstr "workflow changé à \"%s\""
+
+msgid "workflow has no initial state"
+msgstr "le workflow n'a pas d'état initial"
+
+msgid "workflow history item"
+msgstr "entrée de l'historique de workflow"
+
+msgid "workflow to which this state belongs"
+msgstr "workflow auquel cet état appartient"
+
+msgid "workflow to which this transition belongs"
+msgstr "workflow auquel cette transition appartient"
+
+msgid "workflow_of"
+msgstr "workflow de"
+
+msgctxt "Workflow"
+msgid "workflow_of"
+msgstr "workflow de"
+
+msgctxt "CWEType"
+msgid "workflow_of_object"
+msgstr "a pour workflow"
+
+msgid "workflow_of_object"
+msgstr "a pour workflow"
 
 msgid "xbel"
 msgstr "xbel"
@@ -3207,11 +3944,3 @@
 
 msgid "you should probably delete that property"
 msgstr "vous devriez probablement supprimer cette propriété"
-
-#~ msgid ""
-#~ "You have no access to this view or it's not applyable to current data"
-#~ msgstr ""
-#~ "Vous n'avez pas accès à cette vue ou elle ne s'applique pas aux données"
-
-#~ msgid "download image"
-#~ msgstr "image de téléchargement"
--- a/interfaces.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/interfaces.py	Fri Sep 18 11:24:37 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	Thu Sep 17 19:38:04 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	Thu Sep 17 19:38:04 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	Thu Sep 17 19:38:04 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	Thu Sep 17 19:38:04 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	Thu Sep 17 19:38:04 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	Thu Sep 17 19:38:04 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	Thu Sep 17 19:38:04 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	Thu Sep 17 19:38:04 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	Thu Sep 17 19:38:04 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	Thu Sep 17 19:38:04 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')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.5.0_Any.py	Fri Sep 18 11:24:37 2009 +0200
@@ -0,0 +1,10 @@
+add_relation_type('prefered_form')
+
+rql('SET X prefered_form Y WHERE Y canonical TRUE, X identical_to Y')
+checkpoint()
+
+drop_attribute('EmailAddress', 'canonical')
+drop_relation_definition('EmailAddress', 'identical_to', 'EmailAddress')
+
+if 'see_also' in schema:
+    sync_schema_props_perms('see_also', syncprops=False, syncrdefs=False)
--- a/misc/migration/bootstrapmigration_repository.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/misc/migration/bootstrapmigration_repository.py	Fri Sep 18 11:24:37 2009 +0200
@@ -30,6 +30,47 @@
     repo.hm.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
     session.set_shared_data('do-not-insert-cwuri', False)
 
+if applcubicwebversion < (3, 5, 0) and cubicwebversion >= (3, 5, 0):
+    add_entity_type('Workflow')
+    add_entity_type('BaseTransition')
+    add_entity_type('WorkflowTransition')
+    add_entity_type('SubWorkflowExitPoint')
+    # drop explicit 'State allowed_transition Transition' since it should be
+    # infered due to yams inheritance.  However we've to disable the schema
+    # sync hook first to avoid to destroy existing data...
+    from cubicweb.server.schemahooks import after_del_relation_type
+    repo.hm.unregister_hook(after_del_relation_type,
+                            'after_delete_relation', 'relation_type')
+    try:
+        drop_relation_definition('State', 'allowed_transition', 'Transition')
+    finally:
+        repo.hm.register_hook(after_del_relation_type,
+                              'after_delete_relation', 'relation_type')
+    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, ET eid %(et)s, WF eid %(wf)s',
+            {'et': et.eid, 'wf': wf.eid}, 'et', ask_confirm=False)
+
+
+    rql('DELETE TrInfo TI WHERE NOT TI from_state S')
+    rql('SET TI by_transition T WHERE TI from_state FS, TI to_state TS, '
+        'FS allowed_transition T, T destination_state TS')
+    checkpoint()
+
+    drop_relation_definition('State', 'state_of', 'CWEType')
+    drop_relation_definition('Transition', 'transition_of', 'CWEType')
+    drop_relation_definition('CWEType', 'initial_state', 'State')
+
+    sync_schema_props_perms()
+
 if applcubicwebversion < (3, 2, 2) and cubicwebversion >= (3, 2, 1):
     from base64 import b64encode
     for table in ('entities', 'deleted_entities'):
@@ -41,37 +82,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	Thu Sep 17 19:38:04 2009 +0200
+++ b/misc/migration/postcreate.py	Fri Sep 18 11:24:37 2009 +0200
@@ -6,41 +6,59 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 
-activatedeid = add_state(_('activated'), 'CWUser', initial=True)
-deactivatedeid = add_state(_('deactivated'), 'CWUser')
-add_transition(_('deactivate'), 'CWUser',
-               (activatedeid,), deactivatedeid,
-               requiredgroups=('managers',))
-add_transition(_('activate'), 'CWUser',
-               (deactivatedeid,), activatedeid,
-               requiredgroups=('managers',))
+# insert versions
+create_entity('CWProperty', pkey=u'system.version.cubicweb',
+              value=unicode(config.cubicweb_version()))
+for cube in config.cubes():
+    create_entity('CWProperty', pkey=u'system.version.%s' % cube.lower(),
+                  value=unicode(config.cube_version(cube)))
 
-# 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)
+# some entities have been added before schema entities, fix the 'is' and
+# 'is_instance_of' relations
+for rtype in ('is', 'is_instance_of'):
+    sql('INSERT INTO %s_relation '
+        'SELECT X.eid, ET.cw_eid FROM entities as X, cw_CWEType as ET '
+        'WHERE X.type=ET.cw_name AND NOT EXISTS('
+        '      SELECT 1 from is_relation '
+        '      WHERE eid_from=X.eid AND eid_to=ET.cw_eid)' % rtype)
+
+# user workflow
+userwf = add_workflow(_('default user workflow'), 'CWUser')
+activated = userwf.add_state(_('activated'), initial=True)
+deactivated = userwf.add_state(_('deactivated'))
+userwf.add_transition(_('deactivate'), (activated,), deactivated,
+                      requiredgroups=('managers',))
+userwf.add_transition(_('activate'), (deactivated,), activated,
+                      requiredgroups=('managers',))
 
 # 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})
 
-cfg = config.persistent_options_configuration()
-if interactive_mode:
-    cfg.input_config(inputlevel=0)
+# 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': activated.eid}, 'x')
 
-for section, options in cfg.options_by_section():
-    for optname, optdict, value in options:
-        key = '%s.%s' % (section, optname)
-        default = cfg.option_default(optname, optdict)
-        # only record values differing from default
-        if value != default:
-            rql('INSERT CWProperty X: X pkey %(k)s, X value %(v)s', {'k': key, 'v': value})
+# on interactive mode, ask for level 0 persistent options
+if interactive_mode:
+    cfg = config.persistent_options_configuration()
+    cfg.input_config(inputlevel=0)
+    for section, options in cfg.options_by_section():
+        for optname, optdict, value in options:
+            key = '%s.%s' % (section, optname)
+            default = cfg.option_default(optname, optdict)
+            # only record values differing from default
+            if value != default:
+                rql('INSERT CWProperty X: X pkey %(k)s, X value %(v)s', {'k': key, 'v': value})
 
 # add PERM_USE_TEMPLATE_FORMAT permission
 from cubicweb.schema import PERM_USE_TEMPLATE_FORMAT
-eid = add_entity('CWPermission', name=PERM_USE_TEMPLATE_FORMAT,
-                 label=_('use template languages'))
+usetmplperm = create_entity('CWPermission', name=PERM_USE_TEMPLATE_FORMAT,
+                            label=_('use template languages'))
 rql('SET X require_group G WHERE G name "managers", X eid %(x)s',
-    {'x': eid}, 'x')
+    {'x': usetmplperm.eid}, 'x')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rqlrewrite.py	Fri Sep 18 11:24:37 2009 +0200
@@ -0,0 +1,480 @@
+"""RQL rewriting utilities : insert rql expression snippets into rql syntax
+tree.
+
+This is used for instance for read security checking in the repository.
+
+:organization: Logilab
+:copyright: 2007-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
+"""
+__docformat__ = "restructuredtext en"
+
+from rql import nodes as n, stmts, TypeResolverException
+
+from logilab.common.compat import any
+
+from cubicweb import Unauthorized, server, typed_eid
+from cubicweb.server.ssplanner import add_types_restriction
+
+
+def remove_solutions(origsolutions, solutions, defined):
+    """when a rqlst has been generated from another by introducing security
+    assertions, this method returns solutions which are contained in orig
+    solutions
+    """
+    newsolutions = []
+    for origsol in origsolutions:
+        for newsol in solutions[:]:
+            for var, etype in origsol.items():
+                try:
+                    if newsol[var] != etype:
+                        try:
+                            defined[var].stinfo['possibletypes'].remove(newsol[var])
+                        except KeyError:
+                            pass
+                        break
+                except KeyError:
+                    # variable has been rewritten
+                    continue
+            else:
+                newsolutions.append(newsol)
+                solutions.remove(newsol)
+    return newsolutions
+
+
+class Unsupported(Exception): pass
+
+
+class RQLRewriter(object):
+    """insert some rql snippets into another rql syntax tree
+
+    this class *isn't thread safe*
+    """
+
+    def __init__(self, session):
+        self.session = session
+        vreg = session.vreg
+        self.schema = vreg.schema
+        self.annotate = vreg.rqlhelper.annotate
+        self._compute_solutions = vreg.solutions
+
+    def compute_solutions(self):
+        self.annotate(self.select)
+        try:
+            self._compute_solutions(self.session, self.select, self.kwargs)
+        except TypeResolverException:
+            raise Unsupported(str(self.select))
+        if len(self.select.solutions) < len(self.solutions):
+            raise Unsupported()
+
+    def rewrite(self, select, snippets, solutions, kwargs):
+        """
+        snippets: (varmap, list of rql expression)
+                  with varmap a *tuple* (select var, snippet var)
+        """
+        if server.DEBUG:
+            print '---- rewrite', select, snippets, solutions
+        self.select = self.insert_scope = select
+        self.solutions = solutions
+        self.kwargs = kwargs
+        self.u_varname = None
+        self.removing_ambiguity = False
+        self.exists_snippet = {}
+        self.pending_keys = []
+        # we have to annotate the rqlst before inserting snippets, even though
+        # we'll have to redo it latter
+        self.annotate(select)
+        self.insert_snippets(snippets)
+        if not self.exists_snippet and self.u_varname:
+            # U has been inserted than cancelled, cleanup
+            select.undefine_variable(select.defined_vars[self.u_varname])
+        # clean solutions according to initial solutions
+        newsolutions = remove_solutions(solutions, select.solutions,
+                                        select.defined_vars)
+        assert len(newsolutions) >= len(solutions), (
+            'rewritten rql %s has lost some solutions, there is probably '
+            'something wrong in your schema permission (for instance using a '
+            'RQLExpression which insert a relation which doesn\'t exists in '
+            'the schema)\nOrig solutions: %s\nnew solutions: %s' % (
+            select, solutions, newsolutions))
+        if len(newsolutions) > len(solutions):
+            newsolutions = self.remove_ambiguities(snippets, newsolutions)
+        select.solutions = newsolutions
+        add_types_restriction(self.schema, select)
+        if server.DEBUG:
+            print '---- rewriten', select
+
+    def insert_snippets(self, snippets, varexistsmap=None):
+        self.rewritten = {}
+        for varmap, rqlexprs in snippets:
+            if varexistsmap is not None and not varmap in varexistsmap:
+                continue
+            self.varmap = varmap
+            selectvar, snippetvar = varmap
+            assert snippetvar in 'SOX'
+            self.revvarmap = {snippetvar: selectvar}
+            self.varinfo = vi = {}
+            try:
+                vi['const'] = typed_eid(selectvar) # XXX gae
+                vi['rhs_rels'] = vi['lhs_rels'] = {}
+            except ValueError:
+                vi['stinfo'] = sti = self.select.defined_vars[selectvar].stinfo
+                if varexistsmap is None:
+                    vi['rhs_rels'] = dict( (r.r_type, r) for r in sti['rhsrelations'])
+                    vi['lhs_rels'] = dict( (r.r_type, r) for r in sti['relations']
+                                           if not r in sti['rhsrelations'])
+                else:
+                    vi['rhs_rels'] = vi['lhs_rels'] = {}
+            parent = None
+            inserted = False
+            for rqlexpr in rqlexprs:
+                self.current_expr = rqlexpr
+                if varexistsmap is None:
+                    try:
+                        new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, parent)
+                    except Unsupported:
+                        import traceback
+                        traceback.print_exc()
+                        continue
+                    inserted = True
+                    if new is not None:
+                        self.exists_snippet[rqlexpr] = new
+                    parent = parent or new
+                else:
+                    # called to reintroduce snippet due to ambiguity creation,
+                    # so skip snippets which are not introducing this ambiguity
+                    exists = varexistsmap[varmap]
+                    if self.exists_snippet[rqlexpr] is exists:
+                        self.insert_snippet(varmap, rqlexpr.snippet_rqlst, exists)
+            if varexistsmap is None and not inserted:
+                # no rql expression found matching rql solutions. User has no access right
+                raise Unauthorized()
+
+    def insert_snippet(self, varmap, snippetrqlst, parent=None):
+        new = snippetrqlst.where.accept(self)
+        if new is not None:
+            if self.varinfo.get('stinfo', {}).get('optrelations'):
+                assert parent is None
+                self.insert_scope = self.snippet_subquery(varmap, new)
+                self.insert_pending()
+                self.insert_scope = self.select
+                return
+            new = n.Exists(new)
+            if parent is None:
+                self.insert_scope.add_restriction(new)
+            else:
+                grandpa = parent.parent
+                or_ = n.Or(parent, new)
+                grandpa.replace(parent, or_)
+            if not self.removing_ambiguity:
+                try:
+                    self.compute_solutions()
+                except Unsupported:
+                    # some solutions have been lost, can't apply this rql expr
+                    if parent is None:
+                        self.select.remove_node(new, undefine=True)
+                    else:
+                        parent.parent.replace(or_, or_.children[0])
+                        self._cleanup_inserted(new)
+                    raise
+                else:
+                    self.insert_scope = new
+                    self.insert_pending()
+                    self.insert_scope = self.select
+            return new
+        self.insert_pending()
+
+    def insert_pending(self):
+        """pending_keys hold variable referenced by U has_<action>_permission X
+        relation.
+
+        Once the snippet introducing this has been inserted and solutions
+        recomputed, we have to insert snippet defined for <action> of entity
+        types taken by X
+        """
+        while self.pending_keys:
+            key, action = self.pending_keys.pop()
+            try:
+                varname = self.rewritten[key]
+            except KeyError:
+                try:
+                    varname = self.revvarmap[key[-1]]
+                except KeyError:
+                    # variable isn't used anywhere else, we can't insert security
+                    raise Unauthorized()
+            ptypes = self.select.defined_vars[varname].stinfo['possibletypes']
+            if len(ptypes) > 1:
+                # XXX dunno how to handle this
+                self.session.error(
+                    'cant check security of %s, ambigous type for %s in %s',
+                    self.select, varname, key[0]) # key[0] == the rql expression
+                raise Unauthorized()
+            etype = iter(ptypes).next()
+            eschema = self.schema.eschema(etype)
+            if not eschema.has_perm(self.session, action):
+                rqlexprs = eschema.get_rqlexprs(action)
+                if not rqlexprs:
+                    raise Unauthorised()
+                self.insert_snippets([((varname, 'X'), rqlexprs)])
+
+    def snippet_subquery(self, varmap, transformedsnippet):
+        """introduce the given snippet in a subquery"""
+        subselect = stmts.Select()
+        selectvar, snippetvar = varmap
+        subselect.append_selected(n.VariableRef(
+            subselect.get_variable(selectvar)))
+        aliases = [selectvar]
+        subselect.add_restriction(transformedsnippet.copy(subselect))
+        stinfo = self.varinfo['stinfo']
+        for rel in stinfo['relations']:
+            rschema = self.schema.rschema(rel.r_type)
+            if rschema.is_final() or (rschema.inlined and
+                                      not rel in stinfo['rhsrelations']):
+                self.select.remove_node(rel)
+                rel.children[0].name = selectvar
+                subselect.add_restriction(rel.copy(subselect))
+                for vref in rel.children[1].iget_nodes(n.VariableRef):
+                    subselect.append_selected(vref.copy(subselect))
+                    aliases.append(vref.name)
+        if self.u_varname:
+            # generate an identifier for the substitution
+            argname = subselect.allocate_varname()
+            while argname in self.kwargs:
+                argname = subselect.allocate_varname()
+            subselect.add_constant_restriction(subselect.get_variable(self.u_varname),
+                                               'eid', unicode(argname), 'Substitute')
+            self.kwargs[argname] = self.session.user.eid
+        add_types_restriction(self.schema, subselect, subselect,
+                              solutions=self.solutions)
+        myunion = stmts.Union()
+        myunion.append(subselect)
+        aliases = [n.VariableRef(self.select.get_variable(name, i))
+                   for i, name in enumerate(aliases)]
+        self.select.add_subquery(n.SubQuery(aliases, myunion), check=False)
+        self._cleanup_inserted(transformedsnippet)
+        try:
+            self.compute_solutions()
+        except Unsupported:
+            # some solutions have been lost, can't apply this rql expr
+            self.select.remove_subquery(new, undefine=True)
+            raise
+        return subselect
+
+    def remove_ambiguities(self, snippets, newsolutions):
+        # the snippet has introduced some ambiguities, we have to resolve them
+        # "manually"
+        variantes = self.build_variantes(newsolutions)
+        # insert "is" where necessary
+        varexistsmap = {}
+        self.removing_ambiguity = True
+        for (erqlexpr, varmap, oldvarname), etype in variantes[0].iteritems():
+            varname = self.rewritten[(erqlexpr, varmap, oldvarname)]
+            var = self.select.defined_vars[varname]
+            exists = var.references()[0].scope
+            exists.add_constant_restriction(var, 'is', etype, 'etype')
+            varexistsmap[varmap] = exists
+        # insert ORED exists where necessary
+        for variante in variantes[1:]:
+            self.insert_snippets(snippets, varexistsmap)
+            for key, etype in variante.iteritems():
+                varname = self.rewritten[key]
+                try:
+                    var = self.select.defined_vars[varname]
+                except KeyError:
+                    # not a newly inserted variable
+                    continue
+                exists = var.references()[0].scope
+                exists.add_constant_restriction(var, 'is', etype, 'etype')
+        # recompute solutions
+        #select.annotated = False # avoid assertion error
+        self.compute_solutions()
+        # clean solutions according to initial solutions
+        return remove_solutions(self.solutions, self.select.solutions,
+                                self.select.defined_vars)
+
+    def build_variantes(self, newsolutions):
+        variantes = set()
+        for sol in newsolutions:
+            variante = []
+            for key, newvar in self.rewritten.iteritems():
+                variante.append( (key, sol[newvar]) )
+            variantes.add(tuple(variante))
+        # rebuild variantes as dict
+        variantes = [dict(variante) for variante in variantes]
+        # remove variable which have always the same type
+        for key in self.rewritten:
+            it = iter(variantes)
+            etype = it.next()[key]
+            for variante in it:
+                if variante[key] != etype:
+                    break
+            else:
+                for variante in variantes:
+                    del variante[key]
+        return variantes
+
+    def _cleanup_inserted(self, node):
+        # cleanup inserted variable references
+        for vref in node.iget_nodes(n.VariableRef):
+            vref.unregister_reference()
+            if not vref.variable.stinfo['references']:
+                # no more references, undefine the variable
+                del self.select.defined_vars[vref.name]
+
+    def _may_be_shared(self, relation, target, searchedvarname):
+        """return True if the snippet relation can be skipped to use a relation
+        from the original query
+        """
+        # if cardinality is in '?1', we can ignore the relation and use variable
+        # from the original query
+        rschema = self.schema.rschema(relation.r_type)
+        if target == 'object':
+            cardindex = 0
+            ttypes_func = rschema.objects
+            rprop = rschema.rproperty
+        else: # target == 'subject':
+            cardindex = 1
+            ttypes_func = rschema.subjects
+            rprop = lambda x, y, z: rschema.rproperty(y, x, z)
+        for etype in self.varinfo['stinfo']['possibletypes']:
+            for ttype in ttypes_func(etype):
+                if rprop(etype, ttype, 'cardinality')[cardindex] in '+*':
+                    return False
+        return True
+
+    def _use_outer_term(self, snippet_varname, term):
+        key = (self.current_expr, self.varmap, snippet_varname)
+        if key in self.rewritten:
+            insertedvar = self.select.defined_vars.pop(self.rewritten[key])
+            for inserted_vref in insertedvar.references():
+                inserted_vref.parent.replace(inserted_vref, term.copy(self.select))
+        self.rewritten[key] = term.name
+
+    def _get_varname_or_term(self, vname):
+        if vname == 'U':
+            if self.u_varname is None:
+                select = self.select
+                self.u_varname = select.allocate_varname()
+                # generate an identifier for the substitution
+                argname = select.allocate_varname()
+                while argname in self.kwargs:
+                    argname = select.allocate_varname()
+                # insert "U eid %(u)s"
+                var = select.get_variable(self.u_varname)
+                select.add_constant_restriction(select.get_variable(self.u_varname),
+                                                'eid', unicode(argname), 'Substitute')
+                self.kwargs[argname] = self.session.user.eid
+            return self.u_varname
+        key = (self.current_expr, self.varmap, vname)
+        try:
+            return self.rewritten[key]
+        except KeyError:
+            self.rewritten[key] = newvname = self.select.allocate_varname()
+            return newvname
+
+    # visitor methods ##########################################################
+
+    def _visit_binary(self, node, cls):
+        newnode = cls()
+        for c in node.children:
+            new = c.accept(self)
+            if new is None:
+                continue
+            newnode.append(new)
+        if len(newnode.children) == 0:
+            return None
+        if len(newnode.children) == 1:
+            return newnode.children[0]
+        return newnode
+
+    def _visit_unary(self, node, cls):
+        newc = node.children[0].accept(self)
+        if newc is None:
+            return None
+        newnode = cls()
+        newnode.append(newc)
+        return newnode
+
+    def visit_and(self, node):
+        return self._visit_binary(node, n.And)
+
+    def visit_or(self, node):
+        return self._visit_binary(node, n.Or)
+
+    def visit_not(self, node):
+        return self._visit_unary(node, n.Not)
+
+    def visit_exists(self, node):
+        return self._visit_unary(node, n.Exists)
+
+    def visit_relation(self, node):
+        lhs, rhs = node.get_variable_parts()
+        if node.r_type in ('has_add_permission', 'has_update_permission',
+                           'has_delete_permission', 'has_read_permission'):
+            assert lhs.name == 'U'
+            action = node.r_type.split('_')[1]
+            key = (self.current_expr, self.varmap, rhs.name)
+            self.pending_keys.append( (key, action) )
+            return
+        if lhs.name in self.revvarmap:
+            # on lhs
+            # see if we can reuse this relation
+            rels = self.varinfo['lhs_rels']
+            if (node.r_type in rels and isinstance(rhs, n.VariableRef)
+                and rhs.name != 'U' and not rels[node.r_type].neged(strict=True)
+                and self._may_be_shared(node, 'object', lhs.name)):
+                # ok, can share variable
+                term = rels[node.r_type].children[1].children[0]
+                self._use_outer_term(rhs.name, term)
+                return
+        elif isinstance(rhs, n.VariableRef) and rhs.name in self.revvarmap and lhs.name != 'U':
+            # on rhs
+            # see if we can reuse this relation
+            rels = self.varinfo['rhs_rels']
+            if (node.r_type in rels and not rels[node.r_type].neged(strict=True)
+                and self._may_be_shared(node, 'subject', rhs.name)):
+                # ok, can share variable
+                term = rels[node.r_type].children[0]
+                self._use_outer_term(lhs.name, term)
+                return
+        rel = n.Relation(node.r_type, node.optional)
+        for c in node.children:
+            rel.append(c.accept(self))
+        return rel
+
+    def visit_comparison(self, node):
+        cmp_ = n.Comparison(node.operator)
+        for c in node.children:
+            cmp_.append(c.accept(self))
+        return cmp_
+
+    def visit_mathexpression(self, node):
+        cmp_ = n.MathExpression(node.operator)
+        for c in cmp.children:
+            cmp_.append(c.accept(self))
+        return cmp_
+
+    def visit_function(self, node):
+        """generate filter name for a function"""
+        function_ = n.Function(node.name)
+        for c in node.children:
+            function_.append(c.accept(self))
+        return function_
+
+    def visit_constant(self, node):
+        """generate filter name for a constant"""
+        return n.Constant(node.value, node.type)
+
+    def visit_variableref(self, node):
+        """get the sql name for a variable reference"""
+        if node.name in self.revvarmap:
+            if self.varinfo.get('const') is not None:
+                return n.Constant(self.varinfo['const'], 'Int') # XXX gae
+            return n.VariableRef(self.select.get_variable(
+                self.revvarmap[node.name]))
+        vname_or_term = self._get_varname_or_term(node.name)
+        if isinstance(vname_or_term, basestring):
+            return n.VariableRef(self.select.get_variable(vname_or_term))
+        # shared term
+        return vname_or_term.copy(self.select)
--- a/schema.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/schema.py	Fri Sep 18 11:24:37 2009 +0200
@@ -44,6 +44,7 @@
     'owned_by', 'created_by', 'is', 'is_instance_of', 'identity',
     'eid', 'creation_date', 'modification_date', 'has_text', 'cwuri',
     ))
+SYSTEM_RTYPES = set(('require_permission', 'custom_workflow', 'in_state', 'wf_info_for'))
 
 #  set of entity and relation types used to build the schema
 SCHEMA_TYPES = set((
@@ -100,7 +101,7 @@
         etype = ETYPE_NAME_MAP[etype]
     return etype
 
-def display_name(req, key, form=''):
+def display_name(req, key, form='', context=None):
     """return a internationalized string for the key (schema entity or relation
     name) in a given form
     """
@@ -110,8 +111,12 @@
     if form:
         key = key + '_' + form
     # ensure unicode
-    # added .lower() in case no translation are available
-    return unicode(req._(key)).lower()
+    # .lower() in case no translation are available XXX done whatever a translation is there or not!
+    if context is not None:
+        return unicode(req.pgettext(context, key)).lower()
+    else:
+        return unicode(req._(key)).lower()
+
 __builtins__['display_name'] = deprecated('display_name should be imported from cubicweb.schema')(display_name)
 
 def ERSchema_display_name(self, req, form=''):
@@ -642,6 +647,8 @@
             if len(self.rqlst.defined_vars[mainvar].references()) <= 2:
                 _LOGGER.warn('You did not use the %s variable in your RQL '
                              'expression %s', mainvar, self)
+        # syntax tree used by read security (inserted in queries when necessary
+        self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
 
     def __str__(self):
         return self.full_rql
@@ -767,8 +774,6 @@
 class ERQLExpression(RQLExpression):
     def __init__(self, expression, mainvars=None, eid=None):
         RQLExpression.__init__(self, expression, mainvars or 'X', eid)
-        # syntax tree used by read security (inserted in queries when necessary
-        self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
 
     @property
     def full_rql(self):
@@ -847,23 +852,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/base.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/schemas/base.py	Fri Sep 18 11:24:37 2009 +0200
@@ -52,11 +52,10 @@
     alias   = String(fulltextindexed=True, maxsize=56)
     address = String(required=True, fulltextindexed=True,
                      indexed=True, unique=True, maxsize=128)
-    canonical = Boolean(default=False,
-                        description=_('when multiple addresses are equivalent \
+    prefered_form = SubjectRelation('EmailAddress', cardinality='?*',
+                                    description=_('when multiple addresses are equivalent \
 (such as python-projects@logilab.org and python-projects@lists.logilab.org), set this \
-to true on one of them which is the preferred form.'))
-    identical_to = SubjectRelation('EmailAddress')
+to indicate which is the preferred form.'))
 
 class use_email(RelationType):
     """ """
@@ -71,9 +70,7 @@
     """the prefered email"""
     permissions = use_email.permissions
 
-class identical_to(RelationType):
-    """identical_to"""
-    symetric = True
+class prefered_form(RelationType):
     permissions = {
         'read':   ('managers', 'users', 'guests',),
         # XXX should have update permissions on both subject and object,
@@ -207,10 +204,6 @@
         }
 
 
-class see_also(RelationType):
-    """generic relation to link one entity to another"""
-    symetric = True
-
 class ExternalUri(EntityType):
     """a URI representing an object in external data store"""
     uri = String(required=True, unique=True, maxsize=256,
@@ -254,3 +247,30 @@
     name = String(required=True, unique=True, indexed=True,  maxsize=128,
                   description=_('name of the cache'))
     timestamp = Datetime(default='NOW')
+
+
+# "abtract" relation types, not used in cubicweb itself
+
+class identical_to(RelationType):
+    """identical to"""
+    symetric = True
+    permissions = {
+        'read':   ('managers', 'users', 'guests',),
+        # XXX should have update permissions on both subject and object,
+        #     though by doing this we will probably have no way to add
+        #     this relation in the web ui. The easiest way to acheive this
+        #     is probably to be able to have "U has_update_permission O" as
+        #     RQLConstraint of the relation definition, though this is not yet
+        #     possible
+        'add':    ('managers', RRQLExpression('U has_update_permission S'),),
+        'delete': ('managers', RRQLExpression('U has_update_permission S'),),
+        }
+
+class see_also(RelationType):
+    """generic relation to link one entity to another"""
+    symetric = True
+    permissions = {
+        'read':   ('managers', 'users', 'guests',),
+        'add':    ('managers', RRQLExpression('U has_update_permission S'),),
+        'delete': ('managers', RRQLExpression('U has_update_permission S'),),
+        }
--- a/schemas/workflow.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/schemas/workflow.py	Fri Sep 18 11:24:37 2009 +0200
@@ -10,8 +10,36 @@
 
 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='?*',
+                                   constraints=[RQLConstraint('O state_of S')],
+                                   description=_('initial state for this workflow'))
+
+
+class default_workflow(RelationType):
+    """default workflow for an entity type"""
+    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 +52,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='1*',
+                               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 +80,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='1*',
+                                    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 +199,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/selectors.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/selectors.py	Fri Sep 18 11:24:37 2009 +0200
@@ -960,7 +960,14 @@
     """
     def __init__(self, scorefunc, once_is_enough=False):
         super(score_entity, self).__init__(once_is_enough)
-        self.score_entity = scorefunc
+        def intscore(*args, **kwargs):
+            score = scorefunc(*args, **kwargs)
+            if not score:
+                return 0
+            if isinstance(score, (int, long)):
+                return score
+            return 1
+        self.score_entity = intscore
 
 
 # XXX DEPRECATED ##############################################################
--- a/server/__init__.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/__init__.py	Fri Sep 18 11:24:37 2009 +0200
@@ -180,22 +180,6 @@
     handler = config.migration_handler(schema, interactive=False,
                                        cnx=cnx, repo=repo)
     initialize_schema(config, schema, handler)
-    # insert versions
-    handler.cmd_add_entity('CWProperty', pkey=u'system.version.cubicweb',
-                           value=unicode(config.cubicweb_version()))
-    for cube in config.cubes():
-        handler.cmd_add_entity('CWProperty',
-                               pkey=u'system.version.%s' % cube.lower(),
-                               value=unicode(config.cube_version(cube)))
-    # some entities have been added before schema entities, fix the 'is' and
-    # 'is_instance_of' relations
-    for rtype in ('is', 'is_instance_of'):
-        handler.sqlexec(
-            'INSERT INTO %s_relation '
-            'SELECT X.eid, ET.cw_eid FROM entities as X, cw_CWEType as ET '
-            'WHERE X.type=ET.cw_name AND NOT EXISTS('
-            '      SELECT 1 from is_relation '
-            '      WHERE eid_from=X.eid AND eid_to=ET.cw_eid)' % rtype)
     # yoo !
     cnx.commit()
     config.enabled_sources = None
--- a/server/hooks.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/hooks.py	Fri Sep 18 11:24:37 2009 +0200
@@ -13,8 +13,8 @@
 from cubicweb import UnknownProperty, ValidationError, BadConnectionId
 
 from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation
-from cubicweb.server.hookhelper import (check_internal_entity, previous_state,
-                                     get_user_sessions, rproperty)
+from cubicweb.server.hookhelper import (check_internal_entity, 
+                                        get_user_sessions, rproperty)
 from cubicweb.server.repository import FTIndexEntityOp
 
 # special relations that don't have to be checked for integrity, usually
@@ -37,7 +37,8 @@
     # from the database (eg during tests)
     if eschema.eid is None:
         eschema.eid = session.unsafe_execute(
-            'Any X WHERE X is CWEType, X name %(name)s', {'name': str(etype)})[0][0]
+            'Any X WHERE X is CWEType, X name %(name)s',
+            {'name': str(etype)})[0][0]
     return eschema.eid
 
 
@@ -417,51 +418,129 @@
 
 # workflow handling ###########################################################
 
-def before_add_in_state(session, fromeid, rtype, toeid):
-    """check the transition is allowed and record transition information
+from cubicweb.entities.wfobjs import WorkflowTransition, WorkflowException
+
+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
+    fromsource = session.describe(x)[1]
+    # don't try to remove previous state if in_state isn't stored in the system
+    # source
+    if fromsource == 'system' or \
+       not session.repo.sources_by_uri[fromsource].support_relation('in_state'):
+        session.delete_relation(x, 'in_state', oldstate)
+    session.add_relation(x, 'in_state', newstate)
+
+
+def before_add_trinfo(session, entity):
+    """check the transition is allowed, add missing information. Expect that:
+    * wf_info_for inlined relation is set
+    * by_transition or to_state (managers only) inlined relation is set
     """
-    assert rtype == 'in_state'
-    state = previous_state(session, fromeid)
-    etype = session.describe(fromeid)[0]
-    if not (session.is_super_session or 'managers' in session.user.groups):
-        if not state is None:
-            entity = session.entity_from_eid(fromeid)
-            # we should find at least one transition going to this state
-            try:
-                iter(state.transitions(entity, toeid)).next()
-            except StopIteration:
-                _ = session._
-                msg = _('transition from %s to %s does not exist or is not allowed') % (
-                    _(state.name), _(session.entity_from_eid(toeid).name))
-                raise ValidationError(fromeid, {'in_state': msg})
+    # 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, unless we're in the process of changing
+    # entity's workflow
+    if session.transaction_data.get((forentity.eid, 'customwf')):
+        wfeid = session.transaction_data[(forentity.eid, 'customwf')]
+        wf = session.entity_from_eid(wfeid)
+    else:
+        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})
+    # True if we are coming back from subworkflow
+    swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
+    cowpowers = session.is_super_session or 'managers' in session.user.groups
+    # 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 cowpowers:
+            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 not cowpowers and deststate is None:
+            msg = entity.req._("state doesn't belong to entity's workflow")
+            raise ValidationError(entity.eid, {'to_state': msg})
+    else:
+        # check transition is valid and allowed, unless we're coming back from
+        # subworkflow
+        tr = session.entity_from_eid(treid)
+        if swtr is None:
+            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})
+        if entity.get('to_state'):
+            deststateeid = entity['to_state']
+            if not cowpowers and deststateeid != tr.destination().eid:
+                msg = session._("transition isn't allowed")
+                raise ValidationError(entity.eid, {'by_transition': msg})
+            if swtr is None:
+                deststate = session.entity_from_eid(deststateeid)
+                if not cowpowers and deststate is None:
+                    msg = entity.req._("state doesn't belong to entity's workflow")
+                    raise ValidationError(entity.eid, {'to_state': msg})
         else:
-            # not a transition
-            # check state is initial state if the workflow defines one
-            isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s',
-                                            {'etype': etype})
-            if isrset and not toeid == isrset[0][0]:
-                _ = session._
-                msg = _('%s is not the initial state (%s) for this entity') % (
-                    _(session.entity_from_eid(toeid).name), _(isrset.get_entity(0,0).name))
-                raise ValidationError(fromeid, {'in_state': msg})
-    eschema = session.repo.schema[etype]
-    if not 'wf_info_for' in eschema.object_relations():
-        # workflow history not activated for this entity type
-        return
-    rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s'
-    args = {'comment': session.get_shared_data('trcomment', None, pop=True),
-            'e': fromeid, 'ds': toeid}
-    cformat = session.get_shared_data('trcommentformat', None, pop=True)
-    if cformat is not None:
-        args['comment_format'] = cformat
-        rql += ', T comment_format %(comment_format)s'
-    restriction = ['DS eid %(ds)s, E eid %(e)s']
-    if not state is None: # not a transition
-        rql += ', T from_state FS'
-        restriction.append('FS eid %(fs)s')
-        args['fs'] = state.eid
-    rql = '%s WHERE %s' % (rql, ', '.join(restriction))
-    session.unsafe_execute(rql, args, 'e')
+            deststateeid = tr.destination().eid
+    # everything is ok, add missing information on the trinfo entity
+    entity['from_state'] = fromstate.eid
+    entity['to_state'] = deststateeid
+    nocheck = session.transaction_data.setdefault('skip-security', set())
+    nocheck.add((entity.eid, 'from_state', fromstate.eid))
+    nocheck.add((entity.eid, 'to_state', deststateeid))
+
+def after_add_trinfo(session, entity):
+    """change related entity state"""
+    _change_state(session, entity['wf_info_for'],
+                  entity['from_state'], entity['to_state'])
+    forentity = session.entity_from_eid(entity['wf_info_for'])
+    assert forentity.current_state.eid == entity['to_state'], forentity.current_state.name
+    if forentity.main_workflow.eid != forentity.current_workflow.eid:
+        # we're in a subworkflow, check if we've reached an exit point
+        wftr = forentity.subworkflow_input_transition()
+        if wftr is None:
+            # inconsistency detected
+            msg = entity.req._("state doesn't belong to entity's current workflow")
+            raise ValidationError(entity.eid, {'to_state': msg})
+        tostate = wftr.get_exit_point(entity['to_state'])
+        if tostate is not None:
+            # reached an exit point
+            msg = session._('exiting from subworkflow %s')
+            msg %= session._(forentity.current_workflow.name)
+            session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
+            # XXX iirk
+            req = forentity.req
+            forentity.req = session.super_session
+            try:
+                trinfo = forentity.change_state(tostate, msg, u'text/plain',
+                                                tr=wftr)
+            finally:
+                forentity.req = req
 
 
 class SetInitialStateOp(PreCommitOperation):
@@ -473,26 +552,125 @@
         # if there is an initial state and the entity's state is not set,
         # use the initial state as a default state
         pendingeids = session.transaction_data.get('pendingeids', ())
-        if not entity.eid in pendingeids and not entity.in_state:
-            rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s',
-                                   {'name': entity.id})
-            if rset:
-                session.add_relation(entity.eid, 'in_state', rset[0][0])
+        if not entity.eid in pendingeids and not entity.in_state and \
+               entity.main_workflow:
+            state = entity.main_workflow.initial
+            if state:
+                # use super session to by-pass security checks
+                session.super_session.add_relation(entity.eid, 'in_state',
+                                                   state.eid)
 
 
 def set_initial_state_after_add(session, entity):
     SetInitialStateOp(session, entity=entity)
 
 
+def before_add_in_state(session, eidfrom, rtype, eidto):
+    """check state apply, in case of direct in_state change using unsafe_execute
+    """
+    nocheck = session.transaction_data.setdefault('skip-security', ())
+    if (eidfrom, 'in_state', eidto) in nocheck:
+        # state changed through TrInfo insertion, so we already know it's ok
+        return
+    entity = session.entity_from_eid(eidfrom)
+    mainwf = entity.main_workflow
+    if mainwf is None:
+        msg = session._('entity has no workflow set')
+        raise ValidationError(entity.eid, {None: msg})
+    for wf in mainwf.iter_workflows():
+        if wf.state_by_eid(eidto):
+            break
+    else:
+        msg = session._("state doesn't belong to entity's workflow. You may "
+                        "want to set a custom workflow for this entity first.")
+        raise ValidationError(eidfrom, {'in_state': msg})
+    if entity.current_workflow and wf.eid != entity.current_workflow.eid:
+        msg = session._("state doesn't belong to entity's current workflow")
+        raise ValidationError(eidfrom, {'in_state': msg})
+
+
+class CheckTrExitPoint(PreCommitOperation):
+
+    def precommit_event(self):
+        tr = self.session.entity_from_eid(self.treid)
+        outputs = set()
+        for ep in tr.subworkflow_exit:
+            if ep.subwf_state.eid in outputs:
+                msg = self.session._("can't have multiple exits on the same state")
+                raise ValidationError(self.treid, {'subworkflow_exit': msg})
+            outputs.add(ep.subwf_state.eid)
+
+
+def after_add_subworkflow_exit(session, eidfrom, rtype, eidto):
+    CheckTrExitPoint(session, treid=eidfrom)
+
+
+class WorkflowChangedOp(PreCommitOperation):
+    """fix entity current state when changing its workflow"""
+
+    def precommit_event(self):
+        # notice that enforcement that new workflow apply to the entity's type is
+        # done by schema rule, no need to check it here
+        session = self.session
+        pendingeids = session.transaction_data.get('pendingeids', ())
+        if self.eid in pendingeids:
+            return
+        entity = session.entity_from_eid(self.eid)
+        # check custom workflow has not been rechanged to another one in the same
+        # transaction
+        mainwf = entity.main_workflow
+        if mainwf.eid == self.wfeid:
+            deststate = mainwf.initial
+            if not deststate:
+                msg = session._('workflow has no initial state')
+                raise ValidationError(entity.eid, {'custom_workflow': msg})
+            if mainwf.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 %= session._(mainwf.name)
+            session.transaction_data[(entity.eid, 'customwf')] = self.wfeid
+            entity.change_state(deststate, msg, u'text/plain')
+
+
+def set_custom_workflow(session, eidfrom, rtype, eidto):
+    WorkflowChangedOp(session, eid=eidfrom, wfeid=eidto)
+
+
+def del_custom_workflow(session, eidfrom, rtype, eidto):
+    entity = session.entity_from_eid(eidfrom)
+    typewf = entity.cwetype_workflow()
+    if typewf is not None:
+        WorkflowChangedOp(session, eid=eidfrom, wfeid=typewf.eid)
+
+
+def after_del_workflow(session, eid):
+    # workflow cleanup
+    session.execute('DELETE State X WHERE NOT X state_of Y')
+    session.execute('DELETE Transition X WHERE NOT X transition_of Y')
+
+
 def _register_wf_hooks(hm):
     """register workflow related hooks on the hooks manager"""
     if 'in_state' in hm.schema:
-        hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state')
-        hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state')
+        hm.register_hook(before_add_trinfo, 'before_add_entity', 'TrInfo')
+        hm.register_hook(after_add_trinfo, 'after_add_entity', 'TrInfo')
+        #hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state')
         for eschema in hm.schema.entities():
             if 'in_state' in eschema.subject_relations():
                 hm.register_hook(set_initial_state_after_add, 'after_add_entity',
                                  str(eschema))
+        hm.register_hook(set_custom_workflow, 'after_add_relation', 'custom_workflow')
+        hm.register_hook(del_custom_workflow, 'after_delete_relation', 'custom_workflow')
+        hm.register_hook(after_del_workflow, 'after_delete_entity', 'Workflow')
+        hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state')
+        hm.register_hook(after_add_subworkflow_exit, 'after_add_relation', 'subworkflow_exit')
 
 
 # CWProperty hooks #############################################################
--- a/server/hooksmanager.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/hooksmanager.py	Fri Sep 18 11:24:37 2009 +0200
@@ -268,3 +268,80 @@
 from cubicweb import set_log_methods
 set_log_methods(HooksManager, getLogger('cubicweb.hooksmanager'))
 set_log_methods(Hook, getLogger('cubicweb.hooks'))
+
+# base classes for relation propagation ########################################
+
+from cubicweb.server.pool import PreCommitOperation
+
+
+class 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
+        session.unsafe_execute(
+            '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),
+            {'x': meid, 'e': seid}, ('x', 'e'))
+
+
+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])
+        execute = session.unsafe_execute
+        for rel in self.subject_relations:
+            if eschema.has_subject_relation(rel):
+                execute('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):
+                execute('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')
+
+
+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])
+        execute = session.unsafe_execute
+        for rel in self.subject_relations:
+            if eschema.has_subject_relation(rel):
+                execute('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):
+                execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
+                        'R %s X' % (rtype, rel),
+                        {'x': fromeid, 'p': toeid}, 'x')
--- a/server/migractions.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/migractions.py	Fri Sep 18 11:24:37 2009 +0200
@@ -116,7 +116,7 @@
         config = self.config
         repo = self.repo_connect()
         # paths
-        timestamp = datetime.now().strftime('%Y-%m-%d_%H:%M:%S')
+        timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
         instbkdir = osp.join(config.appdatahome, 'backup')
         if not osp.exists(instbkdir):
             os.makedirs(instbkdir)
@@ -415,11 +415,12 @@
         espschema = eschema.specializes()
         if repospschema and not espschema:
             self.rqlexec('DELETE X specializes Y WHERE X is CWEType, X name %(x)s',
-                         {'x': str(repoeschema)})
+                         {'x': str(repoeschema)}, ask_confirm=False)
         elif not repospschema and espschema:
             self.rqlexec('SET X specializes Y WHERE X is CWEType, X name %(x)s, '
                          'Y is CWEType, Y name %(y)s',
-                         {'x': str(repoeschema), 'y': str(espschema)})
+                         {'x': str(repoeschema), 'y': str(espschema)},
+                         ask_confirm=False)
         self.rqlexecall(ss.updateeschema2rql(eschema),
                         ask_confirm=self.verbosity >= 2)
         for rschema, targettypes, role in eschema.relation_definitions(True):
@@ -959,78 +960,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.fire_transition("transition") or 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()
 
@@ -1047,32 +1048,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
--- a/server/msplanner.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/msplanner.py	Fri Sep 18 11:24:37 2009 +0200
@@ -97,9 +97,6 @@
 # str() Constant.value to ensure generated table name won't be unicode
 Constant._ms_table_key = lambda x: str(x.value)
 
-AbstractSource.dont_cross_relations = ()
-AbstractSource.cross_relations = ()
-
 def need_source_access_relation(vargraph):
     if not vargraph:
         return False
@@ -347,7 +344,7 @@
                         # * at least one supported relation specified
                         if not varobj._q_invariant or \
                                any(imap(source.support_relation,
-                                        (r.r_type for r in rels if r.r_type != 'eid'))):
+                                        (r.r_type for r in rels if r.r_type not in ('identity', 'eid')))):
                             sourcesterms.setdefault(source, {}).setdefault(varobj, set()).add(i)
                         # if variable is not invariant and is used by a relation
                         # not supported by this source, we'll have to split the
--- a/server/querier.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/querier.py	Fri Sep 18 11:24:37 2009 +0200
@@ -138,8 +138,8 @@
         # various resource accesors
         self.querier = querier
         self.schema = querier.schema
-        self.rqlhelper = querier._rqlhelper
         self.sqlannotate = querier.sqlgen_annotate
+        self.rqlhelper = session.vreg.rqlhelper
 
     def annotate_rqlst(self):
         if not self.rqlst.annotated:
@@ -265,6 +265,8 @@
                     myrqlst = select.copy(solutions=lchecksolutions)
                     myunion.append(myrqlst)
                     # in-place rewrite + annotation / simplification
+                    lcheckdef = [((varmap, 'X'), rqlexprs)
+                                 for varmap, rqlexprs in lcheckdef]
                     rewrite(myrqlst, lcheckdef, lchecksolutions, self.args)
                     noinvariant.update(noinvariant_vars(restricted, myrqlst, nbtrees))
                 if () in localchecks:
@@ -524,37 +526,33 @@
 
     def set_schema(self, schema):
         self.schema = schema
+        repo = self._repo
         # rql parsing / analysing helper
-        self._rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid',
-                                                               'has_text': 'fti'})
-        self._rql_cache = Cache(self._repo.config['rql-cache-size'])
+        self.solutions = repo.vreg.solutions
+        self._rql_cache = Cache(repo.config['rql-cache-size'])
         self.cache_hit, self.cache_miss = 0, 0
         # rql planner
         # note: don't use repo.sources, may not be built yet, and also "admin"
         #       isn't an actual source
-        if len([uri for uri in self._repo.config.sources() if uri != 'admin']) < 2:
+        rqlhelper = repo.vreg.rqlhelper
+        self._parse = rqlhelper.parse
+        self._annotate = rqlhelper.annotate
+        if len([uri for uri in repo.config.sources() if uri != 'admin']) < 2:
             from cubicweb.server.ssplanner import SSPlanner
-            self._planner = SSPlanner(schema, self._rqlhelper)
+            self._planner = SSPlanner(schema, rqlhelper)
         else:
             from cubicweb.server.msplanner import MSPlanner
-            self._planner = MSPlanner(schema, self._rqlhelper)
+            self._planner = MSPlanner(schema, rqlhelper)
         # sql generation annotator
         self.sqlgen_annotate = SQLGenAnnotator(schema).annotate
 
     def parse(self, rql, annotate=False):
         """return a rql syntax tree for the given rql"""
         try:
-            return self._rqlhelper.parse(unicode(rql), annotate=annotate)
+            return self._parse(unicode(rql), annotate=annotate)
         except UnicodeError:
             raise RQLSyntaxError(rql)
 
-    def solutions(self, session, rqlst, args):
-        assert session is not None
-        def type_from_eid(eid, type_from_eid=self._repo.type_from_eid,
-                          session=session):
-            return type_from_eid(eid, session)
-        self._rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
-
     def plan_factory(self, rqlst, args, session):
         """create an execution plan for an INSERT RQL query"""
         if rqlst.TYPE == 'insert':
@@ -642,7 +640,7 @@
             # bother modifying it. This is not necessary on write queries since
             # a new syntax tree is built from them.
             rqlst = rqlst.copy()
-            self._rqlhelper.annotate(rqlst)
+            self._annotate(rqlst)
         # make an execution plan
         plan = self.plan_factory(rqlst, args, session)
         plan.cache_key = cachekey
--- a/server/repository.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/repository.py	Fri Sep 18 11:24:37 2009 +0200
@@ -30,7 +30,7 @@
 
 from cubicweb import (CW_SOFTWARE_ROOT, CW_MIGRATION_MAP, CW_EVENT_MANAGER,
                       UnknownEid, AuthenticationError, ExecutionError,
-                      ETypeNotSupportedBySources, RTypeNotSupportedBySources,
+                      ETypeNotSupportedBySources, MultiSourcesError,
                       BadConnectionId, Unauthorized, ValidationError,
                       typed_eid)
 from cubicweb.cwvreg import CubicWebVRegistry
@@ -108,6 +108,9 @@
     # hooks responsability to ensure they do not violate relation's cardinality
     if session.is_super_session:
         return
+    ensure_card_respected(session.unsafe_execute, session, eidfrom, rtype, eidto)
+
+def ensure_card_respected(execute, session, eidfrom, rtype, eidto):
     card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
     # one may be tented to check for neweids but this may cause more than one
     # relation even with '1?'  cardinality if thoses relations are added in the
@@ -118,14 +121,11 @@
     if card[0] in '1?':
         rschema = session.repo.schema.rschema(rtype)
         if not rschema.inlined:
-            session.unsafe_execute(
-                'DELETE X %s Y WHERE X eid %%(x)s, NOT Y eid %%(y)s' % rtype,
-                {'x': eidfrom, 'y': eidto}, 'x')
+            execute('DELETE X %s Y WHERE X eid %%(x)s,NOT Y eid %%(y)s' % rtype,
+                    {'x': eidfrom, 'y': eidto}, 'x')
     if card[1] in '1?':
-        session.unsafe_execute(
-            'DELETE X %s Y WHERE NOT X eid %%(x)s, Y eid %%(y)s' % rtype,
-            {'x': eidfrom, 'y': eidto}, 'y')
-
+        execute('DELETE X %s Y WHERE NOT X eid %%(x)s, Y eid %%(y)s' % rtype,
+                {'x': eidfrom, 'y': eidto}, 'y')
 
 class Repository(object):
     """a repository provides access to a set of persistent storages for
@@ -149,6 +149,7 @@
         self._running_threads = []
         # initial schema, should be build or replaced latter
         self.schema = CubicWebSchema(config.appid)
+        self.vreg.schema = self.schema # until actual schema is loaded...
         # querier helper, need to be created after sources initialization
         self.querier = QuerierHelper(self, self.schema)
         # should we reindex in changes?
@@ -192,13 +193,14 @@
             config.bootstrap_cubes()
             self.set_bootstrap_schema(config.load_schema())
             # need to load the Any and CWUser entity types
-            self.vreg.schema = self.schema
             etdirectory = join(CW_SOFTWARE_ROOT, 'entities')
             self.vreg.init_registration([etdirectory])
             self.vreg.load_file(join(etdirectory, '__init__.py'),
                                 '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")
@@ -244,15 +246,16 @@
         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())))
+        if resetvreg:
+            # full reload of all appobjects
+            self.vreg.reset()
+            self.vreg.set_schema(schema)
+        else:
+            self.vreg._set_schema(schema)
         self.querier.set_schema(schema)
         for source in self.sources:
             source.set_schema(schema)
         self.schema = schema
-        if resetvreg:
-            # full reload of all appobjects
-            self.vreg.reset()
-            self.vreg.set_schema(schema)
         self.reset_hooks()
 
     def reset_hooks(self):
@@ -970,12 +973,21 @@
     def locate_relation_source(self, session, subject, rtype, object):
         subjsource = self.source_from_eid(subject, session)
         objsource = self.source_from_eid(object, session)
-        if not (subjsource is objsource and subjsource.support_relation(rtype, 1)):
+        if not subjsource is objsource:
             source = self.system_source
-            if not source.support_relation(rtype, 1):
-                raise RTypeNotSupportedBySources(rtype)
+            if not (subjsource.may_cross_relation(rtype) 
+                    and objsource.may_cross_relation(rtype)):
+                raise MultiSourcesError(
+                    "relation %s can't be crossed among sources"
+                    % rtype)
+        elif not subjsource.support_relation(rtype):
+            source = self.system_source
         else:
             source = subjsource
+        if not source.support_relation(rtype, True):
+            raise MultiSourcesError(
+                "source %s doesn't support write of %s relation"
+                % (source.uri, rtype))
         return source
 
     def locate_etype_source(self, etype):
--- a/server/rqlannotation.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/rqlannotation.py	Fri Sep 18 11:24:37 2009 +0200
@@ -331,7 +331,7 @@
         if isinstance(term, VariableRef) and self.is_ambiguous(term.variable):
             var = term.variable
             if len(var.stinfo['relations'] - var.stinfo['typerels']) == 1 \
-                   or rel.sqlscope is var.sqlscope:
+                   or rel.sqlscope is var.sqlscope or rel.r_type == 'identity':
                 self.restrict(var, frozenset(etypes_func()))
                 try:
                     self.maydeambrels[var].add(rel)
--- a/server/rqlrewrite.py	Thu Sep 17 19:38:04 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,396 +0,0 @@
-"""RQL rewriting utilities, used for read security checking
-
-:organization: Logilab
-:copyright: 2007-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 rql import nodes, stmts, TypeResolverException
-from cubicweb import Unauthorized, server, typed_eid
-from cubicweb.server.ssplanner import add_types_restriction
-
-def remove_solutions(origsolutions, solutions, defined):
-    """when a rqlst has been generated from another by introducing security
-    assertions, this method returns solutions which are contained in orig
-    solutions
-    """
-    newsolutions = []
-    for origsol in origsolutions:
-        for newsol in solutions[:]:
-            for var, etype in origsol.items():
-                try:
-                    if newsol[var] != etype:
-                        try:
-                            defined[var].stinfo['possibletypes'].remove(newsol[var])
-                        except KeyError:
-                            pass
-                        break
-                except KeyError:
-                    # variable has been rewritten
-                    continue
-            else:
-                newsolutions.append(newsol)
-                solutions.remove(newsol)
-    return newsolutions
-
-class Unsupported(Exception): pass
-
-class RQLRewriter(object):
-    """insert some rql snippets into another rql syntax tree"""
-    def __init__(self, querier, session):
-        self.session = session
-        self.annotate = querier._rqlhelper.annotate
-        self._compute_solutions = querier.solutions
-        self.schema = querier.schema
-
-    def compute_solutions(self):
-        self.annotate(self.select)
-        try:
-            self._compute_solutions(self.session, self.select, self.kwargs)
-        except TypeResolverException:
-            raise Unsupported()
-        if len(self.select.solutions) < len(self.solutions):
-            raise Unsupported()
-
-    def rewrite(self, select, snippets, solutions, kwargs):
-        if server.DEBUG:
-            print '---- rewrite', select, snippets, solutions
-        self.select = select
-        self.solutions = solutions
-        self.kwargs = kwargs
-        self.u_varname = None
-        self.removing_ambiguity = False
-        self.exists_snippet = {}
-        # we have to annotate the rqlst before inserting snippets, even though
-        # we'll have to redo it latter
-        self.annotate(select)
-        self.insert_snippets(snippets)
-        if not self.exists_snippet and self.u_varname:
-            # U has been inserted than cancelled, cleanup
-            select.undefine_variable(select.defined_vars[self.u_varname])
-        # clean solutions according to initial solutions
-        newsolutions = remove_solutions(solutions, select.solutions,
-                                        select.defined_vars)
-        assert len(newsolutions) >= len(solutions), \
-               'rewritten rql %s has lost some solutions, there is probably something '\
-               'wrong in your schema permission (for instance using a '\
-              'RQLExpression which insert a relation which doesn\'t exists in '\
-               'the schema)\nOrig solutions: %s\nnew solutions: %s' % (
-            select, solutions, newsolutions)
-        if len(newsolutions) > len(solutions):
-            # the snippet has introduced some ambiguities, we have to resolve them
-            # "manually"
-            variantes = self.build_variantes(newsolutions)
-            # insert "is" where necessary
-            varexistsmap = {}
-            self.removing_ambiguity = True
-            for (erqlexpr, mainvar, oldvarname), etype in variantes[0].iteritems():
-                varname = self.rewritten[(erqlexpr, mainvar, oldvarname)]
-                var = select.defined_vars[varname]
-                exists = var.references()[0].scope
-                exists.add_constant_restriction(var, 'is', etype, 'etype')
-                varexistsmap[mainvar] = exists
-            # insert ORED exists where necessary
-            for variante in variantes[1:]:
-                self.insert_snippets(snippets, varexistsmap)
-                for (erqlexpr, mainvar, oldvarname), etype in variante.iteritems():
-                    varname = self.rewritten[(erqlexpr, mainvar, oldvarname)]
-                    try:
-                        var = select.defined_vars[varname]
-                    except KeyError:
-                        # not a newly inserted variable
-                        continue
-                    exists = var.references()[0].scope
-                    exists.add_constant_restriction(var, 'is', etype, 'etype')
-            # recompute solutions
-            #select.annotated = False # avoid assertion error
-            self.compute_solutions()
-            # clean solutions according to initial solutions
-            newsolutions = remove_solutions(solutions, select.solutions,
-                                            select.defined_vars)
-        select.solutions = newsolutions
-        add_types_restriction(self.schema, select)
-        if server.DEBUG:
-            print '---- rewriten', select
-
-    def build_variantes(self, newsolutions):
-        variantes = set()
-        for sol in newsolutions:
-            variante = []
-            for (erqlexpr, mainvar, oldvar), newvar in self.rewritten.iteritems():
-                variante.append( ((erqlexpr, mainvar, oldvar), sol[newvar]) )
-            variantes.add(tuple(variante))
-        # rebuild variantes as dict
-        variantes = [dict(variante) for variante in variantes]
-        # remove variable which have always the same type
-        for erqlexpr, mainvar, oldvar in self.rewritten:
-            it = iter(variantes)
-            etype = it.next()[(erqlexpr, mainvar, oldvar)]
-            for variante in it:
-                if variante[(erqlexpr, mainvar, oldvar)] != etype:
-                    break
-            else:
-                for variante in variantes:
-                    del variante[(erqlexpr, mainvar, oldvar)]
-        return variantes
-
-    def insert_snippets(self, snippets, varexistsmap=None):
-        self.rewritten = {}
-        for varname, erqlexprs in snippets:
-            if varexistsmap is not None and not varname in varexistsmap:
-                continue
-            try:
-                self.const = typed_eid(varname)
-                self.varname = self.const
-                self.rhs_rels = self.lhs_rels = {}
-            except ValueError:
-                self.varname = varname
-                self.const = None
-                self.varstinfo = stinfo = self.select.defined_vars[varname].stinfo
-                if varexistsmap is None:
-                    self.rhs_rels = dict( (rel.r_type, rel) for rel in stinfo['rhsrelations'])
-                    self.lhs_rels = dict( (rel.r_type, rel) for rel in stinfo['relations']
-                                                  if not rel in stinfo['rhsrelations'])
-                else:
-                    self.rhs_rels = self.lhs_rels = {}
-            parent = None
-            inserted = False
-            for erqlexpr in erqlexprs:
-                self.current_expr = erqlexpr
-                if varexistsmap is None:
-                    try:
-                        new = self.insert_snippet(varname, erqlexpr.snippet_rqlst, parent)
-                    except Unsupported:
-                        continue
-                    inserted = True
-                    if new is not None:
-                        self.exists_snippet[erqlexpr] = new
-                    parent = parent or new
-                else:
-                    # called to reintroduce snippet due to ambiguity creation,
-                    # so skip snippets which are not introducing this ambiguity
-                    exists = varexistsmap[varname]
-                    if self.exists_snippet[erqlexpr] is exists:
-                        self.insert_snippet(varname, erqlexpr.snippet_rqlst, exists)
-            if varexistsmap is None and not inserted:
-                # no rql expression found matching rql solutions. User has no access right
-                raise Unauthorized()
-
-    def insert_snippet(self, varname, snippetrqlst, parent=None):
-        new = snippetrqlst.where.accept(self)
-        if new is not None:
-            try:
-                var = self.select.defined_vars[varname]
-            except KeyError:
-                # not a variable
-                pass
-            else:
-                if var.stinfo['optrelations']:
-                    # use a subquery
-                    subselect = stmts.Select()
-                    subselect.append_selected(nodes.VariableRef(subselect.get_variable(varname)))
-                    subselect.add_restriction(new.copy(subselect))
-                    aliases = [varname]
-                    for rel in var.stinfo['relations']:
-                        rschema = self.schema.rschema(rel.r_type)
-                        if rschema.is_final() or (rschema.inlined and not rel in var.stinfo['rhsrelations']):
-                            self.select.remove_node(rel)
-                            rel.children[0].name = varname
-                            subselect.add_restriction(rel.copy(subselect))
-                            for vref in rel.children[1].iget_nodes(nodes.VariableRef):
-                                subselect.append_selected(vref.copy(subselect))
-                                aliases.append(vref.name)
-                    if self.u_varname:
-                        # generate an identifier for the substitution
-                        argname = subselect.allocate_varname()
-                        while argname in self.kwargs:
-                            argname = subselect.allocate_varname()
-                        subselect.add_constant_restriction(subselect.get_variable(self.u_varname),
-                                                        'eid', unicode(argname), 'Substitute')
-                        self.kwargs[argname] = self.session.user.eid
-                    add_types_restriction(self.schema, subselect, subselect, solutions=self.solutions)
-                    assert parent is None
-                    myunion = stmts.Union()
-                    myunion.append(subselect)
-                    aliases = [nodes.VariableRef(self.select.get_variable(name, i))
-                               for i, name in enumerate(aliases)]
-                    self.select.add_subquery(nodes.SubQuery(aliases, myunion), check=False)
-                    self._cleanup_inserted(new)
-                    try:
-                        self.compute_solutions()
-                    except Unsupported:
-                        # some solutions have been lost, can't apply this rql expr
-                        self.select.remove_subquery(new, undefine=True)
-                        raise
-                    return
-            new = nodes.Exists(new)
-            if parent is None:
-                self.select.add_restriction(new)
-            else:
-                grandpa = parent.parent
-                or_ = nodes.Or(parent, new)
-                grandpa.replace(parent, or_)
-            if not self.removing_ambiguity:
-                try:
-                    self.compute_solutions()
-                except Unsupported:
-                    # some solutions have been lost, can't apply this rql expr
-                    if parent is None:
-                        self.select.remove_node(new, undefine=True)
-                    else:
-                        parent.parent.replace(or_, or_.children[0])
-                        self._cleanup_inserted(new)
-                    raise
-            return new
-
-    def _cleanup_inserted(self, node):
-        # cleanup inserted variable references
-        for vref in node.iget_nodes(nodes.VariableRef):
-            vref.unregister_reference()
-            if not vref.variable.stinfo['references']:
-                # no more references, undefine the variable
-                del self.select.defined_vars[vref.name]
-
-    def _visit_binary(self, node, cls):
-        newnode = cls()
-        for c in node.children:
-            new = c.accept(self)
-            if new is None:
-                continue
-            newnode.append(new)
-        if len(newnode.children) == 0:
-            return None
-        if len(newnode.children) == 1:
-            return newnode.children[0]
-        return newnode
-
-    def _visit_unary(self, node, cls):
-        newc = node.children[0].accept(self)
-        if newc is None:
-            return None
-        newnode = cls()
-        newnode.append(newc)
-        return newnode
-
-    def visit_and(self, et):
-        return self._visit_binary(et, nodes.And)
-
-    def visit_or(self, ou):
-        return self._visit_binary(ou, nodes.Or)
-
-    def visit_not(self, node):
-        return self._visit_unary(node, nodes.Not)
-
-    def visit_exists(self, node):
-        return self._visit_unary(node, nodes.Exists)
-
-    def visit_relation(self, relation):
-        lhs, rhs = relation.get_variable_parts()
-        if lhs.name == 'X':
-            # on lhs
-            # see if we can reuse this relation
-            if relation.r_type in self.lhs_rels and isinstance(rhs, nodes.VariableRef) and rhs.name != 'U':
-                if self._may_be_shared(relation, 'object'):
-                    # ok, can share variable
-                    term = self.lhs_rels[relation.r_type].children[1].children[0]
-                    self._use_outer_term(rhs.name, term)
-                    return
-        elif isinstance(rhs, nodes.VariableRef) and rhs.name == 'X' and lhs.name != 'U':
-            # on rhs
-            # see if we can reuse this relation
-            if relation.r_type in self.rhs_rels and self._may_be_shared(relation, 'subject'):
-                # ok, can share variable
-                term = self.rhs_rels[relation.r_type].children[0]
-                self._use_outer_term(lhs.name, term)
-                return
-        rel = nodes.Relation(relation.r_type, relation.optional)
-        for c in relation.children:
-            rel.append(c.accept(self))
-        return rel
-
-    def visit_comparison(self, cmp):
-        cmp_ = nodes.Comparison(cmp.operator)
-        for c in cmp.children:
-            cmp_.append(c.accept(self))
-        return cmp_
-
-    def visit_mathexpression(self, mexpr):
-        cmp_ = nodes.MathExpression(mexpr.operator)
-        for c in cmp.children:
-            cmp_.append(c.accept(self))
-        return cmp_
-
-    def visit_function(self, function):
-        """generate filter name for a function"""
-        function_ = nodes.Function(function.name)
-        for c in function.children:
-            function_.append(c.accept(self))
-        return function_
-
-    def visit_constant(self, constant):
-        """generate filter name for a constant"""
-        return nodes.Constant(constant.value, constant.type)
-
-    def visit_variableref(self, vref):
-        """get the sql name for a variable reference"""
-        if vref.name == 'X':
-            if self.const is not None:
-                return nodes.Constant(self.const, 'Int')
-            return nodes.VariableRef(self.select.get_variable(self.varname))
-        vname_or_term = self._get_varname_or_term(vref.name)
-        if isinstance(vname_or_term, basestring):
-            return nodes.VariableRef(self.select.get_variable(vname_or_term))
-        # shared term
-        return vname_or_term.copy(self.select)
-
-    def _may_be_shared(self, relation, target):
-        """return True if the snippet relation can be skipped to use a relation
-        from the original query
-        """
-        # if cardinality is in '?1', we can ignore the relation and use variable
-        # from the original query
-        rschema = self.schema.rschema(relation.r_type)
-        if target == 'object':
-            cardindex = 0
-            ttypes_func = rschema.objects
-            rprop = rschema.rproperty
-        else: # target == 'subject':
-            cardindex = 1
-            ttypes_func = rschema.subjects
-            rprop = lambda x, y, z: rschema.rproperty(y, x, z)
-        for etype in self.varstinfo['possibletypes']:
-            for ttype in ttypes_func(etype):
-                if rprop(etype, ttype, 'cardinality')[cardindex] in '+*':
-                    return False
-        return True
-
-    def _use_outer_term(self, snippet_varname, term):
-        key = (self.current_expr, self.varname, snippet_varname)
-        if key in self.rewritten:
-            insertedvar = self.select.defined_vars.pop(self.rewritten[key])
-            for inserted_vref in insertedvar.references():
-                inserted_vref.parent.replace(inserted_vref, term.copy(self.select))
-        self.rewritten[key] = term
-
-    def _get_varname_or_term(self, vname):
-        if vname == 'U':
-            if self.u_varname is None:
-                select = self.select
-                self.u_varname = select.allocate_varname()
-                # generate an identifier for the substitution
-                argname = select.allocate_varname()
-                while argname in self.kwargs:
-                    argname = select.allocate_varname()
-                # insert "U eid %(u)s"
-                var = select.get_variable(self.u_varname)
-                select.add_constant_restriction(select.get_variable(self.u_varname),
-                                                'eid', unicode(argname), 'Substitute')
-                self.kwargs[argname] = self.session.user.eid
-            return self.u_varname
-        key = (self.current_expr, self.varname, vname)
-        try:
-            return self.rewritten[key]
-        except KeyError:
-            self.rewritten[key] = newvname = self.select.allocate_varname()
-            return newvname
--- a/server/schemahooks.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/schemahooks.py	Fri Sep 18 11:24:37 2009 +0200
@@ -759,8 +759,7 @@
 
 def after_del_eetype(session, eid):
     # workflow cleanup
-    session.execute('DELETE State X WHERE NOT X state_of Y')
-    session.execute('DELETE Transition X WHERE NOT X transition_of Y')
+    session.execute('DELETE Workflow X WHERE NOT X workflow_of Y')
 
 
 def before_del_ertype(session, eid):
--- a/server/schemaserial.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/schemaserial.py	Fri Sep 18 11:24:37 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/securityhooks.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/securityhooks.py	Fri Sep 18 11:24:37 2009 +0200
@@ -70,11 +70,17 @@
 
 def before_add_relation(session, fromeid, rtype, toeid):
     if rtype in BEFORE_ADD_RELATIONS and not session.is_super_session:
+        nocheck = session.transaction_data.get('skip-security', ())
+        if (fromeid, rtype, toeid) in nocheck:
+            return
         rschema = session.repo.schema[rtype]
         rschema.check_perm(session, 'add', fromeid, toeid)
 
 def after_add_relation(session, fromeid, rtype, toeid):
     if not rtype in BEFORE_ADD_RELATIONS and not session.is_super_session:
+        nocheck = session.transaction_data.get('skip-security', ())
+        if (fromeid, rtype, toeid) in nocheck:
+            return
         rschema = session.repo.schema[rtype]
         if rtype in ON_COMMIT_ADD_RELATIONS:
             CheckRelationPermissionOp(session, action='add', rschema=rschema,
@@ -84,6 +90,9 @@
 
 def before_del_relation(session, fromeid, rtype, toeid):
     if not session.is_super_session:
+        nocheck = session.transaction_data.get('skip-security', ())
+        if (fromeid, rtype, toeid) in nocheck:
+            return
         session.repo.schema[rtype].check_perm(session, 'delete', fromeid, toeid)
 
 def register_security_hooks(hm):
--- a/server/serverctl.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/serverctl.py	Fri Sep 18 11:24:37 2009 +0200
@@ -312,7 +312,8 @@
         # postgres specific stuff
         if driver == 'postgres':
             # install plpythonu/plpgsql language if not installed by the cube
-            for extlang in ('plpythonu', 'plpgsql'):
+            langs = sys.platform == 'win32' and ('plpgsql',) or ('plpythonu', 'plpgsql')
+            for extlang in langs:
                 helper.create_language(cursor, extlang)
         cursor.close()
         cnx.commit()
@@ -676,7 +677,7 @@
         import tempfile
         srcappid = pop_arg(args, 1, msg='No source instance specified !')
         destappid = pop_arg(args, msg='No destination instance specified !')
-        output = tempfile.mkstemp(dir='/tmp/')[1]
+        output = tempfile.mkstemp()[1]
         if ':' in srcappid:
             host, srcappid = srcappid.split(':')
             _remote_dump(host, srcappid, output, self.config.sudo)
--- a/server/session.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/session.py	Fri Sep 18 11:24:37 2009 +0200
@@ -18,7 +18,7 @@
 from cubicweb import RequestSessionMixIn, Binary, UnknownEid
 from cubicweb.dbapi import ConnectionProperties
 from cubicweb.utils import make_uid
-from cubicweb.server.rqlrewrite import RQLRewriter
+from cubicweb.rqlrewrite import RQLRewriter
 
 ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
 
@@ -210,13 +210,18 @@
         vreg = self.vreg
         language = language or self.user.property_value('ui.language')
         try:
-            self._ = self.__ = vreg.config.translations[language]
+            gettext, pgettext = vreg.config.translations[language]
+            self._ = self.__ = gettext
+            self.pgettext = pgettext
         except KeyError:
             language = vreg.property_value('ui.language')
             try:
-                self._ = self.__ = vreg.config.translations[language]
+                gettext, pgettext = vreg.config.translations[language]
+                self._ = self.__ = gettext
+                self.pgettext = pgettext
             except KeyError:
                 self._ = self.__ = unicode
+                self.pgettext = lambda x,y: y
         self.lang = language
 
     def change_property(self, prop, value):
@@ -543,7 +548,7 @@
         try:
             return self._threaddata._rewriter
         except AttributeError:
-            self._threaddata._rewriter = RQLRewriter(self.repo.querier, self)
+            self._threaddata._rewriter = RQLRewriter(self)
             return self._threaddata._rewriter
 
     def build_description(self, rqlst, args, result):
--- a/server/sources/__init__.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/sources/__init__.py	Fri Sep 18 11:24:37 2009 +0200
@@ -80,6 +80,11 @@
     # a reference to the instance'schema (may differs from the source'schema)
     schema = None
 
+    # multi-sources planning control
+    dont_cross_relations = ()
+    cross_relations = ()
+
+
     def __init__(self, repo, appschema, source_config, *args, **kwargs):
         self.repo = repo
         self.uri = source_config['uri']
@@ -177,6 +182,19 @@
             return wsupport
         return True
 
+    def may_cross_relation(self, rtype):
+        """return True if the relation may be crossed among sources. Rules are:
+
+        * if this source support the relation, can't be crossed unless explicitly
+          specified in .cross_relations
+
+        * if this source doesn't support the relation, can be crossed unless
+          explicitly specified in .dont_cross_relations
+        """
+        if self.support_relation(rtype):
+            return rtype in self.cross_relations
+        return rtype not in self.dont_cross_relations
+
     def eid2extid(self, eid, session=None):
         return self.repo.eid2extid(self, eid, session)
 
--- a/server/sources/ldapuser.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/sources/ldapuser.py	Fri Sep 18 11:24:37 2009 +0200
@@ -336,7 +336,7 @@
                 if sol[varname] == 'CWUser':
                     mainvars.append(varname)
                     break
-        assert mainvars
+        assert mainvars, rqlst
         columns, globtransforms = self.prepare_columns(mainvars, rqlst)
         eidfilters = []
         allresults = []
--- a/server/sources/native.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/sources/native.py	Fri Sep 18 11:24:37 2009 +0200
@@ -94,8 +94,6 @@
     """adapter for source using the native cubicweb schema (see below)
     """
     sqlgen_class = SQLGenerator
-    # need default value on class since migration doesn't call init method
-    has_deleted_entitites_table = True
 
     passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P"
     auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s"
@@ -226,15 +224,6 @@
 
     def init(self):
         self.init_creating()
-        pool = self.repo._get_pool()
-        pool.pool_set()
-        # XXX cubicweb < 2.42 compat
-        if 'deleted_entities' in self.dbhelper.list_tables(pool['system']):
-            self.has_deleted_entitites_table = True
-        else:
-            self.has_deleted_entitites_table = False
-        pool.pool_reset()
-        self.repo._free_pool(pool)
 
     def map_attribute(self, etype, attr, cb):
         self._rql_sqlgen.attr_map['%s.%s' % (etype, attr)] = cb
@@ -242,7 +231,7 @@
     # ISource interface #######################################################
 
     def compile_rql(self, rql):
-        rqlst = self.repo.querier._rqlhelper.parse(rql)
+        rqlst = self.repo.vreg.rqlhelper.parse(rql)
         rqlst.restricted_vars = ()
         rqlst.children[0].solutions = self._sols
         self.repo.querier.sqlgen_annotate(rqlst)
@@ -279,6 +268,9 @@
         # can't claim not supporting a relation
         return True #not rtype == 'content_for'
 
+    def may_cross_relation(self, rtype):
+        return True
+
     def authenticate(self, session, login, password):
         """return CWUser eid for the given login/password if this account is
         defined in this source, else raise `AuthenticationError`
@@ -549,13 +541,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/pyrorql.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/sources/pyrorql.py	Fri Sep 18 11:24:37 2009 +0200
@@ -345,6 +345,7 @@
         cu.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
                    kwargs, 'x')
         self._query_cache.clear()
+        entity.clear_all_caches()
 
     def delete_entity(self, session, etype, eid):
         """delete an entity from the source"""
@@ -360,6 +361,8 @@
                    {'x': self.eid2extid(subject, session),
                     'y': self.eid2extid(object, session)}, ('x', 'y'))
         self._query_cache.clear()
+        session.entity_from_eid(subject).clear_all_caches()
+        session.entity_from_eid(object).clear_all_caches()
 
     def delete_relation(self, session, subject, rtype, object):
         """delete a relation from the source"""
@@ -368,6 +371,8 @@
                    {'x': self.eid2extid(subject, session),
                     'y': self.eid2extid(object, session)}, ('x', 'y'))
         self._query_cache.clear()
+        session.entity_from_eid(subject).clear_all_caches()
+        session.entity_from_eid(object).clear_all_caches()
 
 
 class RQL2RQL(object):
--- a/server/sources/rql2sql.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/sources/rql2sql.py	Fri Sep 18 11:24:37 2009 +0200
@@ -336,6 +336,7 @@
         self._varmap = varmap
         self._query_attrs = {}
         self._state = None
+        self._not_scope_offset = 0
         try:
             # union query for each rqlst / solution
             sql = self.union_sql(union)
@@ -553,7 +554,11 @@
 
     def visit_not(self, node):
         self._state.push_scope()
+        if isinstance(node.children[0], Relation):
+            self._not_scope_offset += 1
         csql = node.children[0].accept(self)
+        if isinstance(node.children[0], Relation):
+            self._not_scope_offset -= 1
         sqls, tables = self._state.pop_scope()
         if node in self._state.done or not csql:
             # already processed or no sql generated by children
@@ -651,10 +656,6 @@
                 sql = self._visit_outer_join_relation(relation, rschema)
             elif rschema.inlined:
                 sql = self._visit_inlined_relation(relation)
-#             elif isinstance(relation.parent, Not):
-#                 self._state.done.add(relation.parent)
-#                 # NOT relation
-#                 sql = self._visit_not_relation(relation, rschema)
             else:
                 # regular (non final) relation
                 sql = self._visit_relation(relation, rschema)
@@ -1080,12 +1081,16 @@
         # a EXISTS node
         if var.sqlscope is var.stmt:
             scope = 0
+        # don't consider not_scope_offset if the variable is only used in one
+        # relation
+        elif len(var.stinfo['relations']) > 1:
+            scope = -1 - self._not_scope_offset
         else:
             scope = -1
         try:
             sql = self._varmap[var.name]
             table = sql.split('.', 1)[0]
-            if scope == -1:
+            if scope < 0:
                 scope = self._varmap_table_scope(var.stmt, table)
             self.add_table(table, scope=scope)
         except KeyError:
@@ -1095,7 +1100,8 @@
                 raise BadRQLQuery(var.stmt.root)
             table = var.name
             sql = '%s.%seid' % (table, SQL_PREFIX)
-            self.add_table('%s%s AS %s' % (SQL_PREFIX, etype, table), table, scope=scope)
+            self.add_table('%s%s AS %s' % (SQL_PREFIX, etype, table), table,
+                           scope=scope)
         return sql, table
 
     def _inlined_var_sql(self, var, rtype):
@@ -1146,8 +1152,8 @@
             key = table
         if key in self._state.tables:
             return
-        if scope == -1:
-            scope = len(self._state.actual_tables) - 1
+        if scope < 0:
+            scope = len(self._state.actual_tables) + scope
         self._state.tables[key] = (scope, table)
         self._state.actual_tables[scope].append(table)
 
--- a/server/ssplanner.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/ssplanner.py	Fri Sep 18 11:24:37 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	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/data/migratedapp/schema.py	Fri Sep 18 11:24:37 2009 +0200
@@ -50,9 +50,9 @@
                                              'PE require_permission P, P name "add_note", '
                                              'P require_group G'),)}
 
+    whatever = Int()  # keep it before `date` for unittest_migraction.test_add_attribute_int
     date = Datetime()
     type = String(maxsize=1)
-    whatever = Int()
     mydate = Date(default='TODAY')
     shortpara = String(maxsize=64)
     ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
@@ -104,7 +104,7 @@
     concerne2 = SubjectRelation(('Affaire', 'Note'), cardinality='1*')
     connait = SubjectRelation('Personne', symetric=True)
 
-class Societe(EntityType):
+class Societe(WorkflowableEntityType):
     permissions = {
         'read': ('managers', 'users', 'guests'),
         'update': ('managers', 'owners'),
@@ -123,7 +123,6 @@
     cp   = String(maxsize=12)
     ville= String(maxsize=32)
 
-    in_state = SubjectRelation('State', cardinality='?*')
 
 class evaluee(RelationDefinition):
     subject = ('Personne', 'CWUser', 'Societe')
--- a/server/test/data/schema.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/data/schema.py	Fri Sep 18 11:24:37 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_extlite.py	Thu Sep 17 19:38:04 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-import threading, os, time
-
-from logilab.common.testlib import TestCase, unittest_main
-from logilab.common.db import get_connection
-
-class SQLiteTC(TestCase):
-    sqlite_file = '_extlite_test.sqlite'
-
-    def _cleanup(self):
-        try:
-            os.remove(self.sqlite_file)
-        except:
-            pass
-
-    def setUp(self):
-        self._cleanup()
-        cnx1 = get_connection('sqlite', database=self.sqlite_file)
-        cu = cnx1.cursor()
-        cu.execute('CREATE TABLE toto(name integer);')
-        cnx1.commit()
-        cnx1.close()
-
-    def tearDown(self):
-        self._cleanup()
-
-    def test(self):
-        lock = threading.Lock()
-
-        def run_thread():
-            cnx2 = get_connection('sqlite', database=self.sqlite_file)
-            lock.acquire()
-            cu = cnx2.cursor()
-            cu.execute('SELECT name FROM toto')
-            self.failIf(cu.fetchall())
-            cnx2.commit()
-            lock.release()
-            time.sleep(0.1)
-            lock.acquire()
-            cu.execute('SELECT name FROM toto')
-            self.failUnless(cu.fetchall())
-            lock.release()
-
-        cnx1 = get_connection('sqlite', database=self.sqlite_file)
-        lock.acquire()
-        thread = threading.Thread(target=run_thread)
-        thread.start()
-        cu = cnx1.cursor()
-        cu.execute('SELECT name FROM toto')
-        lock.release()
-        time.sleep(0.1)
-        cnx1.commit()
-        lock.acquire()
-        cu.execute("INSERT INTO toto(name) VALUES ('toto')")
-        cnx1.commit()
-        lock.release()
-
-if __name__ == '__main__':
-    unittest_main()
--- a/server/test/unittest_hookhelper.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_hookhelper.py	Fri Sep 18 11:24:37 2009 +0200
@@ -49,41 +49,5 @@
         op5 = hooks.CheckORelationOp(session)
         self.assertEquals(session.pending_operations, [op1, op2, op4, op5, op3])
 
-
-    def test_in_state_notification(self):
-        result = []
-        # test both email notification and transition_information
-        # whatever if we can connect to the default stmp server, transaction
-        # should not fail
-        def in_state_changed(session, eidfrom, rtype, eidto):
-            tr = previous_state(session, eidfrom)
-            if tr is None:
-                result.append(tr)
-                return
-            content = u'trÀnsition from %s to %s' % (tr.name, entity_name(session, eidto))
-            result.append(content)
-            SendMailOp(session, msg=content, recipients=['test@logilab.fr'])
-        self.hm.register_hook(in_state_changed,
-                             'before_add_relation', 'in_state')
-        self.execute('INSERT CWUser X: X login "paf", X upassword "wouf", X in_state S, X in_group G WHERE S name "activated", G name "users"')
-        self.assertEquals(result, [None])
-        searchedops = [op for op in self.session.pending_operations
-                       if isinstance(op, SendMailOp)]
-        self.assertEquals(len(searchedops), 0,
-                          self.session.pending_operations)
-        self.commit()
-        self.execute('SET X in_state S WHERE X login "paf", S name "deactivated"')
-        self.assertEquals(result, [None, u'trÀnsition from activated to deactivated'])
-        # one to send the mail, one to close the smtp connection
-        searchedops = [op for op in self.session.pending_operations
-                       if isinstance(op, SendMailOp)]
-        self.assertEquals(len(searchedops), 1,
-                          self.session.pending_operations)
-        self.commit()
-        searchedops = [op for op in self.session.pending_operations
-                       if isinstance(op, SendMailOp)]
-        self.assertEquals(len(searchedops), 0,
-                          self.session.pending_operations)
-
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_hooks.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_hooks.py	Fri Sep 18 11:24:37 2009 +0200
@@ -37,8 +37,8 @@
                           'DELETE CWGroup X WHERE X name "owners"')
 
     def test_delete_required_relations_subject(self):
-        self.execute('INSERT CWUser X: X login "toto", X upassword "hop", X in_group Y, X in_state S '
-                     'WHERE Y name "users", S name "activated"')
+        self.execute('INSERT CWUser X: X login "toto", X upassword "hop", X in_group Y '
+                     'WHERE Y name "users"')
         self.commit()
         self.execute('DELETE X in_group Y WHERE X login "toto", Y name "users"')
         self.assertRaises(ValidationError, self.commit)
@@ -60,18 +60,6 @@
         self.assertRaises(ValidationError,
                           self.commit)
 
-    def test_delete_if_singlecard1(self):
-        self.assertEquals(self.repo.schema['in_state'].inlined, False)
-        ueid = self.create_user('toto')
-        self.commit()
-        self.execute('SET X in_state S WHERE S name "deactivated", X eid %(x)s', {'x': ueid})
-        rset = self.execute('Any S WHERE X in_state S, X eid %(x)s', {'x': ueid})
-        self.assertEquals(len(rset), 1)
-        self.commit()
-        self.assertRaises(Exception, self.execute, 'SET X in_state S WHERE S name "deactivated", X eid %s' % ueid)
-        rset2 = self.execute('Any S WHERE X in_state S, X eid %(x)s', {'x': ueid})
-        self.assertEquals(rset.rows, rset2.rows)
-
     def test_inlined(self):
         self.assertEquals(self.repo.schema['sender'].inlined, True)
         self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"')
@@ -155,6 +143,40 @@
         self.assertEquals(entity.descr, u'R&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(RepositoryBasedTC):
 
@@ -480,177 +502,5 @@
                      'RT name "prenom", E name "Personne"')
         self.commit()
 
-
-class WorkflowHooksTC(RepositoryBasedTC):
-
-    def setUp(self):
-        RepositoryBasedTC.setUp(self)
-        self.s_activated = self.execute('State X WHERE X name "activated"')[0][0]
-        self.s_deactivated = self.execute('State X WHERE X name "deactivated"')[0][0]
-        self.s_dummy = self.execute('INSERT State X: X name "dummy", X state_of E WHERE E name "CWUser"')[0][0]
-        self.create_user('stduser')
-        # give access to users group on the user's wf transitions
-        # so we can test wf enforcing on euser (managers don't have anymore this
-        # enforcement
-        self.execute('SET X require_group G WHERE G name "users", X transition_of ET, ET name "CWUser"')
-        self.commit()
-
-    def tearDown(self):
-        self.execute('DELETE X require_group G WHERE G name "users", X transition_of ET, ET name "CWUser"')
-        self.commit()
-        RepositoryBasedTC.tearDown(self)
-
-    def test_set_initial_state(self):
-        ueid = self.execute('INSERT CWUser E: E login "x", E upassword "x", E in_group G '
-                            'WHERE G name "users"')[0][0]
-        self.failIf(self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s',
-                                 {'x' : ueid}))
-        self.commit()
-        initialstate = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s',
-                                    {'x' : ueid})[0][0]
-        self.assertEquals(initialstate, u'activated')
-
-    def test_initial_state(self):
-        cnx = self.login('stduser')
-        cu = cnx.cursor()
-        self.assertRaises(ValidationError, cu.execute,
-                          'INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, '
-                          'X in_state S WHERE S name "deactivated"', {'pwd': 'oops'})
-        cnx.close()
-        # though managers can do whatever he want
-        self.execute('INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, '
-                     'X in_state S, X in_group G WHERE S name "deactivated", G name "users"', {'pwd': 'oops'})
-        self.commit()
-
-    # test that the workflow is correctly enforced
-    def test_transition_checking1(self):
-        cnx = self.login('stduser')
-        cu = cnx.cursor()
-        ueid = cnx.user(self.current_session()).eid
-        self.assertRaises(ValidationError,
-                          cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                          {'x': ueid, 's': self.s_activated}, 'x')
-        cnx.close()
-
-    def test_transition_checking2(self):
-        cnx = self.login('stduser')
-        cu = cnx.cursor()
-        ueid = cnx.user(self.current_session()).eid
-        self.assertRaises(ValidationError,
-                          cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                          {'x': ueid, 's': self.s_dummy}, 'x')
-        cnx.close()
-
-    def test_transition_checking3(self):
-        cnx = self.login('stduser')
-        cu = cnx.cursor()
-        ueid = cnx.user(self.current_session()).eid
-        cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                      {'x': ueid, 's': self.s_deactivated}, 'x')
-        cnx.commit()
-        self.assertRaises(ValidationError,
-                          cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                          {'x': ueid, 's': self.s_deactivated}, 'x')
-        # get back now
-        cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                      {'x': ueid, 's': self.s_activated}, 'x')
-        cnx.commit()
-        cnx.close()
-
-    def test_transition_checking4(self):
-        cnx = self.login('stduser')
-        cu = cnx.cursor()
-        ueid = cnx.user(self.current_session()).eid
-        cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                   {'x': ueid, 's': self.s_deactivated}, 'x')
-        cnx.commit()
-        self.assertRaises(ValidationError,
-                          cu.execute, 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                          {'x': ueid, 's': self.s_dummy}, 'x')
-        # get back now
-        cu.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                      {'x': ueid, 's': self.s_activated}, 'x')
-        cnx.commit()
-        cnx.close()
-
-    def test_transition_information(self):
-        ueid = self.session.user.eid
-        self.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                      {'x': ueid, 's': self.s_deactivated}, 'x')
-        self.commit()
-        rset = self.execute('TrInfo T ORDERBY T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid})
-        self.assertEquals(len(rset), 2)
-        tr = rset.get_entity(1, 0)
-        #tr.complete()
-        self.assertEquals(tr.comment, None)
-        self.assertEquals(tr.from_state[0].eid, self.s_activated)
-        self.assertEquals(tr.to_state[0].eid, self.s_deactivated)
-
-        self.session.set_shared_data('trcomment', u'il est pas sage celui-la')
-        self.session.set_shared_data('trcommentformat', u'text/plain')
-        self.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                     {'x': ueid, 's': self.s_activated}, 'x')
-        self.commit()
-        rset = self.execute('TrInfo T ORDERBY T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid})
-        self.assertEquals(len(rset), 3)
-        tr = rset.get_entity(2, 0)
-        #tr.complete()
-        self.assertEquals(tr.comment, u'il est pas sage celui-la')
-        self.assertEquals(tr.comment_format, u'text/plain')
-        self.assertEquals(tr.from_state[0].eid, self.s_deactivated)
-        self.assertEquals(tr.to_state[0].eid, self.s_activated)
-        self.assertEquals(tr.owned_by[0].login, 'admin')
-
-    def test_transition_information_on_creation(self):
-        ueid = self.create_user('toto')
-        rset = self.execute('TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid})
-        self.assertEquals(len(rset), 1)
-        tr = rset.get_entity(0, 0)
-        #tr.complete()
-        self.assertEquals(tr.comment, None)
-        self.assertEquals(tr.from_state, [])
-        self.assertEquals(tr.to_state[0].eid, self.s_activated)
-
-    def test_std_users_can_create_trinfo(self):
-        self.create_user('toto')
-        cnx = self.login('toto')
-        cu = cnx.cursor()
-        self.failUnless(cu.execute("INSERT Note X: X type 'a', X in_state S WHERE S name 'todo'"))
-        cnx.commit()
-
-    def test_metadata_cwuri(self):
-        eid = self.execute('INSERT Note X')[0][0]
-        cwuri = self.execute('Any U WHERE X eid %s, X cwuri U' % eid)[0][0]
-        self.assertEquals(cwuri, self.repo.config['base-url'] + 'eid/%s' % eid)
-
-    def test_metadata_creation_modification_date(self):
-        _now = datetime.now()
-        eid = self.execute('INSERT Note X')[0][0]
-        creation_date, modification_date = self.execute('Any CD, MD WHERE X eid %s, '
-                                                        'X creation_date CD, '
-                                                        'X modification_date MD' % eid)[0]
-        self.assertEquals((creation_date - _now).seconds, 0)
-        self.assertEquals((modification_date - _now).seconds, 0)
-
-    def test_metadata__date(self):
-        _now = datetime.now()
-        eid = self.execute('INSERT Note X')[0][0]
-        creation_date = self.execute('Any D WHERE X eid %s, X creation_date D' % eid)[0][0]
-        self.assertEquals((creation_date - _now).seconds, 0)
-
-    def test_metadata_created_by(self):
-        eid = self.execute('INSERT Note X')[0][0]
-        self.commit() # fire operations
-        rset = self.execute('Any U WHERE X eid %s, X created_by U' % eid)
-        self.assertEquals(len(rset), 1) # make sure we have only one creator
-        self.assertEquals(rset[0][0], self.session.user.eid)
-
-    def test_metadata_owned_by(self):
-        eid = self.execute('INSERT Note X')[0][0]
-        self.commit() # fire operations
-        rset = self.execute('Any U WHERE X eid %s, X owned_by U' % eid)
-        self.assertEquals(len(rset), 1) # make sure we have only one owner
-        self.assertEquals(rset[0][0], self.session.user.eid)
-
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_ldapuser.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_ldapuser.py	Fri Sep 18 11:24:37 2009 +0200
@@ -156,7 +156,8 @@
         self.patch_authenticate()
         cnx = self.login('syt', 'dummypassword')
         cu = cnx.cursor()
-        cu.execute('SET X in_state S WHERE X login "alf", S name "deactivated"')
+        alf = cu.execute('Any X WHERE X login "alf"').get_entity(0, 0)
+        alf.fire_transition('deactivate')
         try:
             cnx.commit()
             alf = self.execute('CWUser X WHERE X login "alf"').get_entity(0, 0)
@@ -172,7 +173,8 @@
         finally:
             # restore db state
             self.restore_connection()
-            self.execute('SET X in_state S WHERE X login "alf", S name "activated"')
+            alf = self.execute('Any X WHERE X login "alf"').get_entity(0, 0)
+            alf.fire_transition('activate')
             self.execute('DELETE X in_group G WHERE X login "syt", G name "managers"')
 
     def test_same_column_names(self):
--- a/server/test/unittest_migractions.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_migractions.py	Fri Sep 18 11:24:37 2009 +0200
@@ -45,19 +45,27 @@
         assert self.cnx is self.mh._cnx
         assert self.session is self.mh.session, (self.session.id, self.mh.session.id)
 
+
     def test_add_attribute_int(self):
         self.failIf('whatever' in self.schema)
-        paraordernum = self.mh.rqlexec('Any O WHERE X name "Note", RT name "para", RDEF from_entity X, RDEF relation_type RT, RDEF ordernum O')[0][0]
+        orderdict = dict(self.mh.rqlexec('Any RTN, O WHERE X name "Note", RDEF from_entity X, '
+                                         'RDEF relation_type RT, RDEF ordernum O, RT name RTN'))
         self.mh.cmd_add_attribute('Note', 'whatever')
         self.failUnless('whatever' in self.schema)
         self.assertEquals(self.schema['whatever'].subjects(), ('Note',))
         self.assertEquals(self.schema['whatever'].objects(), ('Int',))
-        paraordernum2 = self.mh.rqlexec('Any O WHERE X name "Note", RT name "para", RDEF from_entity X, RDEF relation_type RT, RDEF ordernum O')[0][0]
-        self.assertEquals(paraordernum2, paraordernum+1)
+        orderdict2 = dict(self.mh.rqlexec('Any RTN, O WHERE X name "Note", RDEF from_entity X, '
+                                          'RDEF relation_type RT, RDEF ordernum O, RT name RTN'))
+        whateverorder = migrschema['whatever'].rproperty('Note', 'Int', 'order')
+        for k, v in orderdict.iteritems():
+            if v >= whateverorder:
+                orderdict[k] = v+1
+        orderdict['whatever'] = whateverorder
+        self.assertDictEquals(orderdict, orderdict2)
         #self.assertEquals([r.type for r in self.schema['Note'].ordered_relations()],
         #                  ['modification_date', 'creation_date', 'owned_by',
         #                   'eid', 'ecrit_par', 'inline1', 'date', 'type',
-        #                   'whatever', 'para', 'in_basket'])
+        #                   'whatever', 'date', 'in_basket'])
         # NB: commit instead of rollback make following test fail with py2.5
         #     this sounds like a pysqlite/2.5 bug (the same eid is affected to
         #     two different entities)
@@ -106,23 +114,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)
@@ -132,6 +131,7 @@
         self.failUnless(self.execute('CWEType X WHERE X name "Folder2"'))
         self.failUnless('filed_under2' in self.schema)
         self.failUnless(self.execute('CWRType X WHERE X name "filed_under2"'))
+        self.schema.rebuild_infered_relations()
         self.assertEquals(sorted(str(rs) for rs in self.schema['Folder2'].subject_relations()),
                           ['created_by', 'creation_date', 'cwuri',
                            'description', 'description_format',
@@ -150,22 +150,25 @@
 
     def test_add_drop_entity_type(self):
         self.mh.cmd_add_entity_type('Folder2')
-        todoeid = self.mh.cmd_add_state(u'todo', 'Folder2', initial=True)
-        doneeid = self.mh.cmd_add_state(u'done', 'Folder2')
-        self.mh.cmd_add_transition(u'redoit', 'Folder2', (doneeid,), todoeid)
-        self.mh.cmd_add_transition(u'markasdone', 'Folder2', (todoeid,), doneeid)
+        wf = self.mh.cmd_add_workflow(u'folder2 wf', 'Folder2')
+        todo = wf.add_state(u'todo', initial=True)
+        done = wf.add_state(u'done')
+        wf.add_transition(u'redoit', done, todo)
+        wf.add_transition(u'markasdone', todo, done)
         self.commit()
         eschema = self.schema.eschema('Folder2')
         self.mh.cmd_drop_entity_type('Folder2')
         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)
         self.mh.cmd_add_relation_type('filed_under2')
+        self.schema.rebuild_infered_relations()
         self.failUnless('filed_under2' in self.schema)
         self.assertEquals(sorted(str(e) for e in self.schema['filed_under2'].subjects()),
                           sorted(str(e) for e in self.schema.entities() if not e.is_final()))
@@ -183,8 +186,8 @@
                           '1*')
         self.mh.cmd_add_relation_definition('Personne', 'concerne2', 'Note')
         self.assertEquals(sorted(self.schema['concerne2'].objects()), ['Affaire', 'Note'])
-        self.mh.add_entity('Personne', nom=u'tot')
-        self.mh.add_entity('Affaire')
+        self.mh.create_entity('Personne', nom=u'tot')
+        self.mh.create_entity('Affaire')
         self.mh.rqlexec('SET X concerne2 Y WHERE X is Personne, Y is Affaire')
         self.commit()
         self.mh.cmd_drop_relation_definition('Personne', 'concerne2', 'Affaire')
@@ -219,11 +222,17 @@
         self.assertEquals(sorted(str(e) for e in self.schema['concerne'].subjects()),
                           ['Affaire', 'Personne'])
         self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
+                          ['Affaire', 'Division', 'Note', 'SubDivision'])
+        self.schema.rebuild_infered_relations() # need to be explicitly called once everything is in place
+        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
                           ['Affaire', 'Note'])
         self.mh.cmd_add_relation_definition('Affaire', 'concerne', 'Societe')
         self.assertEquals(sorted(str(e) for e in self.schema['concerne'].subjects()),
                           ['Affaire', 'Personne'])
         self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
+                          ['Affaire', 'Note', 'Societe'])
+        self.schema.rebuild_infered_relations() # need to be explicitly called once everything is in place
+        self.assertEquals(sorted(str(e) for e in self.schema['concerne'].objects()),
                           ['Affaire', 'Division', 'Note', 'Societe', 'SubDivision'])
         # trick: overwrite self.maxeid to avoid deletion of just reintroduced types
         self.maxeid = self.execute('Any MAX(X)')[0][0]
@@ -451,12 +460,6 @@
         ex = self.assertRaises(ConfigurationError, self.mh.cmd_remove_cube, 'file')
         self.assertEquals(str(ex), "can't remove cube file, used as a dependency")
 
-    def test_set_state(self):
-        user = self.session.user
-        self.mh.set_state(user.eid, 'deactivated')
-        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()
--- a/server/test/unittest_msplanner.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_msplanner.py	Fri Sep 18 11:24:37 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
@@ -347,7 +348,7 @@
 
     def setUp(self):
         BaseMSPlannerTC.setUp(self)
-        self.planner = MSPlanner(self.o.schema, self.o._rqlhelper)
+        self.planner = MSPlanner(self.o.schema, self.repo.vreg.rqlhelper)
 
     _test = test_plan
 
@@ -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',
@@ -1962,13 +1961,23 @@
                      None, None, [self.system], {}, [])],
                    {'x': 999998, 'u': 999999})
 
-    def test_nonregr_identity_no_source_access(self):
+    def test_nonregr_identity_no_source_access_1(self):
         repo._type_source_cache[999999] = ('CWUser', 'ldap', 999998)
         self._test('Any S WHERE S identity U, S eid %(s)s, U eid %(u)s',
                    [('OneFetchStep', [('Any 999999 WHERE 999999 identity 999999', [{}])],
                      None, None, [self.system], {}, [])],
                    {'s': 999999, 'u': 999999})
 
+    def test_nonregr_identity_no_source_access_2(self):
+        repo._type_source_cache[999999] = ('EmailAddress', 'system', 999999)
+        repo._type_source_cache[999998] = ('CWUser', 'ldap', 999998)
+        self._test('Any X WHERE O use_email X, ((EXISTS(O identity U)) OR (EXISTS(O in_group G, G name IN("managers", "staff")))) OR (EXISTS(O in_group G2, U in_group G2, NOT G2 name "users")), X eid %(x)s, U eid %(u)s',
+                   [('OneFetchStep', [('Any 999999 WHERE O use_email 999999, ((EXISTS(O identity 999998)) OR (EXISTS(O in_group G, G name IN("managers", "staff")))) OR (EXISTS(O in_group G2, 999998 in_group G2, NOT G2 name "users"))',
+                                       [{'G': 'CWGroup', 'G2': 'CWGroup', 'O': 'CWUser'}])],
+                     None, None, [self.system], {}, [])],
+                   {'x': 999999, 'u': 999998})
+
+
 class MSPlannerTwoSameExternalSourcesTC(BasePlannerTC):
     """test planner related feature on a 3-sources repository:
 
@@ -1980,7 +1989,7 @@
         self.setup()
         self.add_source(FakeCardSource, 'cards')
         self.add_source(FakeCardSource, 'cards2')
-        self.planner = MSPlanner(self.o.schema, self.o._rqlhelper)
+        self.planner = MSPlanner(self.o.schema, self.repo.vreg.rqlhelper)
         assert repo.sources_by_uri['cards2'].support_relation('multisource_crossed_rel')
         assert 'multisource_crossed_rel' in repo.sources_by_uri['cards2'].cross_relations
         assert repo.sources_by_uri['cards'].support_relation('multisource_crossed_rel')
@@ -2133,7 +2142,7 @@
     def setUp(self):
         self.setup()
         self.add_source(FakeVCSSource, 'vcs')
-        self.planner = MSPlanner(self.o.schema, self.o._rqlhelper)
+        self.planner = MSPlanner(self.o.schema, self.repo.vreg.rqlhelper)
     _test = test_plan
 
     def test_multisource_inlined_rel_skipped(self):
--- a/server/test/unittest_multisources.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_multisources.py	Fri Sep 18 11:24:37 2009 +0200
@@ -30,7 +30,7 @@
 cu = cnx2.cursor()
 ec1 = cu.execute('INSERT Card X: X title "C3: An external card", X wikiid "aaa"')[0][0]
 cu.execute('INSERT Card X: X title "C4: Ze external card", X wikiid "zzz"')
-aff1 = cu.execute('INSERT Affaire X: X ref "AFFREF", X in_state S WHERE S name "pitetre"')[0][0]
+aff1 = cu.execute('INSERT Affaire X: X ref "AFFREF"')[0][0]
 cnx2.commit()
 
 MTIME = datetime.now() - timedelta(0, 10)
@@ -122,7 +122,7 @@
         cu = cnx2.cursor()
         assert cu.execute('Any X WHERE X eid %(x)s', {'x': aff1}, 'x')
         cu.execute('SET X ref "BLAH" WHERE X eid %(x)s', {'x': aff1}, 'x')
-        aff2 = cu.execute('INSERT Affaire X: X ref "AFFREUX", X in_state S WHERE S name "pitetre"')[0][0]
+        aff2 = cu.execute('INSERT Affaire X: X ref "AFFREUX"')[0][0]
         cnx2.commit()
         try:
             # force sync
@@ -233,11 +233,12 @@
 
     def test_not_relation(self):
         states = set(tuple(x) for x in self.execute('Any S,SN WHERE S is State, S name SN'))
+        self.session.user.clear_all_caches()
         userstate = self.session.user.in_state[0]
         states.remove((userstate.eid, userstate.name))
         notstates = set(tuple(x) for x in self.execute('Any S,SN WHERE S is State, S name SN, NOT X in_state S, X eid %(x)s',
                                                        {'x': self.session.user.eid}, 'x'))
-        self.assertEquals(notstates, states)
+        self.assertSetEquals(notstates, states)
         aff1 = self.execute('Any X WHERE X is Affaire, X ref "AFFREF"')[0][0]
         aff1stateeid, aff1statename = self.execute('Any S,SN WHERE X eid %(x)s, X in_state S, S name SN', {'x': aff1}, 'x')[0]
         self.assertEquals(aff1statename, 'pitetre')
@@ -267,6 +268,7 @@
                      {'x': affaire.eid, 'u': ueid})
 
     def test_nonregr2(self):
+        self.session.user.fire_transition('deactivate')
         treid = self.session.user.latest_trinfo().eid
         rset = self.execute('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D',
                             {'x': treid})
--- a/server/test/unittest_querier.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_querier.py	Fri Sep 18 11:24:37 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), '
@@ -290,8 +293,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"')
@@ -355,27 +358,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]
@@ -471,12 +458,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
@@ -726,9 +718,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'])
@@ -1000,20 +992,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)
 
@@ -1033,9 +1023,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]
@@ -1135,7 +1122,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)
 
@@ -1265,9 +1252,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	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_repository.py	Fri Sep 18 11:24:37 2009 +0200
@@ -102,7 +102,7 @@
     def test_login_upassword_accent(self):
         repo = self.repo
         cnxid = repo.connect(*self.default_user_password())
-        repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_state S, X in_group G WHERE S name "activated", G name "users"',
+        repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_group G WHERE G name "users"',
                      {'login': u"barnabé", 'passwd': u"héhéhé".encode('UTF8')})
         repo.commit(cnxid)
         repo.close(cnxid)
@@ -112,7 +112,7 @@
         repo = self.repo
         cnxid = repo.connect(*self.default_user_password())
         # no group
-        repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_state S WHERE S name "activated"',
+        repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s',
                      {'login': u"tutetute", 'passwd': 'tutetute'})
         self.assertRaises(ValidationError, repo.commit, cnxid)
         rset = repo.execute(cnxid, 'CWUser X WHERE X login "tutetute"')
@@ -190,16 +190,13 @@
         repo = self.repo
         cnxid = repo.connect(*self.default_user_password())
         # rollback state change which trigger TrInfo insertion
-        ueid = repo._get_session(cnxid).user.eid
-        rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid})
+        user = repo._get_session(cnxid).user
+        user.fire_transition('deactivate')
+        rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid})
         self.assertEquals(len(rset), 1)
-        repo.execute(cnxid, 'SET X in_state S WHERE X eid %(x)s, S name "deactivated"',
-                     {'x': ueid}, 'x')
-        rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid})
-        self.assertEquals(len(rset), 2)
         repo.rollback(cnxid)
-        rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid})
-        self.assertEquals(len(rset), 1)
+        rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid})
+        self.assertEquals(len(rset), 0)
 
     def test_transaction_interleaved(self):
         self.skip('implement me')
@@ -342,6 +339,22 @@
 #             self.set_debug(False)
 #         print 'test time: %.3f (time) %.3f (cpu)' % ((time() - t), clock() - c)
 
+    def test_delete_if_singlecard1(self):
+        note = self.add_entity('Affaire')
+        p1 = self.add_entity('Personne', nom=u'toto')
+        self.execute('SET A todo_by P WHERE A eid %(x)s, P eid %(p)s',
+                     {'x': note.eid, 'p': p1.eid})
+        rset = self.execute('Any P WHERE A todo_by P, A eid %(x)s',
+                            {'x': note.eid})
+        self.assertEquals(len(rset), 1)
+        p2 = self.add_entity('Personne', nom=u'tutu')
+        self.execute('SET A todo_by P WHERE A eid %(x)s, P eid %(p)s',
+                     {'x': note.eid, 'p': p2.eid})
+        rset = self.execute('Any P WHERE A todo_by P, A eid %(x)s',
+                            {'x': note.eid})
+        self.assertEquals(len(rset), 1)
+        self.assertEquals(rset.rows[0][0], p2.eid)
+
 
 class DataHelpersTC(RepositoryBasedTC):
 
@@ -485,11 +498,11 @@
 
     def test_after_add_inline(self):
         """make sure after_<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	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_rql2sql.py	Fri Sep 18 11:24:37 2009 +0200
@@ -163,7 +163,6 @@
 ]
 
 ADVANCED= [
-
     ("Societe S WHERE S nom 'Logilab' OR S nom 'Caesium'",
      '''SELECT S.cw_eid
 FROM cw_Societe AS S
@@ -339,6 +338,9 @@
 
     ('Any XN ORDERBY XN WHERE X name XN',
      '''SELECT X.cw_name
+FROM cw_BaseTransition AS X
+UNION ALL
+SELECT X.cw_name
 FROM cw_Basket AS X
 UNION ALL
 SELECT X.cw_name
@@ -376,6 +378,12 @@
 UNION ALL
 SELECT X.cw_name
 FROM cw_Transition AS X
+UNION ALL
+SELECT X.cw_name
+FROM cw_Workflow AS X
+UNION ALL
+SELECT X.cw_name
+FROM cw_WorkflowTransition AS X
 ORDER BY 1'''),
 
     # DISTINCT, can use relation under exists scope as principal
@@ -462,6 +470,9 @@
 
     ('Any MAX(X)+MIN(X), N GROUPBY N WHERE X name N;',
      '''SELECT (MAX(T1.C0) + MIN(T1.C0)), T1.C1 FROM (SELECT X.cw_eid AS C0, X.cw_name AS C1
+FROM cw_BaseTransition AS X
+UNION ALL
+SELECT X.cw_eid AS C0, X.cw_name AS C1
 FROM cw_Basket AS X
 UNION ALL
 SELECT X.cw_eid AS C0, X.cw_name AS C1
@@ -498,7 +509,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;',
@@ -624,6 +641,13 @@
 WHERE X.cw_in_state=S.cw_eid
 ORDER BY 2) AS T1'''),
 
+    ('Any O,AA,AB,AC ORDERBY AC DESC '
+     'WHERE NOT S use_email O, S eid 1, O is EmailAddress, O address AA, O alias AB, O modification_date AC, '
+     'EXISTS(A use_email O, EXISTS(A identity B, NOT B in_group D, D name "guests", D is CWGroup), A is CWUser), B eid 2',
+     '''SELECT O.cw_eid, O.cw_address, O.cw_alias, O.cw_modification_date
+FROM cw_EmailAddress AS O
+WHERE NOT EXISTS(SELECT 1 FROM use_email_relation AS rel_use_email0 WHERE rel_use_email0.eid_from=1 AND rel_use_email0.eid_to=O.cw_eid) AND EXISTS(SELECT 1 FROM use_email_relation AS rel_use_email1 WHERE rel_use_email1.eid_to=O.cw_eid AND EXISTS(SELECT 1 FROM cw_CWGroup AS D WHERE rel_use_email1.eid_from=2 AND NOT EXISTS(SELECT 1 FROM in_group_relation AS rel_in_group2 WHERE rel_in_group2.eid_from=2 AND rel_in_group2.eid_to=D.cw_eid) AND D.cw_name=guests))
+ORDER BY 4 DESC'''),
     ]
 
 MULTIPLE_SEL = [
@@ -1029,8 +1053,9 @@
 
     ('Any S,ES,T WHERE S state_of ET, ET name "CWUser", ES allowed_transition T, T destination_state S',
      '''SELECT T.cw_destination_state, rel_allowed_transition1.eid_from, T.cw_eid
-FROM allowed_transition_relation AS rel_allowed_transition1, cw_CWEType AS ET, cw_Transition AS T, state_of_relation AS rel_state_of0
+FROM allowed_transition_relation AS rel_allowed_transition1, cw_Transition AS T, cw_Workflow AS ET, state_of_relation AS rel_state_of0
 WHERE T.cw_destination_state=rel_state_of0.eid_from AND rel_state_of0.eid_to=ET.cw_eid AND ET.cw_name=CWUser AND rel_allowed_transition1.eid_to=T.cw_eid'''),
+
     ('Any O WHERE S eid 0, S in_state O',
      '''SELECT S.cw_in_state
 FROM cw_Affaire AS S
@@ -1106,11 +1131,11 @@
         delete = self.rqlhelper.parse(
             'DELETE X read_permission READ_PERMISSIONSUBJECT,X add_permission ADD_PERMISSIONSUBJECT,'
             'X in_basket IN_BASKETSUBJECT,X delete_permission DELETE_PERMISSIONSUBJECT,'
-            'X initial_state INITIAL_STATESUBJECT,X update_permission UPDATE_PERMISSIONSUBJECT,'
+            'X update_permission UPDATE_PERMISSIONSUBJECT,'
             'X created_by CREATED_BYSUBJECT,X is ISSUBJECT,X is_instance_of IS_INSTANCE_OFSUBJECT,'
             'X owned_by OWNED_BYSUBJECT,X specializes SPECIALIZESSUBJECT,ISOBJECT is X,'
-            'SPECIALIZESOBJECT specializes X,STATE_OFOBJECT state_of X,IS_INSTANCE_OFOBJECT is_instance_of X,'
-            'TO_ENTITYOBJECT to_entity X,TRANSITION_OFOBJECT transition_of X,FROM_ENTITYOBJECT from_entity X '
+            'SPECIALIZESOBJECT specializes X,IS_INSTANCE_OFOBJECT is_instance_of X,'
+            'TO_ENTITYOBJECT to_entity X,FROM_ENTITYOBJECT from_entity X '
             'WHERE X is CWEType')
         self.rqlhelper.compute_solutions(delete)
         def var_sols(var):
@@ -1379,7 +1404,7 @@
 FROM appears AS appears0, entities AS X
 WHERE appears0.words @@ to_tsquery('default', 'hip&hop&momo') AND appears0.uid=X.eid AND X.type='Personne'"""),
 
-            ('Any X WHERE X has_text "toto tata", X name "tutu"',
+            ('Any X WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,File,Folder)',
              """SELECT X.cw_eid
 FROM appears AS appears0, cw_Basket AS X
 WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu
@@ -1391,22 +1416,7 @@
 SELECT X.cw_eid
 FROM appears AS appears0, cw_Folder AS X
 WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu
-UNION ALL
-SELECT X.cw_eid
-FROM appears AS appears0, cw_Image AS X
-WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu
-UNION ALL
-SELECT X.cw_eid
-FROM appears AS appears0, cw_State AS X
-WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu
-UNION ALL
-SELECT X.cw_eid
-FROM appears AS appears0, cw_Tag AS X
-WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu
-UNION ALL
-SELECT X.cw_eid
-FROM appears AS appears0, cw_Transition AS X
-WHERE appears0.words @@ to_tsquery('default', 'toto&tata') AND appears0.uid=X.cw_eid AND X.cw_name=tutu"""),
+"""),
 
             ('Personne X where X has_text %(text)s, X travaille S, S has_text %(text)s',
              """SELECT X.eid
@@ -1543,7 +1553,7 @@
 FROM appears AS appears0, entities AS X
 WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.eid AND X.type='Personne'"""),
 
-            ('Any X WHERE X has_text "toto tata", X name "tutu"',
+            ('Any X WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,File,Folder)',
              """SELECT X.cw_eid
 FROM appears AS appears0, cw_Basket AS X
 WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu
@@ -1555,22 +1565,7 @@
 SELECT X.cw_eid
 FROM appears AS appears0, cw_Folder AS X
 WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu
-UNION ALL
-SELECT X.cw_eid
-FROM appears AS appears0, cw_Image AS X
-WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu
-UNION ALL
-SELECT X.cw_eid
-FROM appears AS appears0, cw_State AS X
-WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu
-UNION ALL
-SELECT X.cw_eid
-FROM appears AS appears0, cw_Tag AS X
-WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu
-UNION ALL
-SELECT X.cw_eid
-FROM appears AS appears0, cw_Transition AS X
-WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata')) AND appears0.uid=X.cw_eid AND X.cw_name=tutu"""),
+"""),
             )):
             yield t
 
@@ -1619,7 +1614,7 @@
              """SELECT X.eid
 FROM appears AS appears0, entities AS X
 WHERE MATCH (appears0.words) AGAINST ('hip hop momo' IN BOOLEAN MODE) AND appears0.uid=X.eid AND X.type='Personne'"""),
-            ('Any X WHERE X has_text "toto tata", X name "tutu"',
+            ('Any X WHERE X has_text "toto tata", X name "tutu", X is IN (Basket,File,Folder)',
              """SELECT X.cw_eid
 FROM appears AS appears0, cw_Basket AS X
 WHERE MATCH (appears0.words) AGAINST ('toto tata' IN BOOLEAN MODE) AND appears0.uid=X.cw_eid AND X.cw_name=tutu
@@ -1631,22 +1626,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_rqlannotation.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_rqlannotation.py	Fri Sep 18 11:24:37 2009 +0200
@@ -95,6 +95,11 @@
         self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
         self.assertEquals(rqlst.defined_vars['Y']._q_invariant, False)
 
+    def test_diff_scope_identity_deamb(self):
+        rqlst = self._prepare('Any X WHERE X concerne Y, Y is Note, EXISTS(Y identity Z, Z migrated_from N)')
+        self.assertEquals(rqlst.defined_vars['Z']._q_invariant, True)
+        self.assertEquals(rqlst.defined_vars['Y']._q_invariant, True)
+
     def test_optional_inlined(self):
         rqlst = self._prepare('Any X,S where X from_state S?')
         self.assertEquals(rqlst.defined_vars['X']._q_invariant, False)
--- a/server/test/unittest_rqlrewrite.py	Thu Sep 17 19:38:04 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,194 +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, TestCase
-from logilab.common.testlib import mock_object
-
-from rql import parse, nodes, RQLHelper
-
-from cubicweb import Unauthorized
-from cubicweb.server.rqlrewrite import RQLRewriter
-from cubicweb.devtools import repotest, TestServerConfiguration
-
-config = TestServerConfiguration('data')
-config.bootstrap_cubes()
-schema = config.load_schema()
-schema.add_relation_def(mock_object(subject='Card', name='in_state', object='State', cardinality='1*'))
-
-rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid',
-                                                 'has_text': 'fti'})
-
-def setup_module(*args):
-    repotest.do_monkey_patch()
-
-def teardown_module(*args):
-    repotest.undo_monkey_patch()
-
-def eid_func_map(eid):
-    return {1: 'CWUser',
-            2: 'Card'}[eid]
-
-def rewrite(rqlst, snippets_map, kwargs):
-    class FakeQuerier:
-        schema = schema
-        @staticmethod
-        def solutions(sqlcursor, mainrqlst, kwargs):
-            rqlhelper.compute_solutions(rqlst, {'eid': eid_func_map}, kwargs=kwargs)
-        class _rqlhelper:
-            @staticmethod
-            def annotate(rqlst):
-                rqlhelper.annotate(rqlst)
-            @staticmethod
-            def simplify(mainrqlst, needcopy=False):
-                rqlhelper.simplify(rqlst, needcopy)
-    rewriter = RQLRewriter(FakeQuerier, mock_object(user=(mock_object(eid=1))))
-    for v, snippets in snippets_map.items():
-        snippets_map[v] = [mock_object(snippet_rqlst=parse('Any X WHERE '+snippet).children[0],
-                                       expression='Any X WHERE '+snippet)
-                           for snippet in snippets]
-    rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map}, kwargs=kwargs)
-    solutions = rqlst.children[0].solutions
-    rewriter.rewrite(rqlst.children[0], snippets_map.items(), solutions, kwargs)
-    test_vrefs(rqlst.children[0])
-    return rewriter.rewritten
-
-def test_vrefs(node):
-    vrefmap = {}
-    for vref in node.iget_nodes(nodes.VariableRef):
-        vrefmap.setdefault(vref.name, set()).add(vref)
-    for var in node.defined_vars.itervalues():
-        assert not (var.stinfo['references'] ^ vrefmap[var.name])
-        assert (var.stinfo['references'])
-
-class RQLRewriteTC(TestCase):
-    """a faire:
-
-    * optimisation: detecter les relations utilisees dans les rqlexpressions qui
-      sont presentes dans la requete de depart pour les reutiliser si possible
-
-    * "has_<ACTION>_permission" ?
-    """
-
-    def test_base_var(self):
-        card_constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
-        rqlst = parse('Card C')
-        rewrite(rqlst, {'C': (card_constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any C WHERE C is Card, B eid %(D)s, "
-                             "EXISTS(C in_state A, B in_group E, F require_state A, "
-                             "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission)")
-
-    def test_multiple_var(self):
-        card_constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
-        affaire_constraints = ('X ref LIKE "PUBLIC%"', 'U in_group G, G name "public"')
-        kwargs = {'u':2}
-        rqlst = parse('Any S WHERE S documented_by C, C eid %(u)s')
-        rewrite(rqlst, {'C': (card_constraint,), 'S': affaire_constraints},
-                kwargs)
-        self.assertTextEquals(rqlst.as_string(),
-                             "Any S WHERE S documented_by C, C eid %(u)s, B eid %(D)s, "
-                             "EXISTS(C in_state A, B in_group E, F require_state A, "
-                             "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission), "
-                             "(EXISTS(S ref LIKE 'PUBLIC%')) OR (EXISTS(B in_group G, G name 'public', G is CWGroup)), "
-                             "S is Affaire")
-        self.failUnless('D' in kwargs)
-
-    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')
-        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, "
-                             "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)")
-
-    def test_simplified_rqlst(self):
-        card_constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
-        rqlst = parse('Any 2') # this is the simplified rql st for Any X WHERE X eid 12
-        rewrite(rqlst, {'2': (card_constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any 2 WHERE B eid %(C)s, "
-                             "EXISTS(2 in_state A, B in_group D, E require_state A, "
-                             "E name 'read', E require_group D, A is State, D is CWGroup, E is CWPermission)")
-
-    def test_optional_var(self):
-        card_constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
-        rqlst = parse('Any A,C WHERE A documented_by C?')
-        rewrite(rqlst, {'C': (card_constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             "Any A,C WHERE A documented_by C?, A is Affaire "
-                             "WITH C BEING "
-                             "(Any C WHERE C in_state B, D in_group F, G require_state B, G name 'read', "
-                             "G require_group F, D eid %(A)s, C is Card)")
-        rqlst = parse('Any A,C,T WHERE A documented_by C?, C title T')
-        rewrite(rqlst, {'C': (card_constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             "Any A,C,T WHERE A documented_by C?, A is Affaire "
-                             "WITH C,T BEING "
-                             "(Any C,T WHERE C in_state B, D in_group F, G require_state B, G name 'read', "
-                             "G require_group F, C title T, D eid %(A)s, C is Card)")
-
-    def test_relation_optimization(self):
-        # since Card in_state State as monovalued cardinality, the in_state
-        # relation used in the rql expression can be ignored and S replaced by
-        # the variable from the incoming query
-        card_constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
-        rqlst = parse('Card C WHERE C in_state STATE')
-        rewrite(rqlst, {'C': (card_constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any C WHERE C in_state STATE, C is Card, A eid %(B)s, "
-                             "EXISTS(A in_group D, E require_state STATE, "
-                             "E name 'read', E require_group D, D is CWGroup, E is CWPermission), "
-                             "STATE is State")
-
-    def test_unsupported_constraint_1(self):
-        # CWUser doesn't have require_permission
-        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
-        rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U')
-        self.assertRaises(Unauthorized, rewrite, rqlst, {'T': (trinfo_constraint,)}, {})
-
-    def test_unsupported_constraint_2(self):
-        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
-        rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U')
-        rewrite(rqlst, {'T': (trinfo_constraint, 'X wf_info_for Y, Y in_group G, G name "managers"')}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any U,T WHERE U is CWUser, T wf_info_for U, "
-                             "EXISTS(U in_group B, B name 'managers', B is CWGroup), T is TrInfo")
-
-    def test_unsupported_constraint_3(self):
-        self.skip('raise unauthorized for now')
-        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
-        rqlst = parse('Any T WHERE T wf_info_for X')
-        rewrite(rqlst, {'T': (trinfo_constraint, 'X in_group G, G name "managers"')}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u'XXX dunno what should be generated')
-
-    def test_add_ambiguity_exists(self):
-        constraint = ('X concerne Y')
-        rqlst = parse('Affaire X')
-        rewrite(rqlst, {'X': (constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any X WHERE X is Affaire, (((EXISTS(X concerne A, A is Division)) OR (EXISTS(X concerne D, D is SubDivision))) OR (EXISTS(X concerne C, C is Societe))) OR (EXISTS(X concerne B, B is Note))")
-
-    def test_add_ambiguity_outerjoin(self):
-        constraint = ('X concerne Y')
-        rqlst = parse('Any X,C WHERE X? documented_by C')
-        rewrite(rqlst, {'X': (constraint,)}, {})
-        # ambiguity are kept in the sub-query, no need to be resolved using OR
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any X,C WHERE X? documented_by C, C is Card WITH X BEING (Any X WHERE X concerne A, X is Affaire)")
-
-
-
-if __name__ == '__main__':
-    unittest_main()
--- a/server/test/unittest_schemaserial.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_schemaserial.py	Fri Sep 18 11:24:37 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	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_security.py	Fri Sep 18 11:24:37 2009 +0200
@@ -27,10 +27,10 @@
 
     def test_check_read_access(self):
         rql = u'Personne U where U nom "managers"'
-        rqlst = self.repo.querier._rqlhelper.parse(rql).children[0]
+        rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
         origgroups = self.schema['Personne'].get_groups('read')
         self.schema['Personne'].set_groups('read', ('users', 'managers'))
-        self.repo.querier._rqlhelper.compute_solutions(rqlst)
+        self.repo.vreg.rqlhelper.compute_solutions(rqlst)
         solution = rqlst.solutions[0]
         check_read_access(self.schema, self.session.user, rqlst, solution)
         cnx = self.login('anon')
@@ -265,7 +265,7 @@
         self.commit()
         cnx = self.login('iaminusersgrouponly')
         cu = cnx.cursor()
-        aff2 = cu.execute("INSERT Affaire X: X sujet 'cool', X in_state S WHERE S name 'pitetre'")[0][0]
+        aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
         soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
         cu.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1},
                    ('a', 's'))
@@ -347,25 +347,26 @@
 
     def test_attribute_security_rqlexpr(self):
         # Note.para attribute editable by managers or if the note is in "todo" state
-        eid = self.execute("INSERT Note X: X para 'bidule', X in_state S WHERE S name 'done'")[0][0]
+        note = self.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
         self.commit()
-        self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': eid}, 'x')
+        note.fire_transition('markasdone')
+        self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid}, 'x')
         self.commit()
         cnx = self.login('iaminusersgrouponly')
         cu = cnx.cursor()
-        cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid}, 'x')
+        cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid}, 'x')
         self.assertRaises(Unauthorized, cnx.commit)
-        eid2 = cu.execute("INSERT Note X: X para 'bidule'")[0][0]
+        note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
         cnx.commit()
-        cu.execute("SET X in_state S WHERE X eid %(x)s, S name 'done'", {'x': eid2}, 'x')
+        note2.fire_transition('markasdone')
         cnx.commit()
-        self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': eid2}, 'x')),
+        self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid}, 'x')),
                           0)
-        cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid2}, 'x')
+        cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}, 'x')
         self.assertRaises(Unauthorized, cnx.commit)
-        cu.execute("SET X in_state S WHERE X eid %(x)s, S name 'todo'", {'x': eid2}, 'x')
+        note2.fire_transition('redoit')
         cnx.commit()
-        cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': eid2}, 'x')
+        cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}, 'x')
         cnx.commit()
 
     def test_attribute_read_security(self):
@@ -398,16 +399,14 @@
         cu.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"')
         cnx.commit()
         self.restore_connection()
-        self.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"')
+        affaire = self.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
+        affaire.fire_transition('abort')
         self.commit()
         self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01"')),
-                          2)
+                          1)
         self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",'
                                            'X owned_by U, U login "admin"')),
                           1) # TrInfo at the above state change
-        self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",'
-                                           'X owned_by U, U login "iaminusersgrouponly"')),
-                          1) # TrInfo created at creation time
         cnx = self.login('iaminusersgrouponly')
         cu = cnx.cursor()
         cu.execute('DELETE Affaire X WHERE X ref "ARCT01"')
@@ -499,29 +498,34 @@
         self.assertRaises(Unauthorized,
                           self.schema['Affaire'].check_perm, session, 'update', eid)
         cu = cnx.cursor()
-        cu.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"')
-        cnx.commit()
-        # though changing a user state (even logged user) is reserved to managers
-        rql = u"SET X in_state S WHERE X eid %(x)s, S name 'deactivated'"
-        # XXX wether it should raise Unauthorized or ValidationError is not clear
-        # the best would probably ValidationError if the transition doesn't exist
-        # from the current state but Unauthorized if it exists but user can't pass it
-        self.assertRaises(ValidationError, cu.execute, rql, {'x': cnx.user(self.current_session()).eid}, 'x')
+        self.schema['Affaire'].set_groups('read', ('users',))
+        try:
+            aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
+            aff.fire_transition('abort')
+            cnx.commit()
+            # though changing a user state (even logged user) is reserved to managers
+            user = cnx.user(self.current_session())
+            # XXX wether it should raise Unauthorized or ValidationError is not clear
+            # the best would probably ValidationError if the transition doesn't exist
+            # from the current state but Unauthorized if it exists but user can't pass it
+            self.assertRaises(ValidationError, user.fire_transition, 'deactivate')
+        finally:
+            self.schema['Affaire'].set_groups('read', ('managers',))
 
     def test_trinfo_security(self):
         aff = self.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0)
         self.commit()
+        aff.fire_transition('abort')
+        self.commit()
         # can change tr info comment
         self.execute('SET TI comment %(c)s WHERE TI wf_info_for X, X ref "ARCT01"',
-                     {'c': u'creation'})
+                     {'c': u'bouh!'})
         self.commit()
         aff.clear_related_cache('wf_info_for', 'object')
-        self.assertEquals(aff.latest_trinfo().comment, 'creation')
+        trinfo = aff.latest_trinfo()
+        self.assertEquals(trinfo.comment, 'bouh!')
         # but not from_state/to_state
-        self.execute('SET X in_state S WHERE X ref "ARCT01", S name "ben non"')
-        self.commit()
         aff.clear_related_cache('wf_info_for', role='object')
-        trinfo = aff.latest_trinfo()
         self.assertRaises(Unauthorized,
                           self.execute, 'SET TI from_state S WHERE TI eid %(ti)s, S name "ben non"',
                           {'ti': trinfo.eid}, 'ti')
--- a/server/test/unittest_ssplanner.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/server/test/unittest_ssplanner.py	Fri Sep 18 11:24:37 2009 +0200
@@ -9,7 +9,7 @@
 from cubicweb.devtools.repotest import BasePlannerTC, test_plan
 from cubicweb.server.ssplanner import SSPlanner
 
-# keep cnx so it's not garbage collected and the associated session is closed
+# keep cnx so it's not garbage collected and the associated session closed
 repo, cnx = init_test_database('sqlite')
 
 class SSPlannerTC(BasePlannerTC):
@@ -18,47 +18,27 @@
 
     def setUp(self):
         BasePlannerTC.setUp(self)
-        self.planner = SSPlanner(self.o.schema, self.o._rqlhelper)
+        self.planner = SSPlanner(self.o.schema, self.repo.vreg.rqlhelper)
         self.system = self.o._repo.system_source
 
     def tearDown(self):
         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/email.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/sobjects/email.py	Fri Sep 18 11:24:37 2009 +0200
@@ -9,6 +9,7 @@
 
 from cubicweb.server.hooksmanager import Hook
 from cubicweb.server.pool import PreCommitOperation
+from cubicweb.server.repository import ensure_card_respected
 
 class SetUseEmailRelationOp(PreCommitOperation):
     """delay this operation to commit to avoid conflict with a late rql query
@@ -26,6 +27,11 @@
     def precommit_event(self):
         session = self.session
         if self.condition():
+            # we've to handle cardinaly by ourselves since we're using unsafe_execute
+            # but use session.execute and not session.unsafe_execute to check we
+            # can change the relation
+            ensure_card_respected(session.execute, session,
+                                  self.fromeid, self.rtype, self.toeid)
             session.unsafe_execute(
                 'SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % self.rtype,
                 {'x': self.fromeid, 'y': self.toeid}, 'x')
--- a/sobjects/supervising.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/sobjects/supervising.py	Fri Sep 18 11:24:37 2009 +0200
@@ -85,27 +85,18 @@
             added.add(entity.eid)
             if entity.e_schema == 'TrInfo':
                 changes.remove(change)
-                if entity.from_state:
-                    try:
-                        changes.remove( ('delete_relation',
-                                         (entity.wf_info_for[0].eid, 'in_state',
-                                          entity.from_state[0].eid)) )
-                    except ValueError:
-                        pass
-                    try:
-                        changes.remove( ('add_relation',
-                                         (entity.wf_info_for[0].eid, 'in_state',
-                                          entity.to_state[0].eid)) )
-                    except ValueError:
-                        pass
-                    event = 'change_state'
-                    change = (event,
-                              (entity.wf_info_for[0],
-                               entity.from_state[0], entity.to_state[0]))
-                    changes.append(change)
+                event = 'change_state'
+                change = (event,
+                          (entity.wf_info_for[0],
+                           entity.from_state[0], entity.to_state[0]))
+                changes.append(change)
         elif event == 'delete_entity':
             deleted.add(changedescr[0])
         index.setdefault(event, set()).add(change)
+    for key in ('delete_relation', 'add_relation'):
+        for change in index.get(key, {}).copy():
+            if change[1][1] == 'in_state':
+                index[key].remove(change)
     # filter changes
     for eid in added:
         try:
@@ -114,14 +105,10 @@
                 # skip meta-relations which are set automatically
                 # XXX generate list below using rtags (category = 'generated')
                 if changedescr[1] in ('created_by', 'owned_by', 'is', 'is_instance_of',
-                                      'from_state', 'to_state', 'wf_info_for',) \
+                                      'from_state', 'to_state', 'by_transition',
+                                      'wf_info_for') \
                        and changedescr[0] == eid:
                     index['add_relation'].remove(change)
-                # skip in_state relation if the entity is being created
-                # XXX this may be automatized by skipping all mandatory relation
-                #     at entity creation time
-                elif changedescr[1] == 'in_state' and changedescr[0] in added:
-                    index['add_relation'].remove(change)
 
         except KeyError:
             break
--- a/sobjects/test/unittest_email.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/sobjects/test/unittest_email.py	Fri Sep 18 11:24:37 2009 +0200
@@ -5,6 +5,7 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from cubicweb import Unauthorized
 from cubicweb.devtools.apptest import EnvBasedTC
 
 class EmailAddressHooksTC(EnvBasedTC):
@@ -30,6 +31,24 @@
         self.assertEquals(self.execute('Any A WHERE U use_email X, U login "admin", X address A')[0][0],
                           'admin@logilab.fr')
 
+    def test_cardinality_check(self):
+        email1 = self.execute('INSERT EmailAddress E: E address "client@client.com", U use_email E WHERE U login "admin"')[0][0]
+        self.commit()
+        self.execute('SET U primary_email E WHERE U login "anon", E address "client@client.com"')
+        self.commit()
+        rset = self.execute('Any X WHERE X use_email E, E eid %(e)s', {'e': email1})
+        self.failIf(rset.rowcount != 1, rset)
+
+    def test_security_check(self):
+        self.create_user('toto')
+        email1 = self.execute('INSERT EmailAddress E: E address "client@client.com", U use_email E WHERE U login "admin"')[0][0]
+        self.commit()
+        cnx = self.login('toto')
+        cu = cnx.cursor()
+        cu.execute('SET U primary_email E WHERE E eid %(e)s, U login "toto"',
+                   {'e': email1})
+        self.assertRaises(Unauthorized, cnx.commit)
+
 
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
--- a/sobjects/test/unittest_notification.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/sobjects/test/unittest_notification.py	Fri Sep 18 11:24:37 2009 +0200
@@ -9,9 +9,9 @@
 from socket import gethostname
 
 from logilab.common.testlib import unittest_main, TestCase
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.apptest import EnvBasedTC, MAILBOX
 
-from cubicweb.sobjects.notification import construct_message_id, parse_message_id
+from cubicweb.common.mail import construct_message_id, parse_message_id
 
 class MessageIdTC(TestCase):
     def test_base(self):
@@ -71,16 +71,14 @@
 
     def test_status_change_view(self):
         req = self.session()
-        u = self.create_user('toto', req=req)
-        assert u.req
-        assert u.rset
-        self.execute('SET X in_state S WHERE X eid %s, S name "deactivated"' % u.eid)
-        v = self.vreg['views'].select('notif_status_change', req, rset=u.rset, row=0)
-        content = v.render(row=0, comment='yeah',
-                           previous_state='activated',
-                           current_state='deactivated')
-        # remove date
-        self.assertEquals(content,
+        u = self.create_user('toto', req=req)#, commit=False) XXX in cw 3.6, and remove set_pool
+        req.set_pool()
+        u.fire_transition('deactivate', comment=u'yeah')
+        self.failIf(MAILBOX)
+        self.commit()
+        self.assertEquals(len(MAILBOX), 1)
+        email = MAILBOX[0]
+        self.assertEquals(email.content,
                           '''
 admin changed status from <activated> to <deactivated> for entity
 'toto'
@@ -89,7 +87,7 @@
 
 url: http://testing.fr/cubicweb/cwuser/toto
 ''')
-        self.assertEquals(v.subject(), 'status changed cwuser #%s (admin)' % u.eid)
+        self.assertEquals(email.subject, 'status changed cwuser #%s (admin)' % u.eid)
 
 if __name__ == '__main__':
     unittest_main()
--- a/sobjects/test/unittest_supervising.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/sobjects/test/unittest_supervising.py	Fri Sep 18 11:24:37 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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/rewrite/bootstrap_cubes	Fri Sep 18 11:24:37 2009 +0200
@@ -0,0 +1,1 @@
+card, person
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/rewrite/schema.py	Fri Sep 18 11:24:37 2009 +0200
@@ -0,0 +1,42 @@
+from yams.buildobjs import EntityType, RelationDefinition, String, SubjectRelation
+from cubicweb.schema import ERQLExpression
+
+class Affaire(EntityType):
+    permissions = {
+        'read':   ('managers',
+                   ERQLExpression('X owned_by U'), ERQLExpression('X concerne S?, S owned_by U')),
+        'add':    ('managers', ERQLExpression('X concerne S, S owned_by U')),
+        'update': ('managers', 'owners', ERQLExpression('X in_state S, S name in ("pitetre", "en cours")')),
+        'delete': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
+        }
+    ref = String(fulltextindexed=True, indexed=True,
+                 constraints=[SizeConstraint(16)])
+    documented_by = SubjectRelation('Card')
+    concerne = SubjectRelation(('Societe', 'Note'))
+
+
+class Societe(EntityType):
+    permissions = {
+        'read': ('managers', 'users', 'guests'),
+        'update': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
+        'delete': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
+        'add': ('managers', 'users',)
+        }
+
+
+class Division(Societe):
+    __specializes_schema__ = True
+
+
+class Note(EntityType):
+    pass
+
+
+class require_permission(RelationDefinition):
+    subject = ('Card', 'Note', 'Person')
+    object = 'CWPermission'
+
+
+class require_state(RelationDefinition):
+    subject = 'CWPermission'
+    object = 'State'
--- a/test/unittest_entity.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/test/unittest_entity.py	Fri Sep 18 11:24:37 2009 +0200
@@ -9,9 +9,10 @@
 
 from datetime import datetime
 
-from cubicweb import Binary
+from cubicweb import Binary, Unauthorized
 from cubicweb.devtools.apptest import EnvBasedTC
 from cubicweb.common.mttransforms import HAS_TAL
+from cubicweb.entities import fetch_config
 
 class EntityTC(EnvBasedTC):
 
@@ -76,8 +77,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 +86,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')
@@ -179,7 +180,6 @@
             Societe.fetch_attrs = sfetch_attrs
 
     def test_related_rql(self):
-        from cubicweb.entities import fetch_config
         Personne = self.vreg['etypes'].etype_class('Personne')
         Note = self.vreg['etypes'].etype_class('Note')
         self.failUnless(issubclass(self.vreg['etypes'].etype_class('SubNote'), Note))
@@ -194,7 +194,40 @@
         self.assertEquals(p.related_rql('evaluee'),
                           'Any X,AA ORDERBY Z DESC WHERE X modification_date Z, E eid %(x)s, E evaluee X, X modification_date AA')
 
-    def test_entity_unrelated(self):
+    def test_unrelated_rql_security_1(self):
+        user = self.request().user
+        rql = user.unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
+        self.assertEquals(rql, 'Any O,AA,AB,AC ORDERBY AC DESC '
+                          'WHERE NOT S use_email O, S eid %(x)s, O is EmailAddress, O address AA, O alias AB, O modification_date AC')
+        self.create_user('toto')
+        self.login('toto')
+        user = self.request().user
+        rql = user.unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
+        self.assertEquals(rql, 'Any O,AA,AB,AC ORDERBY AC DESC '
+                          'WHERE NOT S use_email O, S eid %(x)s, O is EmailAddress, O address AA, O alias AB, O modification_date AC')
+        user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0)
+        self.assertRaises(Unauthorized, user.unrelated_rql, 'use_email', 'EmailAddress', 'subject')
+        self.login('anon')
+        user = self.request().user
+        self.assertRaises(Unauthorized, user.unrelated_rql, 'use_email', 'EmailAddress', 'subject')
+
+    def test_unrelated_rql_security_2(self):
+        email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
+        rql = email.unrelated_rql('use_email', 'CWUser', 'object')[0]
+        self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ASC '
+                          'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD')
+        #rql = email.unrelated_rql('use_email', 'Person', 'object')[0]
+        #self.assertEquals(rql, '')
+        self.login('anon')
+        email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}, 'x').get_entity(0, 0)
+        rql = email.unrelated_rql('use_email', 'CWUser', 'object')[0]
+        self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+                          'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD, '
+                          'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)')
+        #rql = email.unrelated_rql('use_email', 'Person', 'object')[0]
+        #self.assertEquals(rql, '')
+
+    def test_unrelated_base(self):
         p = self.add_entity('Personne', nom=u'di mascio', prenom=u'adrien')
         e = self.add_entity('Tag', name=u'x')
         related = [r.eid for r in e.tags]
@@ -206,14 +239,40 @@
         unrelated = [r[0] for r in e.unrelated('tags', 'Personne', 'subject')]
         self.failIf(p.eid in unrelated)
 
-    def test_entity_unrelated_limit(self):
+    def test_unrelated_limit(self):
         e = self.add_entity('Tag', name=u'x')
         self.add_entity('Personne', nom=u'di mascio', prenom=u'adrien')
-        self.add_entity('Personne', nom=u'di mascio', prenom=u'gwen')
+        self.add_entity('Personne', nom=u'thenault', prenom=u'sylvain')
         self.assertEquals(len(e.unrelated('tags', 'Personne', 'subject', limit=1)),
                           1)
 
-    def test_new_entity_unrelated(self):
+    def test_unrelated_security(self):
+        email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
+        rset = email.unrelated('use_email', 'CWUser', 'object')
+        self.assertEquals([x.login for x in rset.entities()], [u'admin', u'anon'])
+        user = self.request().user
+        rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+        self.assertEquals([x.address for x in rset.entities()], [u'hop'])
+        self.create_user('toto')
+        self.login('toto')
+        email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}, 'x').get_entity(0, 0)
+        rset = email.unrelated('use_email', 'CWUser', 'object')
+        self.assertEquals([x.login for x in rset.entities()], ['toto'])
+        user = self.request().user
+        rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+        self.assertEquals([x.address for x in rset.entities()], ['hop'])
+        user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0)
+        rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+        self.assertEquals([x.address for x in rset.entities()], [])
+        self.login('anon')
+        email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}, 'x').get_entity(0, 0)
+        rset = email.unrelated('use_email', 'CWUser', 'object')
+        self.assertEquals([x.login for x in rset.entities()], [])
+        user = self.request().user
+        rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+        self.assertEquals([x.address for x in rset.entities()], [])
+
+    def test_unrelated_new_entity(self):
         e = self.etype_instance('CWUser')
         unrelated = [r[0] for r in e.unrelated('in_group', 'CWGroup', 'subject')]
         # should be default groups but owners, i.e. managers, users, guests
@@ -328,33 +387,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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_rqlrewrite.py	Fri Sep 18 11:24:37 2009 +0200
@@ -0,0 +1,193 @@
+"""
+
+: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, TestCase
+from logilab.common.testlib import mock_object
+
+from rql import parse, nodes, RQLHelper
+
+from cubicweb import Unauthorized
+from cubicweb.rqlrewrite import RQLRewriter
+from cubicweb.devtools import repotest, TestServerConfiguration
+
+config = TestServerConfiguration('data/rewrite')
+config.bootstrap_cubes()
+schema = config.load_schema()
+schema.add_relation_def(mock_object(subject='Card', name='in_state', object='State', cardinality='1*'))
+
+rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid',
+                                                 'has_text': 'fti'})
+
+def setup_module(*args):
+    repotest.do_monkey_patch()
+
+def teardown_module(*args):
+    repotest.undo_monkey_patch()
+
+def eid_func_map(eid):
+    return {1: 'CWUser',
+            2: 'Card'}[eid]
+
+def rewrite(rqlst, snippets_map, kwargs):
+    class FakeVReg:
+        schema = schema
+        @staticmethod
+        def solutions(sqlcursor, mainrqlst, kwargs):
+            rqlhelper.compute_solutions(rqlst, {'eid': eid_func_map}, kwargs=kwargs)
+        class rqlhelper:
+            @staticmethod
+            def annotate(rqlst):
+                rqlhelper.annotate(rqlst)
+            @staticmethod
+            def simplify(mainrqlst, needcopy=False):
+                rqlhelper.simplify(rqlst, needcopy)
+    rewriter = RQLRewriter(mock_object(vreg=FakeVReg, user=(mock_object(eid=1))))
+    for v, snippets in snippets_map.items():
+        snippets_map[v] = [mock_object(snippet_rqlst=parse('Any X WHERE '+snippet).children[0],
+                                       expression='Any X WHERE '+snippet)
+                           for snippet in snippets]
+    rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map}, kwargs=kwargs)
+    solutions = rqlst.children[0].solutions
+    rewriter.rewrite(rqlst.children[0], snippets_map.items(), solutions, kwargs)
+    test_vrefs(rqlst.children[0])
+    return rewriter.rewritten
+
+def test_vrefs(node):
+    vrefmap = {}
+    for vref in node.iget_nodes(nodes.VariableRef):
+        vrefmap.setdefault(vref.name, set()).add(vref)
+    for var in node.defined_vars.itervalues():
+        assert not (var.stinfo['references'] ^ vrefmap[var.name])
+        assert (var.stinfo['references'])
+
+class RQLRewriteTC(TestCase):
+    """a faire:
+
+    * optimisation: detecter les relations utilisees dans les rqlexpressions qui
+      sont presentes dans la requete de depart pour les reutiliser si possible
+
+    * "has_<ACTION>_permission" ?
+    """
+
+    def test_base_var(self):
+        card_constraint = ('X in_state S, U in_group G, P require_state S,'
+                           'P name "read", P require_group G')
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any C WHERE C is Card, B eid %(D)s, "
+                             "EXISTS(C in_state A, B in_group E, F require_state A, "
+                             "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission)")
+
+    def test_multiple_var(self):
+        card_constraint = ('X in_state S, U in_group G, P require_state S,'
+                           'P name "read", P require_group G')
+        affaire_constraints = ('X ref LIKE "PUBLIC%"', 'U in_group G, G name "public"')
+        kwargs = {'u':2}
+        rqlst = parse('Any S WHERE S documented_by C, C eid %(u)s')
+        rewrite(rqlst, {('C', 'X'): (card_constraint,), ('S', 'X'): affaire_constraints},
+                kwargs)
+        self.assertTextEquals(rqlst.as_string(),
+                             "Any S WHERE S documented_by C, C eid %(u)s, B eid %(D)s, "
+                             "EXISTS(C in_state A, B in_group E, F require_state A, "
+                             "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission), "
+                             "(EXISTS(S ref LIKE 'PUBLIC%')) OR (EXISTS(B in_group G, G name 'public', G is CWGroup)), "
+                             "S is Affaire")
+        self.failUnless('D' in kwargs)
+
+    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, S is in (CWUser, CWGroup)')
+        rewrite(rqlst, {('C', 'X'): (constraint,)}, {'u':1})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "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)")
+
+    def test_simplified_rqlst(self):
+        card_constraint = ('X in_state S, U in_group G, P require_state S,'
+                           'P name "read", P require_group G')
+        rqlst = parse('Any 2') # this is the simplified rql st for Any X WHERE X eid 12
+        rewrite(rqlst, {('2', 'X'): (card_constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any 2 WHERE B eid %(C)s, "
+                             "EXISTS(2 in_state A, B in_group D, E require_state A, "
+                             "E name 'read', E require_group D, A is State, D is CWGroup, E is CWPermission)")
+
+    def test_optional_var(self):
+        card_constraint = ('X in_state S, U in_group G, P require_state S,'
+                           'P name "read", P require_group G')
+        rqlst = parse('Any A,C WHERE A documented_by C?')
+        rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any A,C WHERE A documented_by C?, A is Affaire "
+                             "WITH C BEING "
+                             "(Any C WHERE C in_state B, D in_group F, G require_state B, G name 'read', "
+                             "G require_group F, D eid %(A)s, C is Card)")
+        rqlst = parse('Any A,C,T WHERE A documented_by C?, C title T')
+        rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any A,C,T WHERE A documented_by C?, A is Affaire "
+                             "WITH C,T BEING "
+                             "(Any C,T WHERE C in_state B, D in_group F, G require_state B, G name 'read', "
+                             "G require_group F, C title T, D eid %(A)s, C is Card)")
+
+    def test_relation_optimization(self):
+        # since Card in_state State as monovalued cardinality, the in_state
+        # relation used in the rql expression can be ignored and S replaced by
+        # the variable from the incoming query
+        card_constraint = ('X in_state S, U in_group G, P require_state S,'
+                           'P name "read", P require_group G')
+        rqlst = parse('Card C WHERE C in_state STATE')
+        rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any C WHERE C in_state STATE, C is Card, A eid %(B)s, "
+                             "EXISTS(A in_group D, E require_state STATE, "
+                             "E name 'read', E require_group D, D is CWGroup, E is CWPermission), "
+                             "STATE is State")
+
+    def test_unsupported_constraint_1(self):
+        # CWUser doesn't have require_permission
+        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
+        rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U')
+        self.assertRaises(Unauthorized, rewrite, rqlst, {('T', 'X'): (trinfo_constraint,)}, {})
+
+    def test_unsupported_constraint_2(self):
+        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
+        rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U')
+        rewrite(rqlst, {('T', 'X'): (trinfo_constraint, 'X wf_info_for Y, Y in_group G, G name "managers"')}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any U,T WHERE U is CWUser, T wf_info_for U, "
+                             "EXISTS(U in_group B, B name 'managers', B is CWGroup), T is TrInfo")
+
+    def test_unsupported_constraint_3(self):
+        self.skip('raise unauthorized for now')
+        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
+        rqlst = parse('Any T WHERE T wf_info_for X')
+        rewrite(rqlst, {('T', 'X'): (trinfo_constraint, 'X in_group G, G name "managers"')}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u'XXX dunno what should be generated')
+
+    def test_add_ambiguity_exists(self):
+        constraint = ('X concerne Y')
+        rqlst = parse('Affaire X')
+        rewrite(rqlst, {('X', 'X'): (constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any X WHERE X is Affaire, ((EXISTS(X concerne A, A is Division)) OR (EXISTS(X concerne C, C is Societe))) OR (EXISTS(X concerne B, B is Note))")
+
+    def test_add_ambiguity_outerjoin(self):
+        constraint = ('X concerne Y')
+        rqlst = parse('Any X,C WHERE X? documented_by C')
+        rewrite(rqlst, {('X', 'X'): (constraint,)}, {})
+        # ambiguity are kept in the sub-query, no need to be resolved using OR
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any X,C WHERE X? documented_by C, C is Card WITH X BEING (Any X WHERE X concerne A, X is Affaire)")
+
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/test/unittest_schema.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/test/unittest_schema.py	Fri Sep 18 11:24:37 2009 +0200
@@ -145,7 +145,7 @@
         self.assertEquals(schema.name, 'data')
         entities = [str(e) for e in schema.entities()]
         entities.sort()
-        expected_entities = ['Bookmark', 'Boolean', 'Bytes', 'Card',
+        expected_entities = ['BaseTransition', 'Bookmark', 'Boolean', 'Bytes', 'Card',
                              'Date', 'Datetime', 'Decimal',
                              'CWCache', 'CWConstraint', 'CWConstraintType', 'CWEType',
                              'CWAttribute', 'CWGroup', 'EmailAddress', 'CWRelation',
@@ -153,19 +153,20 @@
                              'ExternalUri', 'File', 'Float', 'Image', 'Int', 'Interval', 'Note',
                              'Password', 'Personne',
                              'RQLExpression',
-                             'Societe', 'State', 'String', 'SubNote', 'Tag', 'Time',
-                             'Transition', 'TrInfo']
+                             'Societe', 'State', 'String', 'SubNote', 'SubWorkflowExitPoint',
+                             'Tag', 'Time', 'Transition', 'TrInfo',
+                             'Workflow', 'WorkflowTransition']
         self.assertListEquals(entities, sorted(expected_entities))
         relations = [str(r) for r in schema.relations()]
         relations.sort()
-        expected_relations = ['add_permission', 'address', 'alias',
-                              'allowed_transition', 'bookmarked_by', 'canonical',
+        expected_relations = ['add_permission', 'address', 'alias', 'allowed_transition',
+                              'bookmarked_by', 'by_transition',
 
                               'cardinality', 'comment', 'comment_format',
                               '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',
@@ -174,7 +175,7 @@
                               'from_entity', 'from_state', 'fulltext_container', 'fulltextindexed',
 
                               'has_text',
-                              'identical_to', 'identity', 'in_group', 'in_state', 'indexed',
+                              'identity', 'in_group', 'in_state', 'indexed',
                               'initial_state', 'inlined', 'internationalizable', 'is', 'is_instance_of',
 
                               'label', 'last_login_time', 'login',
@@ -185,11 +186,11 @@
 
                               'ordernum', 'owned_by',
 
-                              'path', 'pkey', 'prenom', 'primary_email',
+                              'path', 'pkey', 'prefered_form', 'prenom', 'primary_email',
 
                               'read_permission', 'relation_type', 'require_group',
 
-                              'specializes', 'state_of', 'surname', 'symetric', 'synopsis',
+                              'specializes', 'state_of', 'subworkflow', 'subworkflow_exit', 'subworkflow_state', 'surname', 'symetric', 'synopsis',
 
                               'tags', 'timestamp', 'title', 'to_entity', 'to_state', 'transition_of', 'travaille', 'type',
 
@@ -197,13 +198,13 @@
 
                               'value',
 
-                              'wf_info_for', 'wikiid']
+                              'wf_info_for', 'wikiid', 'workflow_of']
 
         self.assertListEquals(relations, expected_relations)
 
         eschema = schema.eschema('CWUser')
         rels = sorted(str(r) for r in eschema.subject_relations())
-        self.assertListEquals(rels, ['created_by', 'creation_date', 'cwuri', 'eid',
+        self.assertListEquals(rels, ['created_by', 'creation_date', 'custom_workflow', 'cwuri', 'eid',
                                      'evaluee', 'firstname', 'has_text', 'identity',
                                      'in_group', 'in_state', 'is',
                                      'is_instance_of', 'last_login_time',
--- a/test/unittest_utils.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/test/unittest_utils.py	Fri Sep 18 11:24:37 2009 +0200
@@ -8,7 +8,11 @@
 
 from logilab.common.testlib import TestCase, unittest_main
 
-from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList
+import simplejson
+import decimal
+import datetime
+
+from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList, CubicWebJsonEncoder
 
 
 class MakeUidTC(TestCase):
@@ -48,6 +52,24 @@
             l.extend(extension)
             yield self.assertEquals, l, expected
 
+class JSONEncoerTests(TestCase):
+
+    def encode(self, value):
+        return simplejson.dumps(value, cls=CubicWebJsonEncoder)
+
+    def test_encoding_dates(self):
+        self.assertEquals(self.encode(datetime.datetime(2009, 9, 9, 20, 30)),
+                          '"2009/09/09 20:30:00"')
+        self.assertEquals(self.encode(datetime.date(2009, 9, 9)),
+                          '"2009/09/09"')
+        self.assertEquals(self.encode(datetime.time(20, 30)),
+                          '"20:30:00"')
+
+    def test_encoding_decimal(self):
+        self.assertEquals(self.encode(decimal.Decimal('1.2')), '1.2')
+
+    def test_encoding_unknown_stuff(self):
+        self.assertEquals(self.encode(TestCase), 'null')
 
 if __name__ == '__main__':
     unittest_main()
--- a/toolsutils.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/toolsutils.py	Fri Sep 18 11:24:37 2009 +0200
@@ -10,9 +10,15 @@
 # XXX move most of this in logilab.common (shellutils ?)
 
 import os, sys
-from os import listdir, makedirs, symlink, environ, chmod, walk, remove
+from os import listdir, makedirs, environ, chmod, walk, remove
 from os.path import exists, join, abspath, normpath
 
+try:
+    from os import symlink
+except ImportError:
+    def symlink(*args):
+        raise NotImplementedError
+
 from logilab.common.clcommands import Command as BaseCommand, \
      main_run as base_main_run
 from logilab.common.compat import any
--- a/utils.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/utils.py	Fri Sep 18 11:24:37 2009 +0200
@@ -11,10 +11,14 @@
 
 import locale
 from md5 import md5
+import datetime as pydatetime
 from datetime import datetime, timedelta, date
 from time import time, mktime
 from random import randint, seed
 from calendar import monthrange
+import decimal
+
+import simplejson
 
 # initialize random seed from current time
 seed()
@@ -324,9 +328,46 @@
                                                  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
 
+
+class CubicWebJsonEncoder(simplejson.JSONEncoder):
+    """define a simplejson encoder to be able to encode yams std types"""
+    def default(self, obj):
+        if isinstance(obj, pydatetime.datetime):
+            return obj.strftime('%Y/%m/%d %H:%M:%S')
+        elif isinstance(obj, pydatetime.date):
+            return obj.strftime('%Y/%m/%d')
+        elif isinstance(obj, pydatetime.time):
+            return obj.strftime('%H:%M:%S')
+        elif isinstance(obj, decimal.Decimal):
+            return float(obj)
+        try:
+            return simplejson.JSONEncoder.default(self, obj)
+        except TypeError:
+            # we never ever want to fail because of an unknown type,
+            # just return None in those cases.
+            return None
--- a/vregistry.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/vregistry.py	Fri Sep 18 11:24:37 2009 +0200
@@ -171,7 +171,7 @@
         raise `NoSelectableObject` if not object apply
         """
         if len(args) > 1:
-            warn('only the request param can not be named when calling select',
+            warn('[3.5] only the request param can not be named when calling select*',
                  DeprecationWarning, stacklevel=3)
         score, winners = 0, []
         for appobject in appobjects:
--- a/web/__init__.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/__init__.py	Fri Sep 18 11:24:37 2009 +0200
@@ -16,7 +16,7 @@
 
 from logilab.common.deprecation import deprecated
 
-from cubicweb.common.uilib import urlquote
+from urllib import quote as urlquote
 from cubicweb.web._exceptions import *
 
 
--- a/web/action.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/action.py	Fri Sep 18 11:24:37 2009 +0200
@@ -32,8 +32,18 @@
                                      'useractions', 'siteactions', 'hidden'),
                          help=_('context where this component should be displayed')),
     }
-    site_wide = True # don't want user to configuration actions eproperties
+    site_wide = True # don't want user to configurate actions
     category = 'moreactions'
+    # actions in category 'moreactions' can specify a sub-menu in which they should be filed
+    submenu = None
+
+    def actual_actions(self):
+        yield self
+
+    def fill_menu(self, box, menu):
+        """add action(s) to the given submenu of the given box"""
+        for action in self.actual_actions():
+            menu.append(box.box_action(action))
 
     def url(self):
         """return the url associated with this action"""
@@ -45,6 +55,9 @@
         if self.category:
             return 'box' + self.category.capitalize()
 
+    def build_action(self, title, path, **kwargs):
+        return UnregisteredAction(self.req, self.rset, title, path, **kwargs)
+
 
 class UnregisteredAction(Action):
     """non registered action used to build boxes. Unless you set them
@@ -75,13 +88,12 @@
                   & partial_may_add_relation())
     registered = accepts_compat(Action.registered)
 
-    category = 'addrelated'
+    submenu = 'addrelated'
 
     def url(self):
         current_entity = self.rset.get_entity(self.row or 0, self.col or 0)
         linkto = '%s:%s:%s' % (self.rtype, current_entity.eid, target(self))
-        return self.build_url(vid='creation', etype=self.etype,
-                              __linkto=linkto,
+        return self.build_url('add/%s' % self.etype, __linkto=linkto,
                               __redirectpath=current_entity.rest_path(), # should not be url quoted!
                               __redirectvid=self.req.form.get('__redirectvid', ''))
 
--- a/web/box.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/box.py	Fri Sep 18 11:24:37 2009 +0200
@@ -60,7 +60,8 @@
         result = []
         actions_by_cat = {}
         for action in actions:
-            actions_by_cat.setdefault(action.category, []).append((action.title, action))
+            actions_by_cat.setdefault(action.category, []).append(
+                (action.title, action) )
         for key, values in actions_by_cat.items():
             actions_by_cat[key] = [act for title, act in sorted(values)]
         for cat in self.categories_in_order:
@@ -150,7 +151,7 @@
     __select__ = EntityBoxTemplate.__select__ & partial_has_related_entities()
 
     def cell_call(self, row, col, **kwargs):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         limit = self.req.property_value('navigation.related-limit') + 1
         role = get_role(self)
         self.w(u'<div class="sideBox">')
@@ -169,7 +170,7 @@
 
     def cell_call(self, row, col, view=None, **kwargs):
         self.req.add_js('cubicweb.ajax.js')
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         box = SideBoxWidget(display_name(self.req, self.rtype), self.id)
         related = self.related_boxitems(entity)
         unrelated = self.unrelated_boxitems(entity)
--- a/web/data/cubicweb.ajax.js	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/data/cubicweb.ajax.js	Fri Sep 18 11:24:37 2009 +0200
@@ -63,7 +63,12 @@
 	roundedCorners(node);
     }
     loadDynamicFragments(node);
-    jQuery(CubicWeb).trigger('ajax-loaded');
+    // XXX simulates document.ready, but the former
+    // only runs once, this one potentially many times
+    // we probably need to unbind the fired events
+    // When this is done, jquery.treeview.js (for instance)
+    // can be unpatched.
+  jQuery(CubicWeb).trigger('ajax-loaded');
 }
 
 /* cubicweb loadxhtml plugin to make jquery handle xhtml response
--- a/web/data/cubicweb.css	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/data/cubicweb.css	Fri Sep 18 11:24:37 2009 +0200
@@ -843,3 +843,11 @@
   border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
   background: #fffff8 url("button.png") bottom left repeat-x;
 }
+
+/********************************/
+/* placement of alt. view icons */
+/********************************/
+
+.otherView {
+  float: right;
+}
--- a/web/data/cubicweb.edition.js	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/data/cubicweb.edition.js	Fri Sep 18 11:24:37 2009 +0200
@@ -60,7 +60,7 @@
 	if (!divNode.length) {
 	    var args = {vid: 'unrelateddivs', relation: selectedValue,
 			rql: rql_for_eid(eid), '__notemplate': 1,
-			callback: function() {_showMatchingSelect(eid, jQuery('#' + divId))}};
+			callback: function() {_showMatchingSelect(eid, jQuery('#' + divId));}};
 	    jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
 	} else {
 	    _showMatchingSelect(eid, divNode);
@@ -354,11 +354,11 @@
     // Success
     if (result[0]) {
 	if (onsuccess) {
-             onsuccess(result[1], formid);
+             onsuccess(result, formid);
 	} else {
 	    document.location.href = result[1];
 	}
-      return;
+      return true;
     }
     unfreezeFormButtons(formid);
     // Failures
@@ -368,15 +368,15 @@
     if ( !isArrayLike(descr) || descr.length != 2 ) {
 	log('got strange error :', descr);
 	updateMessage(descr);
-	return;
+	return false;
     }
     _displayValidationerrors(formid, descr[0], descr[1]);
-    updateMessage(_("please correct errors below"));
+    updateMessage(_('please correct errors below'));
     document.location.hash = '#header';
-    if (onfailure){
+    if (onfailure) {
 	onfailure(formid);
     }
-    return;
+    return false;
 }
 
 
@@ -481,10 +481,10 @@
 	return false;
     }
     d.addCallback(function (result, req) {
-        handleFormValidationResponse(divid+'-form', noop, noop, result);
-	if (reload) {
+        if (handleFormValidationResponse(divid+'-form', noop, noop, result)) {
+          if (reload) {
 	    document.location.href = result[1].split('?')[0];
-	} else {
+	  } else {
 	    var fieldview = getNode('value-' + divid);
 	    // XXX using innerHTML is very fragile and won't work if
 	    // we mix XHTML and HTML
@@ -492,11 +492,10 @@
 	    // switch inline form off only if no error
 	    if (result[0]) {
 		// hide global error messages
-		jQuery('div.errorMessage').remove();
-		jQuery('#appMsg').hide();
 		hideInlineEdit(eid, rtype, divid);
 	    }
-	}
+	  }
+        }
 	return false;
     });
     return false;
@@ -511,15 +510,13 @@
 	var zipped = formContents(form);
 	var d = asyncRemoteExec('validate_form', 'apply', zipped[0], zipped[1]);
     } catch (ex) {
-	log('got exception', ex);
 	return false;
     }
     d.addCallback(function (result, req) {
-        handleFormValidationResponse(divid+'-form', noop, noop, result);
-        if (reload) {
-          document.location.href = result[1];
-        } else {
-	  if (result[0]) {
+	if (handleFormValidationResponse(divid+'-form', noop, noop, result)) {
+          if (reload) {
+            document.location.href = result[1].split('?')[0];
+          } else {
             var d = asyncRemoteExec('reledit_form', eid, rtype, role, default_value, lzone);
             d.addCallback(function (result) {
               // XXX brittle ... replace with loadxhtml
@@ -540,6 +537,8 @@
 }
 
 function hideInlineEdit(eid, rtype, divid) {
+    jQuery('#appMsg').hide();
+    jQuery('div.errorMessage').remove();
     jQuery('#' + divid).show();
     jQuery('#' + divid+'-form').hide();
 }
--- a/web/data/cubicweb.widgets.js	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/data/cubicweb.widgets.js	Fri Sep 18 11:24:37 2009 +0200
@@ -181,13 +181,6 @@
     }
 }
 
-Widgets.TreeView = defclass("TreeView", null, {
-    __init__: function(wdgnode) {
-	jQuery(wdgnode).treeview({toggle: toggleTree,
-				  prerendered: true});
-    }
-});
-
 
 /* widget based on SIMILE's timeline widget
  * http://code.google.com/p/simile-widgets/
Binary file web/data/pdf_icon.gif has changed
--- a/web/formfields.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/formfields.py	Fri Sep 18 11:24:37 2009 +0200
@@ -165,6 +165,7 @@
         try:
             return widget.render(form, self, renderer)
         except TypeError:
+            raise
             warn('widget.render now take the renderer as third argument, please update %s implementation'
                  % widget.__class__.__name__, DeprecationWarning)
             return widget.render(form, self)
@@ -438,9 +439,16 @@
 
 
 class RelationField(Field):
-    def __init__(self, **kwargs):
-        kwargs.setdefault('sort', False)
-        super(RelationField, self).__init__(**kwargs)
+    # XXX (syt): iirc, we originaly don't sort relation vocabulary since we want
+    # to let entity.unrelated_rql control this, usually to get most recently
+    # modified entities in the select box instead of by alphabetical order. Now,
+    # we first use unrelated_rql to get the vocabulary, which may be limited
+    # (hence we get the latest modified entities) and we can sort here for
+    # better readability
+    #
+    # def __init__(self, **kwargs):
+    #     kwargs.setdefault('sort', False)
+    #     super(RelationField, self).__init__(**kwargs)
 
     @staticmethod
     def fromcardinality(card, **kwargs):
@@ -506,6 +514,10 @@
         help = rschema.rproperty(targetschema, eschema, 'description')
     kwargs['required'] = card in '1+'
     kwargs['name'] = rschema.type
+    if role == 'object':
+        kwargs['label'] = (eschema.type + '_object', rschema.type)
+    else:
+        kwargs['label'] = (eschema.type, rschema.type)
     kwargs.setdefault('help', help)
     if rschema.is_final():
         if skip_meta_attr and rschema in eschema.meta_attributes():
--- a/web/request.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/request.py	Fri Sep 18 11:24:37 2009 +0200
@@ -114,7 +114,8 @@
         self.set_default_language(vreg)
 
     def set_language(self, lang):
-        self._ = self.__ = self.translations[lang]
+        gettext, self.pgettext = self.translations[lang]
+        self._ = self.__ = gettext
         self.lang = lang
         self.cnx.set_session_props(lang=lang)
         self.debug('request language: %s', lang)
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 Sep 18 11:24:37 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_application.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/test/unittest_application.py	Fri Sep 18 11:24:37 2009 +0200
@@ -69,14 +69,17 @@
 
 
     def test_from_controller(self):
+        self.req.vreg['controllers'] = {'view': 1, 'login': 1}
         self.assertEquals(self.req.from_controller(), 'view')
         req = FakeRequest(url='project?vid=list')
+        req.vreg['controllers'] = {'view': 1, 'login': 1}
         # this assertion is just to make sure that relative_path can be
         # correctly computed as it is used in from_controller()
         self.assertEquals(req.relative_path(False), 'project')
         self.assertEquals(req.from_controller(), 'view')
         # test on a valid non-view controller
         req = FakeRequest(url='login?x=1&y=2')
+        req.vreg['controllers'] = {'view': 1, 'login': 1}
         self.assertEquals(req.relative_path(False), 'login')
         self.assertEquals(req.from_controller(), 'login')
 
--- a/web/test/unittest_breadcrumbs.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/test/unittest_breadcrumbs.py	Fri Sep 18 11:24:37 2009 +0200
@@ -8,10 +8,10 @@
         self.execute('SET F2 filed_under F1 WHERE F1 eid %(f1)s, F2 eid %(f2)s',
                      {'f1' : f1.eid, 'f2' : f2.eid})
         self.commit()
-        childrset = self.execute('Folder F WHERE F eid %s' % f2.eid)
-        self.assertEquals(childrset.get_entity(0,0).view('breadcrumbs'),
-                          '<a href="http://testing.fr/cubicweb/folder/637" title="">chi&amp;ld</a>')
+        self.assertEquals(f2.view('breadcrumbs'),
+                          '<a href="http://testing.fr/cubicweb/folder/%s" title="">chi&amp;ld</a>' % f2.eid)
+        childrset = f2.as_rset()
         ibc = self.vreg['components'].select('breadcrumbs', self.request(), rset=childrset)
         self.assertEquals(ibc.render(),
-                          """<span id="breadcrumbs" class="pathbar">&#160;&gt;&#160;<a href="http://testing.fr/cubicweb/Folder">folder_plural</a>&#160;&gt;&#160;<a href="http://testing.fr/cubicweb/folder/636" title="">par&amp;ent</a>&#160;&gt;&#160;
-chi&amp;ld</span>""")
+                          """<span id="breadcrumbs" class="pathbar">&#160;&gt;&#160;<a href="http://testing.fr/cubicweb/Folder">folder_plural</a>&#160;&gt;&#160;<a href="http://testing.fr/cubicweb/folder/%s" title="">par&amp;ent</a>&#160;&gt;&#160;
+chi&amp;ld</span>""" % f1.eid)
--- a/web/test/unittest_form.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/test/unittest_form.py	Fri Sep 18 11:24:37 2009 +0200
@@ -54,6 +54,7 @@
         unrelated = [reid for rview, reid in form2.object_relation_vocabulary('tags')]
         self.failIf(t.eid in unrelated, unrelated)
 
+
     def test_form_field_vocabulary_new_entity(self):
         e = self.etype_instance('CWUser')
         form = EntityFieldsForm(self.request(), None, entity=e)
@@ -61,19 +62,19 @@
         # should be default groups but owners, i.e. managers, users, guests
         self.assertEquals(unrelated, [u'guests', u'managers', u'users'])
 
-    def test_subject_in_state_vocabulary(self):
-        # on a new entity
-        e = self.etype_instance('CWUser')
-        form = EntityFieldsForm(self.request(), None, entity=e)
-        states = list(form.subject_in_state_vocabulary('in_state'))
-        self.assertEquals(len(states), 1)
-        self.assertEquals(states[0][0], u'activated') # list of (combobox view, state eid)
-        # on an existant entity
-        e = self.user()
-        form = EntityFieldsForm(self.request(), None, entity=e)
-        states = list(form.subject_in_state_vocabulary('in_state'))
-        self.assertEquals(len(states), 1)
-        self.assertEquals(states[0][0], u'deactivated') # list of (combobox view, state eid)
+    # def test_subject_in_state_vocabulary(self):
+    #     # on a new entity
+    #     e = self.etype_instance('CWUser')
+    #     form = EntityFieldsForm(self.request(), None, entity=e)
+    #     states = list(form.subject_in_state_vocabulary('in_state'))
+    #     self.assertEquals(len(states), 1)
+    #     self.assertEquals(states[0][0], u'activated') # list of (combobox view, state eid)
+    #     # on an existant entity
+    #     e = self.user()
+    #     form = EntityFieldsForm(self.request(), None, entity=e)
+    #     states = list(form.subject_in_state_vocabulary('in_state'))
+    #     self.assertEquals(len(states), 1)
+    #     self.assertEquals(states[0][0], u'deactivated') # list of (combobox view, state eid)
 
     def test_consider_req_form_params(self):
         e = self.etype_instance('CWUser')
@@ -143,7 +144,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 Sep 18 11:24:37 2009 +0200
@@ -0,0 +1,36 @@
+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.ext.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.assertEquals( len(output), len(reference) )
+        # cut begin & end 'cause they contain variyng data
+        self.assertTextEquals(output[150:1500], reference[150:1500])
--- a/web/test/unittest_views_editforms.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/test/unittest_views_editforms.py	Fri Sep 18 11:24:37 2009 +0200
@@ -53,6 +53,7 @@
                                ])
         self.assertListEquals(rbc(e, 'generic'),
                               [('primary_email', 'subject'),
+                               ('custom_workflow', 'subject'),
                                ('connait', 'subject'),
                                ('checked_by', 'object'),
                                ])
--- a/web/test/unittest_views_navigation.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/test/unittest_views_navigation.py	Fri Sep 18 11:24:37 2009 +0200
@@ -95,37 +95,37 @@
         html = navcomp.render()
 
 
-
-class ContentNavigationTC(EnvBasedTC):
+# XXX deactivate, contextual component has been removed
+# class ContentNavigationTC(EnvBasedTC):
 
-    def test_component_context(self):
-        view = mock_object(is_primary=lambda x: True)
-        rset = self.execute('CWUser X LIMIT 1')
-        req = self.request()
-        objs = self.vreg['contentnavigation'].possible_vobjects(
-            req, rset=rset, view=view, context='navtop')
-        # breadcrumbs should be in headers by default
-        clsids = set(obj.id for obj in objs)
-        self.failUnless('breadcrumbs' in clsids)
-        objs = self.vreg['contentnavigation'].possible_vobjects(
-            req, rset=rset, view=view, context='navbottom')
-        # breadcrumbs should _NOT_ be in footers by default
-        clsids = set(obj.id for obj in objs)
-        self.failIf('breadcrumbs' in clsids)
-        self.execute('INSERT CWProperty P: P pkey "contentnavigation.breadcrumbs.context", '
-                     'P value "navbottom"')
-        # breadcrumbs should now be in footers
-        req.cnx.commit()
-        objs = self.vreg['contentnavigation'].possible_vobjects(
-            req, rset=rset, view=view, context='navbottom')
+#     def test_component_context(self):
+#         view = mock_object(is_primary=lambda x: True)
+#         rset = self.execute('CWUser X LIMIT 1')
+#         req = self.request()
+#         objs = self.vreg['contentnavigation'].possible_vobjects(
+#             req, rset=rset, view=view, context='navtop')
+#         # breadcrumbs should be in headers by default
+#         clsids = set(obj.id for obj in objs)
+#         self.failUnless('breadcrumbs' in clsids)
+#         objs = self.vreg['contentnavigation'].possible_vobjects(
+#             req, rset=rset, view=view, context='navbottom')
+#         # breadcrumbs should _NOT_ be in footers by default
+#         clsids = set(obj.id for obj in objs)
+#         self.failIf('breadcrumbs' in clsids)
+#         self.execute('INSERT CWProperty P: P pkey "contentnavigation.breadcrumbs.context", '
+#                      'P value "navbottom"')
+#         # breadcrumbs should now be in footers
+#         req.cnx.commit()
+#         objs = self.vreg['contentnavigation'].possible_vobjects(
+#             req, rset=rset, view=view, context='navbottom')
 
-        clsids = [obj.id for obj in objs]
-        self.failUnless('breadcrumbs' in clsids)
-        objs = self.vreg['contentnavigation'].possible_vobjects(
-            req, rset=rset, view=view, context='navtop')
+#         clsids = [obj.id for obj in objs]
+#         self.failUnless('breadcrumbs' in clsids)
+#         objs = self.vreg['contentnavigation'].possible_vobjects(
+#             req, rset=rset, view=view, context='navtop')
 
-        clsids = [obj.id for obj in objs]
-        self.failIf('breadcrumbs' in clsids)
+#         clsids = [obj.id for obj in objs]
+#         self.failIf('breadcrumbs' in clsids)
 
 
 if __name__ == '__main__':
--- a/web/test/unittest_viewselector.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/test/unittest_viewselector.py	Fri Sep 18 11:24:37 2009 +0200
@@ -17,6 +17,8 @@
                                 treeview, idownloadable, wdoc, debug,
                                 cwproperties, workflow, xmlrss, csvexport)
 
+from cubes.folder import views as folderviews
+
 USERACTIONS = [('myprefs', actions.UserPreferencesAction),
                ('myinfos', actions.UserInfoAction),
                ('logout', actions.LogoutAction)]
@@ -76,7 +78,9 @@
                               ('propertiesform', cwproperties.CWPropertiesForm),
                               ('registry', startup.RegistryView),
                               ('schema', schema.SchemaView),
-                              ('systempropertiesform', cwproperties.SystemCWPropertiesForm)])
+                              ('systempropertiesform', cwproperties.SystemCWPropertiesForm),
+                              ('tree', folderviews.FolderTreeView),
+                              ])
 
     def test_possible_views_noresult(self):
         rset, req = self.env.get_rset_and_req('Any X WHERE X eid 999999')
@@ -254,9 +258,9 @@
         self.assertDictEqual(self.pactions(req, rset),
                              {'useractions': USERACTIONS,
                               'siteactions': SITEACTIONS,
-                              'mainactions': [('edit', actions.ModifyAction),
-                                              ('workflow', workflow.ViewWorkflowAction),],
+                              'mainactions': [('edit', actions.ModifyAction)],
                               'moreactions': [('managepermission', actions.ManagePermissionsAction),
+                                              ('addrelated', actions.AddRelatedActions),
                                               ('delete', actions.DeleteAction),
                                               ('copy', actions.CopyAction),
                                               ],
@@ -445,6 +449,7 @@
                               'siteactions': SITEACTIONS,
                               'mainactions': [('edit', actions.ModifyAction)],
                               'moreactions': [('managepermission', actions.ManagePermissionsAction),
+                                              ('addrelated', actions.AddRelatedActions),
                                               ('delete', actions.DeleteAction),
                                               ('copy', actions.CopyAction),
                                               ('testaction', CWETypeRQLAction),
@@ -456,6 +461,7 @@
                               'siteactions': SITEACTIONS,
                               'mainactions': [('edit', actions.ModifyAction)],
                               'moreactions': [('managepermission', actions.ManagePermissionsAction),
+                                              ('addrelated', actions.AddRelatedActions),
                                               ('delete', actions.DeleteAction),
                                               ('copy', actions.CopyAction),
                                               ],
--- a/web/uicfg.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/uicfg.py	Fri Sep 18 11:24:37 2009 +0200
@@ -149,6 +149,7 @@
 # * 'application'
 # * 'system'
 # * 'schema'
+# * 'hidden'
 # * 'subobject' (not displayed by default)
 
 indexview_etype_section = {'EmailAddress': 'subobject',
@@ -156,6 +157,7 @@
                            'CWGroup': 'system',
                            'CWPermission': 'system',
                            'CWCache': 'system',
+                           'BaseTransition': 'hidden',
                            }
 
 
--- a/web/views/__init__.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/__init__.py	Fri Sep 18 11:24:37 2009 +0200
@@ -8,8 +8,8 @@
 __docformat__ = "restructuredtext en"
 
 import os
+import sys
 import tempfile
-
 from rql import nodes
 
 
@@ -110,13 +110,14 @@
         self.cell_call()
 
     def cell_call(self, row=0, col=0):
-        self.row, self.col = row, col # in case one need it
+        self.row, self.col = row, col # in case one needs it
         _, tmpfile = tempfile.mkstemp('.png')
         try:
             self._generate(tmpfile)
-            self.w(open(tmpfile).read())
+            self.w(open(tmpfile, 'rb').read())
         finally:
             try:
                 os.unlink(tmpfile)
             except Exception, ex:
-                self.warning('cant delete %s: %s', tmpfile, ex)
+                if sys.platform != 'win32':
+                      self.warning("can't delete %s : %s" % (tmpfile, ex))
--- a/web/views/actions.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/actions.py	Fri Sep 18 11:24:37 2009 +0200
@@ -32,10 +32,10 @@
         # if user has no update right but it can modify some relation,
         # display action anyway
         for dummy in AutomaticEntityForm.esrelations_by_category(
-            entity, 'generic', 'add'):
+            entity, 'generic', 'add', strict=True):
             return 1
         for rschema, targetschemas, role in AutomaticEntityForm.erelations_by_category(
-            entity, ('primary', 'secondary'), 'add'):
+            entity, ('primary', 'secondary'), 'add', strict=True):
             if not rschema.is_final():
                 return 1
         return 0
@@ -226,6 +226,71 @@
         return self.build_url('add/%s' % self.rsettype)
 
 
+class AddRelatedActions(Action):
+    """fill 'addrelated' sub-menu of the actions box"""
+    id = 'addrelated'
+    __select__ = Action.__select__ & one_line_rset() & non_final_entity()
+
+    submenu = _('addrelated')
+    order = 20
+
+    def fill_menu(self, box, menu):
+        # when there is only one item in the sub-menu, replace the sub-menu by
+        # item's title prefixed by 'add'
+        menu.label_prefix = self.req._('add')
+        super(AddRelatedActions, self).fill_menu(box, menu)
+
+    def actual_actions(self):
+        entity = self.rset.get_entity(self.row or 0, self.col or 0)
+        eschema = entity.e_schema
+        for rschema, teschema, x in self.add_related_schemas(entity):
+            if x == 'subject':
+                label = 'add %s %s %s %s' % (eschema, rschema, teschema, x)
+                url = self.linkto_url(entity, rschema, teschema, 'object')
+            else:
+                label = 'add %s %s %s %s' % (teschema, rschema, eschema, x)
+                url = self.linkto_url(entity, rschema, teschema, 'subject')
+            yield self.build_action(self.req._(label), url)
+
+    def add_related_schemas(self, entity):
+        """this is actually used ui method to generate 'addrelated' actions from
+        the schema.
+
+        If you don't want any auto-generated actions, you should overrides this
+        method to return an empty list. If you only want some, you can configure
+        them by using uicfg.actionbox_appearsin_addmenu
+        """
+        appearsin_addmenu = uicfg.actionbox_appearsin_addmenu
+        req = self.req
+        eschema = entity.e_schema
+        for role, rschemas in (('subject', eschema.subject_relations()),
+                               ('object', eschema.object_relations())):
+            for rschema in rschemas:
+                if rschema.is_final():
+                    continue
+                # check the relation can be added as well
+                # XXX consider autoform_permissions_overrides?
+                if role == 'subject'and not rschema.has_perm(req, 'add',
+                                                             fromeid=entity.eid):
+                    continue
+                if role == 'object'and not rschema.has_perm(req, 'add',
+                                                            toeid=entity.eid):
+                    continue
+                # check the target types can be added as well
+                for teschema in rschema.targets(eschema, role):
+                    if not appearsin_addmenu.etype_get(eschema, rschema,
+                                                       role, teschema):
+                        continue
+                    if teschema.has_local_role('add') or teschema.has_perm(req, 'add'):
+                        yield rschema, teschema, role
+
+    def linkto_url(self, entity, rtype, etype, target):
+        return self.build_url('add/%s' % etype,
+                              __linkto='%s:%s:%s' % (rtype, entity.eid, target),
+                              __redirectpath=entity.rest_path(), # should not be url quoted!
+                              __redirectvid=self.req.form.get('vid', ''))
+
+
 # logged user actions #########################################################
 
 class UserPreferencesAction(Action):
--- a/web/views/autoform.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/autoform.py	Fri Sep 18 11:24:37 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
@@ -49,9 +48,12 @@
 
     @classmethod
     def erelations_by_category(cls, entity, categories=None, permission=None,
-                               rtags=None):
+                               rtags=None, strict=False):
         """return a list of (relation schema, target schemas, role) matching
         categories and permission
+
+        `strict`:
+          bool telling if having local role is enough (strict = False) or not
         """
         if categories is not None:
             if not isinstance(categories, (list, tuple, set, frozenset)):
@@ -66,6 +68,7 @@
             eid = entity.eid
         else:
             eid = None
+            strict = False
         for rschema, targetschemas, role in eschema.relation_definitions(True):
             # check category first, potentially lower cost than checking
             # permission which may imply rql queries
@@ -85,7 +88,7 @@
                     if not rschema.has_perm(entity.req, permission, eid):
                         continue
                 elif role == 'subject':
-                    if not ((eid is None and rschema.has_local_role(permission)) or
+                    if not ((not strict and rschema.has_local_role(permission)) or
                             rschema.has_perm(entity.req, permission, fromeid=eid)):
                         continue
                     # on relation with cardinality 1 or ?, we need delete perm as well
@@ -97,7 +100,7 @@
                                                  toeid=entity.related(rschema.type, role)[0][0])):
                         continue
                 elif role == 'object':
-                    if not ((eid is None and rschema.has_local_role(permission)) or
+                    if not ((not strict and rschema.has_local_role(permission)) or
                             rschema.has_perm(entity.req, permission, toeid=eid)):
                         continue
                     # on relation with cardinality 1 or ?, we need delete perm as well
@@ -111,7 +114,8 @@
             yield (rschema, targetschemas, role)
 
     @classmethod
-    def esrelations_by_category(cls, entity, categories=None, permission=None):
+    def esrelations_by_category(cls, entity, categories=None, permission=None,
+                                strict=False):
         """filter out result of relations_by_category(categories, permission) by
         removing final relations
 
@@ -119,7 +123,7 @@
         """
         result = []
         for rschema, ttypes, role in cls.erelations_by_category(
-            entity, categories, permission):
+            entity, categories, permission, strict=strict):
             if rschema.is_final():
                 continue
             result.append((rschema.display_name(entity.req, role), rschema, role))
@@ -133,7 +137,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 +167,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')
@@ -197,14 +201,15 @@
         return self.erelations_by_category(self.edited_entity, True, 'add',
                                            self.rinlined)
 
-    def srelations_by_category(self, categories=None, permission=None):
+    def srelations_by_category(self, categories=None, permission=None,
+                               strict=False):
         """filter out result of relations_by_category(categories, permission) by
         removing final relations
 
         return a sorted list of (relation's label, relation'schema, role)
         """
         return self.esrelations_by_category(self.edited_entity, categories,
-                                           permission)
+                                           permission, strict=strict)
 
     def action(self):
         """return the form's action attribute. Default to validateform if not
@@ -236,7 +241,8 @@
         """
         entity = self.edited_entity
         pending_deletes = self.req.get_pending_deletes(entity.eid)
-        for label, rschema, role in self.srelations_by_category('generic', 'add'):
+        for label, rschema, role in self.srelations_by_category('generic', 'add',
+                                                                strict=True):
             relatedrset = entity.related(rschema, role, limit=self.related_limit)
             if rschema.has_perm(self.req, 'delete'):
                 toggleable_rel_link_func = editforms.toggleable_relation_link
@@ -330,7 +336,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 +359,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	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/basecomponents.py	Fri Sep 18 11:24:37 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'&#160;|&#160;'.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):
+        entity = self.entity(0,0)
+        url = entity.absolute_url(vid=vid, __template='pdf-main-template')
+        self.w(u'<a href="%s" class="otherView"><img src="data/pdf_icon.gif" alt="%s"/></a>' %
+               (xml_escape(url), self.req._('download page as pdf')))
+
+
 
 def registration_callback(vreg):
     vreg.register_all(globals().values(), __name__, (SeeAlsoVComponent,))
--- a/web/views/basecontrollers.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/basecontrollers.py	Fri Sep 18 11:24:37 2009 +0200
@@ -17,9 +17,8 @@
 from logilab.common.decorators import cached
 
 from cubicweb import NoSelectableObject, ValidationError, ObjectNotFound, typed_eid
-from cubicweb.utils import strptime
+from cubicweb.utils import strptime, CubicWebJsonEncoder
 from cubicweb.selectors import yes, match_user_groups
-from cubicweb.view import STRICT_DOCTYPE, STRICT_DOCTYPE_NOEXT
 from cubicweb.common.mail import format_mail
 from cubicweb.web import ExplicitLogin, Redirect, RemoteCallFailed, json_dumps
 from cubicweb.web.controller import Controller
@@ -181,48 +180,54 @@
             break
     return (foreid, ex.errors)
 
+
 def _validate_form(req, vreg):
     # XXX should use the `RemoteCallFailed` mechanism
     try:
         ctrl = vreg['controllers'].select('edit', req=req)
     except NoSelectableObject:
-        return (False, {None: req._('not authorized')})
+        return (False, {None: req._('not authorized')}, None)
     try:
         ctrl.publish(None)
     except ValidationError, ex:
-        return (False, _validation_error(req, ex))
+        return (False, _validation_error(req, ex), ctrl._edited_entity)
     except Redirect, ex:
+        if ctrl._edited_entity:
+            ctrl._edited_entity.complete()
         try:
             req.cnx.commit() # ValidationError may be raise on commit
         except ValidationError, ex:
-            return (False, _validation_error(req, ex))
+            return (False, _validation_error(req, ex), ctrl._edited_entity)
         else:
-            return (True, ex.location)
+            return (True, ex.location, ctrl._edited_entity)
     except Exception, ex:
         req.cnx.rollback()
         req.exception('unexpected error while validating form')
-        return (False, req._(str(ex).decode('utf-8')))
-    return (False, '???')
+        return (False, req._(str(ex).decode('utf-8')), ctrl._edited_entity)
+    return (False, '???', None)
 
 
 class FormValidatorController(Controller):
     id = 'validateform'
 
-    def response(self, domid, status, args):
+    def response(self, domid, status, args, entity):
+        callback = str(self.req.form.get('__onsuccess', 'null'))
+        errback = str(self.req.form.get('__onfailure', 'null'))
         self.req.set_content_type('text/html')
-        jsargs = simplejson.dumps( (status, args) )
+        jsargs = simplejson.dumps((status, args, entity), cls=CubicWebJsonEncoder)
         return """<script type="text/javascript">
- window.parent.handleFormValidationResponse('%s', null, null, %s);
-</script>""" %  (domid, jsargs)
+ wp = window.parent;
+ window.parent.handleFormValidationResponse('%s', %s, %s, %s);
+</script>""" %  (domid, callback, errback, jsargs)
 
     def publish(self, rset=None):
         self.req.json_request = True
         # XXX unclear why we have a separated controller here vs
         # js_validate_form on the json controller
-        status, args = _validate_form(self.req, self.vreg)
+        status, args, entity = _validate_form(self.req, self.vreg)
         domid = self.req.form.get('__domid', 'entityForm').encode(
             self.req.encoding)
-        return self.response(domid, status, args)
+        return self.response(domid, status, args, entity)
 
 
 class JSonController(Controller):
@@ -388,7 +393,7 @@
 
     @jsonize
     def js_edit_field(self, action, names, values, rtype, eid, default):
-        success, args = self.validate_form(action, names, values)
+        success, args, _ = self.validate_form(action, names, values)
         if success:
             # Any X,N where we don't seem to use N is an optimisation
             # printable_value won't need to query N again
--- a/web/views/baseforms.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/baseforms.py	Fri Sep 18 11:24:37 2009 +0200
@@ -216,7 +216,8 @@
             return u''
         req = self.req
         _ = self.req._
-        label = u'%s :' % _('This %s' % entity.e_schema).capitalize()
+        __ = _
+        label = u'%s :' % __('This %s' % entity.e_schema).capitalize()
         eid = entity.eid
         html = []
         w = html.append
--- a/web/views/basetemplates.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/basetemplates.py	Fri Sep 18 11:24:37 2009 +0200
@@ -13,8 +13,8 @@
 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
+from cubicweb.schema import display_name
 
 # main templates ##############################################################
 
@@ -266,6 +266,45 @@
             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.ext.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)
+            section = self.req.form.pop('section', 'contentmain')
+            pdf = self.to_pdf(self._stream, section)
+            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):
+            # 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):
@@ -494,4 +533,4 @@
 
 ## vregistry registration callback ############################################
 def registration_callback(vreg):
-    vreg.register_all(globals().values(), modname=__name__)
+    vreg.register_all(globals().values(), __name__)
--- a/web/views/baseviews.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/baseviews.py	Fri Sep 18 11:24:37 2009 +0200
@@ -68,7 +68,7 @@
     _('%d minutes')
     _('%d seconds')
 
-    def cell_call(self, row, col, props=None, displaytime=False, format='text/html'):
+    def cell_call(self, row, col, props=None, format='text/html'):
         etype = self.rset.description[row][col]
         value = self.rset.rows[row][col]
 
@@ -107,7 +107,7 @@
             else:
                 self.w(self.req.__('%%d%sseconds' % space) % int(value.seconds))
             return
-        self.wdata(printable_value(self.req, etype, value, props, displaytime=displaytime))
+        self.wdata(printable_value(self.req, etype, value, props))
 
 
 # XXX deprecated
--- a/web/views/bookmark.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/bookmark.py	Fri Sep 18 11:24:37 2009 +0200
@@ -12,9 +12,12 @@
 from cubicweb import Unauthorized
 from cubicweb.selectors import implements
 from cubicweb.web.htmlwidgets import BoxWidget, BoxMenu, RawBoxItem
-from cubicweb.web import action, box
+from cubicweb.web import action, box, uicfg
 from cubicweb.web.views import primary
 
+_abaa = uicfg.actionbox_appearsin_addmenu
+_abaa.tag_subject_of(('*', 'bookmarked_by', '*'), False)
+_abaa.tag_object_of(('*', 'bookmarked_by', '*'), False)
 
 class FollowAction(action.Action):
     id = 'follow'
--- a/web/views/boxes.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/boxes.py	Fri Sep 18 11:24:37 2009 +0200
@@ -16,6 +16,8 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from warnings import warn
+
 from logilab.mtconverter import xml_escape
 
 from cubicweb.selectors import match_user_groups, non_final_entity
@@ -26,7 +28,7 @@
 from cubicweb.web.box import BoxTemplate
 
 
-class EditBox(BoxTemplate):
+class EditBox(BoxTemplate): # XXX rename to ActionsBox
     """
     box with all actions impacting the entity displayed: edit, copy, delete
     change state, add related entities
@@ -36,9 +38,6 @@
 
     title = _('actions')
     order = 2
-    # class attributes below are actually stored in the uicfg module since we
-    # don't want them to be reloaded
-    appearsin_addmenu = uicfg.actionbox_appearsin_addmenu
 
     def call(self, view=None, **kwargs):
         _ = self.req._
@@ -50,118 +49,59 @@
                 etypelabel = display_name(self.req, iter(etypes).next(), plural)
                 title = u'%s - %s' % (title, etypelabel.lower())
         box = BoxWidget(title, self.id, _class="greyBoxFrame")
+        self._menus_in_order = []
+        self._menus_by_id = {}
         # build list of actions
         actions = self.vreg['actions'].possible_actions(self.req, self.rset,
                                                         view=view)
-        add_menu = BoxMenu(_('add')) # 'addrelated' category
-        other_menu = BoxMenu(_('more actions')) # 'moreactions' category
-        searchstate = self.req.search_state[0]
-        for category, menu in (('mainactions', box),
-                               ('addrelated', add_menu),
-                               ('moreactions', other_menu)):
+        other_menu = self._get_menu('moreactions', _('more actions'))
+        for category, defaultmenu in (('mainactions', box),
+                                      ('moreactions', other_menu),
+                                      ('addrelated', None)):
             for action in actions.get(category, ()):
-                menu.append(self.box_action(action))
-        if self.rset and self.rset.rowcount == 1 and \
-               not self.schema[self.rset.description[0][0]].is_final() and \
-               searchstate == 'normal':
-            entity = self.rset.get_entity(0, 0)
-            #entity.complete()
-            if add_menu.items:
-                self.info('explicit actions defined, ignoring potential rtags for %s',
-                          entity.e_schema)
-            else:
-                # some addrelated actions may be specified but no one is selectable
-                # in which case we should not fallback to schema_actions. The proper
-                # way to avoid this is to override add_related_schemas() on the
-                # entity class to return an empty list
-                for action in self.schema_actions(entity):
-                    add_menu.append(action)
-            self.workflow_actions(entity, box)
+                if category == 'addrelated':
+                    warn('"addrelated" category is deprecated, use "moreaction"'
+                         ' category w/ "addrelated" submenu',
+                         DeprecationWarning)
+                    defaultmenu = self._get_menu('addrelated', _('add'), _('add'))
+                if action.submenu:
+                    menu = self._get_menu(action.submenu)
+                else:
+                    menu = defaultmenu
+                action.fill_menu(self, menu)
         if box.is_empty() and not other_menu.is_empty():
             box.items = other_menu.items
             other_menu.items = []
-        self.add_submenu(box, add_menu, _('add'))
-        self.add_submenu(box, other_menu)
+        else: # ensure 'more actions' menu appears last
+            self._menus_in_order.remove(other_menu)
+            self._menus_in_order.append(other_menu)
+        for submenu in self._menus_in_order:
+            self.add_submenu(box, submenu)
         if not box.is_empty():
             box.render(self.w)
 
+    def _get_menu(self, id, title=None, label_prefix=None):
+        try:
+            return self._menus_by_id[id]
+        except KeyError:
+            if title is None:
+                title = self.req._(id)
+            self._menus_by_id[id] = menu = BoxMenu(title)
+            menu.label_prefix = label_prefix
+            self._menus_in_order.append(menu)
+            return menu
+
     def add_submenu(self, box, submenu, label_prefix=None):
-        if len(submenu.items) == 1:
+        appendanyway = getattr(submenu, 'append_anyway', False)
+        if len(submenu.items) == 1 and not appendanyway:
             boxlink = submenu.items[0]
-            if label_prefix:
-                boxlink.label = u'%s %s' % (label_prefix, boxlink.label)
+            if submenu.label_prefix:
+                boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
             box.append(boxlink)
         elif submenu.items:
             box.append(submenu)
-
-    def schema_actions(self, entity):
-        user = self.req.user
-        actions = []
-        _ = self.req._
-        eschema = entity.e_schema
-        for rschema, teschema, x in self.add_related_schemas(entity):
-            if x == 'subject':
-                label = 'add %s %s %s %s' % (eschema, rschema, teschema, x)
-                url = self.linkto_url(entity, rschema, teschema, 'object')
-            else:
-                label = 'add %s %s %s %s' % (teschema, rschema, eschema, x)
-                url = self.linkto_url(entity, rschema, teschema, 'subject')
-            actions.append(self.mk_action(_(label), url))
-        return actions
-
-    def add_related_schemas(self, entity):
-        """this is actually used ui method to generate 'addrelated' actions from
-        the schema.
-
-        If you're using explicit 'addrelated' actions for an entity types, you
-        should probably overrides this method to return an empty list else you
-        may get some unexpected actions.
-        """
-        req = self.req
-        eschema = entity.e_schema
-        for role, rschemas in (('subject', eschema.subject_relations()),
-                               ('object', eschema.object_relations())):
-            for rschema in rschemas:
-                if rschema.is_final():
-                    continue
-                # check the relation can be added as well
-                # XXX consider autoform_permissions_overrides?
-                if role == 'subject'and not rschema.has_perm(req, 'add',
-                                                             fromeid=entity.eid):
-                    continue
-                if role == 'object'and not rschema.has_perm(req, 'add',
-                                                            toeid=entity.eid):
-                    continue
-                # check the target types can be added as well
-                for teschema in rschema.targets(eschema, role):
-                    if not self.appearsin_addmenu.etype_get(eschema, rschema,
-                                                            role, teschema):
-                        continue
-                    if teschema.has_local_role('add') or teschema.has_perm(req, 'add'):
-                        yield rschema, teschema, role
-
-
-    def workflow_actions(self, entity, box):
-        if 'in_state' in entity.e_schema.subject_relations() and entity.in_state:
-            _ = self.req._
-            state = entity.in_state[0]
-            menu_title = u'%s: %s' % (_('state'), state.view('text'))
-            menu_items = []
-            for tr in state.transitions(entity):
-                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')
-            menu_items.append(self.mk_action(_('view workflow'), wfurl))
-            wfurl = entity.absolute_url(vid='wfhistory')
-            menu_items.append(self.mk_action(_('view history'), wfurl))
-            box.append(BoxMenu(menu_title, menu_items))
-        return None
-
-    def linkto_url(self, entity, rtype, etype, target):
-        return self.build_url(vid='creation', etype=etype,
-                              __linkto='%s:%s:%s' % (rtype, entity.eid, target),
-                              __redirectpath=entity.rest_path(), # should not be url quoted!
-                              __redirectvid=self.req.form.get('vid', ''))
+        elif appendanyway:
+            box.append(RawBoxItem(xml_escape(submenu.label)))
 
 
 class SearchBox(BoxTemplate):
--- a/web/views/csvexport.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/csvexport.py	Fri Sep 18 11:24:37 2009 +0200
@@ -51,7 +51,7 @@
                                         row=rowindex, col=colindex)
                 else:
                     content = self.view('final', rset,
-                                        displaytime=True, format='text/plain',
+                                        format='text/plain',
                                         row=rowindex, col=colindex)
                 csvrow.append(content)
             writer.writerow(csvrow)
--- a/web/views/editcontroller.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/editcontroller.py	Fri Sep 18 11:24:37 2009 +0200
@@ -51,7 +51,8 @@
         try:
             methodname = form.pop('__method', None)
             for eid in req.edited_eids():
-                formparams = req.extract_entity_params(eid)
+                # __type and eid
+                formparams = req.extract_entity_params(eid, minparams=2)
                 if methodname is not None:
                     entity = req.entity_from_eid(eid)
                     method = getattr(entity, methodname)
--- a/web/views/editviews.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/editviews.py	Fri Sep 18 11:24:37 2009 +0200
@@ -50,10 +50,10 @@
         # them. Use fetch_order and not fetch_unrelated_order as sort method
         # since the latter is mainly there to select relevant items in the combo
         # box, it doesn't give interesting result in this context
-        rql = entity.unrelated_rql(rtype, etype, role,
+        rql, args = entity.unrelated_rql(rtype, etype, role,
                                    ordermethod='fetch_order',
                                    vocabconstraints=False)
-        rset = self.req.execute(rql, {'x' : entity.eid}, 'x')
+        rset = self.req.execute(rql, args, tuple(args))
         return rset, 'list', "search-associate-content", True
 
 
@@ -198,9 +198,9 @@
     """same as FinalView but enables inplace-edition when possible"""
     id = 'editable-final'
 
-    def cell_call(self, row, col, props=None, displaytime=False):
+    def cell_call(self, row, col, props=None):
         entity, rtype = self.rset.related_entity(row, col)
         if entity is not None:
             self.w(entity.view('reledit', rtype=rtype))
         else:
-            super(EditableFinalView, self).cell_call(row, col, props, displaytime)
+            super(EditableFinalView, self).cell_call(row, col, props)
--- a/web/views/emailaddress.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/emailaddress.py	Fri Sep 18 11:24:37 2009 +0200
@@ -24,17 +24,9 @@
     def render_entity_attributes(self, entity):
         self.w(u'<h3>')
         entity.view('oneline', w=self.w)
-        if not entity.canonical:
-            canonemailaddr = entity.canonical_form()
-            if canonemailaddr:
-                self.w(u'&#160;(<i>%s</i>)' % canonemailaddr.view('oneline'))
-            self.w(u'</h3>')
-        elif entity.identical_to:
-            self.w(u'</h3>')
-            identicaladdr = [e.view('oneline') for e in entity.identical_to]
-            self.field('identical_to', ', '.join(identicaladdr))
-        else:
-            self.w(u'</h3>')
+        if entity.prefered:
+            self.w(u'&#160;(<i>%s</i>)' % entity.prefered.view('oneline'))
+        self.w(u'</h3>')
         try:
             persons = entity.reverse_primary_email
         except Unauthorized:
@@ -76,7 +68,7 @@
     __select__ = implements('EmailAddress')
 
     def cell_call(self, row, col, **kwargs):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         if entity.reverse_primary_email:
             self.w(u'<b>')
         if entity.alias:
@@ -88,6 +80,7 @@
         if entity.reverse_primary_email:
             self.w(u'</b>')
 
+
 class EmailAddressMailToView(baseviews.OneLineView):
     """A one line view that builds a user clickable URL for an email with
     'mailto:'"""
@@ -96,7 +89,7 @@
     __select__ = implements('EmailAddress')
 
     def cell_call(self, row, col, **kwargs):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         if entity.reverse_primary_email:
             self.w(u'<b>')
         if entity.alias:
@@ -119,4 +112,4 @@
     __select__ = implements('EmailAddress')
 
     def cell_call(self, row, col, **kwargs):
-        self.w(self.entity(row, col).display_address())
+        self.w(self.rset.get_entity(row, col).display_address())
--- a/web/views/formrenderers.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/formrenderers.py	Fri Sep 18 11:24:37 2009 +0200
@@ -87,7 +87,10 @@
     def render_label(self, form, field):
         if field.label is None:
             return u''
-        label = self.req._(field.label)
+        if isinstance(field.label, tuple): # i.e. needs contextual translation
+            label = self.req.pgettext(*field.label)
+        else:
+            label = self.req._(field.label)
         attrs = {'for': form.context[field]['id']}
         if field.required:
             attrs['class'] = 'required'
@@ -407,12 +410,13 @@
             super(EntityFormRenderer, self).render_buttons(w, form)
 
     def relations_form(self, w, form):
-        srels_by_cat = form.srelations_by_category('generic', 'add')
+        srels_by_cat = form.srelations_by_category('generic', 'add', strict=True)
         if not srels_by_cat:
             return u''
         req = self.req
         _ = req._
-        label = u'%s :' % _('This %s' % form.edited_entity.e_schema).capitalize()
+        __ = _
+        label = u'%s :' % __('This %s' % form.edited_entity.e_schema).capitalize()
         eid = form.edited_entity.eid
         w(u'<fieldset class="subentity">')
         w(u'<legend class="iformTitle">%s</legend>' % label)
@@ -484,7 +488,7 @@
 
     def inline_relation_form(self, w, form, rschema, targettype, role):
         entity = form.edited_entity
-        __ = self.req.__
+        __ = self.req.pgettext
         w(u'<div id="inline%sslot">' % rschema)
         existant = entity.has_eid() and entity.related(rschema)
         if existant:
@@ -512,8 +516,9 @@
                 entity.eid, targettype, rschema, role)
             if card in '1?':
                 js = "toggleVisibility('%s'); %s" % (divid, js)
+            ctx = 'inlined:%s.%s.%s' % (entity.e_schema, rschema, role)
             w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>'
-              % (rschema, entity.eid, js, __('add a %s' % targettype)))
+              % (rschema, entity.eid, js, __(ctx, 'add a %s' % targettype)))
             w(u'</div>')
             w(u'<div class="trame_grise">&#160;</div>')
         w(u'</div>')
--- a/web/views/forms.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/forms.py	Fri Sep 18 11:24:37 2009 +0200
@@ -486,6 +486,7 @@
         #       cases, it doesn't make sense to sort results afterwards.
         return vocabfunc(rtype, limit)
 
+    # XXX should be on the field, no?
     def subject_relation_vocabulary(self, rtype, limit=None):
         """defaut vocabulary method for the given relation, looking for
         relation's object entities (i.e. self is the subject)
@@ -529,25 +530,6 @@
                 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 srelations_by_category(self, categories=None, permission=None):
         return ()
 
@@ -556,7 +538,7 @@
 
 
 class CompositeForm(FieldsForm):
-    """form composed for sub-forms"""
+    """form composed of sub-forms"""
     id = 'composite'
     form_renderer_id = id
 
@@ -568,3 +550,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/management.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/management.py	Fri Sep 18 11:24:37 2009 +0200
@@ -79,7 +79,7 @@
     def cell_call(self, row, col):
         self.req.add_js('cubicweb.edition.js')
         self.req.add_css('cubicweb.acl.css')
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         w = self.w
         _ = self.req._
         w(u'<h1><span class="etype">%s</span> <a href="%s">%s</a></h1>'
@@ -237,10 +237,8 @@
             w(u"<b>Package %s version:</b> %s<br/>\n" % (cube, cubeversion))
             cversions.append((cube, cubeversion))
         w(u"</div>")
-        # creates a bug submission link if SUBMIT_URL is set
-        submiturl = self.config['submit-url']
-        submitmail = self.config['submit-mail']
-        if submiturl or submitmail:
+        # creates a bug submission link if submit-mail is set
+        if self.config['submit-mail']:
             form = self.vreg['forms'].select('base', self.req, rset=None,
                                              mainform=False)
             binfo = text_error_description(ex, excinfo, req, eversion, cversions)
@@ -248,15 +246,9 @@
                                  # we must use a text area to keep line breaks
                                  widget=wdgs.TextArea({'class': 'hidden'}))
             form.form_add_hidden('__bugreporting', '1')
-            if submitmail:
-                form.form_buttons = [wdgs.SubmitButton(MAIL_SUBMIT_MSGID)]
-                form.action = req.build_url('reportbug')
-                w(form.form_render())
-            if submiturl:
-                form.form_add_hidden('description_format', 'text/rest')
-                form.form_buttons = [wdgs.SubmitButton(SUBMIT_MSGID)]
-                form.action = submiturl
-                w(form.form_render())
+            form.form_buttons = [wdgs.SubmitButton(MAIL_SUBMIT_MSGID)]
+            form.action = req.build_url('reportbug')
+            w(form.form_render())
 
 
 def exc_message(ex, encoding):
--- a/web/views/primary.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/primary.py	Fri Sep 18 11:24:37 2009 +0200
@@ -232,9 +232,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/schema.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/schema.py	Fri Sep 18 11:24:37 2009 +0200
@@ -12,8 +12,9 @@
 from logilab.mtconverter import xml_escape
 from yams import BASE_TYPES, schema2dot as s2d
 
-from cubicweb.selectors import implements, yes, match_user_groups
-from cubicweb.schema import META_RTYPES, SCHEMA_TYPES
+from cubicweb.selectors import (implements, yes, match_user_groups,
+                                has_related_entities)
+from cubicweb.schema import META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES
 from cubicweb.schemaviewer import SchemaViewer
 from cubicweb.view import EntityView, StartupView
 from cubicweb.common import tags, uilib
@@ -22,7 +23,7 @@
 from cubicweb.web.views import primary, baseviews, tabs, management
 
 ALWAYS_SKIP_TYPES = BASE_TYPES | SCHEMA_TYPES
-SKIP_TYPES = ALWAYS_SKIP_TYPES | META_RTYPES
+SKIP_TYPES = ALWAYS_SKIP_TYPES | META_RTYPES | SYSTEM_RTYPES
 SKIP_TYPES.update(set(('Transition', 'State', 'TrInfo',
                        'CWUser', 'CWGroup',
                        'CWCache', 'CWProperty', 'CWPermission',
@@ -199,7 +200,7 @@
     __select__ = implements('CWEType')
 
     def cell_call(self, row, col, **kwargs):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         final = entity.final
         if final:
             self.w(u'<em class="finalentity">')
@@ -227,7 +228,7 @@
     __select__ = EntityView.__select__ & implements('CWEType')
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         self.w(u'<h2>%s</h2>' % _('Attributes'))
         rset = self.req.execute('Any N,F,D,I,J,DE,A '
                                 'ORDERBY AA WHERE A is CWAttribute, '
@@ -263,18 +264,19 @@
     __select__ = EntityView.__select__ & implements('CWEType')
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         url = entity.absolute_url(vid='schemagraph')
         self.w(u'<img src="%s" alt="%s"/>' % (
             xml_escape(url),
             xml_escape(self.req._('graphical schema for %s') % entity.name)))
 
+
 class CWETypeSPermView(EntityView):
     id = 'cwetype-schema-permissions'
     __select__ = EntityView.__select__ & implements('CWEType')
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         self.w(u'<h2>%s</h2>' % _('Add permissions'))
         rset = self.req.execute('Any P WHERE X add_permission P, '
                                 'X eid %(x)s',
@@ -296,18 +298,28 @@
                                 {'x': entity.eid})
         self.wview('outofcontext', rset, 'null')
 
+
 class CWETypeSWorkflowView(EntityView):
     id = 'cwetype-workflow'
-    __select__ = EntityView.__select__ & implements('CWEType')
+    __select__ = (EntityView.__select__ & implements('CWEType') &
+                  has_related_entities('workflow_of', 'object'))
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
-        if entity.reverse_state_of:
-            self.w(u'<img src="%s" alt="%s"/>' % (
-                    xml_escape(entity.absolute_url(vid='ewfgraph')),
-                    xml_escape(self.req._('graphical workflow for %s') % entity.name)))
-        else:
-            self.w(u'<p>%s</p>' % _('There is no workflow defined for this entity.'))
+        entity = self.rset.get_entity(row, col)
+        if entity.default_workflow:
+            wf = entity.default_workflow[0]
+            self.w(u'<h1>%s (%s)</h1>' % (wf.name, self.req._('default')))
+            self.wf_image(wf)
+        for altwf in entity.reverse_workflow_of:
+            if altwf.eid == wf.eid:
+                continue
+            self.w(u'<h1>%s</h1>' % altwf.name)
+            self.wf_image(altwf)
+
+    def wf_image(self, wf):
+        self.w(u'<img src="%s" alt="%s"/>' % (
+            xml_escape(wf.absolute_url(vid='wfgraph')),
+            xml_escape(self.req._('graphical representation of %s') % wf.name)))
 
 # CWRType ######################################################################
 
@@ -377,7 +389,7 @@
 
     def _generate(self, tmpfile):
         """display schema information for an entity"""
-        entity = self.entity(self.row, self.col)
+        entity = self.rset.get_entity(self.row, self.col)
         eschema = self.vreg.schema.eschema(entity.name)
         visitor = OneHopESchemaVisitor(self.req, eschema,
                                        skiptypes=skip_types(self.req))
@@ -389,7 +401,7 @@
 
     def _generate(self, tmpfile):
         """display schema information for an entity"""
-        entity = self.entity(self.row, self.col)
+        entity = self.rset.get_entity(self.row, self.col)
         rschema = self.vreg.schema.rschema(entity.name)
         visitor = OneHopRSchemaVisitor(self.req, rschema)
         s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
--- a/web/views/tableview.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/tableview.py	Fri Sep 18 11:24:37 2009 +0200
@@ -144,8 +144,11 @@
             actions += self.show_hide_actions(divid, True)
         self.w(u'<div id="%s"' % divid)
         if displayactions:
-            for action in self.vreg['actions'].possible_actions(req, self.rset).get('mainactions', ()):
-                actions.append( (action.url(), req._(action.title), action.html_class(), None) )
+            actionsbycat = self.vreg['actions'].possible_actions(req, self.rset)
+            for action in actionsbycat.get('mainactions', ()):
+                for action in action.actual_actions():
+                    actions.append( (action.url(), req._(action.title),
+                                     action.html_class(), None) )
             self.w(u' cubicweb:displayactions="1">') # close <div tag
         else:
             self.w(u'>') # close <div tag
--- a/web/views/tabs.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/tabs.py	Fri Sep 18 11:24:37 2009 +0200
@@ -71,6 +71,9 @@
         return str('%s_active_tab' % self.config.appid)
 
     def active_tab(self, tabs, default):
+        formtab = self.req.form.get('tab')
+        if formtab in tabs:
+            return formtab
         cookies = self.req.get_cookie()
         cookiename = self.cookie_name
         activetab = cookies.get(cookiename)
@@ -112,7 +115,7 @@
         w(u'<ul>')
         for tab in tabs:
             w(u'<li>')
-            w(u'<a href="#as-%s">' % tab)
+            w(u'<a href="#%s">' % tab)
             w(u'<span onclick="set_tab(\'%s\', \'%s\')">' % (tab, self.cookie_name))
             w(self.req._(tab))
             w(u'</span>')
@@ -121,7 +124,7 @@
         w(u'</ul>')
         w(u'</div>')
         for tab in tabs:
-            w(u'<div id="as-%s">' % tab)
+            w(u'<div id="%s">' % tab)
             if entity:
                 self.lazyview(tab, eid=entity.eid)
             else:
@@ -153,7 +156,7 @@
         role = 'subject'
         vid = 'gallery'
 
-    in this example, entities related to project entity by the'screenshot'
+    in this example, entities related to project entity by the 'screenshot'
     relation (where the project is subject of the relation) will be displayed
     using the 'gallery' view.
     """
--- a/web/views/treeview.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/treeview.py	Fri Sep 18 11:24:37 2009 +0200
@@ -24,7 +24,7 @@
     css_classes = 'treeview widget'
     title = _('tree view')
 
-    def call(self, subvid=None, treeid=None, initial_load=True, initial_thru_ajax=False):
+    def _init_params(self, subvid, treeid, initial_load, initial_thru_ajax):
         if subvid is None:
             subvid = self.req.form.pop('treesubvid', 'oneline') # consume it
         if treeid is None:
@@ -34,6 +34,11 @@
                 treeid = make_uid('throw away uid')
         toplevel_thru_ajax = self.req.form.pop('treeview_top', False) or initial_thru_ajax
         toplevel = toplevel_thru_ajax or (initial_load and not self.req.form.get('fname'))
+        return subvid, treeid, toplevel_thru_ajax, toplevel
+
+    def call(self, subvid=None, treeid=None, initial_load=True, initial_thru_ajax=False):
+        subvid, treeid, toplevel_thru_ajax, toplevel = self._init_params(
+            subvid, treeid, initial_load, initial_thru_ajax)
         ulid = ' '
         if toplevel:
             ulid = ' id="tree-%s"' % treeid
--- a/web/views/workflow.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/workflow.py	Fri Sep 18 11:24:37 2009 +0200
@@ -15,55 +15,68 @@
 from logilab.common.graph import escape, GraphGenerator, DotBackend
 
 from cubicweb import Unauthorized, view
-from cubicweb.selectors import (implements, has_related_entities,
+from cubicweb.selectors import (implements, has_related_entities, one_line_rset,
                                 relation_possible, match_form_params)
 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.views import TmpFileViewMixin, forms
+from cubicweb.schema import display_name
+from cubicweb.web import uicfg, stdmsgs, action, component, form, action
+from cubicweb.web import formfields as ff, formwidgets as fwdgs
+from cubicweb.web.views import TmpFileViewMixin, forms, primary
+
+_abaa = uicfg.actionbox_appearsin_addmenu
+_abaa.tag_subject_of(('BaseTransition', 'condition', 'RQLExpression'), False)
+_abaa.tag_subject_of(('State', 'allowed_transition', 'BaseTransition'), False)
+_abaa.tag_object_of(('SubWorkflowExitPoint', 'destination_state', 'State'),
+                    False)
+_abaa.tag_object_of(('State', 'state_of', 'Workflow'), True)
+_abaa.tag_object_of(('Transition', 'transition_of', 'Workflow'), True)
+_abaa.tag_object_of(('WorkflowTransition', 'transition_of', 'Workflow'), True)
+
+_afs = uicfg.autoform_section
+_afs.tag_subject_of(('TrInfo', 'to_state', '*'), 'generated')
+_afs.tag_subject_of(('TrInfo', 'from_state', '*'), 'generated')
+_afs.tag_object_of(('State', 'allowed_transition', '*'), 'primary')
 
 
 # IWorkflowable views #########################################################
 
-class ChangeStateForm(forms.EntityFieldsForm):
+class ChangeStateForm(forms.CompositeEntityForm):
     id = 'changestate'
 
     form_renderer_id = 'base' # don't want EntityFormRenderer
-    form_buttons = [SubmitButton(stdmsgs.YES),
-                     Button(stdmsgs.NO, cwaction='cancel')]
-
-    __method = StringField(name='__method', initial='set_state',
-                           widget=HiddenInput)
-    state = StringField(eidparam=True, widget=HiddenInput)
-    trcomment = RichTextField(label=_('comment:'), eidparam=True)
+    form_buttons = [fwdgs.SubmitButton(stdmsgs.YES),
+                    fwdgs.Button(stdmsgs.NO, cwaction='cancel')]
 
 
-class ChangeStateFormView(FormViewMixIn, view.EntityView):
+class ChangeStateFormView(form.FormViewMixIn, view.EntityView):
     id = 'statuschange'
     title = _('status change')
     __select__ = implements(IWorkflowable) & match_form_params('treid')
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
-        state = entity.in_state[0]
+        entity = self.rset.get_entity(row, col)
         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()
@@ -111,14 +124,48 @@
     def cell_call(self, row, col, view=None):
         self.wview('wfhistory', self.rset, row=row, col=col, view=view)
 
-# workflow entity types views #################################################
+
+# workflow actions #############################################################
+
+class WorkflowActions(action.Action):
+    """fill 'workflow' sub-menu of the actions box"""
+    id = 'workflow'
+    __select__ = (action.Action.__select__ & one_line_rset() &
+                  relation_possible('in_state'))
+
+    submenu = _('workflow')
+    order = 10
+
+    def fill_menu(self, box, menu):
+        entity = self.rset.get_entity(self.row or 0, self.col or 0)
+        menu.label = u'%s: %s' % (self.req._('state'), entity.printable_state)
+        menu.append_anyway = True
+        super(WorkflowActions, self).fill_menu(box, menu)
+
+    def actual_actions(self):
+        entity = self.rset.get_entity(self.row or 0, self.col or 0)
+        hastr = False
+        for tr in entity.possible_transitions():
+            url = entity.absolute_url(vid='statuschange', treid=tr.eid)
+            yield self.build_action(self.req._(tr.name), url)
+            hastr = True
+        # don't propose to see wf if user can't pass any transition
+        if hastr:
+            wfurl = entity.current_workflow.absolute_url()
+            yield self.build_action(self.req._('view workflow'), wfurl)
+        if entity.workflow_history:
+            wfurl = entity.absolute_url(vid='wfhistory')
+            yield self.build_action(self.req._('view history'), wfurl)
+
+
+# workflow entity types views ##################################################
 
 class CellView(view.EntityView):
     id = 'cell'
     __select__ = implements('TrInfo')
 
     def cell_call(self, row, col, cellvid=None):
-        self.w(self.entity(row, col).view('reledit', rtype='comment'))
+        self.w(self.rset.get_entity(row, col).view('reledit', rtype='comment'))
 
 
 class StateInContextView(view.EntityView):
@@ -131,32 +178,17 @@
                                      row=row, col=col)))
 
 
-# workflow images #############################################################
-
-class ViewWorkflowAction(action.Action):
-    id = 'workflow'
-    __select__ = implements('CWEType') & has_related_entities('state_of', 'object')
+class WorkflowPrimaryView(primary.PrimaryView):
+    __select__ = implements('Workflow')
 
-    category = 'mainactions'
-    title = _('view workflow')
-    def url(self):
-        entity = self.rset.get_entity(self.row or 0, self.col or 0)
-        return entity.absolute_url(vid='workflow')
+    def render_entity_attributes(self, entity):
+        self.w(entity.view('reledit', rtype='description'))
+        self.w(u'<img src="%s" alt="%s"/>' % (
+            xml_escape(entity.absolute_url(vid='wfgraph')),
+            xml_escape(self.req._('graphical workflow for %s') % entity.name)))
 
 
-class CWETypeWorkflowView(view.EntityView):
-    id = 'workflow'
-    __select__ = implements('CWEType')
-    cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
-
-    def cell_call(self, row, col, **kwargs):
-        entity = self.entity(row, col)
-        self.w(u'<h1>%s</h1>' % (self.req._('workflow for %s')
-                                 % display_name(self.req, entity.name)))
-        self.w(u'<img src="%s" alt="%s"/>' % (
-            xml_escape(entity.absolute_url(vid='ewfgraph')),
-            xml_escape(self.req._('graphical workflow for %s') % entity.name)))
-
+# workflow images ##############################################################
 
 class WorkflowDotPropsHandler(object):
     def __init__(self, req):
@@ -164,7 +196,7 @@
 
     def node_properties(self, stateortransition):
         """return default DOT drawing options for a state or transition"""
-        props = {'label': stateortransition.name,
+        props = {'label': stateortransition.printable_value('name'),
                  'fontname': 'Courier'}
         if hasattr(stateortransition, 'state_of'):
             props['shape'] = 'box'
@@ -178,9 +210,11 @@
             if tr.require_group:
                 descr.append('%s %s'% (
                     self._('groups:'),
-                    ','.join(g.name for g in tr.require_group)))
+                    ','.join(g.printable_value('name') for g in tr.require_group)))
             if tr.condition:
-                descr.append('%s %s'% (self._('condition:'), tr.condition))
+                descr.append('%s %s'% (
+                    self._('condition:'),
+                    ' | '.join(e.expression for e in tr.condition)))
             if descr:
                 props['label'] += escape('\n'.join(descr))
         return props
@@ -210,14 +244,14 @@
             yield transition.eid, transition.destination().eid, transition
 
 
-class CWETypeWorkflowImageView(TmpFileViewMixin, view.EntityView):
-    id = 'ewfgraph'
+class WorkflowImageView(TmpFileViewMixin, view.EntityView):
+    id = 'wfgraph'
     content_type = 'image/png'
-    __select__ = implements('CWEType')
+    __select__ = implements('Workflow')
 
     def _generate(self, tmpfile):
         """display schema information for an entity"""
-        entity = self.entity(self.row, self.col)
+        entity = self.rset.get_entity(self.row, self.col)
         visitor = WorkflowVisitor(entity)
         prophdlr = WorkflowDotPropsHandler(self.req)
         generator = GraphGenerator(DotBackend('workflow', 'LR',
--- a/web/views/xmlrss.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/views/xmlrss.py	Fri Sep 18 11:24:37 2009 +0200
@@ -95,8 +95,8 @@
                     val = self.view('textincontext', rset,
                                     row=rowindex, col=colindex)
                 else:
-                    val = self.view('final', rset, displaytime=True,
-                                    row=rowindex, col=colindex, format='text/plain')
+                    val = self.view('final', rset, row=rowindex,
+                                    col=colindex, format='text/plain')
                 w(simple_sgml_tag(tag, val, **attrs))
             w(u' </row>\n')
         w(u'</%s>\n' % self.xml_root)
--- a/web/webconfig.py	Thu Sep 17 19:38:04 2009 +0200
+++ b/web/webconfig.py	Fri Sep 18 11:24:37 2009 +0200
@@ -160,18 +160,6 @@
 if you want to allow everything',
           'group': 'web', 'inputlevel': 1,
           }),
-        ('submit-url',
-         {'type' : 'string',
-          'default': Method('default_submit_url'),
-          'help': ('URL that may be used to report bug in this instance '
-                   'by direct access to the project\'s (jpl) tracker, '
-                   'if you want this feature on. The url should looks like '
-                   'http://mytracker.com/view?__linkto=concerns:1234:subject&etype=Ticket&type=bug&vid=creation '
-                   'where 1234 should be replaced by the eid of your project in '
-                   'the tracker. If you have no idea about what I\'am talking '
-                   'about, you should probably let no value for this option.'),
-          'group': 'web', 'inputlevel': 2,
-          }),
         ('submit-mail',
          {'type' : 'string',
           'default': None,
@@ -196,16 +184,6 @@
           }),
         ))
 
-    def default_submit_url(self):
-        try:
-            cube = self.cubes()[0]
-            cubeeid = self.cube_pkginfo(cube).cube_eid
-        except Exception:
-            return None
-        if cubeeid:
-            return 'http://intranet.logilab.fr/jpl/view?__linkto=concerns:%s:subject&etype=Ticket&type=bug&vid=creation' % cubeeid
-        return None
-
     def fckeditor_installed(self):
         return exists(self.ext_resources['FCKEDITOR_PATH'])