backport old changesets corresponding to new form / controller implementation
authorAdrien Di Mascio <Adrien.DiMascio@logilab.fr>
Tue, 22 Sep 2009 13:08:42 +0200
changeset 3388 b8be8fc77c27
parent 3369 7b88d12b4ee2 (diff)
parent 3387 a357d4147eee (current diff)
child 3389 59c0e6b34dd1
backport old changesets corresponding to new form / controller implementation
web/formfields.py
web/formwidgets.py
web/views/editcontroller.py
web/views/editforms.py
web/views/formrenderers.py
web/views/forms.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,10 @@
+\.svn
+^build$
+^dist$
+\.pyc$
+\.pyo$
+\.bak$
+\.old$
+\~$
+\#.*?\#$
+\.swp$
--- a/.hgtags	Wed Aug 05 09:15:56 2009 +0200
+++ b/.hgtags	Tue Sep 22 13:08:42 2009 +0200
@@ -50,3 +50,29 @@
 2ba27ce8ecd9828693ec53c517e1c8810cbbe33e cubicweb-debian-version-3_3_3-2
 d46363eac5d71bc1570d69337955154dfcd8fcc8 cubicweb-version-3.3.4
 7dc22caa7640bf70fcae55afb6d2326829dacced cubicweb-debian-version-3.3.4-1
+d85937184430b2e7a8d9318100272757026c01a4 cubicweb-version-3.3.5
+82fe8a6d077b34e972664c8e2bace3ae30c94b9d cubicweb-debian-version-3.3.5-1
+cfcc7f6121d6c6b53a1ef9bc96c3b7c82f33d169 cubicweb-version-3.4.2
+e6a8cd8cc910507e5f928ddba6de3cf5e5bdf3d4 cubicweb-debian-version-3.4.2-1
+b2add17d5bc437a807976cd13870f92d3a94f2a6 cubicweb-version-3.4.3
+a3828745e2cf0c4bf1cab6f5397c524ba0510df6 cubicweb-debian-version-3.4.3-1
+2aee4ea585cdba159bc9490741db3fcd25dcaaca cubicweb-version-3.4.4
+e244a0fd7d719c25f4267470342ff8334b2dc8b3 cubicweb-debian-version-3.4.4-1
+3a65f9b3367c7297dc540a53f84e6507cb309892 cubicweb-version-3.4.5
+7fd294cbf6ff3cf34475cc50e972f650a34ae6e8 cubicweb-debian-version-3.4.5-1
+921fdbf8b3038dc27a2ec5398a0fbcbc5b9ba4be cubicweb-version-3.4.6
+52dba800ca4d4b82c47f3befb824bd91ef015368 cubicweb-debian-version-3.4.6-1
+0e549b299f0b357837ea620c561aa843f46de17a cubicweb-version-3.4.7
+ebb92e62eb040a070deb1f2d2434734cfac3af01 cubicweb-debian-version-3.4.7-1
+ba43e084e8841f62c3b4c2449b26a7546233e5fb cubicweb-version-3.4.8
+97273eeaaead11c0f422dc5a4fe2d4f14fc6a2dd cubicweb-debian-version-3.4.8-1
+e916f1e856c83aced0fe73f7ae9068e37edcc38c cubicweb-version-3.4.9
+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
+1f0aa3cd5af2c92df8f9695773b8e465eb6f1795 cubicweb-version-3.5.2
+75cc4aa76fb12c06d4190956aa050cdf19ba4d8f cubicweb-debian-version-3.5.2-1
--- a/MANIFEST.in	Wed Aug 05 09:15:56 2009 +0200
+++ b/MANIFEST.in	Tue Sep 22 13:08:42 2009 +0200
@@ -15,7 +15,7 @@
 recursive-include etwist *.xml *.html
 
 recursive-include i18n *.pot *.po
-recursive-include schemas *.py *.rel *.sql.*
+recursive-include schemas *.py *.sql.*
 
 recursive-include common/test/data *
 recursive-include entities/test/data *
--- a/__init__.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/__init__.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,7 +7,6 @@
 :license: Library General Public License version 2 - http://www.gnu.org/licenses
 """
 __docformat__ = "restructuredtext en"
-from cubicweb.__pkginfo__ import version as __version__
 
 import __builtin__
 # '_' is available in builtins to mark internationalized string but should
@@ -19,9 +18,7 @@
 
 import sys, os, logging
 from StringIO import StringIO
-from urllib import quote as urlquote, unquote as urlunquote
 
-from logilab.common.decorators import cached
 from logilab.common.logging_ext import set_log_methods
 
 if os.environ.get('APYCOT_ROOT'):
@@ -29,6 +26,8 @@
 else:
     logging.basicConfig()
 
+from cubicweb.__pkginfo__ import version as __version__
+
 
 set_log_methods(sys.modules[__name__], logging.getLogger('cubicweb'))
 
@@ -56,175 +55,7 @@
                "Binary objects must use raw strings, not %s" % data.__class__
         StringIO.write(self, data)
 
-
-class RequestSessionMixIn(object):
-    """mixin class containing stuff shared by server session and web request
-    """
-    def __init__(self, vreg):
-        self.vreg = vreg
-        try:
-            encoding = vreg.property_value('ui.encoding')
-        except: # no vreg or property not registered
-            encoding = 'utf-8'
-        self.encoding = encoding
-        # cache result of execution for (rql expr / eids),
-        # should be emptied on commit/rollback of the server session / web
-        # connection
-        self.local_perm_cache = {}
-
-    def property_value(self, key):
-        if self.user:
-            return self.user.property_value(key)
-        return self.vreg.property_value(key)
-
-    def etype_rset(self, etype, size=1):
-        """return a fake result set for a particular entity type"""
-        from cubicweb.rset import ResultSet
-        rset = ResultSet([('A',)]*size, '%s X' % etype,
-                         description=[(etype,)]*size)
-        def get_entity(row, col=0, etype=etype, req=self, rset=rset):
-            return req.vreg.etype_class(etype)(req, rset, row, col)
-        rset.get_entity = get_entity
-        return self.decorate_rset(rset)
-
-    def eid_rset(self, eid, etype=None):
-        """return a result set for the given eid without doing actual query
-        (we have the eid, we can suppose it exists and user has access to the
-        entity)
-        """
-        from cubicweb.rset import ResultSet
-        eid = typed_eid(eid)
-        if etype is None:
-            etype = self.describe(eid)[0]
-        rset = ResultSet([(eid,)], 'Any X WHERE X eid %(x)s', {'x': eid},
-                         [(etype,)])
-        return self.decorate_rset(rset)
-
-    def empty_rset(self):
-        """return a result set for the given eid without doing actual query
-        (we have the eid, we can suppose it exists and user has access to the
-        entity)
-        """
-        from cubicweb.rset import ResultSet
-        return self.decorate_rset(ResultSet([], 'Any X WHERE X eid -1'))
-
-    def entity_from_eid(self, eid, etype=None):
-        try:
-            return self.entity_cache(eid)
-        except KeyError:
-            rset = self.eid_rset(eid, etype)
-            entity = rset.get_entity(0, 0)
-            self.set_entity_cache(entity)
-            return entity
-
-    def entity_cache(self, eid):
-        raise KeyError
-    def set_entity_cache(self, entity):
-        pass
-    # url generation methods ##################################################
-
-    def build_url(self, *args, **kwargs):
-        """return an absolute URL using params dictionary key/values as URL
-        parameters. Values are automatically URL quoted, and the
-        publishing method to use may be specified or will be guessed.
-        """
-        # use *args since we don't want first argument to be "anonymous" to
-        # avoid potential clash with kwargs
-        assert len(args) == 1, 'only 0 or 1 non-named-argument expected'
-        method = args[0]
-        base_url = kwargs.pop('base_url', None)
-        if base_url is None:
-            base_url = self.base_url()
-        if '_restpath' in kwargs:
-            assert method == 'view', method
-            path = kwargs.pop('_restpath')
-        else:
-            path = method
-        if not kwargs:
-            return u'%s%s' % (base_url, path)
-        return u'%s%s?%s' % (base_url, path, self.build_url_params(**kwargs))
-
-
-    def build_url_params(self, **kwargs):
-        """return encoded params to incorporate them in an URL"""
-        args = []
-        for param, values in kwargs.items():
-            if not isinstance(values, (list, tuple)):
-                values = (values,)
-            for value in values:
-                args.append(u'%s=%s' % (param, self.url_quote(value)))
-        return '&'.join(args)
-
-    def url_quote(self, value, safe=''):
-        """urllib.quote is not unicode safe, use this method to do the
-        necessary encoding / decoding. Also it's designed to quote each
-        part of a url path and so the '/' character will be encoded as well.
-        """
-        if isinstance(value, unicode):
-            quoted = urlquote(value.encode(self.encoding), safe=safe)
-            return unicode(quoted, self.encoding)
-        return urlquote(str(value), safe=safe)
-
-    def url_unquote(self, quoted):
-        """returns a unicode unquoted string
-
-        decoding is based on `self.encoding` which is the encoding
-        used in `url_quote`
-        """
-        if isinstance(quoted, unicode):
-            quoted = quoted.encode(self.encoding)
-        try:
-            return unicode(urlunquote(quoted), self.encoding)
-        except UnicodeDecodeError: # might occurs on manually typed URLs
-            return unicode(urlunquote(quoted), 'iso-8859-1')
-
-
-    # session's user related methods #####################################
-
-    @cached
-    def user_data(self):
-        """returns a dictionnary with this user's information"""
-        userinfo = {}
-        if self.is_internal_session:
-            userinfo['login'] = "cubicweb"
-            userinfo['name'] = "cubicweb"
-            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
-        return userinfo
-
-    def is_internal_session(self):
-        """overrided on the server-side"""
-        return False
-
-    # abstract methods to override according to the web front-end #############
-
-    def base_url(self):
-        """return the root url of the instance"""
-        raise NotImplementedError
-
-    def decorate_rset(self, rset):
-        """add vreg/req (at least) attributes to the given result set """
-        raise NotImplementedError
-
-    def describe(self, eid):
-        """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
-        raise NotImplementedError
-
-
-# XXX 2.45 is allowing nicer entity type names, use this map for bw compat
+# use this dictionary for renaming of entity types while keeping bw compath
 ETYPE_NAME_MAP = {# 3.2 migration
                   'ECache': 'CWCache',
                   'EUser': 'CWUser',
@@ -237,31 +68,19 @@
                   'EConstraintType': 'CWConstraintType',
                   'EConstraint': 'CWConstraint',
                   'EPermission': 'CWPermission',
-                   # 2.45 migration
-                  'Eetype': 'CWEType',
-                  'Ertype': 'CWRType',
-                  'Efrdef': 'CWAttribute',
-                  'Enfrdef': 'CWRelation',
-                  'Econstraint': 'CWConstraint',
-                  'Econstrainttype': 'CWConstraintType',
-                  'Epermission': 'CWPermission',
-                  'Egroup': 'CWGroup',
-                  'Euser': 'CWUser',
-                  'Eproperty': 'CWProperty',
-                  'Emailaddress': 'EmailAddress',
-                  'Rqlexpression': 'RQLExpression',
-                  'Trinfo': 'TrInfo',
                   }
 
 
 
 # XXX cubic web cube migration map
 CW_MIGRATION_MAP = {'erudi': 'cubicweb',
-
                     'eaddressbook': 'addressbook',
                     'ebasket': 'basket',
                     'eblog': 'blog',
                     'ebook': 'book',
+                    'eclassschemes': 'keyword',
+                    'eclassfolders': 'folder',
+                    'eclasstags': 'tag',
                     'ecomment': 'comment',
                     'ecompany': 'company',
                     'econference':  'conference',
@@ -281,20 +100,6 @@
                     'ezone': 'zone',
                     'i18ncontent': 'i18ncontent',
                     'svnfile': 'vcsfile',
-
-                    'eclassschemes': 'keyword',
-                    'eclassfolders': 'folder',
-                    'eclasstags': 'tag',
-
-                    'jpl': 'jpl',
-                    'jplintra': 'jplintra',
-                    'jplextra': 'jplextra',
-                    'jplorg': 'jplorg',
-                    'jplrecia': 'jplrecia',
-                    'crm': 'crm',
-                    'agueol': 'agueol',
-                    'docaster': 'docaster',
-                    'asteretud': 'asteretud',
                     }
 
 def neg_role(role):
@@ -314,9 +119,6 @@
     except AttributeError:
         return neg_role(obj.role)
 
-def underline_title(title, car='-'):
-    return title+'\n'+(car*len(title))
-
 
 class CubicWebEventManager(object):
     """simple event / callback manager.
@@ -324,12 +126,12 @@
     Typical usage to register a callback::
 
       >>> from cubicweb import CW_EVENT_MANAGER
-      >>> CW_EVENT_MANAGER.bind('after-source-reload', mycallback)
+      >>> CW_EVENT_MANAGER.bind('after-registry-reload', mycallback)
 
     Typical usage to emit an event::
 
       >>> from cubicweb import CW_EVENT_MANAGER
-      >>> CW_EVENT_MANAGER.emit('after-source-reload')
+      >>> CW_EVENT_MANAGER.emit('after-registry-reload')
 
     emit() accepts an additional context parameter that will be passed
     to the callback if specified (and only in that case)
@@ -353,7 +155,7 @@
     """decorator to ease event / callback binding
 
     >>> from cubicweb import onevent
-    >>> @onevent('before-source-reload')
+    >>> @onevent('before-registry-reload')
     ... def mycallback():
     ...     print 'hello'
     ...
--- a/__pkginfo__.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/__pkginfo__.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,7 +7,7 @@
 distname = "cubicweb"
 modname = "cubicweb"
 
-numversion = (3, 4, 0)
+numversion = (3, 5, 2)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL v2'
--- a/_exceptions.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/_exceptions.py	Tue Sep 22 13:08:42 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 #########################################################
@@ -79,8 +78,6 @@
     msg = 'You are not allowed to perform this operation'
     msg1 = 'You are not allowed to perform %s operation on %s'
     var = None
-    #def __init__(self, *args):
-    #    self.args = args
 
     def __str__(self):
         try:
@@ -118,12 +115,9 @@
     this may be a programming/typo or a misconfiguration error
     """
 
-# class ViewNotFound(ObjectNotFound):
-#     """raised when an unregistered view is called"""
-
 class NoSelectableObject(RegistryException):
     """some views with the given vid have been found but no
-    one is applyable to the result set
+    one is applicable to the result set
     """
 
 class UnknownProperty(RegistryException):
@@ -149,4 +143,3 @@
 
 # pylint: disable-msg=W0611
 from logilab.common.clcommands import BadCommandUsage
-
--- a/appobject.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/appobject.py	Tue Sep 22 13:08:42 2009 +0200
@@ -11,28 +11,12 @@
 
 import types
 from logging import getLogger
-from datetime import datetime, timedelta, time
 
 from logilab.common.decorators import classproperty
 from logilab.common.deprecation import deprecated
 from logilab.common.logging_ext import set_log_methods
 
-from rql.nodes import VariableRef, SubQuery
-from rql.stmts import Union, Select
-
 from cubicweb import Unauthorized, NoSelectableObject
-from cubicweb.utils import UStringIO, ustrftime, strptime, todate, todatetime
-
-ONESECOND = timedelta(0, 1, 0)
-CACHE_REGISTRY = {}
-
-
-class Cache(dict):
-    def __init__(self):
-        super(Cache, self).__init__()
-        _now = datetime.now()
-        self.cache_creation_date = _now
-        self.latest_cache_lookup = _now
 
 
 # selector base classes and operations ########################################
@@ -100,11 +84,15 @@
         return AndSelector(self, other)
     def __rand__(self, other):
         return AndSelector(other, self)
+    def __iand__(self, other):
+        raise NotImplementedError('cant use inplace & (binary and)')
 
     def __or__(self, other):
         return OrSelector(self, other)
     def __ror__(self, other):
         return OrSelector(other, self)
+    def __ior__(self, other):
+        raise NotImplementedError('cant use inplace | (binary or)')
 
     def __invert__(self):
         return NotSelector(self)
@@ -220,7 +208,7 @@
     :__registry__:
       name of the registry for this object (string like 'views',
       'templates'...)
-    :id:
+    :__id__:
       object's identifier in the registry (string like 'main',
       'primary', 'folder_box')
     :__select__:
@@ -229,343 +217,188 @@
     Moreover, the `__abstract__` attribute may be set to True to indicate
     that a appobject is abstract and should not be registered.
 
-    At registration time, the following attributes are set on the class:
-    :vreg:
-      the instance's registry
-    :schema:
-      the instance's schema
-    :config:
-      the instance's configuration
+    At selection time, the following attributes are set on the instance:
+
+    :_cw:
+      current request
+    :cw_extra_kwargs:
+      other received arguments
 
-    At selection time, the following attributes are set on the instance:
-    :req:
-      current request
-    :rset:
+    only if rset is found in arguments (in which case rset/row/col will be
+    removed from cwextra_kwargs):
+
+    :cw_rset:
       context result set or None
-    :row:
+    :cw_row:
       if a result set is set and the context is about a particular cell in the
       result set, and not the result set as a whole, specify the row number we
       are interested in, else None
-    :col:
+    :cw_col:
       if a result set is set and the context is about a particular cell in the
       result set, and not the result set as a whole, specify the col number we
       are interested in, else None
     """
     __registry__ = None
-    id = None
+    __id__ = None
     __select__ = yes()
 
     @classmethod
-    def classid(cls):
-        """returns a unique identifier for the appobject"""
-        return '%s.%s' % (cls.__module__, cls.__name__)
-
-    # XXX bw compat code
-    @classmethod
-    def build___select__(cls):
-        for klass in cls.mro():
-            if klass.__name__ == 'AppObject':
-                continue # the bw compat __selector__ is there
-            klassdict = klass.__dict__
-            if ('__select__' in klassdict and '__selectors__' in klassdict
-                and '__selgenerated__' not in klassdict):
-                raise TypeError("__select__ and __selectors__ can't be used together on class %s" % cls)
-            if '__selectors__' in klassdict and '__selgenerated__' not in klassdict:
-                cls.__selgenerated__ = True
-                # case where __selectors__ is defined locally (but __select__
-                # is in a parent class)
-                selectors = klassdict['__selectors__']
-                if len(selectors) == 1:
-                    # micro optimization: don't bother with AndSelector if there's
-                    # only one selector
-                    select = _instantiate_selector(selectors[0])
-                else:
-                    select = AndSelector(*selectors)
-                cls.__select__ = select
-
-    @classmethod
-    def registered(cls, registry):
+    def __registered__(cls, registry):
         """called by the registry when the appobject has been registered.
 
         It must return the object that will be actually registered (this may be
         the right hook to create an instance for example). By default the
         appobject is returned without any transformation.
         """
-        cls.build___select__()
-        cls.vreg = registry.vreg
-        cls.schema = registry.schema
-        cls.config = registry.config
-        cls.register_properties()
+        pdefs = getattr(cls, 'cw_property_defs', {})
+        for propid, pdef in pdefs.items():
+            pdef = pdef.copy() # may be shared
+            pdef['default'] = getattr(cls, propid, pdef['default'])
+            pdef['sitewide'] = getattr(cls, 'site_wide', pdef.get('sitewide'))
+            registry.vreg.register_property(cls._cwpropkey(propid), **pdef)
         return cls
 
-    @classmethod
-    def vreg_initialization_completed(cls):
-        pass
+    def __init__(self, req, **extra):
+        super(AppObject, self).__init__()
+        self._cw = req
+        try:
+            self.cw_rset = extra.pop('rset')
+            self.cw_row = extra.pop('row', None)
+            self.cw_col = extra.pop('col', None)
+        except KeyError:
+            pass
+        self.cw_extra_kwargs = extra
 
-    # Eproperties definition:
-    # key: id of the property (the actual CWProperty key is build using
-    #      <registry name>.<obj id>.<property id>
-    # value: tuple (property type, vocabfunc, default value, property description)
-    #        possible types are those used by `logilab.common.configuration`
+    # persistent class properties ##############################################
+    #
+    # optional `cw_property_defs` dict on a class defines available persistent
+    # properties for this class:
+    #
+    # * key: id of the property (the actual CWProperty key is build using
+    #        <registry name>.<obj id>.<property id>
+    # * value: tuple (property type, vocabfunc, default value, property description)
+    #         possible types are those used by `logilab.common.configuration`
     #
     # notice that when it exists multiple objects with the same id (adaptation,
     # overriding) only the first encountered definition is considered, so those
     # objects can't try to have different default values for instance.
-
-    property_defs = {}
+    #
+    # you can then access to a property value using self.cw_propval, where self
+    # is an instance of class
 
     @classmethod
-    def register_properties(cls):
-        for propid, pdef in cls.property_defs.items():
-            pdef = pdef.copy() # may be shared
-            pdef['default'] = getattr(cls, propid, pdef['default'])
-            pdef['sitewide'] = getattr(cls, 'site_wide', pdef.get('sitewide'))
-            cls.vreg.register_property(cls.propkey(propid), **pdef)
-
-    @classmethod
-    def propkey(cls, propid):
+    def _cwpropkey(cls, propid):
+        """return cw property key for the property of the given id for this
+        class
+        """
         return '%s.%s.%s' % (cls.__registry__, cls.id, propid)
 
-    @classproperty
-    @deprecated('use __select__ and & or | operators')
-    def __selectors__(cls):
-        selector = cls.__select__
-        if isinstance(selector, AndSelector):
-            return tuple(selector.selectors)
-        if not isinstance(selector, tuple):
-            selector = (selector,)
-        return selector
+    def cw_propval(self, propid):
+        """return cw property value associated to key
 
-    def __init__(self, req=None, rset=None, row=None, col=None, **extra):
-        super(AppObject, self).__init__()
-        self.req = req
-        self.rset = rset
-        self.row = row
-        self.col = col
-        self.extra_kwargs = extra
-
-    def get_cache(self, cachename):
-        """
-        NOTE: cachename should be dotted names as in :
-        - cubicweb.mycache
-        - cubes.blog.mycache
-        - etc.
+        <cls.__registry__>.<cls.id>.<propid>
         """
-        if cachename in CACHE_REGISTRY:
-            cache = CACHE_REGISTRY[cachename]
-        else:
-            cache = CACHE_REGISTRY[cachename] = Cache()
-        _now = datetime.now()
-        if _now > cache.latest_cache_lookup + ONESECOND:
-            ecache = self.req.execute('Any C,T WHERE C is CWCache, C name %(name)s, C timestamp T',
-                                      {'name':cachename}).get_entity(0,0)
-            cache.latest_cache_lookup = _now
-            if not ecache.valid(cache.cache_creation_date):
-                cache.clear()
-                cache.cache_creation_date = _now
-        return cache
+        return self.req.property_value(self._cwpropkey(propid))
+
+    # deprecated ###############################################################
 
-    def propval(self, propid):
-        assert self.req
-        return self.req.property_value(self.propkey(propid))
+    @property
+    @deprecated('[3.6] use self._cw.vreg')
+    def vreg(self):
+        return self._cw.vreg
+
+    @property
+    @deprecated('[3.6] use self._cw.vreg.schema')
+    def schema(self):
+        return self._cw.vreg.schema
 
-    def limited_rql(self):
-        """return a printable rql for the result set associated to the object,
-        with limit/offset correctly set according to maximum page size and
-        currently displayed page when necessary
-        """
-        # try to get page boundaries from the navigation component
-        # XXX we should probably not have a ref to this component here (eg in
-        #     cubicweb.common)
-        nav = self.vreg['components'].select_object('navigation', self.req,
-                                                    rset=self.rset)
-        if nav:
-            start, stop = nav.page_boundaries()
-            rql = self._limit_offset_rql(stop - start, start)
-        # result set may have be limited manually in which case navigation won't
-        # apply
-        elif self.rset.limited:
-            rql = self._limit_offset_rql(*self.rset.limited)
-        # navigation component doesn't apply and rset has not been limited, no
-        # need to limit query
-        else:
-            rql = self.rset.printable_rql()
-        return rql
+    @property
+    @deprecated('[3.6] use self._cw.vreg.config')
+    def config(self):
+        return self._cw.vreg.config
+
+    @property
+    @deprecated('[3.6] use self._cw')
+    def req(self):
+        return self._cw
+
+    @deprecated('[3.6] use self.cw_rset')
+    def get_rset(self):
+        return self.cw_rset
+    @deprecated('[3.6] use self.cw_rset')
+    def set_rset(self, rset):
+        self.cw_rset = rset
+    rset = property(get_rset, set_rset)
 
-    def _limit_offset_rql(self, limit, offset):
-        rqlst = self.rset.syntax_tree()
-        if len(rqlst.children) == 1:
-            select = rqlst.children[0]
-            olimit, ooffset = select.limit, select.offset
-            select.limit, select.offset = limit, offset
-            rql = rqlst.as_string(kwargs=self.rset.args)
-            # restore original limit/offset
-            select.limit, select.offset = olimit, ooffset
-        else:
-            newselect = Select()
-            newselect.limit = limit
-            newselect.offset = offset
-            aliases = [VariableRef(newselect.get_variable(vref.name, i))
-                       for i, vref in enumerate(rqlst.selection)]
-            newselect.set_with([SubQuery(aliases, rqlst)], check=False)
-            newunion = Union()
-            newunion.append(newselect)
-            rql = rqlst.as_string(kwargs=self.rset.args)
-            rqlst.parent = None
-        return rql
+    @property
+    @deprecated('[3.6] use self.cw_row')
+    def row(self):
+        return self.cw_row
+
+    @property
+    @deprecated('[3.6] use self.cw_col')
+    def col(self):
+        return self.cw_col
 
-    def view(self, __vid, rset=None, __fallback_oid=None, __registry='views',
-             **kwargs):
-        """shortcut to self.vreg.view method avoiding to pass self.req"""
-        return self.vreg[__registry].render(__vid, self.req, __fallback_oid,
-                                            rset=rset, **kwargs)
+    @property
+    @deprecated('[3.6] use self.cw_extra_kwargs')
+    def extra_kwargs(self):
+        return self.cw_extra_kwargs
 
+    @deprecated('[3.6] use self._cw.view')
+    def view(self, *args, **kwargs):
+        return self._cw.view(*args, **kwargs)
+
+    @deprecated('[3.6] use self._cw.varmaker')
     def initialize_varmaker(self):
-        varmaker = self.req.get_page_data('rql_varmaker')
-        if varmaker is None:
-            varmaker = self.req.varmaker
-            self.req.set_page_data('rql_varmaker', varmaker)
-        self.varmaker = varmaker
+        self.varmaker = self._cw.varmaker
 
-    # url generation methods ##################################################
+    @deprecated('[3.6] use self._cw.get_cache')
+    def get_cache(self, cachename):
+        return self._cw.get_cache(cachename)
 
-    controller = 'view'
-
+    @deprecated('[3.6] use self._cw.build_url')
     def build_url(self, *args, **kwargs):
-        """return an absolute URL using params dictionary key/values as URL
-        parameters. Values are automatically URL quoted, and the
-        publishing method to use may be specified or will be guessed.
-        """
-        # use *args since we don't want first argument to be "anonymous" to
-        # avoid potential clash with kwargs
-        if args:
-            assert len(args) == 1, 'only 0 or 1 non-named-argument expected'
-            method = args[0]
-        else:
-            method = None
-        # XXX I (adim) think that if method is passed explicitly, we should
-        #     not try to process it and directly call req.build_url()
-        if method is None:
-            method = self.controller
-            if method == 'view' and self.req.from_controller() == 'view' and \
-                   not '_restpath' in kwargs:
-                method = self.req.relative_path(includeparams=False) or 'view'
-        return self.req.build_url(method, **kwargs)
+        return self._cw.build_url(*args, **kwargs)
+
+    @deprecated('[3.6] use self.cw_rset.limited_rql')
+    def limited_rql(self):
+        return self.rset.limited_rql()
 
-    # various resources accessors #############################################
+    @deprecated('[3.6] use self.rset.complete_entity(row,col) instead')
+    def complete_entity(self, row, col=0, skip_bytes=True):
+        return self.rset.complete_entity(row, col, skip_bytes)
 
+    @deprecated('[3.6] use self.rset.get_entity(row,col) instead')
     def entity(self, row, col=0):
-        """short cut to get an entity instance for a particular row/column
-        (col default to 0)
-        """
         return self.rset.get_entity(row, col)
 
-    def complete_entity(self, row, col=0, skip_bytes=True):
-        """short cut to get an completed entity instance for a particular
-        row (all instance's attributes have been fetched)
-        """
-        entity = self.entity(row, col)
-        entity.complete(skip_bytes=skip_bytes)
-        return entity
+    @deprecated('[3.6] use self._cw.user_rql_callback')
+    def user_rql_callback(self, args, msg=None):
+        return self._cw.user_rql_callback(args, msg)
 
-    def user_rql_callback(self, args, msg=None):
-        """register a user callback to execute some rql query and return an url
-        to call it ready to be inserted in html
-        """
-        def rqlexec(req, rql, args=None, key=None):
-            req.execute(rql, args, key)
-        return self.user_callback(rqlexec, args, msg)
-
+    @deprecated('[3.6] use self._cw.user_callback')
     def user_callback(self, cb, args, msg=None, nonify=False):
-        """register the given user callback and return an url to call it ready to be
-        inserted in html
-        """
-        from simplejson import dumps
-        self.req.add_js('cubicweb.ajax.js')
-        cbname = self.req.register_onetime_callback(cb, *args)
-        msg = dumps(msg or '')
-        return "javascript:userCallbackThenReloadPage('%s', %s)" % (
-            cbname, msg)
-
-    # formating methods #######################################################
+        return self._cw.user_callback(cb, args, msg, nonify)
 
-    def tal_render(self, template, variables):
-        """render a precompiled page template with variables in the given
-        dictionary as context
-        """
-        from cubicweb.ext.tal import CubicWebContext
-        context = CubicWebContext()
-        context.update({'self': self, 'rset': self.rset, '_' : self.req._,
-                        'req': self.req, 'user': self.req.user})
-        context.update(variables)
-        output = UStringIO()
-        template.expand(context, output)
-        return output.getvalue()
-
+    @deprecated('[3.6] use self._cw.format_date')
     def format_date(self, date, date_format=None, time=False):
-        """return a string for a date time according to instance's
-        configuration
-        """
-        if date:
-            if date_format is None:
-                if time:
-                    date_format = self.req.property_value('ui.datetime-format')
-                else:
-                    date_format = self.req.property_value('ui.date-format')
-            return ustrftime(date, date_format)
-        return u''
+        return self._cw.format_date(date, date_format, time)
 
+    @deprecated('[3.6] use self._cw.format_timoe')
     def format_time(self, time):
-        """return a string for a time according to instance's
-        configuration
-        """
-        if time:
-            return ustrftime(time, self.req.property_value('ui.time-format'))
-        return u''
+        return self._cw.format_time(time)
 
+    @deprecated('[3.6] use self._cw.format_float')
     def format_float(self, num):
-        """return a string for floating point number according to instance's
-        configuration
-        """
-        if num:
-            return self.req.property_value('ui.float-format') % num
-        return u''
+        return self._cw.format_float(num)
 
+    @deprecated('[3.6] use self._cw.parse_datetime')
     def parse_datetime(self, value, etype='Datetime'):
-        """get a datetime or time from a string (according to etype)
-        Datetime formatted as Date are accepted
-        """
-        assert etype in ('Datetime', 'Date', 'Time'), etype
-        # XXX raise proper validation error
-        if etype == 'Datetime':
-            format = self.req.property_value('ui.datetime-format')
-            try:
-                return todatetime(strptime(value, format))
-            except ValueError:
-                pass
-        elif etype == 'Time':
-            format = self.req.property_value('ui.time-format')
-            try:
-                # (adim) I can't find a way to parse a Time with a custom format
-                date = strptime(value, format) # this returns a DateTime
-                return time(date.hour, date.minute, date.second)
-            except ValueError:
-                raise ValueError('can\'t parse %r (expected %s)' % (value, format))
-        try:
-            format = self.req.property_value('ui.date-format')
-            dt = strptime(value, format)
-            if etype == 'Datetime':
-                return todatetime(dt)
-            return todate(dt)
-        except ValueError:
-            raise ValueError('can\'t parse %r (expected %s)' % (value, format))
+        return self._cw.parse_datetime(value, etype)
 
-    # security related methods ################################################
-
-    def ensure_ro_rql(self, rql):
-        """raise an exception if the given rql is not a select query"""
-        first = rql.split(' ', 1)[0].lower()
-        if first in ('insert', 'set', 'delete'):
-            raise Unauthorized(self.req._('only select queries are authorized'))
+    @deprecated('[3.6] use self.cw_propval')
+    def propval(self, propid):
+        return self._cw.property_value(self._cwpropkey(propid))
 
 set_log_methods(AppObject, getLogger('cubicweb.appobject'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/cubicweb-ctl.bat	Tue Sep 22 13:08:42 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	Wed Aug 05 09:15:56 2009 +0200
+++ b/common/i18n.py	Tue Sep 22 13:08:42 2009 +0200
@@ -9,6 +9,7 @@
 
 import re
 import os
+import sys
 from os.path import join, basename, splitext, exists
 from glob import glob
 
@@ -25,10 +26,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')
@@ -43,10 +48,11 @@
     """display the command, execute it and raise an Exception if returned
     status != 0
     """
+    from subprocess import call
     print cmd.replace(os.getcwd() + os.sep, '')
-    status = os.system(cmd)
+    status = call(cmd, shell=True)
     if status != 0:
-        raise Exception()
+        raise Exception('status = %s' % status)
 
 
 def available_catalogs(i18ndir=None):
@@ -74,15 +80,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/common/mail.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,11 +7,21 @@
 """
 __docformat__ = "restructuredtext en"
 
+from base64 import b64encode, b64decode
+from itertools import repeat
+from time import time
 from email.MIMEMultipart import MIMEMultipart
 from email.MIMEText import MIMEText
 from email.MIMEImage import MIMEImage
 from email.Header import Header
+try:
+    from socket import gethostname
+except ImportError:
+    def gethostname(): # gae
+        return 'XXX'
 
+from cubicweb.view import EntityView
+from cubicweb.entity import Entity
 
 def header(ustring):
     return Header(ustring.encode('UTF-8'), 'UTF-8')
@@ -25,6 +35,34 @@
     return addr
 
 
+def construct_message_id(appid, eid, withtimestamp=True):
+    if withtimestamp:
+        addrpart = 'eid=%s&timestamp=%.10f' % (eid, time())
+    else:
+        addrpart = 'eid=%s' % eid
+    # we don't want any equal sign nor trailing newlines
+    leftpart = b64encode(addrpart, '.-').rstrip().rstrip('=')
+    return '<%s@%s.%s>' % (leftpart, appid, gethostname())
+
+
+def parse_message_id(msgid, appid):
+    if msgid[0] == '<':
+        msgid = msgid[1:]
+    if msgid[-1] == '>':
+        msgid = msgid[:-1]
+    try:
+        values, qualif = msgid.split('@')
+        padding = len(values) % 4
+        values = b64decode(str(values + '='*padding), '.-')
+        values = dict(v.split('=') for v in values.split('&'))
+        fromappid, host = qualif.split('.', 1)
+    except:
+        return None
+    if appid != fromappid or host != gethostname():
+        return None
+    return values
+
+
 def format_mail(uinfo, to_addrs, content, subject="",
                 cc_addrs=(), msgid=None, references=(), config=None):
     """Sends an Email to 'e_addr' with content 'content', and subject 'subject'
@@ -94,3 +132,130 @@
         image = MIMEImage(data)
         image.add_header('Content-ID', '<%s>' % htmlId)
         self.attach(image)
+
+
+class NotificationView(EntityView):
+    """abstract view implementing the "email" API (eg to simplify sending
+    notification)
+    """
+    # XXX refactor this class to work with len(rset) > 1
+
+    msgid_timestamp = True
+
+    # 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 render_emails(self, **kwargs):
+        """generate and send emails for this view (one per recipient)"""
+        self._kwargs = kwargs
+        recipients = self.recipients()
+        if not recipients:
+            self.info('skipping %s notification, no recipients', self.id)
+            return
+        if self.rset is not None:
+            entity = self.entity(self.row or 0, self.col or 0)
+            # if the view is using timestamp in message ids, no way to reference
+            # previous email
+            if not self.msgid_timestamp:
+                refs = [self.construct_message_id(eid)
+                        for eid in entity.notification_references(self)]
+            else:
+                refs = ()
+            msgid = self.construct_message_id(entity.eid)
+        else:
+            refs = ()
+            msgid = None
+        req = self.req
+        self.user_data = req.user_data()
+        origlang = req.lang
+        for something in recipients:
+            if isinstance(something, Entity):
+                # hi-jack self.req to get a session for the returned user
+                self.req = self.req.hijack_user(something)
+                emailaddr = something.get_email()
+            else:
+                emailaddr, lang = something
+                self.req.set_language(lang)
+            # since the same view (eg self) may be called multiple time and we
+            # need a fresh stream at each iteration, reset it explicitly
+            self.w = None
+            # XXX call render before subject to set .row/.col attributes on the
+            #     view
+            try:
+                content = self.render(row=0, col=0, **kwargs)
+                subject = self.subject()
+            except SkipEmail:
+                continue
+            msg = format_mail(self.user_data, [emailaddr], content, subject,
+                              config=self.config, msgid=msgid, references=refs)
+            yield [emailaddr], msg
+        # restore language
+        req.set_language(origlang)
+
+    # 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)])
+
+    def send_on_commit(self, recipients, msg):
+        raise NotImplementedError
+
+    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	Wed Aug 05 09:15:56 2009 +0200
+++ b/common/migration.py	Tue Sep 22 13:08:42 2009 +0200
@@ -157,7 +157,7 @@
                     # take care to X.Y.Z_Any.py / X.Y.Z_common.py: we've to call
                     # cube_upgraded once all script of X.Y.Z have been executed
                     if prevversion is not None and version != prevversion:
-                        self.cube_upgraded(cube, version)
+                        self.cube_upgraded(cube, prevversion)
                     prevversion = version
                     self.process_script(script)
                 self.cube_upgraded(cube, toversion)
@@ -183,12 +183,12 @@
         if not ask_confirm or self.confirm(msg):
             return meth(*args, **kwargs)
 
-    def confirm(self, question, shell=True, abort=True, retry=False):
+    def confirm(self, question, shell=True, abort=True, retry=False, default='y'):
         """ask for confirmation and return true on positive answer
 
         if `retry` is true the r[etry] answer may return 2
         """
-        possibleanswers = ['Y','n']
+        possibleanswers = ['y','n']
         if abort:
             possibleanswers.append('abort')
         if shell:
@@ -196,7 +196,7 @@
         if retry:
             possibleanswers.append('retry')
         try:
-            answer = ASK.ask(question, possibleanswers, 'Y')
+            answer = ASK.ask(question, possibleanswers, default)
         except (EOFError, KeyboardInterrupt):
             answer = 'abort'
         if answer == 'n':
@@ -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:
@@ -338,7 +341,7 @@
         configfile = self.config.main_config_file()
         if self._option_changes:
             read_old_config(self.config, self._option_changes, configfile)
-        _, newconfig = tempfile.mkstemp()
+        fd, newconfig = tempfile.mkstemp()
         for optdescr in self._option_changes:
             if optdescr[0] == 'added':
                 optdict = self.config.get_option_def(optdescr[1])
@@ -346,6 +349,7 @@
                     self.config.input_option(optdescr[1], optdict)
         self.config.generate_config(open(newconfig, 'w'))
         show_diffs(configfile, newconfig)
+        os.close(fd)
         if exists(newconfig):
             os.unlink(newconfig)
 
--- a/common/mixins.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/common/mixins.py	Tue Sep 22 13:08:42 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('trcommentformat'))
-        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,
     }
@@ -299,7 +207,7 @@
     """handle an infinite recursion safety belt"""
     if done is None:
         done = set()
-    entity = view.entity(row, col)
+    entity = view.rset.get_entity(row, col)
     if entity.eid in done:
         msg = entity.req._('loop in %(rel)s relation (%(eid)s)') % {
             'rel': entity.tree_attribute,
@@ -343,7 +251,7 @@
     """a recursive path view"""
     id = 'path'
     item_vid = 'oneline'
-    separator = u'&nbsp;&gt;&nbsp;'
+    separator = u'&#160;&gt;&#160;'
 
     def call(self, **kwargs):
         self.w(u'<div class="pathbar">')
@@ -365,6 +273,7 @@
 
 class ProgressMixIn(object):
     """provide default implementations for IProgress interface methods"""
+    # This is an adapter isn't it ?
 
     @property
     def cost(self):
--- a/common/mttransforms.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/common/mttransforms.py	Tue Sep 22 13:08:42 2009 +0200
@@ -15,6 +15,7 @@
                                  register_pil_transforms,
                                  register_pygments_transforms)
 
+from cubicweb.utils import UStringIO
 from cubicweb.common.uilib import rest_publish, html_publish
 
 HTML_MIMETYPES = ('text/html', 'text/xhtml', 'application/xhtml+xml')
@@ -43,7 +44,7 @@
 ENGINE.add_transform(html_to_html())
 
 try:
-    from cubicweb.ext.tal import compile_template
+    from cubicweb.ext.tal import CubicWebContext, compile_template
 except ImportError:
     HAS_TAL = False
     from cubicweb import schema
@@ -57,8 +58,16 @@
         output = 'text/html'
         output_encoding = 'utf-8'
         def _convert(self, trdata):
-            value = trdata.encode(self.output_encoding)
-            return trdata.appobject.tal_render(compile_template(value), {})
+            context = CubicWebContext()
+            appobject = trdata.appobject
+            context.update({'self': appobject, 'rset': appobject.rset,
+                            'req': appobject.req,
+                            '_' : appobject.req._,
+                            'user': appobject.req.user})
+            output = UStringIO()
+            template = compile_template(trdata.encode(self.output_encoding))
+            template.expand(context, output)
+            return output.getvalue()
 
     ENGINE.add_transform(ept_to_html())
 
--- a/common/test/unittest_mail.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/common/test/unittest_mail.py	Tue Sep 22 13:08:42 2009 +0200
@@ -8,12 +8,12 @@
 """
 
 import os
-import pwd
+import sys
 
 from logilab.common.testlib import unittest_main
 from logilab.common.umessage import message_from_string
 
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.common.mail import format_mail
 
 
@@ -22,10 +22,14 @@
     (man 3 getlogin)
     Another solution would be to use $LOGNAME, $USER or $USERNAME
     """
-    return pwd.getpwuid(os.getuid())[0]
+    if sys.platform != 'win32':
+        import pwd
+        return pwd.getpwuid(os.getuid())[0]
+    else:
+        return os.environ.get('USERNAME')
 
 
-class EmailTC(EnvBasedTC):
+class EmailTC(CubicWebTC):
 
     def test_format_mail(self):
         self.set_option('sender-addr', 'bim@boum.fr')
--- a/common/test/unittest_migration.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/common/test/unittest_migration.py	Tue Sep 22 13:08:42 2009 +0200
@@ -10,8 +10,6 @@
 from logilab.common.testlib import TestCase, unittest_main
 
 from cubicweb.devtools import TestServerConfiguration
-from cubicweb.devtools.apptest import TestEnvironment
-
 from cubicweb.cwconfig import CubicWebConfiguration
 from cubicweb.common.migration import MigrationHelper, filter_scripts
 from cubicweb.server.migractions import ServerMigrationHelper
@@ -98,8 +96,8 @@
         config = ApptestConfiguration('data')
         source = config.sources()['system']
         self.assertEquals(source['db-driver'], 'sqlite')
-        cleanup_sqlite(source['db-name'], removecube=True)
-        init_test_database(driver=source['db-driver'], config=config)
+        cleanup_sqlite(source['db-name'], removetemplate=True)
+        init_test_database(config=config)
 
 
 if __name__ == '__main__':
--- a/common/test/unittest_mixins.py	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 2009 +0200
+++ b/common/test/unittest_uilib.py	Tue Sep 22 13:08:42 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">&nbsp;</td><td class="tree_cell_1_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_1_1</div></td><td class="tree_cell_1_1">&nbsp;</td><td class="tree_cell_1_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_2_1</div></td><td class="tree_cell_0_1">&nbsp;</td><td class="tree_cell_0_2">&nbsp;</td><td rowspan="2">&nbsp;</td></tr>
-<tr><td class="tree_cell_1_3">&nbsp;</td><td class="tree_cell_1_4">&nbsp;</td><td class="tree_cell_1_3">&nbsp;</td><td class="tree_cell_1_4">&nbsp;</td><td class="tree_cell_0_3">&nbsp;</td><td class="tree_cell_0_4">&nbsp;</td></tr>
-<tr><td rowspan="2">&nbsp;</td><td class="tree_cell_2_1">&nbsp;</td><td class="tree_cell_2_2">&nbsp;</td><td rowspan="2">&nbsp;</td><td class="tree_cell_4_1">&nbsp;</td><td class="tree_cell_4_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">child_2_2</div></td><td class="tree_cell_1_1">&nbsp;</td><td class="tree_cell_1_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_1</div></td></tr>
-<tr><td class="tree_cell_2_3">&nbsp;</td><td class="tree_cell_2_4">&nbsp;</td><td class="tree_cell_4_3">&nbsp;</td><td class="tree_cell_4_4">&nbsp;</td><td class="tree_cell_1_3">&nbsp;</td><td class="tree_cell_1_4">&nbsp;</td></tr>
-<tr><td rowspan="2">&nbsp;</td><td class="tree_cell_2_1">&nbsp;</td><td class="tree_cell_2_2">&nbsp;</td><td rowspan="2">&nbsp;</td><td class="tree_cell_0_1">&nbsp;</td><td class="tree_cell_0_2">&nbsp;</td><td rowspan="2">&nbsp;</td><td class="tree_cell_3_1">&nbsp;</td><td class="tree_cell_3_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_2</div></td></tr>
-<tr><td class="tree_cell_2_3">&nbsp;</td><td class="tree_cell_2_4">&nbsp;</td><td class="tree_cell_0_3">&nbsp;</td><td class="tree_cell_0_4">&nbsp;</td><td class="tree_cell_3_3">&nbsp;</td><td class="tree_cell_3_4">&nbsp;</td></tr>
-<tr><td rowspan="2">&nbsp;</td><td class="tree_cell_2_1">&nbsp;</td><td class="tree_cell_2_2">&nbsp;</td><td rowspan="2">&nbsp;</td><td class="tree_cell_0_1">&nbsp;</td><td class="tree_cell_0_2">&nbsp;</td><td rowspan="2">&nbsp;</td><td class="tree_cell_4_1">&nbsp;</td><td class="tree_cell_4_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_3_3</div></td></tr>
-<tr><td class="tree_cell_2_3">&nbsp;</td><td class="tree_cell_2_4">&nbsp;</td><td class="tree_cell_0_3">&nbsp;</td><td class="tree_cell_0_4">&nbsp;</td><td class="tree_cell_4_3">&nbsp;</td><td class="tree_cell_4_4">&nbsp;</td></tr>
-<tr><td rowspan="2">&nbsp;</td><td class="tree_cell_4_1">&nbsp;</td><td class="tree_cell_4_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_1_2</div></td><td class="tree_cell_5_1">&nbsp;</td><td class="tree_cell_5_2">&nbsp;</td><td class="tree_cell" rowspan="2"><div class="tree_cell">child_2_3</div></td><td class="tree_cell_0_1">&nbsp;</td><td class="tree_cell_0_2">&nbsp;</td><td rowspan="2">&nbsp;</td></tr>
-<tr><td class="tree_cell_4_3">&nbsp;</td><td class="tree_cell_4_4">&nbsp;</td><td class="tree_cell_5_3">&nbsp;</td><td class="tree_cell_5_4">&nbsp;</td><td class="tree_cell_0_3">&nbsp;</td><td class="tree_cell_0_4">&nbsp;</td></tr>
-</table>\
-"""
-
-def make_tree(tuple):
-    n = Node(tuple[0])
-    for child in tuple[1]:
-        n.append(make_tree(child))
-    return n
-
-class UIlibHTMLGenerationTC(TestCase):
-    """ a basic tree node, caracterised by an id"""
-    def setUp(self):
-        """ called before each test from this class """
-        self.o = make_tree(tree)
-
-    def test_generated_html(self):
-        s = uilib.render_HTML_tree(self.o, selected_node="child_2_2")
-        self.assertTextEqual(s, generated_html)
-
-
 if __name__ == '__main__':
     unittest_main()
 
--- a/common/uilib.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/common/uilib.py	Tue Sep 22 13:08:42 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')
@@ -116,7 +115,7 @@
 try:
     from lxml import etree
 except (ImportError, AttributeError):
-    # gae environment: lxml not availabel
+    # gae environment: lxml not available
     pass
 else:
 
@@ -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">&nbsp;</td>'
-            s += '<td class="tree_cell_%d_1">&nbsp;</td>' % link_cell
-            s += '<td class="tree_cell_%d_2">&nbsp;</td>' % link_cell
-
-        cell = line[-1]
-        if cell:
-            if cell.id == selected_node:
-                s += '<td class="tree_cell" rowspan="2"><div id="selected" class="tree_cell">%s</div></td>' % (render_node(cell.id))
-            else:
-                s += '<td class="tree_cell" rowspan="2"><div class="tree_cell">%s</div></td>' % (render_node(cell.id))
-        else:
-            s += '<td rowspan="2">&nbsp;</td>'
-
-        s += '</tr>\n'
-        if link_line:
-            s += '<tr>'
-            for j, link_cell in enumerate(link_line):
-                s += '<td class="tree_cell_%d_3">&nbsp;</td>' % link_cell
-                s += '<td class="tree_cell_%d_4">&nbsp;</td>' % link_cell
-            s += '</tr>\n'
-
-    s += '</table>'
-    return s
-
-
-
 # traceback formatting ########################################################
 
 import traceback
@@ -428,7 +308,7 @@
             xml_escape(stackentry[0]), stackentry[1], xml_escape(stackentry[2])))
         if stackentry[3]:
             string = xml_escape(stackentry[3]).decode('utf-8', 'replace')
-            strings.append(u'&nbsp;&nbsp;%s<br/>\n' % (string))
+            strings.append(u'&#160;&#160;%s<br/>\n' % (string))
         # add locals info for each entry
         try:
             local_context = tcbk.tb_frame.f_locals
--- a/cwconfig.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/cwconfig.py	Tue Sep 22 13:08:42 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
@@ -134,6 +135,9 @@
     'float' : 'Float',
     }
 
+_forced_mode = os.environ.get('CW_MODE')
+assert _forced_mode in (None, 'system', 'user')
+
 class CubicWebNoAppConfiguration(ConfigurationMixIn):
     """base class for cubicweb configuration without a specific instance directory
     """
@@ -150,7 +154,7 @@
         CUBES_DIR = '%(APYCOT_ROOT)s/local/share/cubicweb/cubes/' % os.environ
         # create __init__ file
         file(join(CUBES_DIR, '__init__.py'), 'w').close()
-    elif exists(join(CW_SOFTWARE_ROOT, '.hg')) or os.environ.get('CW_MODE') == 'user':
+    elif (exists(join(CW_SOFTWARE_ROOT, '.hg')) and _forced_mode != 'system') or _forced_mode == 'user':
         mode = 'dev'
         CUBES_DIR = abspath(normpath(join(CW_SOFTWARE_ROOT, '../cubes')))
     else:
@@ -316,7 +320,7 @@
         return getattr(cls.cube_pkginfo(cube), '__recommend__', ())
 
     @classmethod
-    def expand_cubes(cls, cubes):
+    def expand_cubes(cls, cubes, with_recommends=False):
         """expand the given list of top level cubes used by adding recursivly
         each cube dependencies
         """
@@ -329,6 +333,12 @@
                     depcube = CW_MIGRATION_MAP.get(depcube, depcube)
                     cubes.append(depcube)
                     todo.append(depcube)
+            if with_recommends:
+                for depcube in cls.cube_recommends(cube):
+                    if depcube not in cubes:
+                        depcube = CW_MIGRATION_MAP.get(depcube, depcube)
+                        cubes.append(depcube)
+                        todo.append(depcube)
         return cubes
 
     @classmethod
@@ -382,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')
@@ -517,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/'
@@ -642,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
@@ -785,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
 
@@ -798,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/cwctl.py	Tue Sep 22 13:08:42 2009 +0200
@@ -5,15 +5,22 @@
 %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
 from logilab.common.shellutils import ASK
 
-from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage, underline_title
+from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage
 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg, CONFIGURATIONS
-from cubicweb.toolsutils import Command, main_run,  rm, create_dir
+from cubicweb.toolsutils import Command, main_run, rm, create_dir, underline_title
+
 
 def wait_process_end(pid, maxtry=10, waittime=1):
     """wait for a process to actually die"""
@@ -23,7 +30,7 @@
     while nbtry < maxtry:
         try:
             kill(pid, signal.SIGUSR1)
-        except OSError:
+        except (OSError, AttributeError): # XXX win32
             break
         nbtry += 1
         sleep(waittime)
@@ -103,7 +110,7 @@
         for appid in args:
             if askconfirm:
                 print '*'*72
-                if not confirm('%s instance %r ?' % (self.name, appid)):
+                if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
                     continue
             self.run_arg(appid)
 
@@ -137,7 +144,7 @@
         for appid in args:
             if askconfirm:
                 print '*'*72
-                if not confirm('%s instance %r ?' % (self.name, appid)):
+                if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
                     continue
             if forkcmd:
                 status = system('%s %s' % (forkcmd, appid))
@@ -146,6 +153,7 @@
             else:
                 self.run_arg(appid)
 
+
 # base commands ###############################################################
 
 class ListCommand(Command):
@@ -307,8 +315,8 @@
         errors = config.i18ncompile(langs)
         if errors:
             print '\n'.join(errors)
-            if not confirm('error while compiling message catalogs, '
-                           'continue anyway ?'):
+            if not ASK.confirm('error while compiling message catalogs, '
+                               'continue anyway ?'):
                 print 'creation not completed'
                 return
         # create the additional data directory for this instance
@@ -359,7 +367,7 @@
 
 # instance commands ########################################################
 
-class StartInstanceCommand(InstanceCommand):
+class StartInstanceCommand(InstanceCommandFork):
     """Start the given instances. If no instance is given, start them all.
 
     <instance>...
@@ -391,17 +399,15 @@
 
     def start_instance(self, appid):
         """start the instance's server"""
-        # use get() since start may be used from other commands (eg upgrade)
-        # without all options defined
-        debug = self.get('debug')
-        force = self.get('force')
-        loglevel = self.get('loglevel')
+        debug = self['debug']
+        force = self['force']
+        loglevel = self['loglevel']
         config = cwcfg.config_for(appid)
         if loglevel is not None:
             loglevel = 'LOG_%s' % loglevel.upper()
             config.global_set_option('log-threshold', loglevel)
             config.init_log(loglevel, debug=debug, force=True)
-        if self.get('profile'):
+        if self['profile']:
             config.global_set_option('profile', self.config.profile)
         helper = self.config_helper(config, cmdname='start')
         pidf = config['pid-file']
@@ -409,8 +415,10 @@
             msg = "%s seems to be running. Remove %s by hand if necessary or use \
 the --force option."
             raise ExecutionError(msg % (appid, pidf))
-        helper.start_command(config, debug)
-        return True
+        helper.start_server(config, debug)
+        if not debug:
+            # in debug mode, we reach this point once the instance is stopped...
+            print 'instance %s %s' % (appid, self.actionverb)
 
 
 class StopInstanceCommand(InstanceCommand):
@@ -463,8 +471,7 @@
         print 'instance %s stopped' % appid
 
 
-class RestartInstanceCommand(StartInstanceCommand,
-                                StopInstanceCommand):
+class RestartInstanceCommand(StartInstanceCommand):
     """Restart the given instances.
 
     <instance>...
@@ -483,14 +490,12 @@
         print ('some specific start order is specified, will first stop all '
                'instances then restart them.')
         # get instances in startorder
-        stopped = []
         for appid in args:
             if askconfirm:
                 print '*'*72
-                if not confirm('%s instance %r ?' % (self.name, appid)):
+                if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
                     continue
-            self.stop_instance(appid)
-            stopped.append(appid)
+            StopInstanceCommand().stop_instance(appid)
         forkcmd = [w for w in sys.argv if not w in args]
         forkcmd[1] = 'start'
         forkcmd = ' '.join(forkcmd)
@@ -500,9 +505,8 @@
                 sys.exit(status)
 
     def restart_instance(self, appid):
-        self.stop_instance(appid)
-        if self.start_instance(appid):
-            print 'instance %s %s' % (appid, self.actionverb)
+        StopInstanceCommand().stop_instance(appid)
+        self.start_instance(appid)
 
 
 class ReloadConfigurationCommand(RestartInstanceCommand):
@@ -553,9 +557,7 @@
             print "running with pid %s" % (pid)
 
 
-class UpgradeInstanceCommand(InstanceCommandFork,
-                                StartInstanceCommand,
-                                StopInstanceCommand):
+class UpgradeInstanceCommand(InstanceCommandFork):
     """Upgrade an instance after cubicweb and/or component(s) upgrade.
 
     For repository update, you will be prompted for a login / password to use
@@ -611,10 +613,6 @@
           }),
         )
 
-    def ordered_instances(self):
-        # need this since mro return StopInstanceCommand implementation
-        return InstanceCommand.ordered_instances(self)
-
     def upgrade_instance(self, appid):
         print '\n' + underline_title('Upgrading the instance %s' % appid)
         from logilab.common.changelog import Version
@@ -661,7 +659,7 @@
             print '-> migration needed from %s to %s for %s' % (fromversion, toversion, cube)
         # only stop once we're sure we have something to do
         if not (cwcfg.mode == 'dev' or self.config.nostartstop):
-            self.stop_instance(appid)
+            StopInstanceCommand().stop_instance(appid)
         # run cubicweb/componants migration scripts
         mih.migrate(vcconf, reversed(toupgrade), self.config)
         # rewrite main configuration file
@@ -676,15 +674,15 @@
         errors = config.i18ncompile(langs)
         if errors:
             print '\n'.join(errors)
-            if not confirm('Error while compiling message catalogs, '
-                           'continue anyway ?'):
+            if not ASK.confirm('Error while compiling message catalogs, '
+                               'continue anyway ?'):
                 print '-> migration not completed.'
                 return
         mih.shutdown()
         print
         print '-> instance migrated.'
         if not (cwcfg.mode == 'dev' or self.config.nostartstop):
-            self.start_instance(appid)
+            StartInstanceCommand().start_instance(appid)
         print
 
 
@@ -716,7 +714,13 @@
 sources for migration will be automatically selected.",
           }),
 
+        ('force',
+         {'short': 'f', 'action' : 'store_true',
+          'default' : False,
+          'help': 'don\'t check instance is up to date.'}
+         ),
         )
+
     def run(self, args):
         appid = pop_arg(args, 99, msg="No instance specified !")
         config = cwcfg.config_for(appid)
@@ -728,6 +732,7 @@
         else:
             sources = ('all',)
         config.set_sources_mode(sources)
+        config.repairing = self.config.force
         mih = config.migration_handler()
         if args:
             for arg in args:
--- a/cwvreg.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/cwvreg.py	Tue Sep 22 13:08:42 2009 +0200
@@ -16,12 +16,18 @@
 
 from cubicweb import (ETYPE_NAME_MAP, Binary, UnknownProperty, UnknownEid,
                       ObjectNotFound, NoSelectableObject, RegistryNotFound,
-                      RegistryOutOfDate, CW_EVENT_MANAGER)
+                      RegistryOutOfDate, CW_EVENT_MANAGER, onevent)
 from cubicweb.utils import dump_class
-from cubicweb.vregistry import VRegistry, Registry
+from cubicweb.vregistry import VRegistry, Registry, class_regid
 from cubicweb.rtags import RTAGS
 
 
+@onevent('before-registry-reload')
+def clear_rtag_objects():
+    for rtag in RTAGS:
+        rtag.clear()
+
+
 def use_interfaces(obj):
     """return interfaces used by the given object by searchinf for implements
     selectors, with a bw compat fallback to accepts_interfaces attribute
@@ -47,15 +53,15 @@
     def __init__(self, vreg):
         super(CWRegistry, self).__init__(vreg.config)
         self.vreg = vreg
-        self.schema = vreg.schema
+
+    @property
+    def schema(self):
+        return self.vreg.schema
 
     def initialization_completed(self):
-        # call vreg_initialization_completed on appobjects and print
-        # registry content
-        for appobjects in self.itervalues():
-            for appobject in appobjects:
-                appobject.vreg_initialization_completed()
+        pass
 
+    @deprecated('[3.6] select object, then use obj.render()')
     def render(self, __oid, req, __fallback_oid=None, rset=None, **kwargs):
         """select object, or fallback object if specified and the first one
         isn't selectable, then render it
@@ -65,23 +71,25 @@
         except NoSelectableObject:
             if __fallback_oid is None:
                 raise
-            obj = self.select(__fallback_oid, req, **kwargs)
+            obj = self.select(__fallback_oid, req, rset=rset, **kwargs)
         return obj.render(**kwargs)
 
+    @deprecated('[3.6] use select_or_none and test for obj.cw_propval("visible")')
     def select_vobject(self, oid, *args, **kwargs):
-        selected = self.select_object(oid, *args, **kwargs)
-        if selected and selected.propval('visible'):
+        selected = self.select_or_none(oid, *args, **kwargs)
+        if selected and selected.cw_propval('visible'):
             return selected
         return None
 
-    def possible_vobjects(self, *args, **kwargs):
+    def poss_visible_objects(self, *args, **kwargs):
         """return an ordered list of possible app objects in a given registry,
         supposing they support the 'visible' and 'order' properties (as most
         visualizable objects)
         """
         return sorted([x for x in self.possible_objects(*args, **kwargs)
-                       if x.propval('visible')],
-                      key=lambda x: x.propval('order'))
+                       if x.cw_propval('visible')],
+                      key=lambda x: x.cw_propval('order'))
+    possible_vobjects = deprecated('[3.6] use poss_visible_objects()')(poss_visible_objects)
 
 
 VRegistry.REGISTRY_FACTORY[None] = CWRegistry
@@ -97,15 +105,24 @@
         clear_cache(self, 'etype_class')
 
     def register(self, obj, **kwargs):
-        oid = kwargs.get('oid') or obj.id
+        oid = kwargs.get('oid') or class_regid(obj)
         if oid != 'Any' and not oid in self.schema:
             self.error('don\'t register %s, %s type not defined in the '
-                       'schema', obj, obj.id)
+                       'schema', obj, oid)
             return
         kwargs['clear'] = True
         super(ETypeRegistry, self).register(obj, **kwargs)
 
     @cached
+    def parent_classes(self, etype):
+        if etype == 'Any':
+            return [self.etype_class('Any')]
+        eschema = self.schema.eschema(etype)
+        parents = [self.etype_class(e.type) for e in eschema.ancestors()]
+        parents.append(self.etype_class('Any'))
+        return parents
+
+    @cached
     def etype_class(self, etype):
         """return an entity class for the given entity type.
 
@@ -129,7 +146,12 @@
             try:
                 objects = self[btype]
                 assert len(objects) == 1, objects
-                cls = objects[0]
+                if btype == etype:
+                    cls = objects[0]
+                else:
+                    # recurse to ensure issubclass(etype_class('Child'),
+                    #                              etype_class('Parent'))
+                    cls = self.etype_class(btype)
                 break
             except ObjectNotFound:
                 pass
@@ -139,12 +161,12 @@
             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__()
+        cls.__id__ = etype
+        cls.__initialize__(self.schema)
         return cls
 
 VRegistry.REGISTRY_FACTORY['etypes'] = ETypeRegistry
@@ -172,7 +194,7 @@
             if vid[0] == '_':
                 continue
             try:
-                view = self.select_best(views, req, rset=rset, **kwargs)
+                view = self._select_best(views, req, rset=rset, **kwargs)
                 if view.linkable():
                     yield view
             except NoSelectableObject:
@@ -228,8 +250,8 @@
             config.init_log(debug=debug)
         super(CubicWebVRegistry, self).__init__(config)
         self.schema = None
+        self.initialized = False
         self.reset()
-        self.initialized = False
 
     def setdefault(self, regid):
         try:
@@ -250,29 +272,47 @@
     def itervalues(self):
         return (value for key, value in self.items())
 
-    def reset(self):
-        super(CubicWebVRegistry, self).reset()
+    def reset(self, path=None, force_reload=None):
+        super(CubicWebVRegistry, self).reset(path, force_reload)
         self._needs_iface = {}
         # two special registries, propertydefs which care all the property
         # definitions, and propertyvals which contains values for those
         # properties
-        self['propertydefs'] = {}
-        self['propertyvalues'] = self.eprop_values = {}
-        for key, propdef in self.config.eproperty_definitions():
-            self.register_property(key, **propdef)
+        if not self.initialized:
+            self['propertydefs'] = {}
+            self['propertyvalues'] = self.eprop_values = {}
+            for key, propdef in self.config.eproperty_definitions():
+                self.register_property(key, **propdef)
+        if path is not None and force_reload:
+            cleanup_sys_modules(path)
+            cubes = self.config.cubes()
+            # if the fs code use some cubes not yet registered into the instance
+            # we should cleanup sys.modules for those as well to avoid potential
+            # bad class reference pb after reloading
+            cfg = self.config
+            for cube in cfg.expand_cubes(cubes, with_recommends=True):
+                if not cube in cubes:
+                    cpath = cfg.build_vregistry_cube_path([cfg.cube_dir(cube)])
+                    cleanup_sys_modules(cpath)
 
     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
-        self.register_objects(self.config.vregistry_path())
+        searchpath = self.config.vregistry_path()
+        self.reset(searchpath, force_reload=False)
+        self.register_objects(searchpath, force_reload=False)
         # map lowered entity type names to their actual name
         self.case_insensitive_etypes = {}
         for etype in self.schema.entities():
             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
@@ -293,8 +333,8 @@
         else:
             self._needs_iface[obj] = ifaces
 
-    def register(self, obj, **kwargs):
-        super(CubicWebVRegistry, self).register(obj, **kwargs)
+    def register(self, obj, *args, **kwargs):
+        super(CubicWebVRegistry, self).register(obj, *args, **kwargs)
         # XXX bw compat
         ifaces = use_interfaces(obj)
         if ifaces:
@@ -302,15 +342,16 @@
 
     def register_objects(self, path, force_reload=None):
         """overriden to remove objects requiring a missing interface"""
+        if force_reload is None:
+            force_reload = self.config.mode == 'dev'
         try:
             self._register_objects(path, force_reload)
         except RegistryOutOfDate:
-            CW_EVENT_MANAGER.emit('before-source-reload')
+            CW_EVENT_MANAGER.emit('before-registry-reload')
             # modification detected, reset and reload
-            self.reset()
-            cleanup_sys_modules(path)
+            self.reset(path, force_reload)
             self._register_objects(path, force_reload)
-            CW_EVENT_MANAGER.emit('after-source-reload')
+            CW_EVENT_MANAGER.emit('after-registry-reload')
 
     def _register_objects(self, path, force_reload=None):
         """overriden to remove objects requiring a missing interface"""
@@ -337,6 +378,8 @@
             implemented_interfaces = set()
             if 'Any' in self.get('etypes', ()):
                 for etype in self.schema.entities():
+                    if etype.is_final():
+                        continue
                     cls = self['etypes'].etype_class(etype)
                     for iface in cls.__implements__:
                         implemented_interfaces.update(iface.__mro__)
@@ -355,16 +398,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
@@ -372,38 +406,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 #####################################################
 
@@ -476,6 +491,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/dbapi.py	Tue Sep 22 13:08:42 2009 +0200
@@ -19,8 +19,9 @@
 from logilab.common.decorators import monkeypatch
 from logilab.common.deprecation import deprecated
 
-from cubicweb import ETYPE_NAME_MAP, ConnectionError, RequestSessionMixIn
-from cubicweb import cwvreg, cwconfig
+from cubicweb import ETYPE_NAME_MAP, ConnectionError, cwvreg, cwconfig
+from cubicweb.req import RequestSessionBase
+
 
 _MARKER = object()
 
@@ -42,9 +43,9 @@
     registries.
     """
     defaultcls = cwvreg.VRegistry.REGISTRY_FACTORY[None]
-    orig_select_best = defaultcls.orig_select_best = defaultcls.select_best
+    orig_select_best = defaultcls.orig_select_best = defaultcls._select_best
     @monkeypatch(defaultcls)
-    def select_best(self, appobjects, *args, **kwargs):
+    def _select_best(self, appobjects, *args, **kwargs):
         """return an instance of the most specific object according
         to parameters
 
@@ -174,7 +175,7 @@
     return repo, cnx
 
 
-class DBAPIRequest(RequestSessionMixIn):
+class DBAPIRequest(RequestSessionBase):
 
     def __init__(self, vreg, cnx=None):
         super(DBAPIRequest, self).__init__(vreg)
@@ -214,10 +215,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):
@@ -284,6 +288,12 @@
 
     # server session compat layer #############################################
 
+    def hijack_user(self, user):
+        """return a fake request/session using specified user"""
+        req = DBAPIRequest(self.vreg)
+        req.set_connection(self.cnx, user)
+        return req
+
     @property
     def user(self):
         if self._user is None and self.cnx:
@@ -354,7 +364,7 @@
 # connection object ###########################################################
 
 class Connection(object):
-    """DB-API 2.0 compatible Connection object for CubicWebt
+    """DB-API 2.0 compatible Connection object for CubicWeb
     """
     # make exceptions available through the connection object
     ProgrammingError = ProgrammingError
--- a/debian.hardy/control	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-Source: cubicweb
-Section: web
-Priority: optional
-Maintainer: Logilab S.A. <contact@logilab.fr>
-Uploaders: Sylvain Thenault <sylvain.thenault@logilab.fr>,
-           Julien Jehannet <julien.jehannet@logilab.fr>,
-           Aurélien Campéas <aurelien.campeas@logilab.fr>
-Build-Depends: debhelper (>= 5), python-dev (>=2.4), python-central (>= 0.5)
-Standards-Version: 3.8.0
-Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.4, << 2.6
-
-
-Package: cubicweb
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-twisted (= ${source:Version}), cubicweb-client (= ${source:Version})
-XB-Recommends: (postgresql, postgresql-plpython, postgresql-contrib) | mysql | sqlite3
-Recommends: postgresql | mysql | sqlite3
-Description: the complete CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package will install all the components you need to run cubicweb on
- a single machine. You can also deploy cubicweb by running the different
- process on different computers, in which case you need to install the
- corresponding packages on the different hosts.
-
-
-Package: cubicweb-server
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Conflicts: cubicweb-multisources
-Replaces: cubicweb-multisources
-Provides: cubicweb-multisources
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-indexer (>= 0.6.1), python-psycopg2 | python-mysqldb | python-pysqlite2
-Recommends: pyro, cubicweb-documentation (= ${source:Version})
-Description: server part of the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides the repository server part of the system.
- .
- This package provides the repository server part of the library and
- necessary shared data files such as the schema library.
-
-
-Package: cubicweb-twisted
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Provides: cubicweb-web-frontend
-Depends: ${python:Depends}, cubicweb-web (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-twisted-web2
-Recommends: pyro, cubicweb-documentation (= ${source:Version})
-Description: twisted-based web interface for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides a twisted based HTTP server to serve
- the adaptative web interface (see cubicweb-web package).
- .
- This package provides only the twisted server part of the library.
-
-
-Package: cubicweb-web
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), python-docutils, python-vobject, python-elementtree
-Recommends: fckeditor
-Description: web interface library for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides an adaptative web interface to the CubicWeb server.
- Install the cubicweb-twisted package to serve this interface via HTTP.
- .
- This package provides the web interface part of the library and
- necessary shared data files such as defaut views, images...
-
-
-Package: cubicweb-common
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, python-logilab-mtconverter (>= 0.6.0), python-simpletal (>= 4.0), graphviz, gettext, python-lxml, python-logilab-common (>= 0.38.1), python-yams (>= 0.20.2), python-rql (>= 0.20.2), python-simplejson (>= 1.3)
-Recommends: python-psyco
-Conflicts: cubicweb-core
-Replaces: cubicweb-core
-Description: common library for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides the common parts of the library used by both server
- code and web application code.
-
-
-Package: cubicweb-ctl
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version})
-Description: tool to manage the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides a control script to manage (create, upgrade, start,
- stop, etc) CubicWeb applications. It also include the init.d script
- to automatically start and stop CubicWeb applications on boot or shutdown.
-
-
-Package: cubicweb-client
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-ctl (= ${source:Version}), pyro
-Description: RQL command line client for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides a RQL (Relation Query Language) command line client using
- pyro to connect to a repository server.
-
-
-Package: cubicweb-dev
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-web (= ${source:Version}), python-pysqlite2
-Suggests: w3c-dtd-xhtml
-Description: tests suite and development tools for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides the CubicWeb tests suite and some development tools
- helping in the creation of application.
-
-
-Package: cubicweb-documentation
-Architecture: all
-Recommends: doc-base
-Description: documentation for the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides the system's documentation.
--- a/debian.hardy/rules	Wed Aug 05 09:15:56 2009 +0200
+++ b/debian.hardy/rules	Tue Sep 22 13:08:42 2009 +0200
@@ -39,13 +39,20 @@
 	# Put all the python library and data in cubicweb-common
 	# and scripts in cubicweb-server
 	dh_install -vi
-	#dh_lintian XXX not before debhelper 7
+	# cwctl in the cubicweb-ctl package
+	rm -f debian/cubicweb-common/usr/share/pyshared/cubicweb/cwctl.py
+	# hercule in the cubicweb-client package
+	rm -f debian/cubicweb-common/usr/share/pyshared/cubicweb/hercule.py
+
 
 	# Remove unittests directory (should be available in cubicweb-dev only)
 	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/server/test
 	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/sobjects/test
 	rm -rf debian/cubicweb-web/usr/lib/${PY_VERSION}/site-packages/cubicweb/web/test
+	rm -rf debian/cubicweb-twisted/usr/lib/${PY_VERSION}/site-packages/cubicweb/etwist/test
+	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/ext/test
 	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/common/test
+	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/entities/test
 
 	# cubes directory must be managed as a valid python module
 	touch debian/cubicweb-common/usr/share/cubicweb/cubes/__init__.py
--- a/debian/changelog	Wed Aug 05 09:15:56 2009 +0200
+++ b/debian/changelog	Tue Sep 22 13:08:42 2009 +0200
@@ -1,3 +1,99 @@
+cubicweb (3.5.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Adrien Di Mascio <Adrien.DiMascio@logilab.fr>  Tue, 22 Sep 2009 09:31:42 +0200
+
+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
+
+ -- Aurélien Campéas <aurelien.campeas@logilab.fr>  Tue, 11 Sep 2009 12:20:00 +0200
+
+cubicweb (3.4.10-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 11 Sep 2009 17:19:37 +0200
+
+cubicweb (3.4.9-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 11 Sep 2009 14:27:39 +0200
+
+cubicweb (3.4.8-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 10 Sep 2009 15:27:17 +0200
+
+cubicweb (3.4.7-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 09 Sep 2009 14:08:41 +0200
+
+cubicweb (3.4.6-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 31 Aug 2009 14:12:30 +0200
+
+cubicweb (3.4.5-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 26 Aug 2009 15:01:42 +0200
+
+cubicweb (3.4.4-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Tue, 18 Aug 2009 14:27:08 +0200
+
+cubicweb (3.4.3-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Nicolas Chauvat <nicolas.chauvat@logilab.fr>  Fri, 14 Aug 2009 17:54:06 +0200
+
+cubicweb (3.4.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 07 Aug 2009 17:50:15 +0200
+
+cubicweb (3.4.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 07 Aug 2009 14:00:33 +0200
+
+cubicweb (3.4.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 07 Aug 2009 10:43:21 +0200
+
+cubicweb (3.3.5-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 06 Aug 2009 09:05:22 +0200
+
 cubicweb (3.3.4-2) unstable; urgency=low
 
   * fix conflicting test files
@@ -108,37 +204,37 @@
 
  -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Tue, 27 Jan 2009 16:05:12 +0100
 
-cubicweb (3.0.3-1) DISTRIBUTION; urgency=low
+cubicweb (3.0.3-1) unstable; urgency=low
 
   * new upstream release
 
  -- Aurélien Campéas <aurelien.campeas@logilab.fr>  Wed, 14 Jan 2009 18:25:21 +0100
 
-cubicweb (3.0.2-1) DISTRIBUTION; urgency=low
+cubicweb (3.0.2-1) unstable; urgency=low
 
   * new upstream release
 
  -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 14 Jan 2009 13:25:21 +0100
 
-cubicweb (3.0.1-1) DISTRIBUTION; urgency=low
+cubicweb (3.0.1-1) unstable; urgency=low
 
   * new upstream release
 
  -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 08 Jan 2009 18:27:37 +0100
 
-cubicweb (3.0.0-1) DISTRIBUTION; urgency=low
+cubicweb (3.0.0-1) unstable; urgency=low
 
   * new upstream release
 
  -- Adrien Di Mascio <Adrien.DiMascio@logilab.fr>  Tue, 23 Dec 2008 18:42:43 +0100
 
-cubicweb (2.99.99-1) DISTRIBUTION; urgency=low
+cubicweb (2.99.99-1) unstable; urgency=low
 
   * new upstream release
 
  -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 22 Dec 2008 17:34:45 +0100
 
-cubicweb (2.99.7-1) DISTRIBUTION; urgency=low
+cubicweb (2.99.7-1) unstable; urgency=low
 
   * remove buggy cubicweb-common.postinst
   * replace erudi|ginco by cubicweb where necessary in existing instance
@@ -146,43 +242,43 @@
 
  -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 19 Dec 2008 17:11:00 +0100
 
-cubicweb (2.99.6-1) DISTRIBUTION; urgency=low
+cubicweb (2.99.6-1) unstable; urgency=low
 
   * new upstream release
 
  -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 19 Dec 2008 15:16:37 +0100
 
-cubicweb (2.99.5-1) DISTRIBUTION; urgency=low
+cubicweb (2.99.5-1) unstable; urgency=low
 
   * new upstream release
 
  -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 17 Dec 2008 14:14:26 +0100
 
-cubicweb (2.99.4-1) DISTRIBUTION; urgency=low
+cubicweb (2.99.4-1) unstable; urgency=low
 
   * new upstream release
 
  -- Adrien Di Mascio <Adrien.DiMascio@logilab.fr>  Fri, 12 Dec 2008 10:00:31 +0100
 
-cubicweb (2.99.3-1) DISTRIBUTION; urgency=low
+cubicweb (2.99.3-1) unstable; urgency=low
 
   * new upstream release
 
  -- Adrien Di Mascio <Adrien.DiMascio@logilab.fr>  Tue, 09 Dec 2008 07:42:16 +0100
 
-cubicweb (2.99.2-1) DISTRIBUTION; urgency=low
+cubicweb (2.99.2-1) unstable; urgency=low
 
   * new upstream release
 
  -- Adrien Di Mascio <adrien.dimascio@logilab.fr>  Mon, 17 Nov 2008 15:02:16 +0100
 
-cubicweb (2.99.1-1) DISTRIBUTION; urgency=low
+cubicweb (2.99.1-1) unstable; urgency=low
 
   * new upstream release
 
  -- Nicolas Chauvat <nicolas.chauvat@logilab.fr>  Thu, 13 Nov 2008 11:02:16 +0100
 
-cubicweb (2.99.0-1) DISTRIBUTION; urgency=low
+cubicweb (2.99.0-1) unstable; urgency=low
 
   * initial public release
 
--- a/debian/control	Wed Aug 05 09:15:56 2009 +0200
+++ b/debian/control	Tue Sep 22 13:08:42 2009 +0200
@@ -6,7 +6,7 @@
            Julien Jehannet <julien.jehannet@logilab.fr>,
            Aurélien Campéas <aurelien.campeas@logilab.fr>,
            Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 7), python-dev (>=2.4), python-central (>= 0.5)
+Build-Depends: debhelper (>= 5), python-dev (>=2.4), python-central (>= 0.5)
 Standards-Version: 3.8.0
 Homepage: http://www.cubicweb.org
 XS-Python-Version: >= 2.4, << 2.6
@@ -62,7 +62,7 @@
 Architecture: all
 XB-Python-Version: ${python:Versions}
 Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3), python-elementtree
-Recommends: python-docutils, python-vobject, fckeditor, python-fyzz
+Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-pysixt, fop
 Description: web interface library for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -76,7 +76,7 @@
 Package: cubicweb-common
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.41.0), python-yams (>= 0.23.0), python-rql (>= 0.22.1), python-lxml
+Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.44.0), python-yams (>= 0.24.0), python-rql (>= 0.22.3), python-lxml
 Recommends: python-simpletal (>= 4.0)
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/debian/cubicweb-ctl.cubicweb.init	Wed Aug 05 09:15:56 2009 +0200
+++ b/debian/cubicweb-ctl.cubicweb.init	Tue Sep 22 13:08:42 2009 +0200
@@ -22,11 +22,14 @@
 # Check if we are sure to not want the start-stop-daemon machinery here
 # Refer to Debian Policy Manual section 9.3.2 (Writing the scripts) for details.
 
-case "$1" in
-	"force-reload")
-		/usr/bin/cubicweb-ctl reload --force
-		;;
-	"*|restart")
-		/usr/bin/cubicweb-ctl $1 --force
-		;;
+case $1 in
+    force-reload)
+        /usr/bin/cubicweb-ctl reload --force
+        ;;
+    status)
+        /usr/bin/cubicweb-ctl status
+        ;;
+    *)
+        /usr/bin/cubicweb-ctl $1 --force
+        ;;
 esac
--- a/debian/cubicweb-dev.install.in	Wed Aug 05 09:15:56 2009 +0200
+++ b/debian/cubicweb-dev.install.in	Tue Sep 22 13:08:42 2009 +0200
@@ -6,6 +6,7 @@
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/ext/test usr/lib/PY_VERSION/site-packages/cubicweb/ext/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/server/test usr/lib/PY_VERSION/site-packages/cubicweb/server/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/test usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/
+debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/hooks/test usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/web/test usr/lib/PY_VERSION/site-packages/cubicweb/web/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/etwist/test usr/lib/PY_VERSION/site-packages/cubicweb/etwist/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/goa/test usr/lib/PY_VERSION/site-packages/cubicweb/goa/
--- a/debian/cubicweb-server.install.in	Wed Aug 05 09:15:56 2009 +0200
+++ b/debian/cubicweb-server.install.in	Tue Sep 22 13:08:42 2009 +0200
@@ -1,4 +1,5 @@
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/server/ usr/lib/PY_VERSION/site-packages/cubicweb
+debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/hooks/ usr/lib/PY_VERSION/site-packages/cubicweb
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/ usr/lib/PY_VERSION/site-packages/cubicweb
-debian/tmp/usr/share/cubicweb/schemas/ usr/share/cubicweb/
+debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/schemas/ usr/lib/PY_VERSION/site-packages/cubicweb
 debian/tmp/usr/share/cubicweb/migration/ usr/share/cubicweb/
--- a/debian/rules	Wed Aug 05 09:15:56 2009 +0200
+++ b/debian/rules	Tue Sep 22 13:08:42 2009 +0200
@@ -48,6 +48,7 @@
 
 	# Remove unittests directory (should be available in cubicweb-dev only)
 	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/server/test
+	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/hooks/test
 	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/sobjects/test
 	rm -rf debian/cubicweb-web/usr/lib/${PY_VERSION}/site-packages/cubicweb/web/test
 	rm -rf debian/cubicweb-twisted/usr/lib/${PY_VERSION}/site-packages/cubicweb/etwist/test
--- a/devtools/__init__.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/devtools/__init__.py	Tue Sep 22 13:08:42 2009 +0200
@@ -13,20 +13,55 @@
 from os.path import (abspath, join, exists, basename, dirname, normpath, split,
                      isfile, isabs)
 
-from cubicweb import CW_SOFTWARE_ROOT, ConfigurationError
+from cubicweb import CW_SOFTWARE_ROOT, ConfigurationError, schema, cwconfig
 from cubicweb.utils import strptime
-from cubicweb.toolsutils import read_config
-from cubicweb.cwconfig import CubicWebConfiguration, merge_options
 from cubicweb.server.serverconfig import ServerConfiguration
 from cubicweb.etwist.twconfig import TwistedConfiguration
 
+cwconfig.CubicWebConfiguration.cls_adjust_sys_path()
+
+# db auto-population configuration #############################################
+
+SYSTEM_ENTITIES = schema.SCHEMA_TYPES | set((
+    'CWGroup', 'CWUser', 'CWProperty',
+    'Workflow', 'State', 'BaseTransition', 'Transition', 'WorkflowTransition',
+    'TrInfo', 'SubWorkflowExitPoint',
+    ))
+
+SYSTEM_RELATIONS = schema.META_RTYPES | set((
+    # workflow related
+    'workflow_of', 'state_of', 'transition_of', 'initial_state', 'allowed_transition',
+    'destination_state', 'in_state', 'wf_info_for', 'from_state', 'to_state',
+    'condition', 'subworkflow', 'subworkflow_state', 'subworkflow_exit',
+    # cwproperty
+    'for_user',
+    # schema definition
+    'specializes',
+    'relation_type', 'from_entity', 'to_entity',
+    'constrained_by', 'cstrtype', 'widget',
+    'read_permission', 'update_permission', 'delete_permission', 'add_permission',
+    # permission
+    'in_group', 'require_group', 'require_permission',
+    # deducted from other relations
+    'primary_email',
+    ))
+
+# content validation configuration #############################################
+
 # validators are used to validate (XML, DTD, whatever) view's content
 # validators availables are :
 #  'dtd' : validates XML + declared DTD
 #  'xml' : guarantees XML is well formed
 #  None : do not try to validate anything
+
+# {'vid': validator}
 VIEW_VALIDATORS = {}
+
+
+# cubicweb test configuration ##################################################
+
 BASE_URL = 'http://testing.fr/cubicweb/'
+
 DEFAULT_SOURCES = {'system': {'adapter' : 'native',
                               'db-encoding' : 'UTF-8', #'ISO-8859-1',
                               'db-user' : u'admin',
@@ -40,13 +75,14 @@
                               },
                    }
 
+
 class TestServerConfiguration(ServerConfiguration):
     mode = 'test'
     set_language = False
     read_instance_schema = False
     bootstrap_schema = False
     init_repository = True
-    options = merge_options(ServerConfiguration.options + (
+    options = cwconfig.merge_options(ServerConfiguration.options + (
         ('anonymous-user',
          {'type' : 'string',
           'default': None,
@@ -66,7 +102,6 @@
 
     def __init__(self, appid, log_threshold=logging.CRITICAL+10):
         ServerConfiguration.__init__(self, appid)
-        self.global_set_option('log-file', None)
         self.init_log(log_threshold, force=True)
         # need this, usually triggered by cubicweb-ctl
         self.load_cwctl_plugins()
@@ -118,30 +153,11 @@
             sources = DEFAULT_SOURCES
         return sources
 
-    def load_defaults(self):
-        super(TestServerConfiguration, self).load_defaults()
-        # note: don't call global set option here, OptionManager may not yet be initialized
-        # add anonymous user
-        self.set_option('anonymous-user', 'anon')
-        self.set_option('anonymous-password', 'anon')
-        # uncomment the line below if you want rql queries to be logged
-        #self.set_option('query-log-file', '/tmp/test_rql_log.' + `os.getpid()`)
-        self.set_option('sender-name', 'cubicweb-test')
-        self.set_option('sender-addr', 'cubicweb-test@logilab.fr')
-        try:
-            send_to =  '%s@logilab.fr' % os.getlogin()
-        except OSError:
-            send_to =  '%s@logilab.fr' % (os.environ.get('USER')
-                                          or os.environ.get('USERNAME')
-                                          or os.environ.get('LOGNAME'))
-        self.set_option('sender-addr', send_to)
-        self.set_option('default-dest-addrs', send_to)
-        self.set_option('base-url', BASE_URL)
-
 
 class BaseApptestConfiguration(TestServerConfiguration, TwistedConfiguration):
     repo_method = 'inmemory'
-    options = merge_options(TestServerConfiguration.options + TwistedConfiguration.options)
+    options = cwconfig.merge_options(TestServerConfiguration.options
+                                     + TwistedConfiguration.options)
     cubicweb_appobject_path = TestServerConfiguration.cubicweb_appobject_path | TwistedConfiguration.cubicweb_appobject_path
     cube_appobject_path = TestServerConfiguration.cube_appobject_path | TwistedConfiguration.cube_appobject_path
 
@@ -163,100 +179,93 @@
         BaseApptestConfiguration.__init__(self, appid, log_threshold=log_threshold)
         self.init_repository = sourcefile is None
         self.sourcefile = sourcefile
-        import re
-        self.global_set_option('embed-allowed', re.compile('.*'))
+        self.global_set_option('anonymous-user', 'anon')
+        self.global_set_option('anonymous-password', 'anon')
+
+    def load_configuration(self):
+        super(ApptestConfiguration, self).load_configuration()
+        self.global_set_option('anonymous-user', 'anon')
+        self.global_set_option('anonymous-password', 'anon')
 
 
-class RealDatabaseConfiguration(ApptestConfiguration):
-    init_repository = False
-    sourcesdef =  {'system': {'adapter' : 'native',
-                              'db-encoding' : 'UTF-8', #'ISO-8859-1',
-                              'db-user' : u'admin',
-                              'db-password' : 'gingkow',
-                              'db-name' : 'seotest',
-                              'db-driver' : 'postgres',
-                              'db-host' : None,
-                              },
-                   'admin' : {'login': u'admin',
-                              'password': u'gingkow',
-                              },
-                   }
+# test database handling #######################################################
 
-    def __init__(self, appid, log_threshold=logging.CRITICAL, sourcefile=None):
-        ApptestConfiguration.__init__(self, appid)
-        self.init_repository = False
+def init_test_database(config=None, configdir='data'):
+    """init a test database for a specific driver"""
+    from cubicweb.dbapi import in_memory_cnx
+    config = config or TestServerConfiguration(configdir)
+    sources = config.sources()
+    driver = sources['system']['db-driver']
+    if driver == 'sqlite':
+        init_test_database_sqlite(config)
+    elif driver == 'postgres':
+        init_test_database_postgres(config)
+    else:
+        raise ValueError('no initialization function for driver %r' % driver)
+    config._cubes = None # avoid assertion error
+    repo, cnx = in_memory_cnx(config, unicode(sources['admin']['login']),
+                              sources['admin']['password'] or 'xxx')
+    if driver == 'sqlite':
+        install_sqlite_patch(repo.querier)
+    return repo, cnx
 
 
-    def sources(self):
-        """
-        By default, we run tests with the sqlite DB backend.
-        One may use its own configuration by just creating a
-        'sources' file in the test directory from wich tests are
-        launched.
-        """
-        self._sources = self.sourcesdef
-        return self._sources
+def reset_test_database(config):
+    """init a test database for a specific driver"""
+    driver = config.sources()['system']['db-driver']
+    if driver == 'sqlite':
+        reset_test_database_sqlite(config)
+    else:
+        raise ValueError('no reset function for driver %r' % driver)
 
 
-def buildconfig(dbuser, dbpassword, dbname, adminuser, adminpassword, dbhost=None):
-    """convenience function that builds a real-db configuration class"""
-    sourcesdef =  {'system': {'adapter' : 'native',
-                              'db-encoding' : 'UTF-8', #'ISO-8859-1',
-                              'db-user' : dbuser,
-                              'db-password' : dbpassword,
-                              'db-name' : dbname,
-                              'db-driver' : 'postgres',
-                              'db-host' : dbhost,
-                              },
-                   'admin' : {'login': adminuser,
-                              'password': adminpassword,
-                              },
-                   }
-    return type('MyRealDBConfig', (RealDatabaseConfiguration,),
-                {'sourcesdef': sourcesdef})
+### postgres test database handling ############################################
 
-def loadconfig(filename):
-    """convenience function that builds a real-db configuration class
-    from a file
-    """
-    return type('MyRealDBConfig', (RealDatabaseConfiguration,),
-                {'sourcesdef': read_config(filename)})
+def init_test_database_postgres(config):
+    """initialize a fresh sqlite databse used for testing purpose"""
+    if config.init_repository:
+        from cubicweb.server import init_repository
+        init_repository(config, interactive=False, drop=True)
 
 
-class LivetestConfiguration(BaseApptestConfiguration):
-    init_repository = False
+### sqlite test database handling ##############################################
+
+def cleanup_sqlite(dbfile, removetemplate=False):
+    try:
+        os.remove(dbfile)
+        os.remove('%s-journal' % dbfile)
+    except OSError:
+        pass
+    if removetemplate:
+        try:
+            os.remove('%s-template' % dbfile)
+        except OSError:
+            pass
 
-    def __init__(self, cube=None, sourcefile=None, pyro_name=None,
-                 log_threshold=logging.CRITICAL):
-        TestServerConfiguration.__init__(self, cube, log_threshold=log_threshold)
-        self.appid = pyro_name or cube
-        # don't change this, else some symlink problems may arise in some
-        # environment (e.g. mine (syt) ;o)
-        # XXX I'm afraid this test will prevent to run test from a production
-        # environment
-        self._sources = None
-        # instance cube test
-        if cube is not None:
-            self.apphome = self.cube_dir(cube)
-        elif 'web' in os.getcwd().split(os.sep):
-            # web test
-            self.apphome = join(normpath(join(dirname(__file__), '..')), 'web')
-        else:
-            # cube test
-            self.apphome = abspath('..')
-        self.sourcefile = sourcefile
-        self.global_set_option('realm', '')
-        self.use_pyro = pyro_name is not None
+def reset_test_database_sqlite(config):
+    import shutil
+    dbfile = config.sources()['system']['db-name']
+    cleanup_sqlite(dbfile)
+    template = '%s-template' % dbfile
+    if exists(template):
+        shutil.copy(template, dbfile)
+        return True
+    return False
 
-    def pyro_enabled(self):
-        if self.use_pyro:
-            return True
-        else:
-            return False
+def init_test_database_sqlite(config):
+    """initialize a fresh sqlite databse used for testing purpose"""
+    # remove database file if it exists
+    dbfile = config.sources()['system']['db-name']
+    if not reset_test_database_sqlite(config):
+        # initialize the database
+        import shutil
+        from cubicweb.server import init_repository
+        init_repository(config, interactive=False)
+        dbfile = config.sources()['system']['db-name']
+        shutil.copy(dbfile, '%s-template' % dbfile)
 
-CubicWebConfiguration.cls_adjust_sys_path()
 
-def install_sqlite_path(querier):
+def install_sqlite_patch(querier):
     """This patch hotfixes the following sqlite bug :
        - http://www.sqlite.org/cvstrac/tktview?tn=1327,33
        (some dates are returned as strings rather thant date objects)
@@ -293,57 +302,3 @@
         return new_execute
     querier.__class__.execute = wrap_execute(querier.__class__.execute)
     querier.__class__._devtools_sqlite_patched = True
-
-def init_test_database(driver='sqlite', configdir='data', config=None,
-                       vreg=None):
-    """init a test database for a specific driver"""
-    from cubicweb.dbapi import in_memory_cnx
-    if vreg and not config:
-        config = vreg.config
-    config = config or TestServerConfiguration(configdir)
-    source = config.sources()
-    if driver == 'sqlite':
-        init_test_database_sqlite(config, source)
-    elif driver == 'postgres':
-        init_test_database_postgres(config, source)
-    else:
-        raise ValueError('no initialization function for driver %r' % driver)
-    config._cubes = None # avoid assertion error
-    repo, cnx = in_memory_cnx(vreg or config, unicode(source['admin']['login']),
-                              source['admin']['password'] or 'xxx')
-    if driver == 'sqlite':
-        install_sqlite_path(repo.querier)
-    return repo, cnx
-
-def init_test_database_postgres(config, source, vreg=None):
-    """initialize a fresh sqlite databse used for testing purpose"""
-    if config.init_repository:
-        from cubicweb.server import init_repository
-        init_repository(config, interactive=False, drop=True, vreg=vreg)
-
-def cleanup_sqlite(dbfile, removecube=False):
-    try:
-        os.remove(dbfile)
-        os.remove('%s-journal' % dbfile)
-    except OSError:
-        pass
-    if removecube:
-        try:
-            os.remove('%s-template' % dbfile)
-        except OSError:
-            pass
-
-def init_test_database_sqlite(config, source, vreg=None):
-    """initialize a fresh sqlite databse used for testing purpose"""
-    import shutil
-    # remove database file if it exists (actually I know driver == 'sqlite' :)
-    dbfile = source['system']['db-name']
-    cleanup_sqlite(dbfile)
-    template = '%s-template' % dbfile
-    if exists(template):
-        shutil.copy(template, dbfile)
-    else:
-        # initialize the database
-        from cubicweb.server import init_repository
-        init_repository(config, interactive=False, vreg=vreg)
-        shutil.copy(dbfile, template)
--- a/devtools/_apptest.py	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,256 +0,0 @@
-"""Hidden internals for the devtools.apptest module
-
-: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
-"""
-__docformat__ = "restructuredtext en"
-
-import sys, traceback
-
-from logilab.common.pytest import pause_tracing, resume_tracing
-
-import yams.schema
-
-from cubicweb.dbapi import repo_connect, ConnectionProperties, ProgrammingError
-from cubicweb.cwvreg import CubicWebVRegistry
-
-from cubicweb.web.application import CubicWebPublisher
-from cubicweb.web import Redirect
-
-from cubicweb.devtools import ApptestConfiguration, init_test_database
-from cubicweb.devtools.fake import FakeRequest
-
-SYSTEM_ENTITIES = ('CWGroup', 'CWUser',
-                   'CWAttribute', 'CWRelation',
-                   'CWConstraint', 'CWConstraintType', 'CWProperty',
-                   'CWEType', 'CWRType',
-                   'State', 'Transition', 'TrInfo',
-                   'RQLExpression',
-                   )
-SYSTEM_RELATIONS = (
-    # virtual relation
-    'identity',
-    # metadata
-    'is', 'is_instance_of', 'owned_by', 'created_by', 'specializes',
-    # workflow related
-    'state_of', 'transition_of', 'initial_state', 'allowed_transition',
-    'destination_state', 'in_state', 'wf_info_for', 'from_state', 'to_state',
-    'condition',
-    # permission
-    'in_group', 'require_group', 'require_permission',
-    'read_permission', 'update_permission', 'delete_permission', 'add_permission',
-    # eproperty
-    'for_user',
-    # schema definition
-    'relation_type', 'from_entity', 'to_entity',
-    'constrained_by', 'cstrtype', 'widget',
-    # deducted from other relations
-    'primary_email',
-                    )
-
-def unprotected_entities(app_schema, strict=False):
-    """returned a Set of each non final entity type, excluding CWGroup, and CWUser...
-    """
-    if strict:
-        protected_entities = yams.schema.BASE_TYPES
-    else:
-        protected_entities = yams.schema.BASE_TYPES.union(set(SYSTEM_ENTITIES))
-    entities = set(app_schema.entities())
-    return entities - protected_entities
-
-
-def ignore_relations(*relations):
-    global SYSTEM_RELATIONS
-    SYSTEM_RELATIONS += relations
-
-class TestEnvironment(object):
-    """TestEnvironment defines a context (e.g. a config + a given connection) in
-    which the tests are executed
-    """
-
-    def __init__(self, appid, reporter=None, verbose=False,
-                 configcls=ApptestConfiguration, requestcls=FakeRequest):
-        config = configcls(appid)
-        self.requestcls = requestcls
-        self.cnx = None
-        config.db_perms = False
-        source = config.sources()['system']
-        if verbose:
-            print "init test database ..."
-        self.vreg = vreg = CubicWebVRegistry(config)
-        self.admlogin = source['db-user']
-        # restore database <=> init database
-        self.restore_database()
-        if verbose:
-            print "init done"
-        config.repository = lambda x=None: self.repo
-        self.app = CubicWebPublisher(config, vreg=vreg)
-        self.verbose = verbose
-        schema = self.vreg.schema
-        # else we may run into problems since email address are ususally share in app tests
-        # XXX should not be necessary anymore
-        schema.rschema('primary_email').set_rproperty('CWUser', 'EmailAddress', 'composite', False)
-        self.deletable_entities = unprotected_entities(schema)
-
-    def restore_database(self):
-        """called by unittests' tearDown to restore the original database
-        """
-        try:
-            pause_tracing()
-            if self.cnx:
-                self.cnx.close()
-            source = self.vreg.config.sources()['system']
-            self.repo, self.cnx = init_test_database(driver=source['db-driver'],
-                                                     vreg=self.vreg)
-            self._orig_cnx = self.cnx
-            resume_tracing()
-        except:
-            resume_tracing()
-            traceback.print_exc()
-            sys.exit(1)
-        # XXX cnx decoration is usually done by the repository authentication manager,
-        # necessary in authentication tests
-        self.cnx.vreg = self.vreg
-        self.cnx.login = source['db-user']
-        self.cnx.password = source['db-password']
-
-
-    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"',
-                              {'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)'
-                       % ','.join(repr(g) for g in groups),
-                       {'x': user.eid}, 'x')
-        user.clear_related_cache('in_group', 'subject')
-        self._orig_cnx.commit()
-        return user
-
-    def login(self, login, password=None):
-        if login == self.admlogin:
-            self.restore_connection()
-        else:
-            self.cnx = repo_connect(self.repo, unicode(login),
-                                    password or str(login),
-                                    ConnectionProperties('inmemory'))
-        if login == self.vreg.config.anonymous_user()[0]:
-            self.cnx.anonymous_connection = True
-        return self.cnx
-
-    def restore_connection(self):
-        if not self.cnx is self._orig_cnx:
-            try:
-                self.cnx.close()
-            except ProgrammingError:
-                pass # already closed
-        self.cnx = self._orig_cnx
-
-    ############################################################################
-
-    def execute(self, rql, args=None, eidkey=None, req=None):
-        """executes <rql>, builds a resultset, and returns a couple (rset, req)
-        where req is a FakeRequest
-        """
-        req = req or self.create_request(rql=rql)
-        return self.cnx.cursor(req).execute(unicode(rql), args, eidkey)
-
-    def create_request(self, rql=None, **kwargs):
-        """executes <rql>, builds a resultset, and returns a
-        couple (rset, req) where req is a FakeRequest
-        """
-        if rql:
-            kwargs['rql'] = rql
-        req = self.requestcls(self.vreg, form=kwargs)
-        req.set_connection(self.cnx)
-        return req
-
-    def get_rset_and_req(self, rql, optional_args=None, args=None, eidkey=None):
-        """executes <rql>, builds a resultset, and returns a
-        couple (rset, req) where req is a FakeRequest
-        """
-        return (self.execute(rql, args, eidkey),
-                self.create_request(rql=rql, **optional_args or {}))
-
-    def check_view(self, rql, vid, optional_args, template='main'):
-        """checks if rendering view raises an exception in this environment
-
-        If any exception is raised in this method, it will be considered
-        as a TestFailure
-        """
-        return self.call_view(vid, rql,
-                              template=template, optional_args=optional_args)
-
-    def call_view(self, vid, rql, template='main', optional_args=None):
-        assert template
-        if optional_args is None:
-            optional_args = {}
-        optional_args['vid'] = vid
-        req = self.create_request(rql=rql, **optional_args)
-        return self.vreg['views'].main_template(req, template)
-
-    def call_edit(self, req):
-        """shortcut for self.app.edit()"""
-        controller = self.vreg.select('controllers', 'edit', req)
-        try:
-            controller.publish()
-        except Redirect:
-            result = 'success'
-        else:
-            raise Exception('edit should raise Redirect on success')
-        req.cnx.commit()
-        return result
-
-    def iter_possible_views(self, req, rset):
-        """returns a list of possible vids for <rql>"""
-        for view in self.vreg['views'].possible_views(req, rset):
-            if view.category == 'startupview':
-                continue
-            yield view.id
-        if rset.rowcount == 1:
-            yield 'edition'
-
-    def iter_startup_views(self, req):
-        """returns the list of startup views"""
-        for view in self.vreg['views'].possible_views(req, None):
-            if view.category != 'startupview':
-                continue
-            yield view.id
-
-    def iter_possible_actions(self, req, rset):
-        """returns a list of possible vids for <rql>"""
-        for action in self.vreg.possible_vobjects('actions', req, rset=rset):
-            yield action
-
-class ExistingTestEnvironment(TestEnvironment):
-
-    def __init__(self, appid, sourcefile, verbose=False):
-        config = ApptestConfiguration(appid, sourcefile=sourcefile)
-        if verbose:
-            print "init test database ..."
-        source = config.sources()['system']
-        self.vreg = CubicWebVRegistry(config)
-        self.cnx = init_test_database(driver=source['db-driver'],
-                                      vreg=self.vreg)[1]
-        if verbose:
-            print "init done"
-        self.app = CubicWebPublisher(config, vreg=self.vreg)
-        self.verbose = verbose
-        # this is done when the publisher is opening a connection
-        self.cnx.vreg = self.vreg
-
-    def setup(self, config=None):
-        """config is passed by TestSuite but is ignored in this environment"""
-        cursor = self.cnx.cursor()
-        self.last_eid = cursor.execute('Any X WHERE X creation_date D ORDERBY D DESC LIMIT 1').rows[0][0]
-
-    def cleanup(self):
-        """cancel inserted elements during tests"""
-        cursor = self.cnx.cursor()
-        cursor.execute('DELETE Any X WHERE X eid > %(x)s', {'x' : self.last_eid}, eid_key='x')
-        print "cleaning done"
-        self.cnx.commit()
--- a/devtools/apptest.py	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,505 +0,0 @@
-"""This module provides misc utilities to test instances
-
-: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
-"""
-__docformat__ = "restructuredtext en"
-
-from copy import deepcopy
-
-import simplejson
-
-from logilab.common.testlib import TestCase
-from logilab.common.pytest import nocoverage
-from logilab.common.umessage import message_from_string
-
-from logilab.common.deprecation import deprecated
-
-from cubicweb.devtools import init_test_database, TestServerConfiguration, ApptestConfiguration
-from cubicweb.devtools._apptest import TestEnvironment
-from cubicweb.devtools.fake import FakeRequest
-
-from cubicweb.dbapi import repo_connect, ConnectionProperties, ProgrammingError
-
-
-MAILBOX = []
-class Email:
-    def __init__(self, recipients, msg):
-        self.recipients = recipients
-        self.msg = msg
-
-    @property
-    def message(self):
-        return message_from_string(self.msg)
-
-    @property
-    def subject(self):
-        return self.message.get('Subject')
-
-    @property
-    def content(self):
-        return self.message.get_payload(decode=True)
-
-    def __repr__(self):
-        return '<Email to %s with subject %s>' % (','.join(self.recipients),
-                                                  self.message.get('Subject'))
-
-class MockSMTP:
-    def __init__(self, server, port):
-        pass
-    def close(self):
-        pass
-    def sendmail(self, helo_addr, recipients, msg):
-        MAILBOX.append(Email(recipients, msg))
-
-from cubicweb import cwconfig
-cwconfig.SMTP = MockSMTP
-
-
-def get_versions(self, checkversions=False):
-    """return the a dictionary containing cubes used by this instance
-    as key with their version as value, including cubicweb version. This is a
-    public method, not requiring a session id.
-
-    replace Repository.get_versions by this method if you don't want versions
-    checking
-    """
-    vcconf = {'cubicweb': self.config.cubicweb_version()}
-    self.config.bootstrap_cubes()
-    for pk in self.config.cubes():
-        version = self.config.cube_version(pk)
-        vcconf[pk] = version
-    self.config._cubes = None
-    return vcconf
-
-
-@property
-def late_binding_env(self):
-    """builds TestEnvironment as late as possible"""
-    if not hasattr(self, '_env'):
-        self.__class__._env = TestEnvironment('data', configcls=self.configcls,
-                                              requestcls=self.requestcls)
-    return self._env
-
-
-class autoenv(type):
-    """automatically set environment on EnvBasedTC subclasses if necessary
-    """
-    def __new__(mcs, name, bases, classdict):
-        env = classdict.get('env')
-        # try to find env in one of the base classes
-        if env is None:
-            for base in bases:
-                env = getattr(base, 'env', None)
-                if env is not None:
-                    classdict['env'] = env
-                    break
-        if not classdict.get('__abstract__')  and not classdict.get('env'):
-            classdict['env'] = late_binding_env
-        return super(autoenv, mcs).__new__(mcs, name, bases, classdict)
-
-
-class EnvBasedTC(TestCase):
-    """abstract class for test using an apptest environment
-    """
-    __metaclass__ = autoenv
-    __abstract__ = True
-    env = None
-    configcls = ApptestConfiguration
-    requestcls = FakeRequest
-
-    # user / session management ###############################################
-
-    def user(self, req=None):
-        if req is None:
-            req = self.env.create_request()
-            return self.env.cnx.user(req)
-        else:
-            return req.user
-
-    def create_user(self, *args, **kwargs):
-        return self.env.create_user(*args, **kwargs)
-
-    def login(self, login, password=None):
-        return self.env.login(login, password)
-
-    def restore_connection(self):
-        self.env.restore_connection()
-
-    # db api ##################################################################
-
-    @nocoverage
-    def cursor(self, req=None):
-        return self.env.cnx.cursor(req or self.request())
-
-    @nocoverage
-    def execute(self, *args, **kwargs):
-        return self.env.execute(*args, **kwargs)
-
-    @nocoverage
-    def commit(self):
-        self.env.cnx.commit()
-
-    @nocoverage
-    def rollback(self):
-        try:
-            self.env.cnx.rollback()
-        except ProgrammingError:
-            pass
-
-    # other utilities #########################################################
-    def set_debug(self, debugmode):
-        from cubicweb.server import set_debug
-        set_debug(debugmode)
-
-    @property
-    def config(self):
-        return self.vreg.config
-
-    def session(self):
-        """return current server side session (using default manager account)"""
-        return self.env.repo._sessions[self.env.cnx.sessionid]
-
-    def request(self, *args, **kwargs):
-        """return a web interface request"""
-        return self.env.create_request(*args, **kwargs)
-
-    @nocoverage
-    def rset_and_req(self, *args, **kwargs):
-        return self.env.get_rset_and_req(*args, **kwargs)
-
-    def entity(self, rql, args=None, eidkey=None, req=None):
-        return self.execute(rql, args, eidkey, req=req).get_entity(0, 0)
-
-    def etype_instance(self, etype, req=None):
-        req = req or self.request()
-        e = self.env.vreg['etypes'].etype_class(etype)(req)
-        e.eid = None
-        return e
-
-    def add_entity(self, etype, **kwargs):
-        rql = ['INSERT %s X' % etype]
-
-        # dict for replacement in RQL Request
-        rql_args = {}
-
-        if kwargs: #
-            rql.append(':')
-            # dict to define new entities variables
-            entities = {}
-
-            # assignement part of the request
-            sub_rql = []
-            for key, value in kwargs.iteritems():
-                # entities
-                if hasattr(value, 'eid'):
-                    new_value = "%s__" % key.upper()
-
-                    entities[new_value] = value.eid
-                    rql_args[new_value] = value.eid
-
-                    sub_rql.append("X %s %s" % (key, new_value))
-                # final attributes
-                else:
-                    sub_rql.append('X %s %%(%s)s' % (key, key))
-                    rql_args[key] = value
-            rql.append(', '.join(sub_rql))
-
-
-            if entities:
-                rql.append('WHERE')
-                # WHERE part of the request (to link entity to they eid)
-                sub_rql = []
-                for key, value in entities.iteritems():
-                    sub_rql.append("%s eid %%(%s)s" % (key, key))
-                rql.append(', '.join(sub_rql))
-
-        rql = ' '.join(rql)
-        rset = self.execute(rql, rql_args)
-        return rset.get_entity(0, 0)
-
-    def set_option(self, optname, value):
-        self.vreg.config.global_set_option(optname, value)
-
-    def pviews(self, req, rset):
-        return sorted((a.id, a.__class__) for a in self.vreg['views'].possible_views(req, rset=rset))
-
-    def pactions(self, req, rset, skipcategories=('addrelated', 'siteactions', 'useractions')):
-        return [(a.id, a.__class__) for a in self.vreg['actions'].possible_vobjects(req, rset=rset)
-                if a.category not in skipcategories]
-
-    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]
-
-    paddrelactions = deprecated()(pactions_by_cats)
-
-    def pactionsdict(self, req, rset, skipcategories=('addrelated', 'siteactions', 'useractions')):
-        res = {}
-        for a in self.vreg['actions'].possible_vobjects(req, rset=rset):
-            if a.category not in skipcategories:
-                res.setdefault(a.category, []).append(a.__class__)
-        return res
-
-
-    def remote_call(self, fname, *args):
-        """remote call simulation"""
-        dump = simplejson.dumps
-        args = [dump(arg) for arg in args]
-        req = self.request(fname=fname, pageid='123', arg=args)
-        ctrl = self.vreg['controllers'].select('json', req)
-        return ctrl.publish(), req
-
-    # default test setup and teardown #########################################
-
-    def setup_database(self):
-        pass
-
-    def setUp(self):
-        self.restore_connection()
-        session = self.session()
-        #self.maxeid = self.execute('Any MAX(X)')
-        session.set_pool()
-        self.maxeid = session.system_sql('SELECT MAX(eid) FROM entities').fetchone()[0]
-        self.app = self.env.app
-        self.vreg = self.env.app.vreg
-        self.schema = self.vreg.schema
-        self.vreg.config.mode = 'test'
-        # set default-dest-addrs to a dumb email address to avoid mailbox or
-        # mail queue pollution
-        self.set_option('default-dest-addrs', ['whatever'])
-        self.setup_database()
-        self.commit()
-        MAILBOX[:] = [] # reset mailbox
-
-    @nocoverage
-    def tearDown(self):
-        self.rollback()
-        # self.env.restore_database()
-        self.env.restore_connection()
-        self.session().unsafe_execute('DELETE Any X WHERE X eid > %s' % self.maxeid)
-        self.commit()
-
-
-# XXX
-try:
-    from cubicweb.web import Redirect
-    from urllib import unquote
-except ImportError:
-    pass # cubicweb-web not installed
-else:
-    class ControllerTC(EnvBasedTC):
-        def setUp(self):
-            super(ControllerTC, self).setUp()
-            self.req = self.request()
-            self.ctrl = self.vreg['controllers'].select('edit', self.req)
-
-        def publish(self, req):
-            assert req is self.ctrl.req
-            try:
-                result = self.ctrl.publish()
-                req.cnx.commit()
-            except Redirect:
-                req.cnx.commit()
-                raise
-            return result
-
-        def expect_redirect_publish(self, req=None):
-            if req is not None:
-                self.ctrl = self.vreg['controllers'].select('edit', req)
-            else:
-                req = self.req
-            try:
-                res = self.publish(req)
-            except Redirect, ex:
-                try:
-                    path, params = ex.location.split('?', 1)
-                except:
-                    path, params = ex.location, ""
-                req._url = path
-                cleanup = lambda p: (p[0], unquote(p[1]))
-                params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
-                return req.relative_path(False), params # path.rsplit('/', 1)[-1], params
-            else:
-                self.fail('expected a Redirect exception')
-
-
-def make_late_binding_repo_property(attrname):
-    @property
-    def late_binding(self):
-        """builds cnx as late as possible"""
-        if not hasattr(self, attrname):
-            # sets explicit test mode here to avoid autoreload
-            from cubicweb.cwconfig import CubicWebConfiguration
-            CubicWebConfiguration.mode = 'test'
-            cls = self.__class__
-            config = self.repo_config or TestServerConfiguration('data')
-            cls._repo, cls._cnx = init_test_database('sqlite',  config=config)
-        return getattr(self, attrname)
-    return late_binding
-
-
-class autorepo(type):
-    """automatically set repository on RepositoryBasedTC subclasses if necessary
-    """
-    def __new__(mcs, name, bases, classdict):
-        repo = classdict.get('repo')
-        # try to find repo in one of the base classes
-        if repo is None:
-            for base in bases:
-                repo = getattr(base, 'repo', None)
-                if repo is not None:
-                    classdict['repo'] = repo
-                    break
-        if name != 'RepositoryBasedTC' and not classdict.get('repo'):
-            classdict['repo'] = make_late_binding_repo_property('_repo')
-            classdict['cnx'] = make_late_binding_repo_property('_cnx')
-        return super(autorepo, mcs).__new__(mcs, name, bases, classdict)
-
-
-class RepositoryBasedTC(TestCase):
-    """abstract class for test using direct repository connections
-    """
-    __metaclass__ = autorepo
-    repo_config = None # set a particular config instance if necessary
-
-    # user / session management ###############################################
-
-    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"',
-                            {'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,
-                      {'x': eid})
-        if commit:
-            self.commit()
-        self.session.reset_pool()
-        return eid
-
-    def login(self, login, password=None):
-        cnx = repo_connect(self.repo, unicode(login), password or login,
-                           ConnectionProperties('inmemory'))
-        self.cnxs.append(cnx)
-        return cnx
-
-    def current_session(self):
-        return self.repo._sessions[self.cnxs[-1].sessionid]
-
-    def restore_connection(self):
-        assert len(self.cnxs) == 1, self.cnxs
-        cnx = self.cnxs.pop()
-        try:
-            cnx.close()
-        except Exception, ex:
-            print "exception occured while closing connection", ex
-
-    # db api ##################################################################
-
-    def execute(self, rql, args=None, eid_key=None):
-        assert self.session.id == self.cnxid
-        rset = self.__execute(self.cnxid, rql, args, eid_key)
-        rset.vreg = self.vreg
-        rset.req = self.session
-        # call to set_pool is necessary to avoid pb when using
-        # instance entities for convenience
-        self.session.set_pool()
-        return rset
-
-    def commit(self):
-        self.__commit(self.cnxid)
-        self.session.set_pool()
-
-    def rollback(self):
-        self.__rollback(self.cnxid)
-        self.session.set_pool()
-
-    def close(self):
-        self.__close(self.cnxid)
-
-    # other utilities #########################################################
-
-    def set_debug(self, debugmode):
-        from cubicweb.server import set_debug
-        set_debug(debugmode)
-
-    def set_option(self, optname, value):
-        self.vreg.config.global_set_option(optname, value)
-
-    def add_entity(self, etype, **kwargs):
-        restrictions = ', '.join('X %s %%(%s)s' % (key, key) for key in kwargs)
-        rql = 'INSERT %s X' % etype
-        if kwargs:
-            rql += ': %s' % ', '.join('X %s %%(%s)s' % (key, key) for key in kwargs)
-        rset = self.execute(rql, kwargs)
-        return rset.get_entity(0, 0)
-
-    def default_user_password(self):
-        config = self.repo.config #TestConfiguration('data')
-        user = unicode(config.sources()['system']['db-user'])
-        passwd = config.sources()['system']['db-password']
-        return user, passwd
-
-    def close_connections(self):
-        for cnx in self.cnxs:
-            try:
-                cnx.rollback()
-                cnx.close()
-            except:
-                continue
-        self.cnxs = []
-
-    pactions = EnvBasedTC.pactions.im_func
-    pactionsdict = EnvBasedTC.pactionsdict.im_func
-
-    # default test setup and teardown #########################################
-
-    def _prepare(self):
-        MAILBOX[:] = [] # reset mailbox
-        if hasattr(self, 'cnxid'):
-            return
-        repo = self.repo
-        self.__execute = repo.execute
-        self.__commit = repo.commit
-        self.__rollback = repo.rollback
-        self.__close = repo.close
-        self.cnxid = self.cnx.sessionid
-        self.session = repo._sessions[self.cnxid]
-        self.cnxs = []
-        # reset caches, they may introduce bugs among tests
-        repo._type_source_cache = {}
-        repo._extid_cache = {}
-        repo.querier._rql_cache = {}
-        for source in repo.sources:
-            source.reset_caches()
-        for s in repo.sources:
-            if hasattr(s, '_cache'):
-                s._cache = {}
-
-    @property
-    def config(self):
-        return self.repo.config
-
-    @property
-    def vreg(self):
-        return self.repo.vreg
-
-    @property
-    def schema(self):
-        return self.repo.schema
-
-    def setUp(self):
-        self._prepare()
-        self.session.set_pool()
-        self.maxeid = self.session.system_sql('SELECT MAX(eid) FROM entities').fetchone()[0]
-
-    def tearDown(self):
-        self.close_connections()
-        self.rollback()
-        self.session.unsafe_execute('DELETE Any X WHERE X eid > %(x)s', {'x': self.maxeid})
-        self.commit()
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/dataimport.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,278 @@
+# -*- 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
+
+  from cubicweb.devtools.dataimport import *
+  # 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	Wed Aug 05 09:15:56 2009 +0200
+++ b/devtools/devctl.py	Tue Sep 22 13:08:42 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,12 +21,13 @@
 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
 from cubicweb.__pkginfo__ import version as cubicwebversion
-from cubicweb.toolsutils import Command, copy_skeleton
+from cubicweb.toolsutils import Command, copy_skeleton, underline_title
 from cubicweb.web.webconfig import WebConfiguration
 from cubicweb.server.serverconfig import ServerConfiguration
 
+
 class DevCubeConfiguration(ServerConfiguration, WebConfiguration):
     """dummy config to get full library schema and entities"""
     creating = True
@@ -111,91 +113,106 @@
 
 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))
+                    add_msg(w, '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)
 
+
 def _iter_vreg_objids(vreg, done, prefix=None):
     for reg, objdict in vreg.items():
         for objects in objdict.values():
@@ -203,7 +220,7 @@
                 objid = '%s_%s' % (reg, obj.id)
                 if objid in done:
                     break
-                if obj.property_defs:
+                if obj.cw_property_defs:
                     yield objid
                     done.add(objid)
                     break
@@ -283,20 +300,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 +407,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 +422,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/devtools/fake.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,12 +7,12 @@
 """
 __docformat__ = "restructuredtext en"
 
-from logilab.common.testlib import mock_object as Mock
 from logilab.common.adbh import get_adv_func_helper
 
 from indexer import get_indexer
 
-from cubicweb import RequestSessionMixIn
+from cubicweb.req import RequestSessionBase
+from cubicweb.cwvreg import CubicWebVRegistry
 from cubicweb.web.request import CubicWebRequestBase
 from cubicweb.devtools import BASE_URL, BaseApptestConfiguration
 
@@ -35,39 +35,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 +122,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
@@ -174,10 +129,10 @@
         return True
 
 
-class FakeSession(RequestSessionMixIn):
+class FakeSession(RequestSessionBase):
     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
@@ -187,7 +142,7 @@
     def execute(self, *args):
         pass
     unsafe_execute = execute
-    
+
     def commit(self, *args):
         self.transaction_data.clear()
     def close(self, *args):
@@ -210,8 +165,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 +205,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/htmlparser.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/devtools/htmlparser.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,6 +7,7 @@
 """
 
 import re
+import sys
 
 from lxml import etree
 
@@ -36,7 +37,11 @@
 class DTDValidator(Validator):
     def __init__(self):
         Validator.__init__(self)
-        self.parser = etree.XMLParser(dtd_validation=True)
+        # XXX understand what's happening under windows
+        validate = True
+        if sys.platform == 'win32':
+            validate = False
+        self.parser = etree.XMLParser(dtd_validation=validate)
 
     def preprocess_data(self, data):
         """used to fix potential blockquote mess generated by docutils"""
@@ -169,3 +174,5 @@
                 except KeyError:
                     continue
         return False
+
+VALMAP = {None: None, 'dtd': DTDValidator, 'xml': SaxOnlyValidator}
--- a/devtools/livetest.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/devtools/livetest.py	Tue Sep 22 13:08:42 2009 +0200
@@ -6,9 +6,10 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 
+import os
 import socket
 import logging
-from os.path import join, dirname, exists
+from os.path import join, dirname, normpath, abspath
 from StringIO import StringIO
 
 #from twisted.application import service, strports
@@ -21,10 +22,9 @@
 
 from logilab.common.testlib import TestCase
 
-import cubicweb.web
 from cubicweb.dbapi import in_memory_cnx
 from cubicweb.etwist.server import CubicWebRootResource
-from cubicweb.devtools import LivetestConfiguration, init_test_database
+from cubicweb.devtools import BaseApptestConfiguration, init_test_database
 
 
 
@@ -50,25 +50,57 @@
 
 
 
+class LivetestConfiguration(BaseApptestConfiguration):
+    init_repository = False
+
+    def __init__(self, cube=None, sourcefile=None, pyro_name=None,
+                 log_threshold=logging.CRITICAL):
+        BaseApptestConfiguration.__init__(self, cube, log_threshold=log_threshold)
+        self.appid = pyro_name or cube
+        # don't change this, else some symlink problems may arise in some
+        # environment (e.g. mine (syt) ;o)
+        # XXX I'm afraid this test will prevent to run test from a production
+        # environment
+        self._sources = None
+        # instance cube test
+        if cube is not None:
+            self.apphome = self.cube_dir(cube)
+        elif 'web' in os.getcwd().split(os.sep):
+            # web test
+            self.apphome = join(normpath(join(dirname(__file__), '..')), 'web')
+        else:
+            # cube test
+            self.apphome = abspath('..')
+        self.sourcefile = sourcefile
+        self.global_set_option('realm', '')
+        self.use_pyro = pyro_name is not None
+
+    def pyro_enabled(self):
+        if self.use_pyro:
+            return True
+        else:
+            return False
+
+
+
 def make_site(cube, options=None):
     from cubicweb.etwist import twconfig # trigger configuration registration
-    sourcefile = options.sourcefile
-    config = LivetestConfiguration(cube, sourcefile,
+    config = LivetestConfiguration(cube, options.sourcefile,
                                    pyro_name=options.pyro_name,
                                    log_threshold=logging.DEBUG)
-    source = config.sources()['system']
-    init_test_database(driver=source['db-driver'], config=config)
+    init_test_database(config=config)
     # if '-n' in sys.argv: # debug mode
     cubicweb = LivetestResource(config, debug=True)
     toplevel = cubicweb
     website = server.Site(toplevel)
     cube_dir = config.cube_dir(cube)
+    source = config.sources()['system']
     for port in xrange(7777, 7798):
         try:
             reactor.listenTCP(port, channel.HTTPFactory(website))
             saveconf(cube_dir, port, source['db-user'], source['db-password'])
             break
-        except CannotListenError, exc:
+        except CannotListenError:
             print "port %s already in use, I will try another one" % port
     else:
         raise
--- a/devtools/migrtest.py	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,153 +0,0 @@
-"""Migration test script
-
-* migration will be played into a chroot of the local machine
-* the database server used can be configured
-* test tested instance may be on another host
-
-
-We are using postgres'.pgpass file. Here is a copy of postgres documentation
-about that:
-
-The file .pgpass in a user's home directory or the file referenced by
-PGPASSFILE can contain passwords to be used if the connection requires
-a password (and no password has been specified otherwise).
-
-
-This file should contain lines of the following format:
-
-hostname:port:database:username:password
-
-Each of the first four fields may be a literal value, or *, which
-matches anything. The password field from the first line that matches
-the current connection parameters will be used. (Therefore, put
-more-specific entries first when you are using wildcards.) If an entry
-needs to contain : or \, escape this character with \. A hostname of
-localhost matches both host (TCP) and local (Unix domain socket)
-connections coming from the local machine.
-
-The permissions on .pgpass must disallow any access to world or group;
-achieve this by the command chmod 0600 ~/.pgpass. If the permissions
-are less strict than this, the file will be ignored.
-
-: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
-"""
-__docformat__ = "restructuredtext en"
-
-from os import system
-from os.path import join, basename
-
-from logilab.common.shellutils import cp, rm
-
-from cubicweb.toolsutils import read_config
-from cubicweb.server.utils import generate_sources_file
-
-# XXXX use db-copy instead
-
-# test environment configuration
-chrootpath = '/sandbox/cubicwebtest'
-tmpdbhost = 'crater'
-tmpdbuser = 'syt'
-tmpdbpasswd = 'syt'
-
-def play_migration(applhome, applhost='', sudo=False):
-    applid = dbname = basename(applhome)
-    testapplhome = join(chrootpath, applhome)
-    # copy instance into the chroot
-    if applhost:
-        system('scp -r %s:%s %s' % (applhost, applhome, testapplhome))
-    else:
-        cp(applhome, testapplhome)
-##     # extract db parameters
-##     sources = read_config(join(testapplhome, 'sources'))
-##     dbname = sources['system']['db-name']
-##     dbhost = sources['system'].get('db-host') or ''
-##     dbuser = sources['system'].get('db-user') or ''
-##     dbpasswd = sources['system'].get('db-password') or ''
-    # generate sources file
-    # XXX multisources
-    sources = {'system': {}}
-    sources['system']['db-encoding'] = 'UTF8' # XXX
-    sources['system']['db-name'] = dbname
-    sources['system']['db-host'] = None
-    sources['system']['db-user'] = tmpdbuser
-    sources['system']['db-password'] = None
-    generate_sources_file(join(testapplhome, 'sources'), sources)
-##     # create postgres password file so we won't need anymore passwords
-##     # XXX may exist!
-##     pgpassfile = expanduser('~/.pgpass')
-##     pgpass = open(pgpassfile, 'w')
-##     if dbpasswd:
-##         pgpass.write('%s:*:%s:%s:%s\n' % (dbhost or applhost or 'localhost',
-##                                           dbname, dbuser, dbpasswd))
-##     if tmpdbpasswd:
-##         pgpass.write('%s:*:%s:%s:%s\n' % (tmpdbhost or 'localhost', dbname,
-##                                           tmpdbuser, tmpdbpasswd))
-##     pgpass.close()
-##     chmod(pgpassfile, 0600)
-    # dump db
-##     dumpcmd = 'pg_dump -Fc -U %s -f /tmp/%s.dump %s' % (
-##         dbuser, dbname, dbname)
-##     if dbhost:
-##         dumpcmd += ' -h %s' % dbhost
-    dumpfile = '/tmp/%s.dump' % applid
-    dumpcmd = 'cubicweb-ctl db-dump --output=%s %s' % (dumpfile, applid)
-    if sudo:
-        dumpcmd = 'sudo %s' % dumpcmd
-    if applhost:
-        dumpcmd = 'ssh %s "%s"' % (applhost, dumpcmd)
-    if system(dumpcmd):
-        raise Exception('error while dumping the database')
-##     if not dbhost and applhost:
-    if applhost:
-        # retrieve the dump
-        if system('scp %s:%s %s' % (applhost, dumpfile, dumpfile)):
-            raise Exception('error while retreiving the dump')
-    # move the dump into the chroot
-    system('mv %s %s%s' % (dumpfile, chrootpath, dumpfile))
-    # locate installed versions
-    vcconf = read_config(join(testapplhome, 'vc.conf'))
-    template = vcconf['TEMPLATE']
-    cubicwebversion = vcconf['CW']
-    templversion = vcconf['TEMPLATE_VERSION']
-    # install the same versions cubicweb and template versions into the chroot
-    system('sudo chroot %s apt-get update' % chrootpath)
-    system('sudo chroot %s apt-get install cubicweb-server=%s cubicweb-client=%s'
-           % (chrootpath, cubicwebversion, cubicwebversion))
-    system('sudo chroot %s apt-get install cubicweb-%s-appl-server=%s cubicweb-%s-appl-client=%s'
-           % (chrootpath, template, templversion, template, templversion))
-    # update and upgrade to the latest version
-    system('sudo chroot %s apt-get install cubicweb-server cubicweb-client' % chrootpath)
-    system('sudo chroot %s apt-get install cubicweb-%s-appl-server cubicweb-%s-appl-client'
-           % (chrootpath, template, template))
-    # create and fill the database
-    system('sudo chroot cubicweb-ctl db-restore %s %s' % (applid, dumpfile))
-##     if not tmpdbhost:
-##         system('createdb -U %s -T template0 -E UTF8 %s' % (tmpdbuser, dbname))
-##         system('pg_restore -U %s -O -Fc -d %s /tmp/%s.dump'
-##                % (tmpdbuser, dbname, dbname))
-##     else:
-##         system('createdb -h %s -U %s -T template0 -E UTF8 %s'
-##                % (tmpdbhost, tmpdbuser, dbname))
-##         system('pg_restore -h %s -U %s -O -Fc -d %s /tmp/%s.dump'
-##                % (tmpdbhost, tmpdbuser, dbname, dbname))
-    # launch upgrade
-    system('sudo chroot %s cubicweb-ctl upgrade %s' % (chrootpath, applid))
-
-    # cleanup
-    rm(testapplhome)
-##     rm(pgpassfile)
-##     if tmpdbhost:
-##         system('dropdb -h %s -U %s %s' % (tmpdbuser, tmpdbhost, dbname))
-##     else:
-##         system('dropdb -U %s %s' % (tmpdbuser, dbname))
-##     if not dbhost and applhost:
-    if applhost:
-        system('ssh %s rm %s' % (applhost, dumpfile))
-    rm('%s%s' % (chrootpath, dumpfile))
-
-
-if __name__ == '__main__':
-    play_migration('/etc/cubicweb.d/jpl', 'lepus')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/realdbtest.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,43 @@
+import logging
+from cubicweb import toolsutils
+from cubicweb.devtools import DEFAULT_SOURCES, BaseApptestConfiguration
+
+class RealDatabaseConfiguration(BaseApptestConfiguration):
+    init_repository = False
+    sourcesdef =  DEFAULT_SOURCES.copy()
+
+    def sources(self):
+        """
+        By default, we run tests with the sqlite DB backend.
+        One may use its own configuration by just creating a
+        'sources' file in the test directory from wich tests are
+        launched.
+        """
+        self._sources = self.sourcesdef
+        return self._sources
+
+
+def buildconfig(dbuser, dbpassword, dbname, adminuser, adminpassword, dbhost=None):
+    """convenience function that builds a real-db configuration class"""
+    sourcesdef =  {'system': {'adapter' : 'native',
+                              'db-encoding' : 'UTF-8', #'ISO-8859-1',
+                              'db-user' : dbuser,
+                              'db-password' : dbpassword,
+                              'db-name' : dbname,
+                              'db-driver' : 'postgres',
+                              'db-host' : dbhost,
+                              },
+                   'admin' : {'login': adminuser,
+                              'password': adminpassword,
+                              },
+                   }
+    return type('MyRealDBConfig', (RealDatabaseConfiguration,),
+                {'sourcesdef': sourcesdef})
+
+
+def loadconfig(filename):
+    """convenience function that builds a real-db configuration class
+    from a file
+    """
+    return type('MyRealDBConfig', (RealDatabaseConfiguration,),
+                {'sourcesdef': toolsutils.read_config(filename)})
--- a/devtools/repotest.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/devtools/repotest.py	Tue Sep 22 13:08:42 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/test/unittest_testlib.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/devtools/test/unittest_testlib.py	Tue Sep 22 13:08:42 2009 +0200
@@ -10,11 +10,11 @@
 from unittest import TestSuite
 
 
-from logilab.common.testlib import (TestCase, unittest_main, mock_object,
+from logilab.common.testlib import (TestCase, unittest_main, 
                                     SkipAwareTextTestRunner)
+
 from cubicweb.devtools import htmlparser
-
-from cubicweb.devtools.testlib import WebTest, EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 
 class WebTestTC(TestCase):
 
@@ -23,7 +23,7 @@
         self.runner = SkipAwareTextTestRunner(stream=output)
 
     def test_error_raised(self):
-        class MyWebTest(WebTest):
+        class MyWebTest(CubicWebTC):
 
             def test_error_view(self):
                 self.add_entity('Bug', title=u"bt")
@@ -39,7 +39,7 @@
         self.assertEquals(len(result.failures), 1)
 
 
-class TestLibTC(EnvBasedTC):
+class TestLibTC(CubicWebTC):
     def test_add_entity_with_relation(self):
         bug = self.add_entity(u'Bug', title=u"toto")
         self.add_entity(u'Bug', title=u"tata", identical_to=bug)
--- a/devtools/testlib.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/devtools/testlib.py	Tue Sep 22 13:08:42 2009 +0200
@@ -1,4 +1,4 @@
-"""this module contains base classes for web tests
+"""this module contains base classes and utilities for cubicweb tests
 
 :organization: Logilab
 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
@@ -7,84 +7,523 @@
 """
 __docformat__ = "restructuredtext en"
 
+import os
 import sys
+import re
+from urllib import unquote
 from math import log
 
-from logilab.common.debugger import Debugger
-from logilab.common.testlib import InnerTest
-from logilab.common.pytest import nocoverage
+import simplejson
+
+import yams.schema
 
-from cubicweb.devtools import VIEW_VALIDATORS
-from cubicweb.devtools.apptest import EnvBasedTC
-from cubicweb.devtools._apptest import unprotected_entities, SYSTEM_RELATIONS
-from cubicweb.devtools.htmlparser import DTDValidator, SaxOnlyValidator, HTMLValidator
-from cubicweb.devtools.fill import insert_entity_queries, make_relations_queries
+from logilab.common.testlib import TestCase, InnerTest
+from logilab.common.pytest import nocoverage, pause_tracing, resume_tracing
+from logilab.common.debugger import Debugger
+from logilab.common.umessage import message_from_string
+from logilab.common.decorators import cached, classproperty
+from logilab.common.deprecation import deprecated
 
-from cubicweb.sobjects.notification import NotificationView
-
-from cubicweb.vregistry import NoSelectableObject
+from cubicweb import NoSelectableObject, cwconfig, devtools, web, server
+from cubicweb.dbapi import repo_connect, ConnectionProperties, ProgrammingError
+from cubicweb.sobjects import notification
+from cubicweb.web import application
+from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS
+from cubicweb.devtools import fake, htmlparser
 
 
-## TODO ###############
-# creation tests: make sure an entity was actually created
-# Existing Test Environment
+# low-level utilities ##########################################################
 
 class CubicWebDebugger(Debugger):
-
+    """special debugger class providing a 'view' function which saves some
+    html into a temporary file and open a web browser to examinate it.
+    """
     def do_view(self, arg):
         import webbrowser
         data = self._getval(arg)
         file('/tmp/toto.html', 'w').write(data)
         webbrowser.open('file:///tmp/toto.html')
 
-def how_many_dict(schema, cursor, how_many, skip):
-    """compute how many entities by type we need to be able to satisfy relations
-    cardinality
-    """
-    # compute how many entities by type we need to be able to satisfy relation constraint
-    relmap = {}
-    for rschema in schema.relations():
-        if rschema.is_final():
-            continue
-        for subj, obj in rschema.iter_rdefs():
-            card = rschema.rproperty(subj, obj, 'cardinality')
-            if card[0] in '1?' and len(rschema.subjects(obj)) == 1:
-                relmap.setdefault((rschema, subj), []).append(str(obj))
-            if card[1] in '1?' and len(rschema.objects(subj)) == 1:
-                relmap.setdefault((rschema, obj), []).append(str(subj))
-    unprotected = unprotected_entities(schema)
-    for etype in skip:
-        unprotected.add(etype)
-    howmanydict = {}
-    for etype in unprotected_entities(schema, strict=True):
-        howmanydict[str(etype)] = cursor.execute('Any COUNT(X) WHERE X is %s' % etype)[0][0]
-        if etype in unprotected:
-            howmanydict[str(etype)] += how_many
-    for (rschema, etype), targets in relmap.iteritems():
-        # XXX should 1. check no cycle 2. propagate changes
-        relfactor = sum(howmanydict[e] for e in targets)
-        howmanydict[str(etype)] = max(relfactor, howmanydict[etype])
-    return howmanydict
-
 
 def line_context_filter(line_no, center, before=3, after=None):
     """return true if line are in context
-    if after is None: after = before"""
+
+    if after is None: after = before
+    """
     if after is None:
         after = before
     return center - before <= line_no <= center + after
 
-## base webtest class #########################################################
-VALMAP = {None: None, 'dtd': DTDValidator, 'xml': SaxOnlyValidator}
+
+def unprotected_entities(schema, strict=False):
+    """returned a set of each non final entity type, excluding "system" entities
+    (eg CWGroup, CWUser...)
+    """
+    if strict:
+        protected_entities = yams.schema.BASE_TYPES
+    else:
+        protected_entities = yams.schema.BASE_TYPES.union(SYSTEM_ENTITIES)
+    return set(schema.entities()) - protected_entities
+
+
+def get_versions(self, checkversions=False):
+    """return the a dictionary containing cubes used by this instance
+    as key with their version as value, including cubicweb version. This is a
+    public method, not requiring a session id.
+
+    replace Repository.get_versions by this method if you don't want versions
+    checking
+    """
+    vcconf = {'cubicweb': self.config.cubicweb_version()}
+    self.config.bootstrap_cubes()
+    for pk in self.config.cubes():
+        version = self.config.cube_version(pk)
+        vcconf[pk] = version
+    self.config._cubes = None
+    return vcconf
+
+
+def refresh_repo(repo):
+    devtools.reset_test_database(repo.config)
+    for pool in repo.pools:
+        pool.reconnect()
+    repo._type_source_cache = {}
+    repo._extid_cache = {}
+    repo.querier._rql_cache = {}
+    for source in repo.sources:
+        source.reset_caches()
+
+
+# email handling, to test emails sent by an application ########################
+
+MAILBOX = []
+
+class Email:
+    """you'll get instances of Email into MAILBOX during tests that trigger
+    some notification.
+
+    * `msg` is the original message object
+
+    * `recipients` is a list of email address which are the recipients of this
+      message
+    """
+    def __init__(self, recipients, msg):
+        self.recipients = recipients
+        self.msg = msg
+
+    @property
+    def message(self):
+        return message_from_string(self.msg)
+
+    @property
+    def subject(self):
+        return self.message.get('Subject')
+
+    @property
+    def content(self):
+        return self.message.get_payload(decode=True)
+
+    def __repr__(self):
+        return '<Email to %s with subject %s>' % (','.join(self.recipients),
+                                                  self.message.get('Subject'))
+
+# the trick to get email into MAILBOX instead of actually sent: monkey patch
+# cwconfig.SMTP object
+class MockSMTP:
+    def __init__(self, server, port):
+        pass
+    def close(self):
+        pass
+    def sendmail(self, helo_addr, recipients, msg):
+        MAILBOX.append(Email(recipients, msg))
+
+cwconfig.SMTP = MockSMTP
+
+
+# base class for cubicweb tests requiring a full cw environments ###############
+
+class CubicWebTC(TestCase):
+    """abstract class for test using an apptest environment
+
+    attributes:
+    `vreg`, the vregistry
+    `schema`, self.vreg.schema
+    `config`, cubicweb configuration
+    `cnx`, dbapi connection to the repository using an admin user
+    `session`, server side session associated to `cnx`
+    `app`, the cubicweb publisher (for web testing)
+    `repo`, the repository object
+
+    `admlogin`, login of the admin user
+    `admpassword`, password of the admin user
+
+    """
+    appid = 'data'
+    configcls = devtools.ApptestConfiguration
+
+    @classproperty
+    def config(cls):
+        """return the configuration object. Configuration is cached on the test
+        class.
+        """
+        try:
+            return cls.__dict__['_config']
+        except KeyError:
+            config = cls._config = cls.configcls(cls.appid)
+            config.mode = 'test'
+            return config
+
+    @classmethod
+    def init_config(cls, config):
+        """configuration initialization hooks. You may want to override this."""
+        source = config.sources()['system']
+        cls.admlogin = unicode(source['db-user'])
+        cls.admpassword = source['db-password']
+        # uncomment the line below if you want rql queries to be logged
+        #config.global_set_option('query-log-file',
+        #                         '/tmp/test_rql_log.' + `os.getpid()`)
+        config.global_set_option('log-file', None)
+        # set default-dest-addrs to a dumb email address to avoid mailbox or
+        # mail queue pollution
+        config.global_set_option('default-dest-addrs', ['whatever'])
+        try:
+            send_to =  '%s@logilab.fr' % os.getlogin()
+            # AttributeError since getlogin not available under all platforms
+        except (OSError, AttributeError):
+            send_to =  '%s@logilab.fr' % (os.environ.get('USER')
+                                          or os.environ.get('USERNAME')
+                                          or os.environ.get('LOGNAME'))
+        config.global_set_option('sender-addr', send_to)
+        config.global_set_option('default-dest-addrs', send_to)
+        config.global_set_option('sender-name', 'cubicweb-test')
+        config.global_set_option('sender-addr', 'cubicweb-test@logilab.fr')
+        # web resources
+        config.global_set_option('base-url', devtools.BASE_URL)
+        try:
+            config.global_set_option('embed-allowed', re.compile('.*'))
+        except: # not in server only configuration
+            pass
+
+    @classmethod
+    def _init_repo(cls):
+        """init the repository and connection to it.
+
+        Repository and connection are cached on the test class. Once
+        initialized, we simply reset connections and repository caches.
+        """
+        if not 'repo' in cls.__dict__:
+            cls._build_repo()
+        else:
+            cls.cnx.rollback()
+            cls._refresh_repo()
+
+    @classmethod
+    def _build_repo(cls):
+        cls.repo, cls.cnx = devtools.init_test_database(config=cls.config)
+        cls.init_config(cls.config)
+        cls.vreg = cls.repo.vreg
+        cls._orig_cnx = cls.cnx
+        cls.config.repository = lambda x=None: cls.repo
+        # necessary for authentication tests
+        cls.cnx.login = cls.admlogin
+        cls.cnx.password = cls.admpassword
+
+    @classmethod
+    def _refresh_repo(cls):
+        refresh_repo(cls.repo)
+
+    # global resources accessors ###############################################
+
+    @property
+    def schema(self):
+        """return the application schema"""
+        return self.vreg.schema
+
+    @property
+    def session(self):
+        """return current server side session (using default manager account)"""
+        return self.repo._sessions[self.cnx.sessionid]
+
+    @property
+    def adminsession(self):
+        """return current server side session (using default manager account)"""
+        return self.repo._sessions[self._orig_cnx.sessionid]
+
+    def set_option(self, optname, value):
+        self.config.global_set_option(optname, value)
+
+    def set_debug(self, debugmode):
+        server.set_debug(debugmode)
+
+    # default test setup and teardown #########################################
+
+    def setUp(self):
+        pause_tracing()
+        self._init_repo()
+        resume_tracing()
+        self.setup_database()
+        self.commit()
+        MAILBOX[:] = [] # reset mailbox
+
+    def setup_database(self):
+        """add your database setup code by overriding this method"""
+
+    # user / session management ###############################################
+
+    def user(self, req=None):
+        """return the application schema"""
+        if req is None:
+            req = self.request()
+            return self.cnx.user(req)
+        else:
+            return req.user
 
-class WebTest(EnvBasedTC):
-    """base class for web tests"""
-    __abstract__ = True
+    def create_user(self, login, groups=('users',), password=None, req=None,
+                    commit=True):
+        """create and return a new user entity"""
+        if password is None:
+            password = login.encode('utf8')
+        cursor = self._orig_cnx.cursor(req or self.request())
+        rset = cursor.execute('INSERT CWUser X: X login %(login)s, X upassword %(passwd)s',
+                              {'login': unicode(login), 'passwd': password})
+        user = rset.get_entity(0, 0)
+        cursor.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)'
+                       % ','.join(repr(g) for g in groups),
+                       {'x': user.eid}, 'x')
+        user.clear_related_cache('in_group', 'subject')
+        if commit:
+            self._orig_cnx.commit()
+        return user
+
+    def login(self, login, password=None):
+        """return a connection for the given login/password"""
+        if login == self.admlogin:
+            self.restore_connection()
+        else:
+            self.cnx = repo_connect(self.repo, unicode(login),
+                                    password or str(login),
+                                    ConnectionProperties('inmemory'))
+        if login == self.vreg.config.anonymous_user()[0]:
+            self.cnx.anonymous_connection = True
+        return self.cnx
+
+    def restore_connection(self):
+        if not self.cnx is self._orig_cnx:
+            try:
+                self.cnx.close()
+            except ProgrammingError:
+                pass # already closed
+        self.cnx = self._orig_cnx
+
+    # db api ##################################################################
+
+    @nocoverage
+    def cursor(self, req=None):
+        return self.cnx.cursor(req or self.request())
+
+    @nocoverage
+    def execute(self, rql, args=None, eidkey=None, req=None):
+        """executes <rql>, builds a resultset, and returns a couple (rset, req)
+        where req is a FakeRequest
+        """
+        req = req or self.request(rql=rql)
+        return self.cnx.cursor(req).execute(unicode(rql), args, eidkey)
+
+    @nocoverage
+    def commit(self):
+        self.cnx.commit()
+
+    @nocoverage
+    def rollback(self):
+        try:
+            self.cnx.rollback()
+        except ProgrammingError:
+            pass
+
+    # # server side db api #######################################################
+
+    def sexecute(self, rql, args=None, eid_key=None):
+        self.session.set_pool()
+        return self.session.execute(rql, args, eid_key)
+
+    # other utilities #########################################################
+
+    def entity(self, rql, args=None, eidkey=None, req=None):
+        return self.execute(rql, args, eidkey, req=req).get_entity(0, 0)
+
+    def add_entity(self, etype, req=None, **kwargs):
+        rql = ['INSERT %s X' % etype]
+        # dict for replacement in RQL Request
+        args = {}
+        if kwargs:
+            rql.append(':')
+            # dict to define new entities variables
+            entities = {}
+            # assignement part of the request
+            sub_rql = []
+            for key, value in kwargs.iteritems():
+                # entities
+                if hasattr(value, 'eid'):
+                    new_value = "%s__" % key.upper()
+                    entities[new_value] = value.eid
+                    args[new_value] = value.eid
+
+                    sub_rql.append("X %s %s" % (key, new_value))
+                # final attributes
+                else:
+                    sub_rql.append('X %s %%(%s)s' % (key, key))
+                    args[key] = value
+            rql.append(', '.join(sub_rql))
+            if entities:
+                rql.append('WHERE')
+                # WHERE part of the request (to link entity to they eid)
+                sub_rql = []
+                for key, value in entities.iteritems():
+                    sub_rql.append("%s eid %%(%s)s" % (key, key))
+                rql.append(', '.join(sub_rql))
+        return self.execute(' '.join(rql), args, req=req).get_entity(0, 0)
+
+    # vregistry inspection utilities ###########################################
+
+    def pviews(self, req, rset):
+        return sorted((a.id, a.__class__)
+                      for a in self.vreg['views'].possible_views(req, rset=rset))
+
+    def pactions(self, req, rset,
+                 skipcategories=('addrelated', 'siteactions', 'useractions')):
+        return [(a.id, a.__class__)
+                for a in self.vreg['actions'].poss_visible_objects(req, rset=rset)
+                if a.category not in skipcategories]
+
+    def pactions_by_cats(self, req, rset, categories=('addrelated',)):
+        return [(a.id, a.__class__)
+                for a in self.vreg['actions'].poss_visible_objects(req, rset=rset)
+                if a.category in categories]
 
-    pdbclass = CubicWebDebugger
-    # this is a hook to be able to define a list of rql queries
-    # that are application dependent and cannot be guessed automatically
-    application_rql = []
+    def pactionsdict(self, req, rset,
+                     skipcategories=('addrelated', 'siteactions', 'useractions')):
+        res = {}
+        for a in self.vreg['actions'].poss_visible_objects(req, rset=rset):
+            if a.category not in skipcategories:
+                res.setdefault(a.category, []).append(a.__class__)
+        return res
+
+    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 list_views_for(self, rset):
+        """returns the list of views that can be applied on `rset`"""
+        req = rset.req
+        only_once_vids = ('primary', 'secondary', 'text')
+        req.data['ex'] = ValueError("whatever")
+        viewsvreg = self.vreg['views']
+        for vid, views in viewsvreg.items():
+            if vid[0] == '_':
+                continue
+            if rset.rowcount > 1 and vid in only_once_vids:
+                continue
+            views = [view for view in views
+                     if view.category != 'startupview'
+                     and not issubclass(view, NotificationView)]
+            if views:
+                try:
+                    view = viewsvreg._select_best(views, req, rset=rset)
+                    if view.linkable():
+                        yield view
+                    else:
+                        not_selected(self.vreg, view)
+                    # else the view is expected to be used as subview and should
+                    # not be tested directly
+                except NoSelectableObject:
+                    continue
+
+    def list_actions_for(self, rset):
+        """returns the list of actions that can be applied on `rset`"""
+        req = rset.req
+        for action in self.vreg['actions'].possible_objects(req, rset=rset):
+            yield action
+
+    def list_boxes_for(self, rset):
+        """returns the list of boxes that can be applied on `rset`"""
+        req = rset.req
+        for box in self.vreg['boxes'].possible_objects(req, rset=rset):
+            yield box
+
+    def list_startup_views(self):
+        """returns the list of startup views"""
+        req = self.request()
+        for view in self.vreg['views'].possible_views(req, None):
+            if view.category == 'startupview':
+                yield view.id
+            else:
+                not_selected(self.vreg, view)
+
+    # web ui testing utilities #################################################
+
+    @property
+    @cached
+    def app(self):
+        """return a cubicweb publisher"""
+        return application.CubicWebPublisher(self.config, vreg=self.vreg)
+
+    requestcls = fake.FakeRequest
+    def request(self, *args, **kwargs):
+        """return a web ui request"""
+        req = self.requestcls(self.vreg, form=kwargs)
+        req.set_connection(self.cnx)
+        return req
+
+    def remote_call(self, fname, *args):
+        """remote json call simulation"""
+        dump = simplejson.dumps
+        args = [dump(arg) for arg in args]
+        req = self.request(fname=fname, pageid='123', arg=args)
+        ctrl = self.vreg['controllers'].select('json', req)
+        return ctrl.publish(), req
+
+    def publish(self, req):
+        """call the publish method of the edit controller"""
+        ctrl = self.vreg['controllers'].select('edit', req)
+        try:
+            result = ctrl.publish()
+            req.cnx.commit()
+        except web.Redirect:
+            req.cnx.commit()
+            raise
+        return result
+
+    def expect_redirect_publish(self, req):
+        """call the publish method of the edit controller, expecting to get a
+        Redirect exception."""
+        try:
+            self.publish(req)
+        except web.Redirect, ex:
+            try:
+                path, params = ex.location.split('?', 1)
+            except:
+                path, params = ex.location, ""
+            req._url = path
+            cleanup = lambda p: (p[0], unquote(p[1]))
+            params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
+            return req.relative_path(False), params # path.rsplit('/', 1)[-1], params
+        else:
+            self.fail('expected a Redirect exception')
+
+    # content validation #######################################################
 
     # validators are used to validate (XML, DTD, whatever) view's content
     # validators availables are :
@@ -99,8 +538,8 @@
         # snippets
         #'text/html': DTDValidator,
         #'application/xhtml+xml': DTDValidator,
-        'application/xml': SaxOnlyValidator,
-        'text/xml': SaxOnlyValidator,
+        'application/xml': htmlparser.SaxOnlyValidator,
+        'text/xml': htmlparser.SaxOnlyValidator,
         'text/plain': None,
         'text/comma-separated-values': None,
         'text/x-vcard': None,
@@ -109,68 +548,9 @@
         'image/png': None,
         }
     # maps vid : validator name (override content_type_validators)
-    vid_validators = dict((vid, VALMAP[valkey])
+    vid_validators = dict((vid, htmlparser.VALMAP[valkey])
                           for vid, valkey in VIEW_VALIDATORS.iteritems())
 
-    no_auto_populate = ()
-    ignored_relations = ()
-
-    def custom_populate(self, how_many, cursor):
-        pass
-
-    def post_populate(self, cursor):
-        pass
-
-    @nocoverage
-    def auto_populate(self, how_many):
-        """this method populates the database with `how_many` entities
-        of each possible type. It also inserts random relations between them
-        """
-        cu = self.cursor()
-        self.custom_populate(how_many, cu)
-        vreg = self.vreg
-        howmanydict = how_many_dict(self.schema, cu, how_many, self.no_auto_populate)
-        for etype in unprotected_entities(self.schema):
-            if etype in self.no_auto_populate:
-                continue
-            nb = howmanydict.get(etype, how_many)
-            for rql, args in insert_entity_queries(etype, self.schema, vreg, nb):
-                cu.execute(rql, args)
-        edict = {}
-        for etype in unprotected_entities(self.schema, strict=True):
-            rset = cu.execute('%s X' % etype)
-            edict[str(etype)] = set(row[0] for row in rset.rows)
-        existingrels = {}
-        ignored_relations = SYSTEM_RELATIONS + self.ignored_relations
-        for rschema in self.schema.relations():
-            if rschema.is_final() or rschema in ignored_relations:
-                continue
-            rset = cu.execute('DISTINCT Any X,Y WHERE X %s Y' % rschema)
-            existingrels.setdefault(rschema.type, set()).update((x, y) for x, y in rset)
-        q = make_relations_queries(self.schema, edict, cu, ignored_relations,
-                                   existingrels=existingrels)
-        for rql, args in q:
-            cu.execute(rql, args)
-        self.post_populate(cu)
-        self.commit()
-
-    @nocoverage
-    def _check_html(self, output, view, template='main-template'):
-        """raises an exception if the HTML is invalid"""
-        try:
-            validatorclass = self.vid_validators[view.id]
-        except KeyError:
-            if template is None:
-                default_validator = HTMLValidator
-            else:
-                default_validator = DTDValidator
-            validatorclass = self.content_type_validators.get(view.content_type,
-                                                              default_validator)
-        if validatorclass is None:
-            return None
-        validator = validatorclass()
-        return validator.parse_string(output.strip())
-
 
     def view(self, vid, rset=None, req=None, template='main-template',
              **kwargs):
@@ -244,9 +624,145 @@
             raise AssertionError, msg, tcbk
 
 
+    @nocoverage
+    def _check_html(self, output, view, template='main-template'):
+        """raises an exception if the HTML is invalid"""
+        try:
+            validatorclass = self.vid_validators[view.id]
+        except KeyError:
+            if template is None:
+                default_validator = htmlparser.HTMLValidator
+            else:
+                default_validator = htmlparser.DTDValidator
+            validatorclass = self.content_type_validators.get(view.content_type,
+                                                              default_validator)
+        if validatorclass is None:
+            return None
+        validator = validatorclass()
+        return validator.parse_string(output.strip())
+
+    # deprecated ###############################################################
+
+    @deprecated('[3.4] use self.vreg["etypes"].etype_class(etype)(self.request())')
+    def etype_instance(self, etype, req=None):
+        req = req or self.request()
+        e = self.vreg['etypes'].etype_class(etype)(req)
+        e.eid = None
+        return e
+
+    @nocoverage
+    @deprecated('[3.4] use req = self.request(); rset = req.execute()')
+    def rset_and_req(self, rql, optional_args=None, args=None, eidkey=None):
+        """executes <rql>, builds a resultset, and returns a
+        couple (rset, req) where req is a FakeRequest
+        """
+        return (self.execute(rql, args, eidkey),
+                self.request(rql=rql, **optional_args or {}))
+
+
+# auto-populating test classes and utilities ###################################
+
+from cubicweb.devtools.fill import insert_entity_queries, make_relations_queries
+
+def how_many_dict(schema, cursor, how_many, skip):
+    """compute how many entities by type we need to be able to satisfy relations
+    cardinality
+    """
+    # compute how many entities by type we need to be able to satisfy relation constraint
+    relmap = {}
+    for rschema in schema.relations():
+        if rschema.is_final():
+            continue
+        for subj, obj in rschema.iter_rdefs():
+            card = rschema.rproperty(subj, obj, 'cardinality')
+            if card[0] in '1?' and len(rschema.subjects(obj)) == 1:
+                relmap.setdefault((rschema, subj), []).append(str(obj))
+            if card[1] in '1?' and len(rschema.objects(subj)) == 1:
+                relmap.setdefault((rschema, obj), []).append(str(subj))
+    unprotected = unprotected_entities(schema)
+    for etype in skip:
+        unprotected.add(etype)
+    howmanydict = {}
+    for etype in unprotected_entities(schema, strict=True):
+        howmanydict[str(etype)] = cursor.execute('Any COUNT(X) WHERE X is %s' % etype)[0][0]
+        if etype in unprotected:
+            howmanydict[str(etype)] += how_many
+    for (rschema, etype), targets in relmap.iteritems():
+        # XXX should 1. check no cycle 2. propagate changes
+        relfactor = sum(howmanydict[e] for e in targets)
+        howmanydict[str(etype)] = max(relfactor, howmanydict[etype])
+    return howmanydict
+
+
+class AutoPopulateTest(CubicWebTC):
+    """base class for test with auto-populating of the database"""
+    __abstract__ = True
+
+    pdbclass = CubicWebDebugger
+    # this is a hook to be able to define a list of rql queries
+    # that are application dependent and cannot be guessed automatically
+    application_rql = []
+
+    no_auto_populate = ()
+    ignored_relations = ()
+
     def to_test_etypes(self):
         return unprotected_entities(self.schema, strict=True)
 
+    def custom_populate(self, how_many, cursor):
+        pass
+
+    def post_populate(self, cursor):
+        pass
+
+    @nocoverage
+    def auto_populate(self, how_many):
+        """this method populates the database with `how_many` entities
+        of each possible type. It also inserts random relations between them
+        """
+        cu = self.cursor()
+        self.custom_populate(how_many, cu)
+        vreg = self.vreg
+        howmanydict = how_many_dict(self.schema, cu, how_many, self.no_auto_populate)
+        for etype in unprotected_entities(self.schema):
+            if etype in self.no_auto_populate:
+                continue
+            nb = howmanydict.get(etype, how_many)
+            for rql, args in insert_entity_queries(etype, self.schema, vreg, nb):
+                cu.execute(rql, args)
+        edict = {}
+        for etype in unprotected_entities(self.schema, strict=True):
+            rset = cu.execute('%s X' % etype)
+            edict[str(etype)] = set(row[0] for row in rset.rows)
+        existingrels = {}
+        ignored_relations = SYSTEM_RELATIONS | set(self.ignored_relations)
+        for rschema in self.schema.relations():
+            if rschema.is_final() or rschema in ignored_relations:
+                continue
+            rset = cu.execute('DISTINCT Any X,Y WHERE X %s Y' % rschema)
+            existingrels.setdefault(rschema.type, set()).update((x, y) for x, y in rset)
+        q = make_relations_queries(self.schema, edict, cu, ignored_relations,
+                                   existingrels=existingrels)
+        for rql, args in q:
+            cu.execute(rql, args)
+        self.post_populate(cu)
+        self.commit()
+
+    def iter_individual_rsets(self, etypes=None, limit=None):
+        etypes = etypes or self.to_test_etypes()
+        for etype in etypes:
+            if limit:
+                rql = 'Any X LIMIT %s WHERE X is %s' % (limit, etype)
+            else:
+                rql = 'Any X WHERE X is %s' % etype
+            rset = self.execute(rql)
+            for row in xrange(len(rset)):
+                if limit and row > limit:
+                    break
+                # XXX iirk
+                rset2 = rset.limit(limit=1, offset=row)
+                yield rset2
+
     def iter_automatic_rsets(self, limit=10):
         """generates basic resultsets for each entity type"""
         etypes = self.to_test_etypes()
@@ -267,54 +783,6 @@
         for rql in self.application_rql:
             yield self.execute(rql)
 
-
-    def list_views_for(self, rset):
-        """returns the list of views that can be applied on `rset`"""
-        req = rset.req
-        only_once_vids = ('primary', 'secondary', 'text')
-        req.data['ex'] = ValueError("whatever")
-        viewsvreg = self.vreg['views']
-        for vid, views in viewsvreg.items():
-            if vid[0] == '_':
-                continue
-            if rset.rowcount > 1 and vid in only_once_vids:
-                continue
-            views = [view for view in views
-                     if view.category != 'startupview'
-                     and not issubclass(view, NotificationView)]
-            if views:
-                try:
-                    view = viewsvreg.select_best(views, req, rset=rset)
-                    if view.linkable():
-                        yield view
-                    else:
-                        not_selected(self.vreg, view)
-                    # else the view is expected to be used as subview and should
-                    # not be tested directly
-                except NoSelectableObject:
-                    continue
-
-    def list_actions_for(self, rset):
-        """returns the list of actions that can be applied on `rset`"""
-        req = rset.req
-        for action in self.vreg['actions'].possible_objects(req, rset=rset):
-            yield action
-
-    def list_boxes_for(self, rset):
-        """returns the list of boxes that can be applied on `rset`"""
-        req = rset.req
-        for box in self.vreg['boxes'].possible_objects(req, rset=rset):
-            yield box
-
-    def list_startup_views(self):
-        """returns the list of startup views"""
-        req = self.request()
-        for view in self.vreg['views'].possible_views(req, None):
-            if view.category == 'startupview':
-                yield view.id
-            else:
-                not_selected(self.vreg, view)
-
     def _test_everything_for(self, rset):
         """this method tries to find everything that can be tested
         for `rset` and yields a callable test (as needed in generative tests)
@@ -333,7 +801,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)
 
@@ -342,8 +810,16 @@
         return '%s_%s_%s' % ('_'.join(rset.column_types(0)), objid, objtype)
 
 
-class AutomaticWebTest(WebTest):
+# concrete class for automated application testing  ############################
+
+class AutomaticWebTest(AutoPopulateTest):
     """import this if you wan automatic tests to be ran"""
+    def setUp(self):
+        AutoPopulateTest.setUp(self)
+        # access to self.app for proper initialization of the authentication
+        # machinery (else some views may fail)
+        self.app
+
     ## one each
     def test_one_each_config(self):
         self.auto_populate(1)
@@ -365,17 +841,7 @@
             yield self.view, vid, None, req
 
 
-class RealDBTest(WebTest):
-
-    def iter_individual_rsets(self, etypes=None, limit=None):
-        etypes = etypes or unprotected_entities(self.schema, strict=True)
-        for etype in etypes:
-            rset = self.execute('Any X WHERE X is %s' % etype)
-            for row in xrange(len(rset)):
-                if limit and row > limit:
-                    break
-                rset2 = rset.limit(limit=1, offset=row)
-                yield rset2
+# registry instrumentization ###################################################
 
 def not_selected(vreg, appobject):
     try:
@@ -383,16 +849,17 @@
     except (KeyError, AttributeError):
         pass
 
+
 def vreg_instrumentize(testclass):
+    # XXX broken
     from cubicweb.devtools.apptest import TestEnvironment
-    env = testclass._env = TestEnvironment('data', configcls=testclass.configcls,
-                                           requestcls=testclass.requestcls)
+    env = testclass._env = TestEnvironment('data', configcls=testclass.configcls)
     for reg in env.vreg.values():
         reg._selected = {}
         try:
             orig_select_best = reg.__class__.__orig_select_best
         except:
-            orig_select_best = reg.__class__.select_best
+            orig_select_best = reg.__class__._select_best
         def instr_select_best(self, *args, **kwargs):
             selected = orig_select_best(self, *args, **kwargs)
             try:
@@ -402,9 +869,10 @@
             except AttributeError:
                 pass # occurs on reg used to restore database
             return selected
-        reg.__class__.select_best = instr_select_best
+        reg.__class__._select_best = instr_select_best
         reg.__class__.__orig_select_best = orig_select_best
 
+
 def print_untested_objects(testclass, skipregs=('hooks', 'etypes')):
     for regname, reg in testclass._env.vreg.iteritems():
         if regname in skipregs:
--- a/doc/book/en/B0015-define-permissions.en.txt	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/B0015-define-permissions.en.txt	Tue Sep 22 13:08:42 2009 +0200
@@ -114,7 +114,7 @@
 	require_group = SubjectRelation('EGroup', cardinality='+*',
 					description=_('groups to which the permission is granted'))
 	require_state = SubjectRelation('State',
-				    description=_("entity'state in which the permission is applyable"))
+				    description=_("entity'state in which the permission is applicable"))
 	# can be used on any entity
 	require_permission = ObjectRelation('**', cardinality='*1', composite='subject',
 					    description=_("link a permission to the entity. This "
--- a/doc/book/en/admin/setup.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/admin/setup.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -77,6 +77,150 @@
 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.
+
+Pyro enable remote access to cubicweb repository instances. Get it
+there::
+
+  http://sourceforge.net/projects/pyro/files/
+
+To access LDAP/Active directory directories, we need the python-ldap
+package. Windows binaries are available from::
+
+  http://www.osuch.org/python-ldap
+
+Check out the latest release.
+
+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
+
+Simplejson will be provided within the forest, but a win32 compiled
+version will run much faster::
+
+  http://www.osuch.org/python-simplejson%3Awin32
+
+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 +279,6 @@
 Databases configuration
 -----------------------
 
-
-
 .. _ConfigurationPostgresql:
 
 PostgreSQL configuration
--- a/doc/book/en/development/datamodel/define-workflows.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/datamodel/define-workflows.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -1,7 +1,5 @@
 .. -*- coding: utf-8 -*-
 
-.. _Workflow:
-
 Define a Workflow
 =================
 
@@ -32,29 +30,23 @@
 There are two ways to create a workflow: from the user interface,
 or by defining it in ``migration/postcreate.py``.
 This script is executed each time a new ``cubicweb-ctl db-init`` is done.
-We strongly recommand to create the workflow in ``migration/postcreate.py``
+We strongly recommend to create the workflow in ``migration/postcreate.py``
 and we will now show you how. Read `Under the hood`_ to understand why.
 
-Update the schema
-~~~~~~~~~~~~~~~~~
-If we want a State for our BlogEntry, we have to define a relation
-``in_state`` in the schema of BlogEntry. So we add
-the line ``in_state (...)``::
+The state of a entity is managed by the `in_state` attribute which can be added to you entity schema by two ways:
+
+* direct inheritance by subclassing your class from `cubicweb.schema.WorkflowableEntityType`
+* by delegation using `cubicweb.schema.make_worflowable` (usable as a decorator)
+
+About our example of BlogEntry, we must have:
 
-  class BlogEntry(EntityType):
-      title = String(maxsize=100, required=True)
-      publish_date = Date(default='TODAY')
-      text_format = String(meta=True, internationalizable=True, maxsize=50,
-                           default='text/rest', constraints=[format_constraint])
-      text = String(fulltextindexed=True)
-      category = String(vocabulary=('important','business'))
-      entry_of = SubjectRelation('Blog', cardinality='?*')
-      in_state = SubjectRelation('State', cardinality='1*')
+.. sourcecode:: python
+
+  from cubicweb.schema import WorkflowableEntityType
 
-As you updated the schema, you have to re-execute ``cubicweb-ctl db-init``
-to initialize the database and migrate your existing entities.
+  class BlogEntry(EntityType, WorkflowableEntityType):
+      ...
 
-[WRITE ABOUT MIGRATION]
 
 Create states, transitions and group permissions
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -65,7 +57,9 @@
 We will only discuss the methods we use to create a workflow in this example.
 
 To define our workflow for BlogDemo, please add the following lines
-to ``migration/postcreate.py``::
+to ``migration/postcreate.py``:
+
+.. sourcecode:: python
 
   _ = unicode
 
@@ -73,32 +67,34 @@
 
 This adds the `moderators` user group.
 
-::
+.. sourcecode:: python
+
+  wf = add_workflow(u'your workflow description', 'BlogEntry')
 
-  submitted = add_state(_('submitted'), 'BlogEntry', initial=True)
-  published = add_state(_('published'), 'BlogEntry')
+At first, instanciate a new workflow object with a gentle description and the concerned entity types (this one can be a tuple for multiple value).
+
+.. sourcecode:: python
 
-``add_state`` expects as first argument the name of the state you want
-to create, then the entity type on which the state can be applied,
-and an optional argument to say if it is supposed to be the initial state
-of the entity type.
+  submitted = wf.add_state(_('submitted'), initial=True)
+  published = wf.add_state(_('published'))
 
-::
+``add_state`` expects as first argument the name of the state you want to create and an optional argument to say if it is supposed to be the initial state of the entity type.
 
-  add_transition(_('approve_blogentry'), 'BlogEntry', (submitted,), published, ('moderators', 'managers'),)
+.. sourcecode:: python
+
+  wf.add_transition(_('approve_blogentry'), (submitted,), published, ('moderators', 'managers'),)
 
 
 ``add_transition`` expects
 
-  * as the first argument the name of the
-    transition, then the entity type on which the transition can be applied,
-  * then the list of states on which the transition can be trigged,
+  * as the first argument the name of the transition
+  * then the list of states on which the transition can be triggered,
   * the target state of the transition,
   * and the permissions
     (e.g. a list of user groups who can apply the transition; the user
     has to belong to at least one of the listed group to perform the action).
 
-::
+.. sourcecode:: python
 
   checkpoint()
 
@@ -106,22 +102,20 @@
   Do not forget to add the `_()` in front of all states and transitions names while creating
   a workflow so that they will be identified by the i18n catalog scripts.
 
-In addition to the user group condition, we could have added a RQL condition.
-In this case, the user can only perform the action if
-the two conditions are satisfied.
+In addition to the user group conditions which the user needs to belong to one of those, we could have added a RQL condition.
+In this case, the user can only perform the action if the two conditions are satisfied.
 
-If we use a RQL condition on a transition, we can use the following
-variables:
+If we use a RQL condition on a transition, we can use the following variables:
 
 * `%(eid)s`, object's eid
 * `%(ueid)s`, user executing the query eid
 * `%(seid)s`, the object's current state eid
 
 
-.. image:: ../../images/lax-book.03-transitions-view.en.png
+.. image:: ../../images/03-transitions-view.en.png
 
 You can notice that in the action box of a BlogEntry, the state
-is now listed as well as the possible transitions defined by the workflow.
+is now listed as well as the possible transitions for the current state defined by the workflow.
 The transitions will only be displayed for users having the right permissions.
 In our example, the transition `approve_blogentry` will only be displayed
 for the users belonging to the group `moderators` or `managers`.
@@ -132,28 +126,26 @@
 
 A workflow is a collection of entities of type ``State`` and of type ``Transition``
 which are standard *CubicWeb* entity types.
-For instance, the following lines::
+
+For instance, the preceding lines:
 
-  submitted = add_state(_('submitted'), 'BlogEntry', initial=True)
-  published = add_state(_('published'), 'BlogEntry')
+.. sourcecode:: python
+
+  submitted = wf.add_state(_('submitted'), initial=True)
+  published = wf.add_state(_('published'))
 
 will create two entities of type ``State``, one with name 'submitted', and the other
-with name 'published'. Whereas::
+with name 'published'. Whereas:
 
-  add_transition(_('approve_blogentry'), 'BlogEntry', (submitted,), published, ('moderators', 'managers'),)
+.. sourcecode:: python
+
+  wf.add_transition(_('approve_blogentry'), (submitted,), published, ('moderators', 'managers'),)
 
-will create an entity of type ``Transition`` with name 'approve_blogentry' which will
+will create an entity of type ``Transition`` with name `approve_blogentry` which will
 be linked to the ``State`` entities created before.
-As a consequence, we could use the administration interface to do these operations.
-But it is not recommanded because it will be uselessly complicated
-and will be only local to your instance.
+As a consequence, we could use the administration interface to do these operations. But it is not recommended because it will be uselessly complicated and will be only local to your instance.
+
+Indeed, if you create the states and transitions through the user interface, next time you initialize the database you will have to re-create all the entities.
+The user interface should only be a reference for you to view the states and transitions, but is not the appropriate interface to define your application workflow.
 
 
-Indeed, if you create the states and transitions through the user interface,
-next time you initialize the database
-you will have to re-create all the entities.
-The user interface should only be a reference for you to view the states
-and transitions, but is not the appropriate interface to define your
-application workflow.
-
-
--- a/doc/book/en/development/datamodel/definition.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/datamodel/definition.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -89,7 +89,7 @@
     * `*`: 0..n
 
   - `meta` : boolean indicating that the relation is a meta-relation (false by
-    default)
+    default, will disappear in *CubicWeb* 3.5)
 
 * optional properties for attributes :
 
@@ -216,8 +216,8 @@
 of the cube (``migration/precreate.py``).
 
 
-Use of RQL expression for writing rights
-`````````````````````````````````````````
+Use of RQL expression for write permissions
+```````````````````````````````````````````
 It is possible to define RQL expression to provide update permission
 (`add`, `delete` and `update`) on relation and entity types.
 
@@ -249,7 +249,7 @@
 * we can also defined rights on attributes of an entity (non-final relation),
   knowing that :
 
-  - to defines RQL expression, we have to use the class `RQLExpression`
+  - to define RQL expression, we have to use the class `RQLExpression`
     in which X represents the entity the attribute belongs to
 
   - the permissions `add` and `delete` are equivalent. Only `add`/`read`
@@ -321,7 +321,7 @@
     attr_name = attr_type(properties*)
 
 where `attr_type` is one of the type listed above and `properties` is
-a list of  the attribute needs to statisfy (see :ref:`properties`
+a list of the attribute needs to statisfy (see :ref:`properties`
 for more details).
 
 
@@ -390,7 +390,7 @@
 	require_group = SubjectRelation('CWGroup', cardinality='+*',
 					description=_('groups to which the permission is granted'))
 	require_state = SubjectRelation('State',
-				    description=_("entity'state in which the permission is applyable"))
+                                        description=_("entity's state in which the permission is applicable"))
 	# can be used on any entity
 	require_permission = ObjectRelation('**', cardinality='*1', composite='subject',
 					    description=_("link a permission to the entity. This "
@@ -434,6 +434,6 @@
 * in such case, we have to protect both the entity type "Version" and the relation
   associating a version to a project ("version_of")
 
-* because of the genricity of the entity type `CWPermission`, we have to execute
+* because of the genericity of the entity type `CWPermission`, we have to execute
   a unification with the groups and/or the states if necessary in the expression
   ("U in_group G, P require_group G" in the above example)
--- a/doc/book/en/development/datamodel/inheritance.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/datamodel/inheritance.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -5,4 +5,14 @@
 When describing a data model, entities can inherit from other entities as is
 common in object-oriented programming.
 
+You have the possibility to adapt some entity attributes, as follow:
+
+.. sourcecode:: python
+
+    from cubes.OTHER_CUBE import entities
+    class EntityExample(entities.EntityExample):
+        def dc_long_title(self):
+            return '%s (%s)' % (self.name, self.description)
+
+
 XXX WRITME
--- a/doc/book/en/development/devcore/appobject.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/devcore/appobject.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -16,7 +16,7 @@
 We can find a certain number of attributes and methods defined in this class
 and common to all the application objects.
 
-At the recording, the following attributes are dynamically added to
+At recording time, the following attributes are dynamically added to
 the *subclasses*:
 
 * `vreg`, the `vregistry` of the instance
@@ -27,28 +27,15 @@
 
 * `req`, `Request` instance
 * `rset`, the *result set* associated to the object if necessary
-* `cursor`, rql cursor on the session
-
 
 :URL handling:
-  * `build_url(method=None, **kwargs)`, returns an absolute URL based on
-    the given arguments. The *controller* supposed to handle the response,
-    can be specified through the special parameter `method` (the connection
-    is theoretically done automatically :).
-
-  * `datadir_url()`, returns the directory of the instance data
-    (contains static files such as images, css, js...)
-
-  * `base_url()`, shortcut to `req.base_url()`
-
-  * `url_quote(value)`, version *unicode safe* of the function `urllib.quote`
+  * `build_url(*args, **kwargs)`, returns an absolute URL based on the
+    given arguments. The *controller* supposed to handle the response,
+    can be specified through the first positional parameter (the
+    connection is theoretically done automatically :).
 
 :Data manipulation:
 
-  * `etype_rset(etype, size=1)`, shortcut to `vreg.etype_rset()`
-
-  * `eid_rset(eid, rql=None, descr=True)`, returns a *result set* object for
-    the given eid
   * `entity(row, col=0)`, returns the entity corresponding to the data position
     in the *result set* associated to the object
 
@@ -57,15 +44,12 @@
 
 :Data formatting:
   * `format_date(date, date_format=None, time=False)` returns a string for a
-    mx date time according to instance's configuration
-  * `format_time(time)` returns a string for a mx date time according to
+    date time according to instance's configuration
+  * `format_time(time)` returns a string for a date time according to
     instance's configuration
 
 :And more...:
 
-  * `external_resource(rid, default=_MARKER)`, access to a value defined in the
-    configuration file `external_resource`
-
   * `tal_render(template, variables)`, renders a precompiled page template with
     variables in the given dictionary as context
 
@@ -73,13 +57,14 @@
   When we inherit from `AppObject` (even not directly), you *always* have to use
   **super()** to get the methods and attributes of the superclasses, and not
   use the class identifier.
+
   For example, instead of writting: ::
 
       class Truc(PrimaryView):
           def f(self, arg1):
               PrimaryView.f(self, arg1)
 
-  You'd better write: ::
+  You must write: ::
 
       class Truc(PrimaryView):
           def f(self, arg1):
--- a/doc/book/en/development/devcore/vreg.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/devcore/vreg.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -1,4 +1,4 @@
-.. -*- coding: utf-8 -*-
+. -*- coding: utf-8 -*-
 
 The VRegistry
 --------------
@@ -22,16 +22,15 @@
     Once the function `registration_callback(vreg)` is implemented, all the objects
     have to be explicitly registered as it disables the automatic object registering.
 
-* the old registration mechanism will be removed when there will be no reference
-  left to the registerers module in cubicweb and the library of cubes.
-
 Examples:
 
 .. sourcecode:: python
 
    # web/views/basecomponents.py
    def registration_callback(vreg):
+      # register everything in the module except SeeAlsoComponent
       vreg.register_all(globals().values(), __name__, (SeeAlsoVComponent,))
+      # conditionally register SeeAlsoVComponent
       if 'see_also' in vreg.schema:
           vreg.register(SeeAlsoVComponent)
 
@@ -66,7 +65,7 @@
 
 The object's selector is defined by its `__select__` class attribute.
 
-When two selectors are combined using the `&` operator (former `chainall`), it
+When two selectors are combined using the `&` operator (formerly `chainall`), it
 means that both should return a positive score. On success, the sum of scores is returned.
 
 When two selectors are combined using the `|` operator (former `chainfirst`), it
@@ -76,87 +75,112 @@
 Of course you can use paren to balance expressions.
 
 
-For instance, if you're selecting the primary (eg `id = 'primary'`) view (eg
+For instance, if you are selecting the primary (eg `id = 'primary'`) view (eg
 `__registry__ = 'view'`) for a result set containing a `Card` entity, 2 objects
 will probably be selectable:
 
-* the default primary view (`__select__ = implements('Any')`), meaning that the object is selectable for any kind of entity type
-
-* the specific `Card` primary view (`__select__ = implements('Card')`, meaning that the object is selectable for Card entities
+* the default primary view (`__select__ = implements('Any')`), meaning
+  that the object is selectable for any kind of entity type
 
-Other primary views specific to other entity types won't be selectable in this
-case. Among selectable objects, the implements selector will return a higher score
-to the second view since it's more specific, so it will be selected as expected.
+* the specific `Card` primary view (`__select__ = implements('Card')`,
+  meaning that the object is selectable for Card entities
+
+Other primary views specific to other entity types won't be selectable
+in this case. Among selectable objects, the implements selector will
+return a higher score than the second view since it's more specific,
+so it will be selected as expected.
 
 
 Example
 ````````
 
-XXX this part needs to be translated
-
-Le but final : quand on est sur un Blog, on veut que le lien rss de celui-ci pointe
-vers les entrées de ce blog, non vers l'entité blog elle-même.
+The goal: when on a Blog, one wants the RSS link to refer to blog
+entries, not to the blog entity itself.
 
-L'idée générale pour résoudre ça : on définit une méthode sur les classes d'entité
-qui renvoie l'url du flux rss pour l'entité en question. Avec une implémentation
-par défaut sur AnyEntity et une implémentation particulière sur Blog qui fera ce
-qu'on veut.
+To do that, one defines a method on entity classes that returns the
+RSS stream url for a given entity. The default implementation on
+AnyEntity and a specific implementation on Blog will do what we want.
 
-La limitation : on est embêté dans le cas ou par ex. on a un result set qui contient
-plusieurs entités Blog (ou autre chose), car on ne sait pas sur quelle entité appeler
-la méthode sus-citée. Dans ce cas, on va conserver le comportement actuel (eg appel
-à limited_rql)
+But when we have a result set containing several Blog entities (or
+different entities), we don't know on which entity to call the
+aforementioned method. In this case, we keep the current behaviour
+(e.g : call to limited_rql).
 
-Donc : on veut deux cas ici, l'un pour un rset qui contient une et une seule entité,
-l'autre pour un rset qui contient plusieurs entité.
+Hence we have two cases here, one for a single-entity rsets, the other
+for multi-entities rsets.
 
-Donc... On a déja dans web/views/boxes.py la classe RSSIconBox qui fonctionne. Son
-sélecteur ::
+In web/views/boxes.py lies the RSSIconBox class. Look at its selector ::
 
   class RSSIconBox(ExtResourcesBoxTemplate):
     """just display the RSS icon on uniform result set"""
     __select__ = ExtResourcesBoxTemplate.__select__ & non_final_entity()
 
+It takes into account:
 
-indique qu'il prend en compte :
+* the inherited selection criteria (one has to look them up in the
+  class hierarchy to know the details)
 
-* les conditions d'apparition de la boite (faut remonter dans les classes parentes
-  pour voir le détail)
-* non_final_entity, qui filtre sur des rset contenant une liste d'entité non finale
+* non_final_entity, which filters on rsets containing non final
+  entities (a 'final entity' being synonym for entity attribute)
 
-ça correspond donc à notre 2eme cas. Reste à fournir un composant plus spécifique
-pour le 1er cas ::
+This matches our second case. Hence we have to provide a specific
+component for the first case::
 
   class EntityRSSIconBox(RSSIconBox):
     """just display the RSS icon on uniform result set for a single entity"""
     __select__ = RSSIconBox.__select__ & one_line_rset()
 
+Here, one adds the one_line_rset selector, which filters result sets
+of size 1. When one chains selectors, the final score is the sum of
+the score of each individual selector (unless one of them returns 0,
+in which case the object is non selectable). Thus, on a multiple
+entities selector, one_line_rset makes the EntityRSSIconBox class non
+selectable. For an rset with one entity, the EntityRSSIconBox class
+will have a higher score then RSSIconBox, which is what we wanted.
 
-Ici, on ajoute le selector one_line_rset, qui filtre sur des result set de taille 1. Il faut
-savoir que quand on chaine des selecteurs, le score final est la somme des scores
-renvoyés par chaque sélecteur (sauf si l'un renvoie zéro, auquel cas l'objet est
-non sélectionnable). Donc ici, sur un rset avec plusieurs entités, onelinerset_selector
-rendra la classe EntityRSSIconBox non sélectionnable, et on obtiendra bien la
-classe RSSIconBox. Pour un rset avec une entité, la classe EntityRSSIconBox aura un
-score supérieur à RSSIconBox et c'est donc bien elle qui sera sélectionnée.
+Of course, once this is done, you have to ::
+
+* fill in the call method of EntityRSSIconBox
 
-Voili voilou, il reste donc pour finir tout ça :
+* provide the default implementation of the method returning the RSS
+  stream url on AnyEntity
 
-* à définir le contenu de la méthode call de EntityRSSIconBox
-* fournir l'implémentation par défaut de la méthode renvoyant l'url du flux rss sur
-  AnyEntity
-* surcharger cette methode dans blog.Blog
-
+* redefine this method on Blog.
 
 When to use selectors?
 ```````````````````````
 
-Il faut utiliser les sélecteurs pour faire des choses différentes en
-fonction de ce qu'on a en entrée. Dès qu'on a un "if" qui teste la
-nature de `self.rset` dans un objet, il faut très sérieusement se
-poser la question s'il ne vaut pas mieux avoir deux objets différent
-avec des sélecteurs approprié.
+Selectors are to be used whenever arises the need of dispatching on
+the shape or content of a result set. That is, almost all the time.
 
 Debugging
 `````````
-XXX explain traced_selection context manager
+
+Once in a while, one needs to understand why a view (or any AppObject)
+is, or is not selected appropriately. Looking at which selectors fired
+(or did not) is the way. There exists a traced_selection context
+manager to help with that.
+
+Here is an example ::
+
+.. sourcecode:: python
+
+    def possible_objects(self, registry, *args, **kwargs):
+        """return an iterator on possible objects in a registry for this result set
+
+        actions returned are classes, not instances
+        """
+        from cubicweb.selectors import traced_selection
+        with traced_selection():
+            for vobjects in self.registry(registry).values():
+                try:
+                    yield self.select(vobjects, *args, **kwargs)
+                except NoSelectableObject:
+                    continue
+
+Don't forget the 'from __future__ import with_statement' at the module
+top-level.
+
+This will yield additional WARNINGs in the logs, like this::
+
+    2009-01-09 16:43:52 - (cubicweb.selectors) WARNING: selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
--- a/doc/book/en/development/devweb/internationalization.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/devweb/internationalization.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -45,7 +45,8 @@
 
 The goal of the *built-in* function `_` is only **to mark the
 translatable strings**, it will only return the string to translate
-it-self, but not its translation (it's actually refering to the `unicode` builtin).
+itself, but not its translation (it's actually another name for the
+`unicode` builtin).
 
 In the other hand the request's method `self.req._` is meant to retrieve the
 proper translation of translation strings in the requested language.
@@ -81,8 +82,8 @@
 
 
 * `i18ncubicweb` updates Cubicweb framework's translation
-  catalogs. Unless you work on the framework development, you don't
-  need to use this command.
+  catalogs. Unless you actually work on the framework itself, you
+  don't need to use this command.
 
 * `i18ncube` updates the translation catalogs of *one particular
   cube* (or of all cubes). After this command is
@@ -90,7 +91,7 @@
   directory of your template. This command will of course not remove
   existing translations still in use.
 
-* `i18ninstance` recompile the translation catalogs of *one particular
+* `i18ninstance` recompiles the translation catalogs of *one particular
   instance* (or of all instances) after the translation catalogs of
   its cubes have been updated. This command is automatically
   called every time you create or update your instance. The compiled
--- a/doc/book/en/development/devweb/js.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/devweb/js.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -3,16 +3,70 @@
 Javascript
 ----------
 
-XXX jquery...
+*CubicWeb* uses quite a bit of javascript in its user interface and
+ships with jquery (1.3.x) and parts of the jquery UI
+library, plus a number of homegrown files and also other thirparty
+libraries.
+
+All javascript files are stored in cubicweb/web/data/. There are
+around thirty js files there. In a cube it goes to data/.
+
+Obviously one does not want javascript pieces to be loaded all at
+once, hence the framework provides a number of mechanisms and
+conventions to deal with javascript resources.
 
 Conventions
 ~~~~~~~~~~~
 
-XXX external_resources variable
-    naming convention
-    request.add_js
+It is good practice to name cube specific js files after the name of
+the cube, like this : 'cube.mycube.js', so as to avoid name clashes.
 
+XXX external_resources variable (which needs love)
 
 CubicWeb javascrip api
 ~~~~~~~~~~~~~~~~~~~~~~
-XXX explain diffenrent files and main functions
+
+Javascript resources are typically loaded on demand, from views. The
+request object (available as self.req from most application objects,
+for instance views and entities objects) has a few methods to do that:
+
+* `add_js(self, jsfiles, localfile=True)` which takes a sequence of
+  javascript files and writes proper entries into the HTML header
+  section. The localfile parameter allows to declare resources which
+  are not from web/data (for instance, residing on a content delivery
+  network).
+
+* `add_onload(self, jscode)` which adds one raw javascript code
+  snippet inline in the html headers. This is quite useful for setting
+  up early jQuery(document).ready(...) initialisations.
+
+Overview of what's available
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* jquery.* : jquery and jquery UI library
+
+* cubicweb.python.js : adds a number of practical extension to stdanrd
+  javascript objects (on Date, Array, String, some list and dictionary
+  operations), and a pythonesque way to build classes. Defines a
+  CubicWeb namespace.
+
+* cubicweb.htmlhelpers.js : a small bag of convenience functions used
+  in various other cubicweb javascript resources (baseuri, progress
+  cursor handling, popup login box, html2dom function, etc.)
+
+* cubicweb.ajax.js : concentrates all ajax related facilities (it
+  extends jQuery with the loahxhtml function, provides a handfull of
+  high-level ajaxy operations like asyncRemoteExec, reloadComponent,
+  replacePageChunk, getDomFromResponse)
+
+* cubicweb.widgets.js : provides a widget namespace and constructors
+  and helpers for various widgets (mainly facets and timeline)
+
+* cubicweb.edition.js : used by edition forms
+
+* cubicweb.preferences.js : used by the preference form
+
+* cubicweb.facets.js : used by the facets mechanism
+
+xxx massmailing, gmap, fckcwconfig, timeline-bundle, timeline-ext,
+calendar, goa, flotn tazy, tabs, bookmarks
--- a/doc/book/en/development/devweb/views.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/devweb/views.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -34,26 +34,32 @@
     * the `category` attribute may be used in the interface to regroup related
       objects together
 
-At instantiation time, the standard `req`, `rset`, and `cursor`
-attributes are added and the `w` attribute will be set at rendering
-time.
+At instantiation time, the standard `req` and `rset` attributes are
+added and the `w` attribute will be set at rendering time.
 
-A view writes to its output stream thanks to its attribute `w` (`UStreamIO`).
+A view writes to its output stream thanks to its attribute `w` (an
+`UStreamIO`).
 
 The basic interface for views is as follows (remember that the result set has a
 tabular structure with rows and columns, hence cells):
 
-* `dispatch(**context)`, render the view by calling `call` or
+* `render(**context)`, render the view by calling `call` or
   `cell_call` depending on the given parameters
-* `call(**kwargs)`, call the view for a complete result set or null (default
-  implementation calls `cell_call()` on each cell of the result set)
-* `cell_call(row, col, **kwargs)`, call the view for a given cell of a result set
+
+* `call(**kwargs)`, call the view for a complete result set or null
+  (the default implementation calls `cell_call()` on each cell of the
+  result set)
+
+* `cell_call(row, col, **kwargs)`, call the view for a given cell of a
+  result set
+
 * `url()`, returns the URL enabling us to get the view with the current
   result set
+
 * `view(__vid, rset, __fallback_vid=None, **kwargs)`, call the view of identifier
   `__vid` on the given result set. It is possible to give a view identifier
   of fallback that will be used if the view requested is not applicable to the
-  result set
+  result set. This is actually defined on the AppObject class.
 
 * `wview(__vid, rset, __fallback_vid=None, **kwargs)`, similar to `view` except
   the flow is automatically passed in the parameters
@@ -70,8 +76,8 @@
 
 * `EntityView`, view applying to lines or cell containing an entity (e.g. an eid)
 * `StartupView`, start view that does not require a result set to apply to
-* `AnyRsetView`, view applied to any result set
-* `EmptyRsetView`, view applied to an empty result set
+* `AnyRsetView`, view applicable to any result set
+* `EmptyRsetView`, view applicable to an empty result set
 
 
 Examples of views class
@@ -103,11 +109,8 @@
         __select__ = one_line_rset() & match_search_state('linksearch') & implements('Any')
 
 
-Example of a view customization
--------------------------------
-
-[FIXME] XXX Example needs to be rewritten as it shows how to modify cell_call which
-contredicts our advise of not modifying it.
+Example of view customization and creation
+------------------------------------------
 
 We'll show you now an example of a ``primary`` view and how to customize it.
 
@@ -116,32 +119,26 @@
 
 .. sourcecode:: python
 
-   from cubicweb.view import EntityView
-   from cubicweb.selectors import implements
+  from cubicweb.selectors import implements
+  from cubicweb.web.views.primary improt Primaryview
 
-   class BlogEntryPrimaryView(EntityView):
-       id = 'primary'
-       __select__ =implements('Blog')
+  class BlogEntryPrimaryView(PrimaryView):
+    __select__ = PrimaryView.__select__ & implements('BlogEntry')
 
-       def cell_call(self, row, col):
-           entity = self.entity(row, col)
-           self.w(u'<h1>%s</h1>' % entity.title)
-           self.w(u'<p>published on %s in category %s</p>' % \
-                  (entity.publish_date.strftime('%Y-%m-%d'), entity.category))
-           self.w(u'<p>%s</p>' % entity.text)
+      def render_entity_attributes(self, entity):
+          self.w(u'<p>published on %s</p>' %
+                 entity.publish_date.strftime('%Y-%m-%d'))
+          super(BlogEntryPrimaryView, self).render_entity_attributes(entity)
 
-The above source code defines a new primary view (`line 03`) for
-``BlogEntry`` (`line 05`).
+The above source code defines a new primary view for
+``BlogEntry``. The `id` class attribute is not repeated there since it
+is inherited through the `primary.PrimaryView` class.
 
-Since views are applied to result sets which can be tables of
-data, we have to recover the entity from its (row,col)-coordinates (`line 08`).
-We will get to this in more detail later.
+The selector for this view chains the selector of the inherited class
+with its own specific criterion.
 
 The view method ``self.w()`` is used to output data. Here `lines
-09-12` output HTML tags and values of the entity's attributes.
-
-When displaying the same blog entry as before, you will notice that the
-page is now looking much nicer. [FIXME: it is not clear to what this refers.]
+08-09` output HTML for the publication date of the entry.
 
 .. image:: ../../images/lax-book.09-new-view-blogentry.en.png
    :alt: blog entries now look much nicer
@@ -150,34 +147,74 @@
 
 .. sourcecode:: python
 
- class BlogPrimaryView(EntityView):
+ from logilab.mtconverter import xml_escape
+ from cubicweb.selectors import implements, one_line_rset
+ from cubicweb.web.views.primary import Primaryview
+
+ class BlogPrimaryView(PrimaryView):
      id = 'primary'
-     __select__ =implements('Blog')
+     __select__ = PrimaryView.__select__ & implements('Blog')
+     rql = 'Any BE ORDERBY D DESC WHERE BE entry_of B, BE publish_date D, B eid %(b)s'
+
+     def render_entity_relations(self, entity):
+         rset = self.req.execute(self.rql, {'b' : entity.eid})
+         for entry in rset.entities():
+             self.w(u'<p>%s</p>' % entry.view('inblogcontext'))
+
+ class BlogEntryInBlogView(EntityView):
+     id = 'inblogcontext'
+     __select__ = implements('BlogEntry')
 
      def cell_call(self, row, col):
-         entity = self.entity(row, col)
-         self.w(u'<h1>%s</h1>' % entity.title)
-         self.w(u'<p>%s</p>' % entity.description)
-         rset = self.req.execute('Any E WHERE E entry_of B, B eid "%s"' % entity.eid)
-         self.wview('primary', rset)
+         entity = self.rset.get_entity(row, col)
+         self.w(u'<a href="%s" title="%s">%s</a>' %
+                entity.absolute_url(),
+                xml_escape(entity.content[:50]),
+                xml_escape(entity.description))
 
-In the above source code, `lines 01-08` are similar to the previous
-view we defined. [FIXME: defined where ?]
+This happens in two places. First we override the
+render_entity_relations method of a Blog's primary view. Here we want
+to display our blog entries in a custom way.
 
-At `line 09`, a simple request is made to build a result set with all
+At `line 10`, a simple request is made to build a result set with all
 the entities linked to the current ``Blog`` entity by the relationship
 ``entry_of``. The part of the framework handling the request knows
-about the schema and infer that such entities have to be of the
-``BlogEntry`` kind and retrieves them.
+about the schema and infers that such entities have to be of the
+``BlogEntry`` kind and retrieves them (in the prescribed publish_date
+order).
+
+The request returns a selection of data called a result set. Result
+set objects have an .entities() method returning a generator on
+requested entities (going transparently through the `ORM` layer).
+
+At `line 13` the view 'inblogcontext' is applied to each blog entry to
+output HTML. (Note that the 'inblogcontext' view is not defined
+whatsoever in *CubicWeb*. You are absolutely free to define whole view
+families.) We juste arrange to wrap each blogentry output in a 'p'
+html element.
 
-The request returns a selection of data called a result set. At
-`line 10` the view 'primary' is applied to this result set to output
-HTML.
+Next, we define the 'inblogcontext' view. This is NOT a primary view,
+with its well-defined sections (title, metadata, attribtues,
+relations/boxes). All a basic view has to define is cell_call.
+
+Since views are applied to result sets which can be tables of data, we
+have to recover the entity from its (row,col)-coordinates (`line
+20`). Then we can spit some HTML.
+
+But careful: all strings manipulated in *CubicWeb* are actually
+unicode strings. While web browsers are usually tolerant to incoherent
+encodings they are being served, we should not abuse it. Hence we have
+to properly escape our data. The xml_escape() function has to be used
+to safely fill (X)HTML elements from Python unicode strings.
+
 
 **This is to be compared to interfaces and protocols in object-oriented
 languages. Applying a given view called 'a_view' to all the entities
 of a result set only requires to have for each entity of this result set,
-an available view called 'a_view' which accepts the entity.**
+an available view called 'a_view' which accepts the entity.
+
+Instead of merely using type based dispatch, we do predicate dispatch
+which quite more powerful**
 
 Assuming we added entries to the blog titled `MyLife`, displaying it
 now allows to read its description and all its entries.
--- a/doc/book/en/development/entityclasses/data-as-objects.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/entityclasses/data-as-objects.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -1,7 +1,8 @@
 Access to persistent data
 --------------------------
 
-XXX is provided by the :class:`Entity <cubicweb.entity.entity>` class
+Python-level access to persistent data is provided by the
+:class:`Entity <cubicweb.entity>` class.
 
 An entity class is bound to a schema entity type.  Descriptors are added when
 classes are registered in order to initialize the class according to its schema:
@@ -22,8 +23,6 @@
 
   * `rest_path()`, returns a relative REST URL to get the entity
 
-  * `format(attr)`, returns the format (MIME type) of the field given un parameter
-
   * `printable_value(attr, value=_marker, attrtype=None, format='text/html')`,
     returns a string enabling the display of an attribute value in a given format
     (the value is automatically recovered if necessary)
@@ -33,7 +32,7 @@
   * `as_rset()`, converts the entity into an equivalent result set simulating the
      request `Any X WHERE X eid _eid_`
 
-  * `complete(skip_bytes=True)`, executes a request that recovers in one time
+  * `complete(skip_bytes=True)`, executes a request that recovers all at once
     all the missing attributes of an entity
 
   * `get_value(name)`, returns the value associated to the attribute name given
@@ -52,9 +51,6 @@
   * `copy_relations(ceid)`, copies the relations of the entities having the eid
     given in the parameters on the current entity
 
-  * `last_modified(view)`, returns the date the object has been modified
-    (used by HTTP cache handling)
-
   * `delete()` allows to delete the entity
 
 
@@ -66,24 +62,25 @@
 in `mycube.entities` module (or in a submodule if we want to split code among
 multiple files) so that it will be available on both server and client side.
 
-The class `AnyEntity` is loaded dynamically from the class `Entity`
-(`cubciweb.entity`). We define a sub-class to add methods or to
-specialize the handling of a given entity type
+The class `AnyEntity` is a sub-class of Entity that add methods to it,
+and helps specializing (by further subclassing) the handling of a
+given entity type.
 
-The methods defined for `AnyEntity` or `Entity` are the following ones:
+The methods defined for `AnyEntity`, in addition to `Entity`, are the
+following ones:
 
 :Standard meta-data (Dublin Core):
 
-  * `dc_title()`, returns a unicode string corresponding to the meta-data
-    `Title` (used by default the first attribute non-meta of the entity
-    schema)
+  * `dc_title()`, returns a unicode string corresponding to the
+    meta-data `Title` (used by default is the first non-meta attribute
+    of the entity schema)
 
   * `dc_long_title()`, same as dc_title but can return a more
-    detailled title
+    detailed title
 
   * `dc_description(format='text/plain')`, returns a unicode string
-    corresponding to the meta-data `Description` (look for a description
-    attribute by default)
+    corresponding to the meta-data `Description` (looks for a
+    description attribute by default)
 
   * `dc_authors()`, returns a unicode string corresponding to the meta-data
     `Authors` (owners by default)
--- a/doc/book/en/development/entityclasses/load-sort.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/entityclasses/load-sort.rst	Tue Sep 22 13:08:42 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	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/testing/index.rst	Tue Sep 22 13:08:42 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/doc/book/en/development/webstdlib/basetemplates.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/webstdlib/basetemplates.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -126,8 +126,8 @@
                       title=False, message=False)
 
     def get_searchbox(self, view, context):
-        boxes = list(self.vreg.possible_vobjects('boxes', self.req, self.rset,
-                                                 view=view, context=context))
+        boxes = list(self.vreg.poss_visible_objects('boxes', self.req, self.rset,
+                                                    view=view, context=context))
         if boxes:
             for box in boxes:
                 if box.id == 'search_box':
--- a/doc/book/en/development/webstdlib/baseviews.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/webstdlib/baseviews.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -30,10 +30,11 @@
 Entity views
 ````````````
 *incontext, outofcontext*
-    Those are used to display a link to an entity, depending if the entity is
-    considered as displayed in or out of context (of another entity).  By default
-    it respectively returns the result of `textincontext` and `textoutofcontext`
-    wrapped in a link leading to the primary view of the entity.
+    Those are used to display a link to an entity, depending on the
+    entity having to be displayed in or out of context
+    (of another entity).  By default it respectively returns the
+    result of `textincontext` and `textoutofcontext` wrapped in a link
+    leading to the primary view of the entity.
 
 *oneline*
     This view is used when we can't tell if the entity should be considered as
@@ -56,6 +57,12 @@
 *adaptedlistitem*
     This view redirects by default to the `outofcontext` view.
 
+*csv*
+    This view applies to entity groups, which are individually
+    displayed using the `incontext` view. It displays each entity as a
+    coma separated list. It is NOT related to the well-known text file
+    format.
+
 Text entity views
 ~~~~~~~~~~~~~~~~~
 *text*
--- a/doc/book/en/development/webstdlib/primary.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/development/webstdlib/primary.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -9,8 +9,9 @@
 Rendering methods and attributes for ``PrimaryView``
 ----------------------------------------------------
 
-By default, *CubicWeb* provides a primary view for each new entity type
-you create. The first view you might be interested in modifying.
+By default, *CubicWeb* provides a primary view for every available
+entity type. This is the first view you might be interested in
+modifying.
 
 Let's have a quick look at the EntityView ``PrimaryView`` as well as
 its rendering method
@@ -24,55 +25,63 @@
         show_attr_label = True
         show_rel_label = True
         skip_none = True
-        skip_attrs = ('eid', 'creation_date', 'modification_date')
-        skip_rels = ()
+        rsection = uicfg.primaryview_section
+        display_ctrl = uicfg.primaryview_display_ctrl
         main_related_section = True
 
         ...
 
     def cell_call(self, row, col):
         self.row = row
-        self.render_entity(self.complete_entity(row, col))
+        self.maxrelated = self.req.property_value('navigation.related-limit')
+        entity = self.complete_entity(row, col)
+        self.render_entity(entity)
 
     def render_entity(self, entity):
-        """return html to display the given entity"""
-        siderelations = []
         self.render_entity_title(entity)
         self.render_entity_metadata(entity)
         # entity's attributes and relations, excluding meta data
         # if the entity isn't meta itself
-        self.w(u'<div>')
+        boxes = self._prepare_side_boxes(entity)
+        if boxes or hasattr(self, 'render_side_related'):
+            self.w(u'<table width="100%"><tr><td style="width: 75%">')
+        self.render_entity_summary(entity)
         self.w(u'<div class="mainInfo">')
-        self.render_entity_attributes(entity, siderelations)
-        self.w(u'</div>')
         self.content_navigation_components('navcontenttop')
+        self.render_entity_attributes(entity)
         if self.main_related_section:
-            self.render_entity_relations(entity, siderelations)
+            self.render_entity_relations(entity)
         self.w(u'</div>')
         # side boxes
-        self.w(u'<div class="primaryRight">')
-        self.render_side_related(entity, siderelations)
-        self.w(u'</div>')
-        self.w(u'<div class="clear"></div>')
+        if boxes or hasattr(self, 'render_side_related'):
+            self.w(u'</td><td>')
+            self.w(u'<div class="primaryRight">')
+            if hasattr(self, 'render_side_related'):
+                warn('render_side_related is deprecated')
+                self.render_side_related(entity, [])
+            self.render_side_boxes(boxes)
+            self.w(u'</div>')
+            self.w(u'</td></tr></table>')
         self.content_navigation_components('navcontentbottom')
 
     ...
 
-``cell_call`` is executed for each entity of a result set and apply ``render_entity``.
+``cell_call`` is executed for each entity of a result set.
 
 The methods you want to modify while customizing a ``PrimaryView`` are:
 
 *render_entity_title(self, entity)*
     Renders the entity title based on the assumption that the method
-    ``def content_title(self)`` is implemented for the given entity type.
+    ``def dc_title(self)`` is implemented for the given entity type.
 
 *render_entity_metadata(self, entity)*
-    Renders the entity metadata based on the assumption that the method
-    ``def summary(self)`` is implemented for the given entity type.
+    Renders the entity metadata by calling the 'metadata' view on the
+    entity. This generic view is in cubicweb.views.baseviews.
 
-*render_entity_attributes(self, entity, siderelations)*
-    Renders all the attribute of an entity with the exception of attribute
-    of type `Password` and `Bytes`.
+*render_entity_attributes(self, entity)*
+    Renders all the attribute of an entity with the exception of
+    attribute of type `Password` and `Bytes`. The skip_none class
+    attribute controls the display of None valued attributes.
 
 *content_navigation_components(self, context)*
     This method is applicable only for entity type implementing the interface
@@ -81,15 +90,15 @@
     entities of this type, either at the top or at the bottom of the page
     given the context (navcontent{top|bottom}).
 
-*render_entity_relations(self, entity, siderelations)*
+*render_entity_relations(self, entity)*
     Renders all the relations of the entity in the main section of the page.
 
-*render_side_related(self, entity, siderelations)*
+*render_side_boxes(self, entity, boxes)*
     Renders all the relations of the entity in a side box. This is equivalent
     to *render_entity_relations* in addition to render the relations
     in a box.
 
-Also, please note that by setting the following attributes in you class,
+Also, please note that by setting the following attributes in your class,
 you can already customize some of the rendering:
 
 *show_attr_label*
--- a/doc/book/en/intro/concepts/index.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/intro/concepts/index.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -59,15 +59,15 @@
 
 .. image:: ../../images/archi_globale.en.png
 
-The command ``cubicweb-ctl list`` displays the list of instances installed on
-your system.
+The command ``cubicweb-ctl list`` also displays the list of instances
+installed on your system.
 
 On a Unix system, the instances are usually stored in the directory
 :file:`/etc/cubicweb.d/`. During development, the :file:`~/etc/cubicweb.d/`
 directory is looked up, as well as the paths in :envvar:`CW_INSTANCES_DIR`
 environment variable.
 
-The term application is used to refer at "something that should do something as a
+The term application is used to refer to "something that should do something as a
 whole", eg more like a project and so can refer to an instance or to a cube,
 depending on the context. This book will try to use *application*, *cube* and
 *instance* as appropriate.
@@ -77,7 +77,7 @@
 
 The data repository [#]_ provides access to one or more data sources (including
 SQL databases, LDAP repositories, Mercurial or Subversion version control
-systems, other CubicWeb repositories, GAE's DataStore, etc).
+systems, other CubicWeb instance repositories, GAE's DataStore, etc).
 
 All interactions with the repository are done using the Relation Query Language
 (RQL). The repository federates the data sources and hides them from the
@@ -99,13 +99,13 @@
 Web Engine
 ----------
 
-The web engine replies to http requests and runs the user interface and most of
-the application logic.
+The web engine replies to http requests and runs the user interface
+and most of the application logic.
 
-By default the web engine provides a generated user interface based on the data
-model of the instance. Entities can be created, displayed, updated and
-deleted. As the default user interface is not very fancy, it is usually
-necessary to develop your own.
+By default the web engine provides a default user interface based on
+the data model of the instance. Entities can be created, displayed,
+updated and deleted. As the default user interface is not very fancy,
+it is usually necessary to develop your own.
 
 Schema (Data Model)
 -------------------
@@ -120,7 +120,7 @@
 `Date`, `Time`, `Datetime`, `Interval`, `Password`, `Bytes`, `RichString`. See
 :ref:`yams.BASE_TYPES` for details.
 
-A `relation type` is used to define a binary oriented relation between two
+A `relation type` is used to define an oriented binary relation between two
 entity types.  The left-hand part of a relation is named the `subject` and the
 right-hand part is named the `object`.
 
@@ -143,8 +143,6 @@
 Registries and Objects
 ----------------------
 
-XXX registry, register, registries, registers ???
-
 Application objects
 ~~~~~~~~~~~~~~~~~~~
 
@@ -159,15 +157,15 @@
 
   object's `__registry__` : object's `id` : [list of app objects]
 
-The base class of appobjects is `AppRsetObject` (module `cubicweb.appobject`).
+The base class of appobjects is `AppObject` (module `cubicweb.appobject`).
 
 The `vregistry`
 ~~~~~~~~~~~~~~~
 
-At startup, the `registry` or registers base, inspects a number of directories
-looking for compatible classes definition. After a recording process, the objects
-are assigned to registers so that they can be selected dynamically while the
-instance is running.
+At startup, the `registry` inspects a number of directories looking
+for compatible classes definition. After a recording process, the
+objects are assigned to registers so that they can be selected
+dynamically while the instance is running.
 
 Selectors
 ~~~~~~~~~
@@ -185,7 +183,7 @@
   that case, the object with the greatest score is selected. There should always
   be a single appobject with a greater score than others.
 
-* get all appobjects applying to a context by specifying a registry.In
+* get all appobjects applying to a context by specifying a registry. In
   that case, every object with the a postive score is selected.
 
 * get the object within a particular registry/identifier. In that case no
@@ -195,10 +193,9 @@
 Selector sets are the glue that tie views to the data model. Using them
 appropriately is an essential part of the construction of well behaved cubes.
 
-
 When no score is higher than the others, an exception is raised in development
 mode to let you know that the engine was not able to identify the view to
-apply. This error is silented in production mode and one of the objects with the
+apply. This error is silenced in production mode and one of the objects with the
 higher score is picked.
 
 If no object has a positive score, ``NoSelectableObject`` exception is raised.
@@ -231,7 +228,14 @@
 Result set
 ~~~~~~~~~~
 
-XXX feed me
+Every request made (using RQL) to the data repository returns an
+object we call a Result Set. It enables easy use of the retrieved
+data, providing a translation layer between the backend's native
+datatypes and *CubicWeb* schema's EntityTypes.
+
+Result sets provide access to the raw data, yielding either basic
+Python data types, or schema-defined high-level entities, in a
+straightforward way.
 
 
 Views
@@ -239,6 +243,10 @@
 
 ** *CubicWeb* is data driven **
 
+The view system is loosely coupled to data through a selection
+system. Views are, in essence, defined by an id, a selection predicate
+and an entry point (generaly producing html).
+
 XXX feed me.
 
 
@@ -246,4 +254,31 @@
 -----
 ** *CubicWeb* provides an extensible data repository **
 
-XXX feed me.
+The data model defined using Yams types allows to express the data
+model in a comfortable way. However several aspects of the data model
+can not be expressed there. For instance:
+
+* managing computed attributes
+
+* enforcing complicated structural invariants
+
+* real-world side-effects linked to data events (email notification
+  being a prime example)
+
+The hook system is much like the triggers of an SQL database engine,
+except that:
+
+* it is not limited to one specific SQL backend (every one of them
+  having an idiomatic way to encode triggers), nor to SQL backends at
+  all (think about LDAP or a Subversion repository)
+
+* it is well-coupled to the rest of the framework
+
+Hooks are basically functions that dispatch on both:
+
+* events : after/before add/update/delete on entities/relations
+
+* entity or relation types
+
+They are an essential building block of any moderately complicated
+cubicweb application.
--- a/doc/book/en/intro/tutorial/create-cube.rst	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/en/intro/tutorial/create-cube.rst	Tue Sep 22 13:08:42 2009 +0200
@@ -3,12 +3,12 @@
 Create your cube
 ----------------
 
-The packages ``cubicweb`` and ``cubicweb-dev`` installs a command line tool
-for *CubicWeb* called ``cubicweb-ctl``. This tool provides a wide range of
-commands described in details in :ref:`cubicweb-ctl`.
+The packages ``cubicweb`` and ``cubicweb-dev`` install a command line
+tool for *CubicWeb* called ``cubicweb-ctl``. This tool provides a wide
+range of commands described in details in :ref:`cubicweb-ctl`.
 
-Once your *CubicWeb* development environment is set up, you can create a new
-cube::
+Once your *CubicWeb* development environment is set up, you can create
+a new cube::
 
   cubicweb-ctl newcube blog
 
@@ -40,15 +40,15 @@
 
 
 A Blog has a title and a description. The title is a string that is
-required by the class EntityType and must be less than 50 characters.
-The description is a string that is not constrained.
+required and must be less than 50 characters.  The
+description is a string that is not constrained.
 
 A BlogEntry has a title, a publish_date and a content. The title is a
 string that is required and must be less than 100 characters. The
 publish_date is a Date with a default value of TODAY, meaning that
 when a BlogEntry is created, its publish_date will be the current day
 unless it is modified. The content is a string that will be indexed in
-the full-text index and has no constraint.
+the database full-text index and has no constraint.
 
 A BlogEntry also has a relationship ``entry_of`` that links it to a
 Blog. The cardinality ``?*`` means that a BlogEntry can be part of
@@ -172,9 +172,9 @@
 Define your entity views
 ------------------------
 
-Each entity defined in a model inherits default views allowing
-different rendering of the data. You can redefine each of them
-according to your needs and preferences. So let's see how the
+Each entity defined in a model is associated with default views
+allowing different rendering of the data. You can redefine each of
+them according to your needs and preferences. So let's see how the
 views are defined.
 
 
@@ -183,72 +183,74 @@
 
 A view is defined by a Python class which includes:
 
-  - an identifier (all objects in *CubicWeb* are entered in a registry
-    and this identifier will be used as a key)
+  - an identifier (all objects in *CubicWeb* are recorded in a
+    registry and this identifier will be used as a key)
 
   - a filter to select the result sets it can be applied to
 
-A view has a set of methods complying
-with the `View` class interface (`cubicweb.common.view`).
+A view has a set of methods complying with the `View` class interface
+(`cubicweb.common.view`).
 
 *CubicWeb* provides a lot of standard views for the type `EntityView`;
 for a complete list, read the code in directory ``cubicweb/web/views/``.
 
-A view is applied on a `result set` which contains a set of
-entities we are trying to display. *CubicWeb* uses a selector
-mechanism which computes for each available view a score:
-the view with the highest score is then used to display the given `result set`.
-The standard library of selectors is in
-``cubicweb.common.selector`` and a library of methods used to
-compute scores is available in ``cubicweb.vregistry.vreq``.
+A view is applied on a `result set` which contains a set of entities
+we are trying to display. *CubicWeb* uses a selector mechanism which
+computes for each available view a score: the view with the highest
+score is then used to display the given `result set`.  The standard
+library of selectors is in ``cubicweb.selector``.
 
 It is possible to define multiple views for the same identifier
 and to associate selectors and filters to allow the application
-to find the best way to render the data.
+to find the most appropriate way to render the data.
 
-For example, the view named ``primary`` is the one used to display
-a single entity. We will now show you how to customize this view.
+For example, the view named ``primary`` is the one used to display a
+single entity. We will now show you how to create a primary view for
+BlogEntry.
 
 
-View customization
-~~~~~~~~~~~~~~~~~~
+Primary view customization
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you wish to modify the way a `BlogEntry` is rendered, you will have
+to subclass the `primary` view, for instance in the module ``views``
+of the cube ``cubes/blog/views.py``.
+
+The standard primary view is the most sophisticated view of all. It
+has more than a call() method. It is a template. Actually the entry
+point calls the following sequence of (redefinable) methods:
+
+ * render_entity_title
 
-If you wish to modify the way a `BlogEntry` is rendered, you will have to
-overwrite the `primary` view defined in the module ``views`` of the cube
-``cubes/blog/views.py``.
+ * render_entity_metadata
+
+ * render_entity_attributes
+
+ * render_entity_relations
 
-We can for example add in front of the publication date a prefix specifying
-that the date we see is the publication date.
+ * render_side_boxes
+
+Excepted side boxes, we can see all of them already in action in the
+blog entry view. This is all described in more details in
+:ref:`primary`.
+
+We can for example add in front of the publication date a prefix
+specifying that the date we see is the publication date.
 
 To do so, please apply the following changes:
 
 .. sourcecode:: python
 
-  from cubicweb.web.views import baseviews
-
-
-  class BlogEntryPrimaryView(baseviews.PrimaryView):
-
-    accepts = ('BlogEntry',)
-
-    def render_entity_title(self, entity):
-        self.w(u'<h1>%s</h1>' % html_escape(entity.dc_title()))
-
-    def content_format(self, entity):
-        return entity.view('reledit', rtype='content_format')
+  from cubicweb.selectors import implements
+  from cubicweb.web.views import primary
 
-    def cell_call(self, row, col):
-        entity = self.entity(row, col)
+  class BlogEntryPrimaryView(primary.PrimaryView):
+    __select__ = implements('BlogEntry')
 
-        # display entity attributes with prefixes
-        self.w(u'<h1>%s</h1>' % entity.title)
-        self.w(u'<p>published on %s</p>' % entity.publish_date.strftime('%Y-%m-%d'))
-        self.w(u'<p>%s</p>' % entity.content)
-
-        # display relations
-        siderelations = []
-        if self.main_related_section:
-            self.render_entity_relations(entity, siderelations)
+      def render_entity_attributes(self, entity):
+          self.w(u'<p>published on %s</p>' %
+                 entity.publish_date.strftime('%Y-%m-%d'))
+          super(BlogEntryPrimaryView, self).render_entity_attributes(entity)
 
 .. note::
   When a view is modified, it is not required to restart the instance
--- a/doc/book/fr/01-introduction.fr.txt	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/fr/01-introduction.fr.txt	Tue Sep 22 13:08:42 2009 +0200
@@ -6,13 +6,13 @@
 ===========================
 
 `CubicWeb` nous permet de développer des instances d'applications web
-basées sur un ou plusieurs `cube`. 
+basées sur un ou plusieurs `cube`.
 
-Ce à quoi nous réferrons en parlant de `cube` est un modèle définissant 
-des types de données et des vues. Un `cube` est un composant re-utilisable 
+Ce à quoi nous réferrons en parlant de `cube` est un modèle définissant
+des types de données et des vues. Un `cube` est un composant re-utilisable
 regroupé avec d'autres cubes sur le système de fichiers.
 
-Un `instance` réferre à une installation spécifique d'un ou plusieurs cubes 
+Un `instance` réferre à une installation spécifique d'un ou plusieurs cubes
 où sont regroupés tous les fichiers de configuration de l'application web finale.
 
 Dans ce document, nous allons vous montrer comment créer un cube et l'utiliser
@@ -32,7 +32,7 @@
   |
   |-- data/
   |   |-- cubes.blog.css
-  |   |-- cubes.blog.js  
+  |   |-- cubes.blog.js
   |   |-- external_resources
   |
   |-- debian/
@@ -77,7 +77,7 @@
   |-- views.py
 
 Toute modification apportée à votre modele de données devra
-etre effectué dans ce répertoire. 
+etre effectué dans ce répertoire.
 
 
 
@@ -102,17 +102,17 @@
     title = String(required=True, fulltextindexed=True, maxsize=256)
     publish_date = Date(default='TODAY')
     content = String(required=True, fulltextindexed=True)
-    entry_of = SubjectRelation('Blog', cardinality='?*') 
+    entry_of = SubjectRelation('Blog', cardinality='?*')
 
-Un ``Blog`` a un titre et une description. Le titre est une chaîne 
+Un ``Blog`` a un titre et une description. Le titre est une chaîne
 de caractères requise par la classe parente EntityType et ne doit
-pas excéder 50 caractères. La description est une chaîne de 
+pas excéder 50 caractères. La description est une chaîne de
 caractères sans contraintes.
 
 Une ``BlogEntry`` a un titre, une date de publication et du texte
-étant son contenu. Le titre est une chaîne de caractères qui ne 
+étant son contenu. Le titre est une chaîne de caractères qui ne
 doit pas excéder 100 caractères. La date de publication est de type Date et a
-pour valeur par défaut TODAY, ce qui signifie que lorsqu'une 
+pour valeur par défaut TODAY, ce qui signifie que lorsqu'une
 ``BlogEntry`` sera créée, sa date de publication sera la date
 courante a moins de modifier ce champ. Le texte est une chaîne de
 caractères qui sera indexée en plein texte et sans contraintes.
@@ -121,7 +121,7 @@
 relie à un ``Blog``. La cardinalité ``?*`` signifie que BlogEntry
 peut faire partie de zero a un Blog (``?`` signifie `zero ou un`) et
 qu'un Blog peut avoir une infinité de BlogEntry (``*`` signifie
-`n'importe quel nombre incluant zero`). 
+`n'importe quel nombre incluant zero`).
 Par soucis de complétude, nous rappellerons que ``+`` signifie
 `un ou plus`.
 
@@ -130,7 +130,7 @@
 --------------------
 
 ::
-  
+
   cubicweb-ctl create blog blogdemo
 
 Cette commande va créer un répertoire ``~/etc/cubicweb.d/blogdemo``
@@ -150,7 +150,7 @@
 Vous pouvez à présent accéder  à votre application web vous permettant de
 créer des blogs et d'y poster des messages en visitant l'URL http://localhost:8080/.
 Un premier formulaire d'authentification va vous être proposé. Par défaut,
-l'application n'autorisera pas d'utilisateur anonyme à accéder a votre 
+l'application n'autorisera pas d'utilisateur anonyme à accéder a votre
 application. Vous devrez donc utiliser l'utilisateur administrateur que
 vous aurez crée lors de l'initialisation de votre base de données via
 ``cubicweb-ctl create``.
@@ -166,7 +166,7 @@
 
 Rappelez-vous que pour le moment, tout a été géré par la plate-forme
 `CubicWeb` et que la seule chose qui a été fournie est le schéma de
-données. 
+données.
 
 Créons des entités
 ------------------
@@ -186,7 +186,7 @@
    :alt: from to create blog
 
 En cliquant sur le logo situé dans le coin gauche de la fenêtre,
-vous allez être redirigé vers la page d'accueil. Ensuite, si vous allez 
+vous allez être redirigé vers la page d'accueil. Ensuite, si vous allez
 sur le lien Blog, vous devriez voir la liste des entités Blog, en particulier
 celui que vous venez juste de créer ``Tech-Blog``.
 
@@ -212,7 +212,7 @@
 un peut de texte avant de ``Valider``. Vous venez d'ajouter un article
 sans avoir précisé à quel Blog il appartenait. Dans la colonne de gauche
 se trouve une boite intitulé ``actions``, cliquez sur le menu ``modifier``.
-Vous êtes de retour sur le formulaire d'édition de l'article que vous 
+Vous êtes de retour sur le formulaire d'édition de l'article que vous
 venez de créer, à ceci près que ce formulaire a maintenant une nouvelle
 section intitulée ``ajouter relation``. Choisissez ``entry_of`` dans ce menu,
 cela va faire apparaitre une deuxième menu déroulant dans lequel vous
@@ -225,7 +225,7 @@
    :alt: editing a blog entry to add a relation to a blog
 
 Validez vos modifications en cliquant sur ``Valider``. L'entité article
-qui est listée contient maintenant un lien vers le Blog auquel il 
+qui est listée contient maintenant un lien vers le Blog auquel il
 appartient, ``MyLife``.
 
 .. image:: images/cbw-detail-one-blogentry.fr.png
@@ -248,7 +248,7 @@
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Une vue est une classe Python qui inclut:
-  
+
   - un identifiant (tous les objets dans `CubicWeb` sont listés
     dans un registre et cet identifiant est utilisé comme la clé)
 
@@ -262,32 +262,32 @@
 des entités que nous cherchons à appliquer. `CubicWeb` utilise un
 sélecteur qui permet de calculer un score et d'identifier la vue
 la plus adaptée au `result set` que nous voulons afficher. La librarie
-standard des sélecteurs se trouve dans ``cubicweb.common.selector`` 
-et une librairie des méthodes utilisées pour calculer les scores 
+standard des sélecteurs se trouve dans ``cubicweb.common.selector``
+et une librairie des méthodes utilisées pour calculer les scores
 est dans ``cubicweb.vregistry.vreq``.
 
 
 Il est possible de définir plusieurs vues ayant le meme identifiant
-et d'y associer des sélecteurs et des filtres afin de permettre à 
-l'application de s'adapter au mieux aux données que nous avons 
+et d'y associer des sélecteurs et des filtres afin de permettre à
+l'application de s'adapter au mieux aux données que nous avons
 besoin d'afficher. Nous verrons cela plus en détails dans :ref:`DefinitionVues`.
 
 On peut citer l'exemple de la vue nommée ``primary`` qui est celle utilisée
-pour visualiser une entité seule. Nous allons vous montrer comment modifier 
+pour visualiser une entité seule. Nous allons vous montrer comment modifier
 cette vue.
 
 Modification des vues
 ~~~~~~~~~~~~~~~~~~~~~
 Si vous souhaitez modifier la manière dont est rendue un article (`Blogentry`),
-vous devez surcharger la vue ``primary`` définie dans le module ``views`` de 
+vous devez surcharger la vue ``primary`` définie dans le module ``views`` de
 votre cube, ``cubes/blog/views.py``.
 
 Nous pourrions par exemple ajouter devant la date de publication un préfixe
 indiquant que la date visualisée est la date de publication.
 
-Pour cela appliquez les modifications suivantes: 
+Pour cela appliquez les modifications suivantes:
 
-:: 
+::
 
   from cubicweb.web.views import baseviews
 
@@ -303,13 +303,13 @@
         return entity.view('reledit', rtype='content_format')
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
 
         # display entity attributes with prefixes
         self.w(u'<h1>%s</h1>' % entity.title)
         self.w(u'<p>published on %s</p>' % entity.publish_date.strftime('%Y-%m-%d'))
         self.w(u'<p>%s</p>' % entity.content)
-        
+
         # display relations
         siderelations = []
         if self.main_related_section:
@@ -329,10 +329,10 @@
 
 
 
-Le code que nous avons modifié définit une vue primaire pour une entité de 
-type `BlogEntry`. 
+Le code que nous avons modifié définit une vue primaire pour une entité de
+type `BlogEntry`.
 
-Etant donné que les vues sont appliquées sur des `result sets` et que 
+Etant donné que les vues sont appliquées sur des `result sets` et que
 les `result sets` peuvent être des tableaux de données, il est indispensable
 de récupérer l'entité selon ses coordonnées (row,col).
 
--- a/doc/book/fr/05-define-views.fr.txt	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/fr/05-define-views.fr.txt	Tue Sep 22 13:08:42 2009 +0200
@@ -20,17 +20,17 @@
 * `cell_call(row, col, **kwargs)`, appelle la vue pour une cellule donnée d'un
   result set
 * `url()`, retourne l'url permettant d'obtenir cette vue avec le result set en
-  cours 
+  cours
 * `view(__vid, rset, __fallback_vid=None, **kwargs)`, appelle la vue
   d'identificant `__vid` sur le result set donné. Il est possible de données un
   identificant de vue de "fallback" qui sera utilisé si la vue demandée n'est
   pas applicable au result set
-  
+
 * `wview(__vid, rset, __fallback_vid=None, **kwargs)`, pareil que `view` mais
   passe automatiquement le flux en argument
-  
+
 * `html_headers()`, retourne une liste d'en-tête HTML à placer par le template
-  principal 
+  principal
 
 * `page_title()`, retourne le titre à utiliser dans l'en tête HTML `title`
 
@@ -55,7 +55,7 @@
 
 [FROM-LAX-BOOK]
 
-Tip: when modifying views, you do not need to restart the local 
+Tip: when modifying views, you do not need to restart the local
 server. Just save the file in your editor and reload the page in your
 browser to see the changes.
 
@@ -63,7 +63,7 @@
 
 - an identifier (all objects in `LAX` are entered in a registry
   and this identifier will be used as a key)
-  
+
 - a filter to select the resulsets it can be applied to
 
 `LAX` provides a lot of standard views, for a complete list, you
@@ -83,14 +83,14 @@
   05.     accepts = ('BlogEntry',)
   06.
   07.     def cell_call(self, row, col):
-  08.         entity = self.entity(row, col)
+  08.         entity = self.rset.get_entity(row, col)
   09.         self.w(u'<h1>%s</h1>' % entity.title)
   10.         self.w(u'<p>published on %s in category %s</p>' % \
   11.                (entity.publish_date.strftime('%Y-%m-%d'), entity.category))
   12.         self.w(u'<p>%s</p>' % entity.text)
 
 The above source code defines a new primary view (`line 03`) for
-``BlogEntry`` (`line 05`). 
+``BlogEntry`` (`line 05`).
 
 Since views are applied to resultsets and resulsets can be tables of
 data, it is needed to recover the entity from its (row,col)
@@ -108,11 +108,11 @@
 Let us now improve the primary view of a blog ::
 
   01. class BlogPrimaryView(baseviews.PrimaryView):
-  02. 
+  02.
   03.     accepts = ('Blog',)
   04.
   05.     def cell_call(self, row, col):
-  06.         entity = self.entity(row, col)
+  06.         entity = self.rset.get_entity(row, col)
   07.         self.w(u'<h1>%s</h1>' % entity.title)
   08.         self.w(u'<p>%s</p>' % entity.description)
   09.         rset = self.req.execute('Any E WHERE E entry_of B, B eid "%s"' % entity.eid)
@@ -127,9 +127,9 @@
 about the schema and infer that such entities have to be of the
 ``BlogEntry`` kind and retrieves them.
 
-The request returns a selection of data called a resultset. At 
+The request returns a selection of data called a resultset. At
 `line 10` the view 'primary' is applied to this resultset to output
-HTML. 
+HTML.
 
 **This is to be compared to interfaces and protocols in object-oriented
 languages. Applying a given view to all the entities of a resultset only
@@ -159,7 +159,7 @@
 
 * create view "blogentry table" with title, publish_date, category
 
-We will show that by default the view that displays 
+We will show that by default the view that displays
 "Any E,D,C WHERE E publish_date D, E category C" is the table view.
 Of course, the same can be obtained by calling
 self.wview('table',rset)
@@ -244,4 +244,4 @@
 ----------------------------------
 Certains navigateurs (dont firefox) n'aime pas les `<div>` vides (par vide
 j'entend sans contenu dans la balise, il peut y avoir des attributs), faut
-toujours mettre `<div></div>` même s'il n'y a rien dedans, et non `<div/>`. 
+toujours mettre `<div></div>` même s'il n'y a rien dedans, et non `<div/>`.
--- a/doc/book/fr/06-define-workflows.fr.txt	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/fr/06-define-workflows.fr.txt	Tue Sep 22 13:08:42 2009 +0200
@@ -2,11 +2,92 @@
 
 Définition de workflow
 ======================
-On peut mettre une condition rql ou/et un groupe auquel doit appartenir l'utilisateur.
+
+Avant-propos
+------------
+
+Un worflow décrit comment les entités vont être utilisés à travers différents états. Nous avons donc pour un workflow donné un ensemble d'états, un "graphe de transition" c'est-à-dire la liste des transitions possibles d'un état à un autre.
+
+Nous allons définir ici un simple workflow pour l'exemple du blog avec seulement deux états: `en attente` et `publié`. Il est nécessaire d'avoir préalablement créé une application simple *CubicWeb* en dix minutes (voir :ref:`BlogFiveMinutes`).
+
+Mise en place du workflow
+-------------------------
+
+Nous allons créer un workflow pour contrôler la qualité des BlogEntry soumis à l'instance. Lorsque un BlogEntry est créé par un utilisateur, son état doit être `en attente`. Pour être visible par tous, il doit être ensuite mis à l'état `publié`. Pour le changement d'état d'`en attente` à `publié`, nous avons besoin d'une transition que nous appellerons `approuve_blogentry`.
+
+Un état BlogEntry ne doit pas pouvoir être modifiable par les utilisateurs. Nous allons donc créé un groupe de modération `moderateurs` et ce groupe aura les permissions idoines pour publier un BlogEntry.
+
+Il existe deux manières de créer un workflow: depuis l'interface utilisateur ou en le définissant dans le fichier ``migration/postcreate.py``.
+Ce script est exécuté à chaque lancement de la commande ``cubicweb-ctl db-init``.
+Nous encourageons vivement la création dans ``migration/postcreate.py`` que nous allons vous montrer ici. Lire `Sous le capot`_ pour en comprendre les raisons.
+
+L'état d'une entité est sauvegardé par l'attribut `in_state` qui peut être ajouté à votre schéma d'entité par deux façons:
+
+* héritage direct en utilisant la classe `cubicweb.schema.WorkflowableEntityType`
+* par délégation en utilisant `cubicweb.schema.make_worflowable` (utilisable comme un décorateur également)
+
+Pour notre exemple de BlogEntry, nous devons avoir:
+
+.. sourcecode:: python
+
+  from cubicweb.schema import WorkflowableEntityType
+
+  class BlogEntry(EntityType, WorkflowableEntityType):
+      ...
+
+
+Création des états, transitions et les permissions de groupe
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Le script ``postcreate.py`` est exécuté dans un environnement spécial où plusieurs primitives *CubicWeb* peuvent être utilsées.
+Elles sont toutes définies dans ``class ServerMigrationHelper``.
+Nous allons maintenant voir dans le prochain example celles utilisées pour créer un workflow.
+
+Pour définir notre workflow pour BlogDemo, veuillez ajouter les lignes suivantes au script ``migration/postcreate.py``:
 
-Si on met à la fois un(ou plusieurs) groupe et une condition RQL, il faut que les deux soient respectés.
+.. sourcecode:: python
+
+  _ = unicode
+
+  moderators = add_entity('CWGroup', name=u"modérateurs")
+
+Cela va ajouter le groupe utilisateur `moderators`.
+
+.. sourcecode:: python
+
+  wf = add_workflow(u'une description succincte de votre workflow', 'BlogEntry')
+
+Ceci va premièrement instancier un nouvel objet workflow avec une description sommaire mais pertinente et le type d'entité concerné (un tuple pour être utilisé pour des valeurs multiples).
+
+.. sourcecode:: python
+
+  submitted = wf.add_state(_('submitted'), initial=True)
+  published = wf.add_state(_('published'))
+
+``add_state`` attend comme premier argument le nom de l'état que vous voulez créer et un argument optionnel pour signifier si c'est l'état initial supposé pour ce type d'entité.
 
-Si on met plusieurs groupes, il faut que l'utilisateur soit dans un des groupes.
+.. sourcecode:: python
+
+  wf.add_transition(_('approuve_blogentry'), (submitted,), published, ('moderators', 'managers'),)
+
+``add_transition`` attend:
+
+  * comme premier argument le nom de la transition
+  * ensuite la liste des états pour lesquels les transitions peuvent être tirées,
+  * l'état attendu en fin de transition,
+  * et les permissions
+    (c'est-à-dire la liste des goupes utilisateurs qui peuvnet appliquer la transition; l'utilisateur devant appartenir à l'un des groupes listés pour être autoriser à exécuter l'action).
+
+.. sourcecode:: python
+
+
+  checkpoint()
+
+.. note::
+  Dans le script de création d'un workflow, penser à mettre `_()` autour des noms d'états et de transitions pour que ceux si soient pris en compte par les scripts de gestion des catalogues i18n.
+
+En complément de condition sur des groupes utilisateur dont l'utilisateur doit appartenir à l'in d'entre eux, vous pouvez utiliser une RQL condition.
+Dans ce cas, l'utilisateur peut seulement exécuter une action si les deux conditions sont satisfaites.
 
 Pour la condition RQL sur une transition, on peut y mettre les substitutions suivantes :
 
@@ -14,141 +95,35 @@
 * `%(ueid)s`, eid de l'utilisateur qui fait la requête
 * `%(seid)s`, eid de l'état courant de l'objet
 
-Dans le script de création d'un workflow, penser à mettre `_()` autour des noms d'états et de transitions
-pour que ceux si soient pris en compte par les scripts de gestion des catalogues i18n.
-
-General
--------
-
-A workflow can be defined in a `LAX` application thanks to the system 
-entities ``State`` and ``Transition``. Those are defined within all 
-LAX application and can be set-up through the main administrator interface.
-
-Once your schema is defined, you can start creating the set of states and
-the required transitions for your applications entities.
-
-You first need to define the states and then the transitions between those
-to complete your workflow.
+.. image:: ../../images/03-transitions-view.en.png
 
-A ``State`` defines the status of an entity. While creating a new state, 
-you will be first given the option to select the entity type the state
-can be applied to. By choosing ``Apply``, a new section will be displayed
-in the editing screen to enable you to add relation to the state you are
-creating.
-
-A ``Transition`` is also based on an entity type it can be applied to.
-By choosing ``Apply``, a new section will be displayed in the editing 
-screen to enable you to add relation to the transition you are
-creating.
-
-At the transition level you will also define the group of user which can
-aplly this transition to an object.
-
-
-Example of a simple workflow
-----------------------------
-
-Please see the tutorial to view and example of a simple workflow.
+Vous pouvez remarqué que dans la boîte d'action d'un BlogEntry, l'état est maintenant listé ainsi que les possibles transitions définis pour l'état en cours dans le workflow. Les transitions ne sont seulement affichées pour les utilisateurs ayant les bonnes permissions.
+Dans notre exemple, la transition `approuve_blogentry` sera seulement affichée pour les utilisateurs appartenant au groupe `moderators` or `managers`.
 
 
-[Create a simple workflow for BlogDemo, to have a moderator approve new blog 
-entry to be published. This implies, specify a dedicated group of blog
-moderator as well as hide the view of a blog entry to the user until
-it reaches the state published]
-
-Set-up a workflow
------------------
-
-Before starting, make sure you refresh your mind by reading [link to
-definition_workflow chapter].
+Sous le capot
+~~~~~~~~~~~~~
 
-We want to create a workflow to control the quality of the BlogEntry 
-submitted on your application. When a BlogEntry is created by a user
-its state should be `submitted`. To be visible to all, it needs to
-be in the state `published`. To move from `submitted` to `published`
-we need a transition that we can name `approve_blogentry`.
-
-We do not want every user to be allowed to change the state of a 
-BlogEntry. We need to define a group of user, `moderators`, and 
-this group will have appropriate permissions to approve BlogEntry
-to be published and visible to all.
+Un workflow est une collection d'entités de type `State`` et ``Transition`` qui sont des types d'entités standards de *CubicWeb*.
 
-There are two ways to create a workflow, form the user interface,
-and also by defining it in ``migration/postcreate.py``. This script
-is executed each time a new ``./bin/laxctl db-init`` is done. 
-If you create the states and transitions through the user interface
-this means that next time you will need to initialize the database
-you will have to re-create all the entities. 
-We strongly recommand you create the workflow in ``migration\postcreate.py``
-and we will now show you how.
-The user interface would only be a reference for you to view the states 
-and transitions but is not the appropriate interface to define your
-application workflow.
+Par exemple, les lignes précédentes:
 
-Update the schema
-~~~~~~~~~~~~~~~~~
-To enable a BlogEntry to have a State, we have to define a relation
-``in_state`` in the schema of BlogEntry. Please do as follows, add
-the line ``in_state (...)``::
+.. sourcecode:: python
 
-  class BlogEntry(EntityType):
-      title = String(maxsize=100, required=True)
-      publish_date = Date(default='TODAY')
-      text_format = String(meta=True, internationalizable=True, maxsize=50,
-                           default='text/rest', constraints=[format_constraint])
-      text = String(fulltextindexed=True)
-      category = String(vocabulary=('important','business'))
-      entry_of = SubjectRelation('Blog', cardinality='?*')
-      in_state = SubjectRelation('State', cardinality='1*')
+  submitted = wf.add_state(_('en attente'), initial=True)
+  published = wf.add_state(_('publié'))
 
-As you updated the schema, you will have re-execute ``./bin/laxctl db-init``
-to initialize the database and migrate your existing entities.
-[WRITE ABOUT MIGRATION]
-
-Create states, transitions and group permissions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-At the time the ``postcreate.py`` script is executed, several methods
-can be used. They are all defined in the ``class ServerMigrationHelper``.
-We will only discuss the method we use to create a wrokflow here.
+vont créé deux entités de type ``State``, l'une avec le nom 'submitted' et l'autre avec le nom 'published'. Tandis que:
 
-To define our workflow for BlogDemo, please add the following lines
-to ``migration/postcreate.py``::
-  
-  _ = unicode
-
-  moderators      = add_entity('CWGroup', name=u"moderators")
+.. sourcecode:: python
 
-  submitted = add_state(_('submitted'), 'BlogEntry', initial=True)
-  published = add_state(_('published'), 'BlogEntry')
-
-  add_transition(_('approve_blogentry'), 'BlogEntry', (submitted,), published, ('moderators', 'managers'),)
-
-  checkpoint()
+  wf.add_transition(_('approuve_blogentry'), (submitted,), published, ('moderators', 'managers'),)
 
-``add_entity`` is used here to define the new group of users that we
-need to define the transitions, `moderators`.
-If this group required by the transition is not defined before the
-transition is created, it will not create the relation `transition 
-require the group moderator`.
-
-``add_state`` expects as the first argument the name of the state you are
-willing to create, then the entity type on which the state can be applied, 
-and an optionnal argument to set if the state is the initial state
-of the entity type or not.
+va créé une entité de type ``Transition`` avec le nom `approuve_blogentry` qui sera relié aux entités ``State`` créées précédemment.
 
-``add_transition`` expects as the first argument the name of the 
-transition, then the entity type on which we can apply the transition,
-then the list of possible initial states from which the transition
-can be applied, the target state of the transition, and the permissions
-(e.g. list of the groups of users who can apply the transition).
+Dès lors, nous pouvons utiliser l'interface d'administration pour ces opérations. Mais ce n'est pas recommandé à cause de la complexité superflue et du fait que ces changements ne seront locaux qu'à cette instance.
 
-.. image:: images/lax-book.03-transitions-view.fr.png
-
-You can now notice that in the actions box of a BlogEntry, the state
-is now listed as well as the possible transitions from this state
-defined by the workflow. This transition, as defined in the workflow,
-will only being displayed for the users belonging to the group
-moderators of managers.
+En effet, si vous créez les états et les transitions à travers l'interface utilisateur, la prochaine initialisation de la base de données vous oblige à recréer toutes les entités.
+L'interface utilisateur devrait être seulement connu par vous pour la visualisation des états et transitions, mais ce n'est pas celle appropriée pour définir vos workflows applicatifs.
 
 
--- a/doc/book/fr/20-01-intro.fr.txt	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/fr/20-01-intro.fr.txt	Tue Sep 22 13:08:42 2009 +0200
@@ -37,7 +37,7 @@
 données manipulées. La syntaxe de la définition est la même que celle
 proposée par `Google AppEngine` mais il faut remplacer la ligne
 d'import::
-  
+
   from google.appengine.ext import db
 
 par celle-ci::
@@ -48,7 +48,7 @@
 Un exemple de schéma de données pour un ``Blog`` pourrait être::
 
   from ginco.goa import db
-  
+
   class BlogEntry(db.Model):
       # un titre à donner à l'entrée
       title = db.StringProperty(required=True)
@@ -57,8 +57,8 @@
       # le contenu de l'entrée
       content = db.TextProperty()
       # une entrée peut en citer une autre
-      cites = db.SelfReferenceProperty() 
-      
+      cites = db.SelfReferenceProperty()
+
 
 Personnalisation des vues
 -------------------------
@@ -75,7 +75,7 @@
 - un identifiant (tous les objets dans `LAX` sont enregistrés
   dans un registre et cet identifiant sert de clé pour y retrouver
   la vue)
-  
+
 - une description des types de données auxquels elle s'applique
 
 Il existe dans `LAX` des vues prédéfinies et utilisées par le moteur
@@ -87,17 +87,17 @@
 Par exemple, si on souhaite modifier la page principale d'une entrée de
 blog, il faut surcharger la vue ``primary`` des objets ``BlogEntry`` dans
 le fichier ``myapp/views.py``::
-  
+
   from ginco.web.views import baseviews
-  
+
   class BlogEntryPrimaryView(baseviews.PrimaryView):
       accepts = ('BlogEntry',)
-      
+
       def cell_call(self, row, col):
-          entity = self.entity(row, col)
+          entity = self.rset.get_entity(row, col)
           self.w(u'<h1>%s</h1>' % entity.title)
           self.w(u'<div>%s</div>' entity.content)
-    
+
 
 Génération du graphique de schéma
 ---------------------------------
@@ -105,13 +105,13 @@
 Il existe une vue ``schema`` qui permet d'afficher un graphique
 représantant les différents types d'entités définis dans le schéma
 ainsi que les relations entre ces types. Ce graphique doit être généré
-statiquement. Le script à utiliser pour générer ce schéma est 
+statiquement. Le script à utiliser pour générer ce schéma est
 dans ``myapp/tools``. Ce script nécessite d'avoir accès aux
 bibliothèques fournies par le SDK de ``Google AppEngine``. Il faut
 donc modifier son PYTHONPATH::
 
   $ export PYTHONPATH=GAE_ROOT/google:GAE_ROOT/lib/yaml
-  $ python tools/generate_schema_img.py 
+  $ python tools/generate_schema_img.py
 
 
 Génération des fichiers de traduction
--- a/doc/book/fr/20-04-develop-views.fr.txt	Wed Aug 05 09:15:56 2009 +0200
+++ b/doc/book/fr/20-04-develop-views.fr.txt	Tue Sep 22 13:08:42 2009 +0200
@@ -13,7 +13,7 @@
 
 - an identifier (all objects in `LAX` are entered in a registry
   and this identifier will be used as a key)
-  
+
 - a filter to select the resulsets it can be applied to
 
 `LAX` provides a lot of standard views, for a complete list, you
@@ -25,13 +25,13 @@
 override the view ``primary`` in ``BlogDemo/views.py`` ::
 
   from ginco.web.views import baseviews
-  
+
   class BlogEntryPrimaryView(baseviews.PrimaryView):
 
       accepts = ('BlogEntry',)
-      
+
       def cell_call(self, row, col):
-          entity = self.entity(row, col)
+          entity = self.rset.get_entity(row, col)
           self.w(u'<h1>%s</h1>' % entity.title)
           self.w(u'<div>%s</div>' % entity.publish_date)
           self.w(u'<div>%s</div>' % entity.category)
@@ -91,7 +91,7 @@
 
 [WRITE ME]
 
-* show how urls are mapped to selections and views and explain URLRewriting 
+* show how urls are mapped to selections and views and explain URLRewriting
 
 Security
 =========
--- a/entities/__init__.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/entities/__init__.py	Tue Sep 22 13:08:42 2009 +0200
@@ -150,15 +150,15 @@
 
     # edition helper functions ################################################
 
-    def linked_to(self, rtype, target, remove=True):
+    def linked_to(self, rtype, role, remove=True):
         """if entity should be linked to another using __linkto form param for
-        the given relation/target, return eids of related entities
+        the given relation/role, return eids of related entities
 
         This method is consuming matching link-to information from form params
         if `remove` is True (by default).
         """
         try:
-            return self.__linkto[(rtype, target)]
+            return self.__linkto[(rtype, role)]
         except AttributeError:
             self.__linkto = {}
         except KeyError:
@@ -166,15 +166,15 @@
         linktos = list(self.req.list_form_param('__linkto'))
         linkedto = []
         for linkto in linktos[:]:
-            ltrtype, eid, lttarget = linkto.split(':')
-            if rtype == ltrtype and target == lttarget:
+            ltrtype, eid, ltrole = linkto.split(':')
+            if rtype == ltrtype and role == ltrole:
                 # delete __linkto from form param to avoid it being added as
                 # hidden input
                 if remove:
                     linktos.remove(linkto)
                     self.req.form['__linkto'] = linktos
                 linkedto.append(typed_eid(eid))
-        self.__linkto[(rtype, target)] = linkedto
+        self.__linkto[(rtype, role)] = linkedto
         return linkedto
 
     # edit controller callbacks ###############################################
@@ -204,85 +204,6 @@
         """
         return ()
 
-    # XXX deprecates, may be killed once old widgets system is gone ###########
-
-    @classmethod
-    def get_widget(cls, rschema, x='subject'):
-        """return a widget to view or edit a relation
-
-        notice that when the relation support multiple target types, the widget
-        is necessarily the same for all those types
-        """
-        # let ImportError propage if web par isn't available
-        from cubicweb.web.widgets import widget
-        if isinstance(rschema, basestring):
-            rschema = cls.schema.rschema(rschema)
-        if x == 'subject':
-            tschema = rschema.objects(cls.e_schema)[0]
-            wdg = widget(cls.vreg, cls, rschema, tschema, 'subject')
-        else:
-            tschema = rschema.subjects(cls.e_schema)[0]
-            wdg = widget(cls.vreg, tschema, rschema, cls, 'object')
-        return wdg
-
-    @deprecated('use EntityFieldsForm.subject_relation_vocabulary')
-    def subject_relation_vocabulary(self, rtype, limit):
-        form = self.vreg.select('forms', 'edition', self.req, entity=self)
-        return form.subject_relation_vocabulary(rtype, limit)
-
-    @deprecated('use EntityFieldsForm.object_relation_vocabulary')
-    def object_relation_vocabulary(self, rtype, limit):
-        form = self.vreg.select('forms', 'edition', self.req, entity=self)
-        return form.object_relation_vocabulary(rtype, limit)
-
-    @deprecated('use AutomaticEntityForm.[e]relations_by_category')
-    def relations_by_category(self, categories=None, permission=None):
-        from cubicweb.web.views.autoform import AutomaticEntityForm
-        return AutomaticEntityForm.erelations_by_category(self, categories, permission)
-
-    @deprecated('use AutomaticEntityForm.[e]srelations_by_category')
-    def srelations_by_category(self, categories=None, permission=None):
-        from cubicweb.web.views.autoform import AutomaticEntityForm
-        return AutomaticEntityForm.esrelations_by_category(self, categories, permission)
-
-    def attribute_values(self, attrname):
-        if self.has_eid() or attrname in self:
-            try:
-                values = self[attrname]
-            except KeyError:
-                values = getattr(self, attrname)
-            # actual relation return a list of entities
-            if isinstance(values, list):
-                return [v.eid for v in values]
-            return (values,)
-        # the entity is being created, try to find default value for
-        # this attribute
-        try:
-            values = self.req.form[attrname]
-        except KeyError:
-            try:
-                values = self[attrname] # copying
-            except KeyError:
-                values = getattr(self, 'default_%s' % attrname,
-                                 self.e_schema.default(attrname))
-                if callable(values):
-                    values = values()
-        if values is None:
-            values = ()
-        elif not isinstance(values, (list, tuple)):
-            values = (values,)
-        return values
-
-    def use_fckeditor(self, attr):
-        """return True if fckeditor should be used to edit entity's attribute named
-        `attr`, according to user preferences
-        """
-        if self.req.use_fckeditor() and self.e_schema.has_metadata(attr, 'format'):
-            if self.has_eid() or '%s_format' % attr in self:
-                return self.attr_metadata(attr, 'format') == 'text/html'
-            return self.req.property_value('ui.default-text-format') == 'text/html'
-        return False
-
 # XXX:  store a reference to the AnyEntity class since it is hijacked in goa
 #       configuration and we need the actual reference to avoid infinite loops
 #       in mro
--- a/entities/lib.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/entities/lib.py	Tue Sep 22 13:08:42 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
@@ -84,9 +82,24 @@
         return super(EmailAddress, self).after_deletion_path()
 
 
-from logilab.common.deprecation import class_renamed
-Emailaddress = class_renamed('Emailaddress', EmailAddress)
-Emailaddress.id = 'Emailaddress'
+class Bookmark(AnyEntity):
+    """customized class for Bookmark entities"""
+    id = 'Bookmark'
+    fetch_attrs, fetch_order = fetch_config(['title', 'path'])
+
+    def actual_url(self):
+        url = self.req.build_url(self.path)
+        if self.title:
+            urlparts = list(urlsplit(url))
+            if urlparts[3]:
+                urlparts[3] += '&vtitle=%s' % self.req.url_quote(self.title)
+            else:
+                urlparts[3] = 'vtitle=%s' % self.req.url_quote(self.title)
+            url = urlunsplit(urlparts)
+        return url
+
+    def action_url(self):
+        return self.absolute_url() + '/follow'
 
 
 class CWProperty(AnyEntity):
@@ -111,26 +124,6 @@
         return 'view', {}
 
 
-class Bookmark(AnyEntity):
-    """customized class for Bookmark entities"""
-    id = 'Bookmark'
-    fetch_attrs, fetch_order = fetch_config(['title', 'path'])
-
-    def actual_url(self):
-        url = self.req.build_url(self.path)
-        if self.title:
-            urlparts = list(urlsplit(url))
-            if urlparts[3]:
-                urlparts[3] += '&vtitle=%s' % self.req.url_quote(self.title)
-            else:
-                urlparts[3] = 'vtitle=%s' % self.req.url_quote(self.title)
-            url = urlunsplit(urlparts)
-        return url
-
-    def action_url(self):
-        return self.absolute_url() + '/follow'
-
-
 class CWCache(AnyEntity):
     """Cache"""
     id = 'CWCache'
--- a/entities/schemaobjs.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/entities/schemaobjs.py	Tue Sep 22 13:08:42 2009 +0200
@@ -63,14 +63,18 @@
         * raise ValidationError if inlining is'nt possible
         * eventually return True
         """
-        rtype = self.name
-        rschema = self.schema.rschema(rtype)
+        rschema = self.schema.rschema(self.name)
         if inlined == rschema.inlined:
             return False
         if inlined:
-            for (stype, otype) in rschema.iter_rdefs():
-                card = rschema.rproperty(stype, otype, 'cardinality')[0]
+            # don't use the persistent schema, we may miss cardinality changes
+            # in the same transaction
+            for rdef in self.reverse_relation_type:
+                card = rdef.cardinality[0]
                 if not card in '?1':
+                    rtype = self.name
+                    stype = rdef.stype
+                    otype = rdef.otype
                     msg = self.req._("can't set inlined=%(inlined)s, "
                                      "%(stype)s %(rtype)s %(otype)s "
                                      "has cardinality=%(card)s")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/test/data/migration/postcreate.py	Tue Sep 22 13:08:42 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	Wed Aug 05 09:15:56 2009 +0200
+++ b/entities/test/data/schema.py	Tue Sep 22 13:08:42 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	Wed Aug 05 09:15:56 2009 +0200
+++ b/entities/test/unittest_base.py	Tue Sep 22 13:08:42 2009 +0200
@@ -11,16 +11,14 @@
 from logilab.common.decorators import clear_cache
 from logilab.common.interface import implements
 
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 
 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
 
 
-class BaseEntityTC(EnvBasedTC):
+class BaseEntityTC(CubicWebTC):
 
     def setup_database(self):
         self.member = self.create_user('member')
@@ -58,161 +56,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 +86,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 +93,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):
+class InterfaceTC(CubicWebTC):
 
     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,12 +105,16 @@
         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):
+class SpecializedEntityClassesTC(CubicWebTC):
 
     def select_eclass(self, etype):
         # clear selector cache
@@ -295,11 +134,12 @@
                 id = etype
             self.vreg.register_appobject_class(Foo)
             eclass = self.select_eclass('SubDivision')
+            self.failUnless(eclass.__autogenerated__)
+            self.failIf(eclass is Foo)
             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	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,417 @@
+from cubicweb.devtools.testlib import CubicWebTC
+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(CubicWebTC):
+
+    def test_wf_construction(self):
+        wf = add_wf(self, 'Company')
+        foo = wf.add_state(u'foo', initial=True)
+        bar = wf.add_state(u'bar')
+        self.assertEquals(wf.state_by_name('bar').eid, bar.eid)
+        self.assertEquals(wf.state_by_name('barrr'), None)
+        baz = wf.add_transition(u'baz', (foo,), bar, ('managers',))
+        self.assertEquals(wf.transition_by_name('baz').eid, baz.eid)
+        self.assertEquals(len(baz.require_group), 1)
+        self.assertEquals(baz.require_group[0].name, 'managers')
+
+    def test_duplicated_state(self):
+        wf = add_wf(self, 'Company')
+        wf.add_state(u'foo', initial=True)
+        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(CubicWebTC):
+
+    def setup_database(self):
+        rschema = self.schema['in_state']
+        for x, y in rschema.iter_rdefs():
+            self.assertEquals(rschema.rproperty(x, y, 'cardinality'), '1*')
+        self.member = self.create_user('member')
+
+    def test_workflow_base(self):
+        e = self.create_user('toto')
+        self.assertEquals(e.state, 'activated')
+        e.change_state('deactivated', u'deactivate 1')
+        self.commit()
+        e.change_state('activated', u'activate 1')
+        self.commit()
+        e.change_state('deactivated', u'deactivate 2')
+        self.commit()
+        e.clear_related_cache('wf_info_for', 'object')
+        self.assertEquals([tr.comment for tr in e.reverse_wf_info_for],
+                          ['deactivate 1', 'activate 1', 'deactivate 2'])
+        self.assertEquals(e.latest_trinfo().comment, 'deactivate 2')
+
+    def test_possible_transitions(self):
+        user = self.entity('CWUser X')
+        trs = list(user.possible_transitions())
+        self.assertEquals(len(trs), 1)
+        self.assertEquals(trs[0].name, u'deactivate')
+        self.assertEquals(trs[0].destination().name, u'deactivated')
+        # test a std user get no possible transition
+        cnx = self.login('member')
+        # fetch the entity using the new session
+        trs = list(cnx.user().possible_transitions())
+        self.assertEquals(len(trs), 0)
+
+    def _test_manager_deactivate(self, user):
+        user.clear_related_cache('in_state', 'subject')
+        self.assertEquals(len(user.in_state), 1)
+        self.assertEquals(user.state, 'deactivated')
+        trinfo = user.latest_trinfo()
+        self.assertEquals(trinfo.previous_state.name, 'activated')
+        self.assertEquals(trinfo.new_state.name, 'deactivated')
+        self.assertEquals(trinfo.comment, 'deactivate user')
+        self.assertEquals(trinfo.comment_format, 'text/plain')
+        return trinfo
+
+    def test_change_state(self):
+        user = self.user()
+        user.change_state('deactivated', comment=u'deactivate user')
+        trinfo = self._test_manager_deactivate(user)
+        self.assertEquals(trinfo.transition, None)
+
+    def test_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(CubicWebTC):
+
+    def setup_database(self):
+        self.member = self.create_user('member')
+
+    def test_custom_wf_replace_state_no_history(self):
+        """member in inital state with no previous history, state is simply
+        redirected when changing workflow
+        """
+        wf = add_wf(self, 'CWUser')
+        wf.add_state('asleep', initial=True)
+        self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
+                     {'wf': wf.eid, 'x': self.member.eid})
+        self.member.clear_all_caches()
+        self.assertEquals(self.member.state, 'activated')# no change before commit
+        self.commit()
+        self.member.clear_all_caches()
+        self.assertEquals(self.member.current_workflow.eid, wf.eid)
+        self.assertEquals(self.member.state, 'asleep')
+        self.assertEquals(self.member.workflow_history, [])
+
+    def test_custom_wf_replace_state_keep_history(self):
+        """member in inital state with some history, state is redirected and
+        state change is recorded to history
+        """
+        self.member.fire_transition('deactivate')
+        self.member.fire_transition('activate')
+        wf = add_wf(self, 'CWUser')
+        wf.add_state('asleep', initial=True)
+        self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
+                     {'wf': wf.eid, 'x': self.member.eid})
+        self.commit()
+        self.member.clear_all_caches()
+        self.assertEquals(self.member.current_workflow.eid, wf.eid)
+        self.assertEquals(self.member.state, 'asleep')
+        self.assertEquals(parse_hist(self.member.workflow_history),
+                          [('activated', 'deactivated', 'deactivate', None),
+                           ('deactivated', 'activated', 'activate', None),
+                           ('activated', 'asleep', None, 'workflow changed to "CWUser"')])
+
+    def test_custom_wf_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"'),])
+
+
+class WorkflowHooksTC(CubicWebTC):
+
+    def setUp(self):
+        CubicWebTC.setUp(self)
+        self.wf = self.session.user.current_workflow
+        self.session.set_pool()
+        self.s_activated = self.wf.state_by_name('activated').eid
+        self.s_deactivated = self.wf.state_by_name('deactivated').eid
+        self.s_dummy = self.wf.add_state(u'dummy').eid
+        self.wf.add_transition(u'dummy', (self.s_deactivated,), self.s_dummy)
+        ueid = self.create_user('stduser', commit=False).eid
+        # test initial state is set
+        rset = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s',
+                            {'x' : ueid})
+        self.failIf(rset, rset.rows)
+        self.commit()
+        initialstate = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s',
+                                    {'x' : ueid})[0][0]
+        self.assertEquals(initialstate, u'activated')
+        # give access to users group on the user's wf transitions
+        # so we can test wf enforcing on euser (managers don't have anymore this
+        # enforcement
+        self.execute('SET X require_group G '
+                     'WHERE G name "users", X transition_of WF, WF eid %(wf)s',
+                     {'wf': self.wf.eid})
+        self.commit()
+
+    # XXX currently, we've to rely on hooks to set initial state, or to use unsafe_execute
+    # def test_initial_state(self):
+    #     cnx = self.login('stduser')
+    #     cu = cnx.cursor()
+    #     self.assertRaises(ValidationError, cu.execute,
+    #                       'INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, '
+    #                       'X in_state S WHERE S name "deactivated"', {'pwd': 'oops'})
+    #     cnx.close()
+    #     # though managers can do whatever he want
+    #     self.execute('INSERT CWUser X: X login "badaboum", X upassword %(pwd)s, '
+    #                  'X in_state S, X in_group G WHERE S name "deactivated", G name "users"', {'pwd': 'oops'})
+    #     self.commit()
+
+    # test that the workflow is correctly enforced
+    def test_transition_checking1(self):
+        cnx = self.login('stduser')
+        user = cnx.user(self.session)
+        ex = self.assertRaises(ValidationError,
+                               user.fire_transition, 'activate')
+        self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"})
+        cnx.close()
+
+    def test_transition_checking2(self):
+        cnx = self.login('stduser')
+        user = cnx.user(self.session)
+        ex = self.assertRaises(ValidationError,
+                               user.fire_transition, 'dummy')
+        self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"})
+        cnx.close()
+
+    def test_transition_checking3(self):
+        cnx = self.login('stduser')
+        session = self.session
+        user = cnx.user(session)
+        user.fire_transition('deactivate')
+        cnx.commit()
+        session.set_pool()
+        ex = self.assertRaises(ValidationError,
+                               user.fire_transition, 'deactivate')
+        self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"})
+        # get back now
+        user.fire_transition('activate')
+        cnx.commit()
+        cnx.close()
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/entities/wfobjs.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/entities/wfobjs.py	Tue Sep 22 13:08:42 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,46 +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):
-        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()
 
 
@@ -94,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
@@ -110,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/entity.py	Tue Sep 22 13:08:42 2009 +0200
@@ -15,13 +15,15 @@
 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.schema import RQLVocabularyConstraint, RQLConstraint, bw_normalize_etype
+from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint
+from cubicweb.rqlrewrite import RQLRewriter
 
 from cubicweb.common.uilib import printable_value, soup2xhtml
 from cubicweb.common.mixins import MI_REL_TRIGGERS
@@ -38,100 +40,6 @@
     return '1'
 
 
-_MODE_TAGS = set(('link', 'create'))
-_CATEGORY_TAGS = set(('primary', 'secondary', 'generic', 'generated')) # , 'metadata'))
-
-try:
-    from cubicweb.web import formwidgets, uicfg
-
-    def _dispatch_rtags(tags, rtype, role, stype, otype):
-        for tag in tags:
-            if tag in _MODE_TAGS:
-                uicfg.actionbox_appearsin_addmenu.tag_relation(
-                    (stype, rtype, otype, role), tag == 'create')
-            elif tag in _CATEGORY_TAGS:
-                uicfg.autoform_section.tag_relation((stype, rtype, otype, role),
-                                                    tag)
-            elif tag == 'inlineview':
-                uicfg.autoform_is_inlined.tag_relation((stype, rtype, otype, role), True)
-            else:
-                raise ValueError(tag)
-
-except ImportError:
-
-    _dispatch_rtags = None
-
-def _get_etype(bases, classdict):
-    try:
-        return classdict['id']
-    except KeyError:
-        for base in bases:
-            etype = getattr(base, 'id', None)
-            if etype and etype != 'Any':
-                return etype
-
-def _get_defs(attr, name, bases, classdict):
-    try:
-        yield name, classdict.pop(attr)
-    except KeyError:
-        for base in bases:
-            try:
-                value = getattr(base, attr)
-                delattr(base, attr)
-                yield base.__name__, value
-            except AttributeError:
-                continue
-
-class _metaentity(type):
-    """this metaclass sets the relation tags on the entity class
-    and deals with the `widgets` attribute
-    """
-    def __new__(mcs, name, bases, classdict):
-        # collect baseclass' rtags
-        etype = _get_etype(bases, classdict)
-        if etype and _dispatch_rtags is not None:
-            for name, rtags in _get_defs('__rtags__', name, bases, classdict):
-                warn('%s: __rtags__ is deprecated' % name, DeprecationWarning)
-                for relation, tags in rtags.iteritems():
-                    # tags must become an iterable
-                    if isinstance(tags, basestring):
-                        tags = (tags,)
-                    # relation must become a 3-uple (rtype, targettype, role)
-                    if isinstance(relation, basestring):
-                        _dispatch_rtags(tags, relation, 'subject', etype, '*')
-                        _dispatch_rtags(tags, relation, 'object', '*', etype)
-                    elif len(relation) == 1: # useful ?
-                        _dispatch_rtags(tags, relation[0], 'subject', etype, '*')
-                        _dispatch_rtags(tags, relation[0], 'object', '*', etype)
-                    elif len(relation) == 2:
-                        rtype, ttype = relation
-                        ttype = bw_normalize_etype(ttype) # XXX bw compat
-                        _dispatch_rtags(tags, rtype, 'subject', etype, ttype)
-                        _dispatch_rtags(tags, rtype, 'object', ttype, etype)
-                    elif len(relation) == 3:
-                        rtype, ttype, role = relation
-                        ttype = bw_normalize_etype(ttype)
-                        if role == 'subject':
-                            _dispatch_rtags(tags, rtype, 'subject', etype, ttype)
-                        else:
-                            _dispatch_rtags(tags, rtype, 'object', ttype, etype)
-                    else:
-                        raise ValueError('bad rtag definition (%r)' % (relation,))
-            for name, widgets in _get_defs('widgets', name, bases, classdict):
-                warn('%s: widgets is deprecated' % name, DeprecationWarning)
-                for rtype, wdgname in widgets.iteritems():
-                    if wdgname in ('URLWidget', 'EmbededURLWidget', 'RawDynamicComboBoxWidget'):
-                        warn('%s widget is deprecated' % wdgname, DeprecationWarning)
-                        continue
-                    if wdgname == 'StringWidget':
-                        wdgname = 'TextInput'
-                    widget = getattr(formwidgets, wdgname)
-                    assert hasattr(widget, 'render')
-                    uicfg.autoform_field_kwargs.tag_subject_of(
-                        (etype, rtype, '*'), {'widget': widget})
-        return super(_metaentity, mcs).__new__(mcs, name, bases, classdict)
-
-
 class Entity(AppObject, dict):
     """an entity instance has e_schema automagically set on
     the class and instances has access to their issuing cursor.
@@ -155,42 +63,38 @@
                          as composite relations or relations that have '?1' as object
                          cardinality
     """
-    __metaclass__ = _metaentity
     __registry__ = 'etypes'
     __select__ = yes()
 
     # class attributes that must be set in class definition
-    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
 
-    MODE_TAGS = set(('link', 'create'))
-    CATEGORY_TAGS = set(('primary', 'secondary', 'generic', 'generated')) # , 'metadata'))
     @classmethod
-    def __initialize__(cls):
+    def __initialize__(cls, schema):
         """initialize a specific entity class by adding descriptors to access
         entity type's attributes and relations
         """
-        etype = cls.id
+        etype = cls.__id__
         assert etype != 'Any', etype
-        cls.e_schema = eschema = cls.schema.eschema(etype)
+        cls.e_schema = eschema = schema.eschema(etype)
         for rschema, _ in eschema.attribute_definitions():
             if rschema.type == 'eid':
                 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
@@ -205,7 +109,7 @@
         """return a rql to fetch all entities of the class type"""
         restrictions = restriction or []
         if settype:
-            restrictions.append('%s is %s' % (mainvar, cls.id))
+            restrictions.append('%s is %s' % (mainvar, cls.__id__))
         if fetchattrs is None:
             fetchattrs = cls.fetch_attrs
         selection = [mainvar]
@@ -238,7 +142,7 @@
                 rschema = eschema.subject_relation(attr)
             except KeyError:
                 cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
-                            attr, cls.id)
+                            attr, cls.__id__)
                 continue
             if not user.matching_groups(rschema.get_groups('read')):
                 continue
@@ -251,12 +155,18 @@
                 desttype = rschema.objects(eschema.type)[0]
                 card = rschema.rproperty(eschema, desttype, 'cardinality')[0]
                 if card not in '?1':
+                    cls.warning('bad relation %s specified in fetch attrs for %s',
+                                 attr, cls)
                     selection.pop()
                     restrictions.pop()
                     continue
-                if card == '?':
-                    restrictions[-1] += '?' # left outer join if not mandatory
-                destcls = cls.vreg['etypes'].etype_class(desttype)
+                # XXX we need outer join in case the relation is not mandatory
+                # (card == '?')  *or if the entity is being added*, since in
+                # that case the relation may still be missing. As we miss this
+                # later information here, systematically add it.
+                restrictions[-1] += '?'
+                # XXX user.req.vreg iiiirk
+                destcls = user.req.vreg['etypes'].etype_class(desttype)
                 destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs,
                                             selection, orderby, restrictions,
                                             user, ordermethod, visited=visited)
@@ -267,14 +177,6 @@
 
     @classmethod
     @cached
-    def parent_classes(cls):
-        parents = [cls.vreg['etypes'].etype_class(e.type)
-                   for e in cls.e_schema.ancestors()]
-        parents.append(cls.vreg['etypes'].etype_class('Any'))
-        return parents
-
-    @classmethod
-    @cached
     def _rest_attr_info(cls):
         mainattr, needcheck = 'eid', True
         if cls.rest_attr:
@@ -291,7 +193,7 @@
         return mainattr, needcheck
 
     def __init__(self, req, rset=None, row=None, col=0):
-        AppObject.__init__(self, req, rset, row, col)
+        AppObject.__init__(self, req, rset=rset, row=row, col=col)
         dict.__init__(self)
         self._related_cache = {}
         if rset is not None:
@@ -310,6 +212,9 @@
     def __hash__(self):
         return id(self)
 
+    def __cmp__(self, other):
+        raise NotImplementedError('comparison not implemented for %s' % self.__class__)
+
     def pre_add_hook(self):
         """hook called by the repository before doing anything to add the entity
         (before_add entity hooks have not been called yet). This give the
@@ -380,11 +285,11 @@
                 kwargs['_restpath'] = self.rest_path(kwargs.get('base_url'))
             except TypeError:
                 warn('%s: rest_path() now take use_ext_eid argument, '
-                     'please update' % self.id, DeprecationWarning)
+                     'please update' % self.__id__, DeprecationWarning)
                 kwargs['_restpath'] = self.rest_path()
         else:
             kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
-        return self.build_url(method, **kwargs)
+        return self.req.build_url(method, **kwargs)
 
     def rest_path(self, use_ext_eid=False):
         """returns a REST-like (relative) path for this entity"""
@@ -449,7 +354,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
@@ -480,13 +386,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
@@ -522,7 +421,7 @@
     def as_rset(self):
         """returns a resultset containing `self` information"""
         rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
-                         {'x': self.eid}, [(self.id,)])
+                         {'x': self.eid}, [(self.__id__,)])
         return self.req.decorate_rset(rset)
 
     def to_complete_relations(self):
@@ -617,14 +516,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
@@ -681,14 +580,16 @@
         self.set_related_cache(rtype, role, rset)
         return self.related(rtype, role, limit, entities)
 
-    def related_rql(self, rtype, role='subject'):
-        rschema = self.schema[rtype]
+    def related_rql(self, rtype, role='subject', targettypes=None):
+        rschema = self.req.vreg.schema[rtype]
         if role == 'subject':
-            targettypes = rschema.objects(self.e_schema)
+            if targettypes is None:
+                targettypes = rschema.objects(self.e_schema)
             restriction = 'E eid %%(x)s, E %s X' % rtype
             card = greater_card(rschema, (self.e_schema,), targettypes, 0)
         else:
-            targettypes = rschema.subjects(self.e_schema)
+            if targettypes is None:
+                targettypes = rschema.subjects(self.e_schema)
             restriction = 'E eid %%(x)s, X %s E' % rtype
             card = greater_card(rschema, targettypes, (self.e_schema,), 1)
         if len(targettypes) > 1:
@@ -715,28 +616,17 @@
 
     # 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):
-            rtype = self.schema.rschema(rtype)
+            rtype = self.req.vreg.schema.rschema(rtype)
         if role == 'subject':
             evar, searchedvar = 'S', 'O'
             subjtype, objtype = self.e_schema, targettype
@@ -745,8 +635,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
@@ -763,20 +662,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 ################################################
 
@@ -801,7 +709,7 @@
         """set cached values for the given relation"""
         if rset:
             related = list(rset.entities(col))
-            rschema = self.schema.rschema(rtype)
+            rschema = self.req.vreg.schema.rschema(rtype)
             if role == 'subject':
                 rcard = rschema.rproperty(self.e_schema, related[0].e_schema,
                                           'cardinality')[1]
@@ -827,9 +735,14 @@
             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, **kwargs):
+    def set_attributes(self, _cw_unsafe=False, **kwargs):
         assert kwargs
         relations = []
         for key in kwargs:
@@ -838,8 +751,12 @@
         self.update(kwargs)
         # and now update the database
         kwargs['x'] = self.eid
-        self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
-                         kwargs, 'x')
+        if _cw_unsafe:
+            self.req.unsafe_execute(
+                'SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs, 'x')
+        else:
+            self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
+                             kwargs, 'x')
 
     def delete(self):
         assert self.has_eid(), self.eid
@@ -923,6 +840,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 ##########################################
 
@@ -939,11 +870,9 @@
         return eobj.get_value(self._attrname)
 
     def __set__(self, eobj, value):
-        # XXX bw compat
-        # would be better to generate UPDATE queries than the current behaviour
-        eobj.warning("deprecated usage, don't use 'entity.attr = val' notation)")
         eobj[self._attrname] = value
-
+        if hasattr(eobj, 'edited_attributes'):
+            eobj.edited_attributes.add(self._attrname)
 
 class Relation(object):
     """descriptor that controls schema relation access"""
--- a/etwist/server.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/etwist/server.py	Tue Sep 22 13:08:42 2009 +0200
@@ -15,8 +15,7 @@
 from urlparse import urlsplit, urlunsplit
 import hotshot
 
-from twisted.application import service, strports
-from twisted.scripts._twistd_unix import daemonize
+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
@@ -30,18 +29,41 @@
 
 from cubicweb.etwist.request import CubicWebTwistedRequestAdapter
 
+def daemonize():
+    # XXX unix specific
+    # XXX factorize w/ code in cw.server.server and cw.server.serverctl
+    # (start-repository command)
+    # See http://www.erlenstar.demon.co.uk/unix/faq_toc.html#TOC16
+    if os.fork():   # launch child and...
+        return -1
+    os.setsid()
+    if os.fork():   # launch child and...
+        os._exit(0) # kill off parent again.
+    # move to the root to avoit mount pb
+    os.chdir('/')
+    # set paranoid umask
+    os.umask(077)
+    null = os.open('/dev/null', os.O_RDWR)
+    for i in range(3):
+        try:
+            os.dup2(null, i)
+        except OSError, e:
+            if e.errno != errno.EBADF:
+                raise
+    os.close(null)
+    return None
 
 def start_task(interval, func):
     lc = task.LoopingCall(func)
     lc.start(interval)
 
 def start_looping_tasks(repo):
-    for interval, func in repo._looping_tasks:
+    for interval, func, args in repo._looping_tasks:
         repo.info('starting twisted task %s with interval %.2fs',
                   func.__name__, interval)
-        def catch_error_func(repo=repo, func=func):
+        def catch_error_func(repo=repo, func=func, args=args):
             try:
-                func()
+                func(*args)
             except:
                 repo.exception('error in looping task')
         start_task(interval, catch_error_func)
@@ -110,13 +132,13 @@
                 start_task(1, self.pyro_loop_event)
             self.appli.repo.start_looping_tasks()
         self.set_url_rewriter()
-        CW_EVENT_MANAGER.bind('after-source-reload', self.set_url_rewriter)
+        CW_EVENT_MANAGER.bind('after-registry-reload', self.set_url_rewriter)
         interval = min(config['cleanup-session-time'] or 120,
                        config['cleanup-anonymous-session-time'] or 720) / 2.
         start_task(interval, self.appli.session_handler.clean_sessions)
 
     def set_url_rewriter(self):
-        self.url_rewriter = self.appli.vreg['components'].select_object('urlrewriter')
+        self.url_rewriter = self.appli.vreg['components'].select_or_none('urlrewriter')
 
     def shutdown_event(self):
         """callback fired when the server is shutting down to properly
@@ -365,11 +387,26 @@
     port = config['port'] or 8080
     reactor.listenTCP(port, channel.HTTPFactory(website))
     baseurl = config['base-url'] or config.default_base_url()
-    print "-> Instance started on", baseurl
+    logger = getLogger('cubicweb.twisted')
+    logger.info('instance started on %s', baseurl)
     if not debug:
-        daemonize()
+        if daemonize():
+            # child process
+            return
         if config['pid-file']:
+            # ensure the directory where the pid-file should be set exists (for
+            # instance /var/run/cubicweb may be deleted on computer restart)
+            piddir = os.path.dirname(config['pid-file'])
+            if not os.path.exists(piddir):
+                os.makedirs(piddir)
             file(config['pid-file'], 'w').write(str(os.getpid()))
+    if config['uid'] is not None:
+        try:
+            uid = int(config['uid'])
+        except ValueError:
+            from pwd import getpwnam
+            uid = getpwnam(config['uid']).pw_uid
+        os.setuid(uid)
     if config['profile']:
         prof = hotshot.Profile(config['profile'])
         prof.runcall(reactor.run)
--- a/etwist/test/unittest_server.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/etwist/test/unittest_server.py	Tue Sep 22 13:08:42 2009 +0200
@@ -5,11 +5,11 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.etwist.server import host_prefixed_baseurl
 
 
-class HostPrefixedBaseURLTC(EnvBasedTC):
+class HostPrefixedBaseURLTC(CubicWebTC):
 
     def _check(self, baseurl, host, waited):
         self.assertEquals(host_prefixed_baseurl(baseurl, host), waited,
--- a/etwist/twctl.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/etwist/twctl.py	Tue Sep 22 13:08:42 2009 +0200
@@ -8,7 +8,6 @@
 
 import sys
 
-from cubicweb import underline_title
 from cubicweb.toolsutils import CommandHandler
 
 # trigger configuration registration
@@ -18,7 +17,7 @@
     cmdname = 'start'
     cfgname = 'twisted'
 
-    def start_command(self, config, debug):
+    def start_server(self, config, debug):
         from cubicweb.etwist import server
         server.run(config, debug)
 
--- a/ext/test/unittest_rest.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/ext/test/unittest_rest.py	Tue Sep 22 13:08:42 2009 +0200
@@ -6,11 +6,11 @@
 :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
+from cubicweb.devtools.testlib import CubicWebTC
 
 from cubicweb.ext.rest import rest_publish
 
-class RestTC(EnvBasedTC):
+class RestTC(CubicWebTC):
     def context(self):
         return self.execute('CWUser X WHERE X login "admin"').get_entity(0, 0)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/xhtml2fo.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,139 @@
+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	Wed Aug 05 09:15:56 2009 +0200
+++ b/gettext.py	Tue Sep 22 13:08:42 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/appobjects/components.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/appobjects/components.py	Tue Sep 22 13:08:42 2009 +0200
@@ -28,7 +28,7 @@
     __select__ = one_line_rset() & match_search_state('linksearch') & accept
 
     def cell_call(self, row, col):
-        entity = self.entity(0, 0)
+        entity = self.rset.get_entity(0, 0)
         role, eid, rtype, etype = self.req.search_state[1]
         assert entity.eid == typed_eid(eid)
         rset = entity.unrelated(rtype, etype, role, ordermethod='fetch_order')
@@ -74,7 +74,7 @@
         label = display_name(req, etype, 'plural')
         view = self.vreg.select('views', 'list', req, req.etype_rset(etype))
         url = view.url()
-        etypelink = u'&nbsp;<a href="%s">%s</a>' % (xml_escape(url), label)
+        etypelink = u'&#160;<a href="%s">%s</a>' % (xml_escape(url), label)
         yield (label, etypelink, self.add_entity_link(eschema, req))
 
 ManageView.entity_types = entity_types_no_count
--- a/goa/appobjects/gauthservice.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/appobjects/gauthservice.py	Tue Sep 22 13:08:42 2009 +0200
@@ -17,7 +17,7 @@
 
     def anon_user_link(self):
         self.w(self.req._('anonymous'))
-        self.w(u'&nbsp;[<a class="logout" href="%s">%s</a>]'
+        self.w(u'&#160;[<a class="logout" href="%s">%s</a>]'
                % (users.create_login_url(self.req.url()), self.req._('login')))
 
 class GAELogoutAction(LogoutAction):
--- a/goa/appobjects/sessions.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/appobjects/sessions.py	Tue Sep 22 13:08:42 2009 +0200
@@ -73,9 +73,9 @@
 class GAEPersistentSessionManager(AbstractSessionManager):
     """manage session data associated to a session identifier"""
 
-    def __init__(self, *args, **kwargs):
-        super(GAEPersistentSessionManager, self).__init__(*args, **kwargs)
-        self._repo = self.config.repository(vreg=self.vreg)
+    def __init__(self, vreg, *args, **kwargs):
+        super(GAEPersistentSessionManager, self).__init__(vreg, *args, **kwargs)
+        self._repo = self.config.repository(vreg=vreg)
 
     def get_session(self, req, sessionid):
         """return existing session for the given session identifier"""
--- a/goa/db.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/db.py	Tue Sep 22 13:08:42 2009 +0200
@@ -35,7 +35,8 @@
 
 from logilab.common.decorators import cached, iclassmethod
 
-from cubicweb import RequestSessionMixIn, Binary, entities
+from cubicweb import Binary, entities
+from cubicweb.req import RequestSessionBase
 from cubicweb.rset import ResultSet
 from cubicweb.entity import metaentity
 from cubicweb.server.utils import crypt_password
@@ -92,7 +93,7 @@
 def needrequest(wrapped):
     def wrapper(cls, *args, **kwargs):
         req = kwargs.pop('req', None)
-        if req is None and args and isinstance(args[0], RequestSessionMixIn):
+        if req is None and args and isinstance(args[0], RequestSessionBase):
             args = list(args)
             req = args.pop(0)
         if req is None:
@@ -155,7 +156,7 @@
         #
         # Entity prototype:
         #   __init__(self, req, rset, row=None, col=0)
-        if args and isinstance(args[0], RequestSessionMixIn) or 'req' in kwargs:
+        if args and isinstance(args[0], RequestSessionBase) or 'req' in kwargs:
             super(Model, self).__init__(*args, **kwargs)
             self._gaeinitargs = None
         else:
--- a/goa/dbmyams.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/dbmyams.py	Tue Sep 22 13:08:42 2009 +0200
@@ -160,12 +160,6 @@
 import os
 from cubicweb import CW_SOFTWARE_ROOT
 
-if os.environ.get('APYCOT_ROOT'):
-    SCHEMAS_LIB_DIRECTORY = join(os.environ['APYCOT_ROOT'],
-                                 'local', 'share', 'cubicweb', 'schemas')
-else:
-    SCHEMAS_LIB_DIRECTORY = join(CW_SOFTWARE_ROOT, 'schemas')
-
 def load_schema(config, schemaclasses=None, extrahook=None):
     """high level method to load all the schema for a lax instance"""
     # IMPORTANT NOTE: dbmodel schemas must be imported **BEFORE**
@@ -174,7 +168,6 @@
     for compname in config['included-cubes']:
         __import__('%s.schema' % compname)
     loader = GaeSchemaLoader(use_gauthservice=config['use-google-auth'], db=db)
-    loader.lib_directory = SCHEMAS_LIB_DIRECTORY
     if schemaclasses is not None:
         for cls in schemaclasses:
             loader.load_dbmodel(cls.__name__, goadb.extract_dbmodel(cls))
@@ -188,7 +181,7 @@
                      'read_permission', 'add_permission',
                      'delete_permission', 'update_permission'):
         loader.import_yams_schema(erschema, 'bootstrap')
-    loader.handle_file(join(SCHEMAS_LIB_DIRECTORY, 'base.py'))
+    loader.handle_file(join(CW_SOFTWARE_ROOT, 'schemas', 'base.py'))
     cubes = config['included-yams-cubes']
     for cube in reversed(config.expand_cubes(cubes)):
         config.info('loading cube %s', cube)
--- a/goa/doc/quickstart.txt	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/doc/quickstart.txt	Tue Sep 22 13:08:42 2009 +0200
@@ -1,3 +1,5 @@
+.. -*- coding: utf-8 -*-
+
 Introduction
 =============
 
@@ -11,7 +13,7 @@
   application.
 
 *result set*
-  objet qui encaspule les résultats d'une requête adressée à l'entrepôt 
+  objet qui encaspule les résultats d'une requête adressée à l'entrepôt
   de données et des informations sur cette requête.
 
 *vue*
@@ -23,8 +25,8 @@
 Définition d'une application de Blog
 ====================================
 
-La première chose à faire est de copier le squelette depuis le répertoire 
-``lax/skel`` vers un nouveau répertoire qui sera votre application 
+La première chose à faire est de copier le squelette depuis le répertoire
+``lax/skel`` vers un nouveau répertoire qui sera votre application
 ``Google AppEngine``::
 
   $ cp -r lax/skel myapp
@@ -36,7 +38,7 @@
 données manipulées. La syntaxe de la définition est la même que celle
 proposée par `Google AppEngine`_ mais il faut remplacer la ligne
 d'import::
-  
+
   from google.appengine.ext import db
 
 par celle-ci::
@@ -47,7 +49,7 @@
 Un exemple de schéma de données pour un ``Blog`` pourrait être::
 
   from cubicweb.goa import db
-  
+
   class Blog(db.Model):
       # un titre à donner à l'entrée
       title = db.StringProperty(required=True)
@@ -56,15 +58,15 @@
       # le contenu de l'entrée
       content = db.TextProperty()
       # une entrée peut en citer une autre
-      cites = db.SelfReferenceProperty() 
-      
+      cites = db.SelfReferenceProperty()
+
 
 Personnalisation des vues
 -------------------------
 
 ``LAX`` permet de générer directement, à partir de la définition
-du schéma, des vues de consultation, d'ajout et de modification 
-pour tous les types de donées manipulés. Il est toutefois 
+du schéma, des vues de consultation, d'ajout et de modification
+pour tous les types de donées manipulés. Il est toutefois
 généralement souhaitable de personnaliser les vues de consultations.
 
 Dans ``LAX``, les vues sont représentées par des classes Python.
@@ -74,7 +76,7 @@
 - un identifiant (tous les objets dans ``LAX`` sont enregistrés
   dans un registre et cet identifiant sert de clé pour y retrouver
   la vue)
-  
+
 - une description des types de données auxquels elle s'applique
 
 Il existe dans ``LAX`` des vues prédéfinies et utilisées par le moteur
@@ -86,17 +88,17 @@
 Par exemple, si on souhaite modifier la page principale d'une entrée de
 blog, il faut surcharger la vue ``primary`` des objets ``Blog`` dans
 le fichier ``myapp/views.py``::
-  
+
   from cubicweb.web.views import baseviews
-  
+
   class BlogPrimaryView(baseviews.PrimaryView):
       accepts = ('Blog',)
-      
+
       def cell_call(self, row, col):
-          entity = self.entity(row, col)
+          entity = self.rset.get_entity(row, col)
           self.w(u'<h1>%s</h1>' % entity.title)
           self.w(u'<div>%s</div>' entity.content)
-    
+
 
 Génération du graphique de schéma
 ---------------------------------
@@ -104,13 +106,13 @@
 Il existe une vue ``schema`` qui permet d'afficher un graphique
 représantant les différents types d'entités définis dans le schéma
 ainsi que les relations entre ces types. Ce graphique doit être généré
-statiquement. Le script à utiliser pour générer ce schéma est 
+statiquement. Le script à utiliser pour générer ce schéma est
 dans ``myapp/tools``. Ce script nécessite d'avoir accès aux
 bibliothèques fournies par le SDK de ``Google AppEngine``. Il faut
 donc modifier son PYTHONPATH::
 
   $ export PYTHONPATH=GAE_ROOT/google:GAE_ROOT/lib/yaml
-  $ python tools/generate_schema_img.py 
+  $ python tools/generate_schema_img.py
 
 
 Génération des fichiers de traduction
--- a/goa/gaesource.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/gaesource.py	Tue Sep 22 13:08:42 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	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/goactl.py	Tue Sep 22 13:08:42 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',
@@ -112,7 +117,6 @@
     'web/httpcache.py',
     'web/request.py',
     'web/webconfig.py',
-    'web/widgets.py',
 
     'web/views/__init__.py',
     'web/views/actions.py',
--- a/goa/skel/views.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/skel/views.py	Tue Sep 22 13:08:42 2009 +0200
@@ -19,7 +19,7 @@
     accepts = ('BlogEntry',)
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         self.w(u'<h1>%s</h1>' % entity.dc_title())
         entity.view('metadata', w=self.w)
         self.w(entity.printable_value('text'))
--- a/goa/test/unittest_editcontroller.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/test/unittest_editcontroller.py	Tue Sep 22 13:08:42 2009 +0200
@@ -401,11 +401,11 @@
             #    which fires a Redirect
             # 2/ When re-publishing the copy form, the publisher implicitly commits
             try:
-                self.env.app.publish('edit', self.req)
+                self.app.publish('edit', self.req)
             except Redirect:
                 self.req.form['rql'] = 'Any X WHERE X eid %s' % p.eid
                 self.req.form['vid'] = 'copy'
-                self.env.app.publish('view', self.req)
+                self.app.publish('view', self.req)
             rset = self.req.execute('CWUser P WHERE P surname "Boom"')
             self.assertEquals(len(rset), 0)
         finally:
--- a/goa/test/unittest_views.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/test/unittest_views.py	Tue Sep 22 13:08:42 2009 +0200
@@ -49,7 +49,7 @@
         self.vreg['views'].render('hcal', self.req, rset=self.blog.rset)
 
     def test_django_index(self):
-        self.vreg'views'].render('index', self.req, rset=None)
+        self.vreg['views'].render('index', self.req, rset=None)
 
 for vid in ('primary', 'oneline', 'incontext', 'outofcontext', 'text'):
     setattr(SomeViewsTC, 'test_%s'%vid, lambda self, vid=vid: self.blog.view(vid))
--- a/goa/testlib.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/goa/testlib.py	Tue Sep 22 13:08:42 2009 +0200
@@ -6,7 +6,11 @@
 """
 __docformat__ = "restructuredtext en"
 
-from logilab.common.testlib import TestCase
+from logilab.common.testlib import TestCase, TestSkipped
+try:
+    import google.appengine
+except ImportError:
+    raise TestSkipped('Can not import google.appengine. Skip this module')
 
 import os, os.path as osp
 import time
@@ -18,34 +22,17 @@
 rqlannotation.SQLGenAnnotator = goarqlannotation.SQLGenAnnotator
 rqlannotation.set_qdata = goarqlannotation.set_qdata
 
-try:
-    from google.appengine.api import apiproxy_stub_map
-    from google.appengine.api import datastore_file_stub
-    from google.appengine.ext import db as gdb
-    from cubicweb.goa import db, do_monkey_patch
-    import_appengine_failed = None
-except ImportError, exc:
-    # XXX necessary ?
-    class db:
-        class Model:
-            pass
-        class DummyProperty:
-            def __init__(self, *args, **kwargs):
-                pass
-        TextProperty = DummyProperty
-        StringProperty = DummyProperty
-        BlobProperty = DummyProperty
-        DateProperty = DummyProperty
-        ReferenceProperty = DummyProperty
-        SelfReferenceProperty = DummyProperty
-    import_appengine_failed = 'cannot import appengine: %s' % exc
-
+from google.appengine.api import apiproxy_stub_map
+from google.appengine.api import datastore_file_stub
+from google.appengine.ext import db as gdb
 
 from cubicweb.devtools.fake import FakeRequest
-from cubicweb.goa.goavreg import GAEVregistry
+
+from cubicweb.goa import db, do_monkey_patch
+from cubicweb.goa.goavreg import GAEVRegistry
 from cubicweb.goa.goaconfig import GAEConfiguration
 from cubicweb.goa.dbinit import (create_user, create_groups, fix_entities,
-                              init_persistent_schema, insert_versions)
+                                 init_persistent_schema, insert_versions)
 
 import logging
 logger = logging.getLogger()
@@ -82,8 +69,6 @@
         apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub)
 
     def setUp(self):
-        if import_appengine_failed:
-            self.skip(import_appengine_failed)
         # Ensure we're in UTC.
         os.environ['TZ'] = 'UTC'
         time.tzset()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/__init__.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,1 @@
+"""core hooks"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/bookmark.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,31 @@
+"""bookmark related hooks
+
+: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
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+
+
+class AutoDeleteBookmarkOp(hook.Operation):
+    bookmark = None # make pylint happy
+    def precommit_event(self):
+        if not self.session.deleted_in_transaction(self.bookmark.eid):
+            if not self.bookmark.bookmarked_by:
+                self.bookmark.delete()
+
+
+class DelBookmarkedByHook(hook.Hook):
+    """ensure user logins are stripped"""
+    __id__ = 'autodelbookmark'
+    __select__ = hook.Hook.__select__ & entity_implements('bookmarked_by',)
+    category = 'bookmark'
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        AutoDeleteBookmarkOp(self._cw,
+                             bookmark=self._cw.entity_from_eid(self.eidfrom))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/email.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,68 @@
+"""hooks to ensure use_email / primary_email relations consistency
+
+: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
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb.server import hook
+from cubicweb.server.repository import ensure_card_respected
+
+class SetUseEmailRelationOp(hook.Operation):
+    """delay this operation to commit to avoid conflict with a late rql query
+    already setting the relation
+    """
+    rtype = 'use_email'
+    entity = email = None # make pylint happy
+
+    def condition(self):
+        """check entity has use_email set for the email address"""
+        return not any(e for e in self.entity.use_email
+                       if self.email.eid == e.eid)
+
+    def precommit_event(self):
+        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.entity.eid, 'y': self.email.eid}, 'x')
+
+class SetPrimaryEmailRelationOp(SetUseEmailRelationOp):
+    rtype = 'primary_email'
+
+    def condition(self):
+        """check entity has no primary_email set"""
+        return not self.entity.primary_email
+
+
+class SetPrimaryEmailHook(hook.Hook):
+    """notify when a bug or story or version has its state modified"""
+    __id__ = 'setprimaryemail'
+    __select__ = hook.Hook.__select__ & hook.match_rtype('use_email')
+    category = 'email'
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        entity = self._cw.entity_from_eid(self.eidfrom)
+        if 'primary_email' in entity.e_schema.subject_relations():
+            SetPrimaryEmailRelationOp(self._cw, entity=entity,
+                                      email=self._cw.entity_from_eid(self.eidto))
+
+class SetUseEmailHook(hook.Hook):
+    """notify when a bug or story or version has its state modified"""
+    __id__ = 'setprimaryemail'
+    __select__ = hook.Hook.__select__ & hook.match_rtype('primary_email')
+    category = 'email'
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        entity = self._cw.entity_from_eid(self.eidfrom)
+        if 'use_email' in entity.e_schema.subject_relations():
+            SetUseEmailRelationOp(self._cw, entity=entity,
+                                  email=self._cw.entity_from_eid(self.eidto))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/integrity.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,257 @@
+"""Core hooks: check for data integrity according to the instance'schema
+validity
+
+: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
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb import ValidationError
+from cubicweb.selectors import entity_implements
+from cubicweb.common.uilib import soup2xhtml
+from cubicweb.server import hook
+
+# special relations that don't have to be checked for integrity, usually
+# because they are handled internally by hooks (so we trust ourselves)
+DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
+                                'is', 'is_instance_of',
+                                'wf_info_for', 'from_state', 'to_state'))
+DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of',
+                                'wf_info_for', 'from_state', 'to_state'))
+
+
+class _CheckRequiredRelationOperation(hook.LateOperation):
+    """checking relation cardinality has to be done after commit in
+    case the relation is being replaced
+    """
+    eid, rtype = None, None
+
+    def precommit_event(self):
+        # recheck pending eids
+        if self.session.deleted_in_transaction(self.eid):
+            return
+        if self.session.unsafe_execute(*self._rql()).rowcount < 1:
+            etype = self.session.describe(self.eid)[0]
+            _ = self.session._
+            msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')
+            msg %= {'rtype': _(self.rtype), 'etype': _(etype), 'eid': self.eid}
+            raise ValidationError(self.eid, {self.rtype: msg})
+
+    def commit_event(self):
+        pass
+
+    def _rql(self):
+        raise NotImplementedError()
+
+
+class _CheckSRelationOp(_CheckRequiredRelationOperation):
+    """check required subject relation"""
+    def _rql(self):
+        return 'Any O WHERE S eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
+
+
+class _CheckORelationOp(_CheckRequiredRelationOperation):
+    """check required object relation"""
+    def _rql(self):
+        return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
+
+
+class IntegrityHook(hook.Hook):
+    __abstract__ = True
+    category = 'integrity'
+
+
+class CheckCardinalityHook(IntegrityHook):
+    """check cardinalities are satisfied"""
+    __id__ = 'checkcard'
+    events = ('after_add_entity', 'before_delete_relation')
+
+    def __call__(self):
+        getattr(self, self.event)()
+
+    def checkrel_if_necessary(self, opcls, rtype, eid):
+        """check an equivalent operation has not already been added"""
+        for op in self._cw.pending_operations:
+            if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid:
+                break
+        else:
+            opcls(self._cw, rtype=rtype, eid=eid)
+
+    def after_add_entity(self):
+        eid = self.entity.eid
+        eschema = self.entity.e_schema
+        for rschema, targetschemas, x in eschema.relation_definitions():
+            # skip automatically handled relations
+            if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
+                continue
+            if x == 'subject':
+                subjtype = eschema
+                objtype = targetschemas[0].type
+                cardindex = 0
+                opcls = _CheckSRelationOp
+            else:
+                subjtype = targetschemas[0].type
+                objtype = eschema
+                cardindex = 1
+                opcls = _CheckORelationOp
+            card = rschema.rproperty(subjtype, objtype, 'cardinality')
+            if card[cardindex] in '1+':
+                self.checkrel_if_necessary(opcls, rschema.type, eid)
+
+    def before_delete_relation(self):
+        rtype = self.rtype
+        if rtype in DONT_CHECK_RTYPES_ON_DEL:
+            return
+        session = self._cw
+        eidfrom, eidto = self.eidfrom, self.eidto
+        card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
+        pendingrdefs = session.transaction_data.get('pendingrdefs', ())
+        if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
+            return
+        if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
+            self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom)
+        if card[1] in '1+' and not session.deleted_in_transaction(eidto):
+            self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto)
+
+
+class _CheckConstraintsOp(hook.LateOperation):
+    """check a new relation satisfy its constraints
+    """
+    def precommit_event(self):
+        eidfrom, rtype, eidto = self.rdef
+        # first check related entities have not been deleted in the same
+        # transaction
+        if self.session.deleted_in_transaction(eidfrom):
+            return
+        if self.session.deleted_in_transaction(eidto):
+            return
+        for constraint in self.constraints:
+            try:
+                constraint.repo_check(self.session, eidfrom, rtype, eidto)
+            except NotImplementedError:
+                self.critical('can\'t check constraint %s, not supported',
+                              constraint)
+
+    def commit_event(self):
+        pass
+
+
+class CheckConstraintHook(IntegrityHook):
+    """check the relation satisfy its constraints
+
+    this is delayed to a precommit time operation since other relation which
+    will make constraint satisfied may be added later.
+    """
+    __id__ = 'checkconstraint'
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        constraints = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
+                                'constraints')
+        if constraints:
+            _CheckConstraintsOp(self._cw, constraints=constraints,
+                               rdef=(self.eidfrom, self.rtype, self.eidto))
+
+
+class CheckUniqueHook(IntegrityHook):
+    __id__ = 'checkunique'
+    events = ('before_add_entity', 'before_update_entity')
+
+    def __call__(self):
+        entity = self.entity
+        eschema = entity.e_schema
+        for attr in entity.edited_attributes:
+            if eschema.subject_relation(attr).is_final() and \
+                   eschema.has_unique_values(attr):
+                val = entity[attr]
+                if val is None:
+                    continue
+                rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
+                rset = self._cw.unsafe_execute(rql, {'val': val})
+                if rset and rset[0][0] != entity.eid:
+                    msg = self._cw._('the value "%s" is already used, use another one')
+                    raise ValidationError(entity.eid, {attr: msg % val})
+
+
+class _DelayedDeleteOp(hook.Operation):
+    """delete the object of composite relation except if the relation
+    has actually been redirected to another composite
+    """
+
+    def precommit_event(self):
+        session = self.session
+        # don't do anything if the entity is being created or deleted
+        if not (session.deleted_in_transaction(self.eid) or
+                session.added_in_transaction(self.eid)):
+            etype = session.describe(self.eid)[0]
+            session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
+                                   % (etype, self.relation),
+                                   {'x': self.eid}, 'x')
+
+
+class DeleteCompositeOrphanHook(IntegrityHook):
+    """delete the composed of a composite relation when this relation is deleted
+    """
+    __id__ = 'deletecomposite'
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
+                                                 'composite')
+        if composite == 'subject':
+            _DelayedDeleteOp(self._cw, eid=self.eidto,
+                             relation='Y %s X' % self.rtype)
+        elif composite == 'object':
+            _DelayedDeleteOp(self._cw, eid=self.eidfrom,
+                             relation='X %s Y' % self.rtype)
+
+
+class DontRemoveOwnersGroupHook(IntegrityHook):
+    """delete the composed of a composite relation when this relation is deleted
+    """
+    __id__ = 'checkownersgroup'
+    __select__ = IntegrityHook.__select__ & entity_implements('CWGroup')
+    events = ('before_delete_entity', 'before_update_entity')
+
+    def __call__(self):
+        if self.event == 'before_delete_entity' and self.entity.name == 'owners':
+            raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
+        elif self.event == 'before_update_entity' and 'name' in self.entity.edited_attributes:
+            newname = self.entity.pop('name')
+            oldname = self.entity.name
+            if oldname == 'owners' and newname != oldname:
+                raise ValidationError(self.entity.eid, {'name': self._cw._('can\'t be changed')})
+            self.entity['name'] = newname
+
+
+class TidyHtmlFields(IntegrityHook):
+    """tidy HTML in rich text strings"""
+    __id__ = 'htmltidy'
+    events = ('before_add_entity', 'before_update_entity')
+
+    def __call__(self):
+        entity = self.entity
+        metaattrs = entity.e_schema.meta_attributes()
+        for metaattr, (metadata, attr) in metaattrs.iteritems():
+            if metadata == 'format' and attr in entity.edited_attributes:
+                try:
+                    value = entity[attr]
+                except KeyError:
+                    continue # no text to tidy
+                if isinstance(value, unicode): # filter out None and Binary
+                    if getattr(entity, str(metaattr)) == 'text/html':
+                        entity[attr] = soup2xhtml(value, self._cw.encoding)
+
+
+class StripCWUserLoginHook(IntegrityHook):
+    """ensure user logins are stripped"""
+    __id__ = 'stripuserlogin'
+    __select__ = IntegrityHook.__select__ & entity_implements('CWUser')
+    events = ('before_add_entity', 'before_update_entity',)
+
+    def __call__(self):
+        user = self.entity
+        if 'login' in user.edited_attributes and user.login:
+            user.login = user.login.strip()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/metadata.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,162 @@
+"""Core hooks: set generic metadata
+
+: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
+"""
+__docformat__ = "restructuredtext en"
+
+
+from datetime import datetime
+
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+from cubicweb.server.repository import FTIndexEntityOp
+
+
+def eschema_type_eid(session, etype):
+    """get eid of the CWEType entity for the given yams type"""
+    eschema = session.repo.schema.eschema(etype)
+    # eschema.eid is None if schema has been readen from the filesystem, not
+    # 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]
+    return eschema.eid
+
+
+class MetaDataHook(hook.Hook):
+    __abstract__ = True
+    category = 'metadata'
+
+
+class InitMetaAttrsHook(MetaDataHook):
+    """before create a new entity -> set creation and modification date
+
+    this is a conveniency hook, you shouldn't have to disable it
+    """
+    __id__ = 'metaattrsinit'
+    events = ('before_add_entity',)
+
+    def __call__(self):
+        timestamp = datetime.now()
+        self.entity.setdefault('creation_date', timestamp)
+        self.entity.setdefault('modification_date', timestamp)
+        if not self._cw.get_shared_data('do-not-insert-cwuri'):
+            cwuri = u'%seid/%s' % (self._cw.base_url(), self.entity.eid)
+            self.entity.setdefault('cwuri', cwuri)
+
+
+class UpdateMetaAttrsHook(MetaDataHook):
+    """update an entity -> set modification date"""
+    __id__ = 'metaattrsupdate'
+    events = ('before_update_entity',)
+
+    def __call__(self):
+        self.entity.setdefault('modification_date', datetime.now())
+
+
+class _SetCreatorOp(hook.Operation):
+
+    def precommit_event(self):
+        session = self.session
+        if session.deleted_in_transaction(self.entity.eid):
+            # entity have been created and deleted in the same transaction
+            return
+        if not self.entity.created_by:
+            session.add_relation(self.entity.eid, 'created_by', session.user.eid)
+
+
+class SetIsHook(MetaDataHook):
+    """create a new entity -> set is relation"""
+    __id__ = 'setis'
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        if hasattr(self.entity, '_cw_recreating'):
+            return
+        session = self._cw
+        entity = self.entity
+        try:
+            session.add_relation(entity.eid, 'is',
+                                 eschema_type_eid(session, entity.id))
+        except IndexError:
+            # during schema serialization, skip
+            return
+        for etype in entity.e_schema.ancestors() + [entity.e_schema]:
+            session.add_relation(entity.eid, 'is_instance_of',
+                                 eschema_type_eid(session, etype))
+
+
+class SetOwnershipHook(MetaDataHook):
+    """create a new entity -> set owner and creator metadata"""
+    __id__ = 'setowner'
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        asession = self._cw.actual_session()
+        if not asession.is_internal_session:
+            self._cw.add_relation(self.entity.eid, 'owned_by', asession.user.eid)
+            _SetCreatorOp(asession, entity=self.entity)
+
+
+class _SyncOwnersOp(hook.Operation):
+    def precommit_event(self):
+        self.session.unsafe_execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,'
+                                    'NOT EXISTS(X owned_by U, X eid %(x)s)',
+                                    {'c': self.compositeeid, 'x': self.composedeid},
+                                    ('c', 'x'))
+
+
+class SyncCompositeOwner(MetaDataHook):
+    """when adding composite relation, the composed should have the same owners
+    has the composite
+    """
+    __id__ = 'synccompositeowner'
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        if self.rtype == 'wf_info_for':
+            # skip this special composite relation # XXX (syt) why?
+            return
+        eidfrom, eidto = self.eidfrom, self.eidto
+        composite = self._cw.schema_rproperty(self.rtype, eidfrom, eidto, 'composite')
+        if composite == 'subject':
+            _SyncOwnersOp(self._cw, compositeeid=eidfrom, composedeid=eidto)
+        elif composite == 'object':
+            _SyncOwnersOp(self._cw, compositeeid=eidto, composedeid=eidfrom)
+
+
+class FixUserOwnershipHook(MetaDataHook):
+    """when a user has been created, add owned_by relation on itself"""
+    __id__ = 'fixuserowner'
+    __select__ = MetaDataHook.__select__ & entity_implements('CWUser')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        self._cw.add_relation(self.entity.eid, 'owned_by', self.entity.eid)
+
+
+class UpdateFTIHook(MetaDataHook):
+    """sync fulltext index when relevant relation is added / removed
+    """
+    __id__ = 'updateftirel'
+    events = ('after_add_relation', 'after_delete_relation')
+
+    def __call__(self):
+        rtype = self.rtype
+        session = self._cw
+        if self.event == 'after_add_relation':
+            # Reindexing the contained entity is enough since it will implicitly
+            # reindex the container entity.
+            ftcontainer = session.vreg.schema.rschema(rtype).fulltext_container
+            if ftcontainer == 'subject':
+                FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidto))
+            elif ftcontainer == 'object':
+                FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidfrom))
+        elif session.repo.schema.rschema(rtype).fulltext_container:
+            FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidto))
+            FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidfrom))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/notification.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,139 @@
+"""some hooks to handle notification on entity's changes
+
+: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
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.common.textutils import normalize_text
+
+from cubicweb import RegistryException
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+
+
+class RenderAndSendNotificationView(hook.Operation):
+    """delay rendering of notification view until precommit"""
+    def precommit_event(self):
+        view = self.view
+        if view.rset is not None and not view.rset:
+            return # entity added and deleted in the same transaction (cache effect)
+        if view.cw_rset and self.session.deleted_in_transaction(view.cw_rset[cw_rset.cw_row or 0][cw_rset.cw_col or 0]):
+            return # entity added and deleted in the same transaction
+        self.view.render_and_send(**getattr(self, 'viewargs', {}))
+
+
+class NotificationHook(hook.Hook):
+    __abstract__ = True
+    category = 'notification'
+
+    def select_view(self, vid, rset, row=0, col=0):
+        return self._cw.vreg['views'].select_or_none(vid, self._cw,
+                                                     rset=rset, row=0, col=0)
+
+
+class StatusChangeHook(NotificationHook):
+    """notify when a workflowable entity has its state modified"""
+    __id__ = 'notifystatuschange'
+    __select__ = NotificationHook.__select__ & entity_implements('TrInfo')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        if not entity.from_state: # not a transition
+            return
+        rset = entity.related('wf_info_for')
+        view = self.select_view('notif_status_change', rset=rset, row=0)
+        if view is None:
+            return
+        comment = entity.printable_value('comment', format='text/plain')
+        # XXX don't try to wrap rest until we've a proper transformation (see
+        # #103822)
+        if comment and entity.comment_format != 'text/rest':
+            comment = normalize_text(comment, 80)
+        RenderAndSendNotificationView(self._cw, view=view, viewargs={
+            'comment': comment, 'previous_state': entity.previous_state.name,
+            'current_state': entity.new_state.name})
+
+
+class RelationChangeHook(NotificationHook):
+    __id__ = 'notifyrelationchange'
+    events = ('before_add_relation', 'after_add_relation',
+              'before_delete_relation', 'after_delete_relation')
+
+    def __call__(self):
+        """if a notification view is defined for the event, send notification
+        email defined by the view
+        """
+        rset = self._cw.eid_rset(self.eidfrom)
+        view = self.select_view('notif_%s_%s' % (self.event,  self.rtype),
+                                rset=rset, row=0)
+        if view is None:
+            return
+        RenderAndSendNotificationView(self._cw, view=view)
+
+
+class EntityChangeHook(NotificationHook):
+    """if a notification view is defined for the event, send notification
+    email defined by the view
+    """
+    __id__ = 'notifyentitychange'
+    events = ('after_add_entity', 'after_update_entity')
+
+    def __call__(self):
+        rset = self.entity.as_rset()
+        view = self.select_view('notif_%s' % self.event, rset=rset, row=0)
+        if view is None:
+            return
+        RenderAndSendNotificationView(self._cw, view=view)
+
+
+# supervising ##################################################################
+
+class SomethingChangedHook(NotificationHook):
+    __id__ = 'supervising'
+    events = ('before_add_relation', 'before_delete_relation',
+              'after_add_entity', 'before_update_entity')
+
+    def __call__(self):
+        # XXX use proper selectors
+        if self._cw.is_super_session or self._cw.repo.config.repairing:
+            return # ignore changes triggered by hooks or maintainance shell
+        dest = self._cw.vreg.config['supervising-addrs']
+        if not dest: # no supervisors, don't do this for nothing...
+            return
+        if self._call():
+            SupervisionMailOp(self._cw)
+
+    def _call(self):
+        event = self.event.split('_', 1)[1]
+        if event == 'update_entity':
+            if self._cw.added_in_transaction(self.entity.eid):
+                return False
+            if self.entity.e_schema == 'CWUser':
+                if not (self.entity.edited_attributes - frozenset(('eid', 'modification_date',
+                                                                   'last_login_time'))):
+                    # don't record last_login_time update which are done
+                    # automatically at login time
+                    return False
+        self._cw.transaction_data.setdefault('pendingchanges', []).append(
+            (event, self))
+        return True
+
+
+class EntityDeleteHook(SomethingChangedHook):
+    __id__ = 'supervisingentitydel'
+    events = ('before_delete_entity',)
+
+    def _call(self):
+        try:
+            title = self.entity.dc_title()
+        except:
+            # may raise an error during deletion process, for instance due to
+            # missing required relation
+            title = '#%s' % eid
+        self._cw.transaction_data.setdefault('pendingchanges', []).append(
+            ('delete_entity', (self.eid, str(self.entity.e_schema), title)))
+        return True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/security.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,130 @@
+"""Security hooks: check permissions to add/delete/update entities according to
+the user connected to a session
+
+: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
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb import Unauthorized
+from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook
+
+
+def check_entity_attributes(session, entity):
+    eid = entity.eid
+    eschema = entity.e_schema
+    # ._default_set is only there on entity creation to indicate unspecified
+    # attributes which has been set to a default value defined in the schema
+    defaults = getattr(entity, '_default_set', ())
+    try:
+        editedattrs = entity.edited_attributes
+    except AttributeError:
+        editedattrs = entity
+    for attr in editedattrs:
+        if attr in defaults:
+            continue
+        rschema = eschema.subject_relation(attr)
+        if rschema.is_final(): # non final relation are checked by other hooks
+            # add/delete should be equivalent (XXX: unify them into 'update' ?)
+            rschema.check_perm(session, 'add', eid)
+
+
+class _CheckEntityPermissionOp(hook.LateOperation):
+    def precommit_event(self):
+        #print 'CheckEntityPermissionOp', self.session.user, self.entity, self.action
+        self.entity.check_perm(self.action)
+        check_entity_attributes(self.session, self.entity)
+
+    def commit_event(self):
+        pass
+
+
+class _CheckRelationPermissionOp(hook.LateOperation):
+    def precommit_event(self):
+        self.rschema.check_perm(self.session, self.action, self.eidfrom, self.eidto)
+
+    def commit_event(self):
+        pass
+
+
+class SecurityHook(hook.Hook):
+    __abstract__ = True
+    category = 'security'
+    __select__ = hook.Hook.__select__ & hook.regular_session()
+
+
+class AfterAddEntitySecurityHook(SecurityHook):
+    __id__ = 'securityafteraddentity'
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        _CheckEntityPermissionOp(self._cw, entity=self.entity, action='add')
+
+
+class AfterUpdateEntitySecurityHook(SecurityHook):
+    __id__ = 'securityafterupdateentity'
+    events = ('after_update_entity',)
+
+    def __call__(self):
+        try:
+            # check user has permission right now, if not retry at commit time
+            self.entity.check_perm('update')
+            check_entity_attributes(self._cw, self.entity)
+        except Unauthorized:
+            self.entity.clear_local_perm_cache('update')
+            _CheckEntityPermissionOp(self._cw, entity=self.entity, action='update')
+
+
+class BeforeDelEntitySecurityHook(SecurityHook):
+    __id__ = 'securitybeforedelentity'
+    events = ('before_delete_entity',)
+
+    def __call__(self):
+        self.entity.check_perm('delete')
+
+
+class BeforeAddRelationSecurityHook(SecurityHook):
+    __id__ = 'securitybeforeaddrelation'
+    events = ('before_add_relation',)
+
+    def __call__(self):
+        if self.rtype in BEFORE_ADD_RELATIONS:
+            nocheck = self._cw.transaction_data.get('skip-security', ())
+            if (self.eidfrom, self.rtype, self.eidto) in nocheck:
+                return
+            rschema = self._cw.repo.schema[self.rtype]
+            rschema.check_perm(self._cw, 'add', self.eidfrom, self.eidto)
+
+
+class AfterAddRelationSecurityHook(SecurityHook):
+    __id__ = 'securityafteraddrelation'
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        if not self.rtype in BEFORE_ADD_RELATIONS:
+            nocheck = self._cw.transaction_data.get('skip-security', ())
+            if (self.eidfrom, self.rtype, self.eidto) in nocheck:
+                return
+            rschema = self._cw.repo.schema[self.rtype]
+            if self.rtype in ON_COMMIT_ADD_RELATIONS:
+                _CheckRelationPermissionOp(self._cw, action='add',
+                                           rschema=rschema,
+                                           eidfrom=self.eidfrom,
+                                           eidto=self.eidto)
+            else:
+                rschema.check_perm(self._cw, 'add', self.eidfrom, self.eidto)
+
+
+class BeforeDelRelationSecurityHook(SecurityHook):
+    __id__ = 'securitybeforedelrelation'
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        nocheck = self._cw.transaction_data.get('skip-security', ())
+        if (self.eidfrom, self.rtype, self.eidto) in nocheck:
+            return
+        self._cw.repo.schema[self.rtype].check_perm(self._cw, 'delete',
+                                                       self.eidfrom, self.eidto)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/syncschema.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,1134 @@
+"""schema hooks:
+
+- synchronize the living schema object with the persistent schema
+- perform physical update on the source when necessary
+
+checking for schema consistency is done in hooks.py
+
+: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
+"""
+__docformat__ = "restructuredtext en"
+
+from yams.schema import BASE_TYPES
+from yams.buildobjs import EntityType, RelationType, RelationDefinition
+from yams.schema2sql import eschema2sql, rschema2sql, type_from_constraints
+
+from cubicweb import ValidationError, RepositoryError
+from cubicweb.selectors import entity_implements
+from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS
+from cubicweb.server import hook, schemaserial as ss
+from cubicweb.server.sqlutils import SQL_PREFIX
+
+
+TYPE_CONVERTER = { # XXX
+    'Boolean': bool,
+    'Int': int,
+    'Float': float,
+    'Password': str,
+    'String': unicode,
+    'Date' : unicode,
+    'Datetime' : unicode,
+    'Time' : unicode,
+    }
+
+# core entity and relation types which can't be removed
+CORE_ETYPES = list(BASE_TYPES) + ['CWEType', 'CWRType', 'CWUser', 'CWGroup',
+                                  'CWConstraint', 'CWAttribute', 'CWRelation']
+CORE_RTYPES = ['eid', 'creation_date', 'modification_date', 'cwuri',
+               'login', 'upassword', 'name',
+               'is', 'instanceof', 'owned_by', 'created_by', 'in_group',
+               'relation_type', 'from_entity', 'to_entity',
+               'constrainted_by',
+               'read_permission', 'add_permission',
+               'delete_permission', 'updated_permission',
+               ]
+
+def get_constraints(session, entity):
+    constraints = []
+    for cstreid in session.transaction_data.get(entity.eid, ()):
+        cstrent = session.entity_from_eid(cstreid)
+        cstr = CONSTRAINTS[cstrent.type].deserialize(cstrent.value)
+        cstr.eid = cstreid
+        constraints.append(cstr)
+    return constraints
+
+def add_inline_relation_column(session, etype, rtype):
+    """add necessary column and index for an inlined relation"""
+    table = SQL_PREFIX + etype
+    column = SQL_PREFIX + rtype
+    try:
+        session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer'
+                               % (table, column)), rollback_on_failure=False)
+        session.info('added column %s to table %s', column, table)
+    except:
+        # silent exception here, if this error has not been raised because the
+        # column already exists, index creation will fail anyway
+        session.exception('error while adding column %s to table %s',
+                          table, column)
+    # create index before alter table which may expectingly fail during test
+    # (sqlite) while index creation should never fail (test for index existence
+    # is done by the dbhelper)
+    session.pool.source('system').create_index(session, table, column)
+    session.info('added index on %s(%s)', table, column)
+    session.transaction_data.setdefault('createdattrs', []).append(
+        '%s.%s' % (etype, rtype))
+
+def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
+    errors = {}
+    # don't use getattr(entity, attr), we would get the modified value if any
+    for attr in entity.edited_attributes:
+        if attr in ro_attrs:
+            newval = entity.pop(attr)
+            origval = getattr(entity, attr)
+            if newval != origval:
+                errors[attr] = session._("can't change the %s attribute") % \
+                               display_name(session, attr)
+            entity[attr] = newval
+    if errors:
+        raise ValidationError(entity.eid, errors)
+
+
+# operations for low-level database alteration  ################################
+
+class DropTable(hook.Operation):
+    """actually remove a database from the instance's schema"""
+    table = None # make pylint happy
+    def precommit_event(self):
+        dropped = self.session.transaction_data.setdefault('droppedtables',
+                                                           set())
+        if self.table in dropped:
+            return # already processed
+        dropped.add(self.table)
+        self.session.system_sql('DROP TABLE %s' % self.table)
+        self.info('dropped table %s', self.table)
+
+
+class DropRelationTable(DropTable):
+    def __init__(self, session, rtype):
+        super(DropRelationTable, self).__init__(
+            session, table='%s_relation' % rtype)
+        session.transaction_data.setdefault('pendingrtypes', set()).add(rtype)
+
+
+class DropColumn(hook.Operation):
+    """actually remove the attribut's column from entity table in the system
+    database
+    """
+    table = column = None # make pylint happy
+    def precommit_event(self):
+        session, table, column = self.session, self.table, self.column
+        # drop index if any
+        session.pool.source('system').drop_index(session, table, column)
+        try:
+            session.system_sql('ALTER TABLE %s DROP COLUMN %s'
+                               % (table, column), rollback_on_failure=False)
+            self.info('dropped column %s from table %s', column, table)
+        except Exception, ex:
+            # not supported by sqlite for instance
+            self.error('error while altering table %s: %s', table, ex)
+
+
+# base operations for in-memory schema synchronization  ########################
+
+class MemSchemaNotifyChanges(hook.SingleLastOperation):
+    """the update schema operation:
+
+    special operation which should be called once and after all other schema
+    operations. It will trigger internal structures rebuilding to consider
+    schema changes.
+    """
+
+    def __init__(self, session):
+        hook.SingleLastOperation.__init__(self, session)
+
+    def commit_event(self):
+        rebuildinfered = self.session.data.get('rebuild-infered', True)
+        self.session.repo.set_schema(self.repo.schema, rebuildinfered=rebuildinfered)
+
+
+class MemSchemaOperation(hook.Operation):
+    """base class for schema operations"""
+    def __init__(self, session, kobj=None, **kwargs):
+        self.kobj = kobj
+        # once Operation.__init__ has been called, event may be triggered, so
+        # do this last !
+        hook.Operation.__init__(self, session, **kwargs)
+        # every schema operation is triggering a schema update
+        MemSchemaNotifyChanges(session)
+
+    def prepare_constraints(self, subjtype, rtype, objtype):
+        constraints = rtype.rproperty(subjtype, objtype, 'constraints')
+        self.constraints = list(constraints)
+        rtype.set_rproperty(subjtype, objtype, 'constraints', self.constraints)
+
+
+class MemSchemaEarlyOperation(MemSchemaOperation):
+    def insert_index(self):
+        """schema operation which are inserted at the begining of the queue
+        (typically to add/remove entity or relation types)
+        """
+        i = -1
+        for i, op in enumerate(self.session.pending_operations):
+            if not isinstance(op, MemSchemaEarlyOperation):
+                return i
+        return i + 1
+
+
+class MemSchemaPermOperation(MemSchemaOperation):
+    """base class to synchronize schema permission definitions"""
+    def __init__(self, session, perm, etype_eid):
+        self.perm = perm
+        try:
+            self.name = session.entity_from_eid(etype_eid).name
+        except IndexError:
+            self.error('changing permission of a no more existant type #%s',
+                etype_eid)
+        else:
+            hook.Operation.__init__(self, session)
+
+
+# operations for high-level source database alteration  ########################
+
+class SourceDbCWETypeRename(hook.Operation):
+    """this operation updates physical storage accordingly"""
+    oldname = newname = None # make pylint happy
+
+    def precommit_event(self):
+        # we need sql to operate physical changes on the system database
+        sqlexec = self.session.system_sql
+        sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname,
+                                                     SQL_PREFIX, self.newname))
+        self.info('renamed table %s to %s', self.oldname, self.newname)
+        sqlexec('UPDATE entities SET type=%s WHERE type=%s',
+                (self.newname, self.oldname))
+        sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
+                (self.newname, self.oldname))
+
+
+class SourceDbCWRTypeUpdate(hook.Operation):
+    """actually update some properties of a relation definition"""
+    rschema = values = entity = None # make pylint happy
+
+    def precommit_event(self):
+        session = self.session
+        rschema = self.rschema
+        if rschema.is_final() or not 'inlined' in self.values:
+            return # nothing to do
+        inlined = self.values['inlined']
+        entity = self.entity
+        # check in-lining is necessary / possible
+        if not entity.inlined_changed(inlined):
+            return # nothing to do
+        # inlined changed, make necessary physical changes!
+        sqlexec = self.session.system_sql
+        rtype = rschema.type
+        eidcolumn = SQL_PREFIX + 'eid'
+        if not inlined:
+            # need to create the relation if it has not been already done by
+            # another event of the same transaction
+            if not rschema.type in session.transaction_data.get('createdtables', ()):
+                tablesql = rschema2sql(rschema)
+                # create the necessary table
+                for sql in tablesql.split(';'):
+                    if sql.strip():
+                        sqlexec(sql)
+                session.transaction_data.setdefault('createdtables', []).append(
+                    rschema.type)
+            # copy existant data
+            column = SQL_PREFIX + rtype
+            for etype in rschema.subjects():
+                table = SQL_PREFIX + str(etype)
+                sqlexec('INSERT INTO %s_relation SELECT %s, %s FROM %s WHERE NOT %s IS NULL'
+                        % (rtype, eidcolumn, column, table, column))
+            # drop existant columns
+            for etype in rschema.subjects():
+                DropColumn(session, table=SQL_PREFIX + str(etype),
+                             column=SQL_PREFIX + rtype)
+        else:
+            for etype in rschema.subjects():
+                try:
+                    add_inline_relation_column(session, str(etype), rtype)
+                except Exception, ex:
+                    # the column probably already exists. this occurs when the
+                    # entity's type has just been added or if the column has not
+                    # been previously dropped
+                    self.error('error while altering table %s: %s', etype, ex)
+                # copy existant data.
+                # XXX don't use, it's not supported by sqlite (at least at when i tried it)
+                #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '
+                #        'FROM %(rtype)s_relation '
+                #        'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from'
+                #        % locals())
+                table = SQL_PREFIX + str(etype)
+                cursor = sqlexec('SELECT eid_from, eid_to FROM %(table)s, '
+                                 '%(rtype)s_relation WHERE %(table)s.%(eidcolumn)s='
+                                 '%(rtype)s_relation.eid_from' % locals())
+                args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()]
+                if args:
+                    column = SQL_PREFIX + rtype
+                    cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
+                                       % (table, column, eidcolumn), args)
+                # drop existant table
+                DropRelationTable(session, rtype)
+
+
+class SourceDbCWAttributeAdd(hook.Operation):
+    """an attribute relation (CWAttribute) has been added:
+    * add the necessary column
+    * set default on this column if any and possible
+    * register an operation to add the relation definition to the
+      instance's schema on commit
+
+    constraints are handled by specific hooks
+    """
+    entity = None # make pylint happy
+
+    def init_rdef(self, **kwargs):
+        entity = self.entity
+        fromentity = entity.stype
+        self.session.execute('SET X ordernum Y+1 '
+                             'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
+                             'X ordernum >= %(order)s, NOT X eid %(x)s',
+                             {'x': entity.eid, 'se': fromentity.eid,
+                              'order': entity.ordernum or 0})
+        subj = str(fromentity.name)
+        rtype = entity.rtype.name
+        obj = str(entity.otype.name)
+        constraints = get_constraints(self.session, entity)
+        rdef = RelationDefinition(subj, rtype, obj,
+                                  description=entity.description,
+                                  cardinality=entity.cardinality,
+                                  constraints=constraints,
+                                  order=entity.ordernum,
+                                  eid=entity.eid,
+                                  **kwargs)
+        MemSchemaRDefAdd(self.session, rdef)
+        return rdef
+
+    def precommit_event(self):
+        session = self.session
+        entity = self.entity
+        # entity.defaultval is a string or None, but we need a correctly typed
+        # value
+        default = entity.defaultval
+        if default is not None:
+            default = TYPE_CONVERTER[entity.otype.name](default)
+        rdef = self.init_rdef(default=default,
+                              indexed=entity.indexed,
+                              fulltextindexed=entity.fulltextindexed,
+                              internationalizable=entity.internationalizable)
+        sysource = session.pool.source('system')
+        attrtype = type_from_constraints(sysource.dbhelper, rdef.object,
+                                         rdef.constraints)
+        # XXX should be moved somehow into lgc.adbh: sqlite doesn't support to
+        # add a new column with UNIQUE, it should be added after the ALTER TABLE
+        # using ADD INDEX
+        if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
+            extra_unique_index = True
+            attrtype = attrtype.replace(' UNIQUE', '')
+        else:
+            extra_unique_index = False
+        # added some str() wrapping query since some backend (eg psycopg) don't
+        # allow unicode queries
+        table = SQL_PREFIX + rdef.subject
+        column = SQL_PREFIX + rdef.name
+        try:
+            session.system_sql(str('ALTER TABLE %s ADD COLUMN %s %s'
+                                   % (table, column, attrtype)),
+                               rollback_on_failure=False)
+            self.info('added column %s to table %s', table, column)
+        except Exception, ex:
+            # the column probably already exists. this occurs when
+            # the entity's type has just been added or if the column
+            # has not been previously dropped
+            self.error('error while altering table %s: %s', table, ex)
+        if extra_unique_index or entity.indexed:
+            try:
+                sysource.create_index(session, table, column,
+                                      unique=extra_unique_index)
+            except Exception, ex:
+                self.error('error while creating index for %s.%s: %s',
+                           table, column, ex)
+
+
+class SourceDbCWRelationAdd(SourceDbCWAttributeAdd):
+    """an actual relation has been added:
+    * if this is an inlined relation, add the necessary column
+      else if it's the first instance of this relation type, add the
+      necessary table and set default permissions
+    * register an operation to add the relation definition to the
+      instance's schema on commit
+
+    constraints are handled by specific hooks
+    """
+    entity = None # make pylint happy
+
+    def precommit_event(self):
+        session = self.session
+        entity = self.entity
+        rdef = self.init_rdef(composite=entity.composite)
+        schema = session.vreg.schema
+        rtype = rdef.name
+        rschema = session.vreg.schema.rschema(rtype)
+        # this have to be done before permissions setting
+        if rschema.inlined:
+            # need to add a column if the relation is inlined and if this is the
+            # first occurence of "Subject relation Something" whatever Something
+            # and if it has not been added during other event of the same
+            # transaction
+            key = '%s.%s' % (rdef.subject, rtype)
+            try:
+                alreadythere = bool(rschema.objects(rdef.subject))
+            except KeyError:
+                alreadythere = False
+            if not (alreadythere or
+                    key in session.transaction_data.get('createdattrs', ())):
+                add_inline_relation_column(session, rdef.subject, rtype)
+        else:
+            # need to create the relation if no relation definition in the
+            # schema and if it has not been added during other event of the same
+            # transaction
+            if not (rschema.subjects() or
+                    rtype in session.transaction_data.get('createdtables', ())):
+                try:
+                    rschema = session.vreg.schema.rschema(rtype)
+                    tablesql = rschema2sql(rschema)
+                except KeyError:
+                    # fake we add it to the schema now to get a correctly
+                    # initialized schema but remove it before doing anything
+                    # more dangerous...
+                    rschema = session.vreg.schema.add_relation_type(rdef)
+                    tablesql = rschema2sql(rschema)
+                    session.vreg.schema.del_relation_type(rtype)
+                # create the necessary table
+                for sql in tablesql.split(';'):
+                    if sql.strip():
+                        session.system_sql(sql)
+                session.transaction_data.setdefault('createdtables', []).append(
+                    rtype)
+
+
+class SourceDbRDefUpdate(hook.Operation):
+    """actually update some properties of a relation definition"""
+    rschema = values = None # make pylint happy
+
+    def precommit_event(self):
+        etype = self.kobj[0]
+        table = SQL_PREFIX + etype
+        column = SQL_PREFIX + self.rschema.type
+        if 'indexed' in self.values:
+            sysource = self.session.pool.source('system')
+            if self.values['indexed']:
+                sysource.create_index(self.session, table, column)
+            else:
+                sysource.drop_index(self.session, table, column)
+        if 'cardinality' in self.values and self.rschema.is_final():
+            adbh = self.session.pool.source('system').dbhelper
+            if not adbh.alter_column_support:
+                # not supported (and NOT NULL not set by yams in that case, so
+                # no worry)
+                return
+            atype = self.rschema.objects(etype)[0]
+            constraints = self.rschema.rproperty(etype, atype, 'constraints')
+            coltype = type_from_constraints(adbh, atype, constraints,
+                                            creating=False)
+            # XXX check self.values['cardinality'][0] actually changed?
+            sql = adbh.sql_set_null_allowed(table, column, coltype,
+                                            self.values['cardinality'][0] != '1')
+            self.session.system_sql(sql)
+
+
+class SourceDbCWConstraintAdd(hook.Operation):
+    """actually update constraint of a relation definition"""
+    entity = None # make pylint happy
+    cancelled = False
+
+    def precommit_event(self):
+        rdef = self.entity.reverse_constrained_by[0]
+        session = self.session
+        # when the relation is added in the same transaction, the constraint
+        # object is created by the operation adding the attribute or relation,
+        # so there is nothing to do here
+        if session.added_in_transaction(rdef.eid):
+            return
+        subjtype, rtype, objtype = session.vreg.schema.schema_by_eid(rdef.eid)
+        cstrtype = self.entity.type
+        oldcstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
+        newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
+        table = SQL_PREFIX + str(subjtype)
+        column = SQL_PREFIX + str(rtype)
+        # alter the physical schema on size constraint changes
+        if newcstr.type() == 'SizeConstraint' and (
+            oldcstr is None or oldcstr.max != newcstr.max):
+            adbh = self.session.pool.source('system').dbhelper
+            card = rtype.rproperty(subjtype, objtype, 'cardinality')
+            coltype = type_from_constraints(adbh, objtype, [newcstr],
+                                            creating=False)
+            sql = adbh.sql_change_col_type(table, column, coltype, card != '1')
+            try:
+                session.system_sql(sql, rollback_on_failure=False)
+                self.info('altered column %s of table %s: now VARCHAR(%s)',
+                          column, table, newcstr.max)
+            except Exception, ex:
+                # not supported by sqlite for instance
+                self.error('error while altering table %s: %s', table, ex)
+        elif cstrtype == 'UniqueConstraint' and oldcstr is None:
+            session.pool.source('system').create_index(
+                self.session, table, column, unique=True)
+
+
+class SourceDbCWConstraintDel(hook.Operation):
+    """actually remove a constraint of a relation definition"""
+    rtype = subjtype = objtype = None # make pylint happy
+
+    def precommit_event(self):
+        cstrtype = self.cstr.type()
+        table = SQL_PREFIX + str(self.subjtype)
+        column = SQL_PREFIX + str(self.rtype)
+        # alter the physical schema on size/unique constraint changes
+        if cstrtype == 'SizeConstraint':
+            try:
+                self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT'
+                                        % (table, column),
+                                        rollback_on_failure=False)
+                self.info('altered column %s of table %s: now TEXT',
+                          column, table)
+            except Exception, ex:
+                # not supported by sqlite for instance
+                self.error('error while altering table %s: %s', table, ex)
+        elif cstrtype == 'UniqueConstraint':
+            self.session.pool.source('system').drop_index(
+                self.session, table, column, unique=True)
+
+
+# operations for in-memory schema synchronization  #############################
+
+class MemSchemaCWETypeAdd(MemSchemaEarlyOperation):
+    """actually add the entity type to the instance's schema"""
+    eid = None # make pylint happy
+    def commit_event(self):
+        self.schema.add_entity_type(self.kobj)
+
+
+class MemSchemaCWETypeRename(MemSchemaOperation):
+    """this operation updates physical storage accordingly"""
+    oldname = newname = None # make pylint happy
+
+    def commit_event(self):
+        self.session.vreg.schema.rename_entity_type(self.oldname, self.newname)
+
+
+class MemSchemaCWETypeDel(MemSchemaOperation):
+    """actually remove the entity type from the instance's schema"""
+    def commit_event(self):
+        try:
+            # del_entity_type also removes entity's relations
+            self.schema.del_entity_type(self.kobj)
+        except KeyError:
+            # s/o entity type have already been deleted
+            pass
+
+
+class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation):
+    """actually add the relation type to the instance's schema"""
+    eid = None # make pylint happy
+    def commit_event(self):
+        rschema = self.schema.add_relation_type(self.kobj)
+        rschema.set_default_groups()
+
+
+class MemSchemaCWRTypeUpdate(MemSchemaOperation):
+    """actually update some properties of a relation definition"""
+    rschema = values = None # make pylint happy
+
+    def commit_event(self):
+        # structure should be clean, not need to remove entity's relations
+        # at this point
+        self.rschema.__dict__.update(self.values)
+
+
+class MemSchemaCWRTypeDel(MemSchemaOperation):
+    """actually remove the relation type from the instance's schema"""
+    def commit_event(self):
+        try:
+            self.schema.del_relation_type(self.kobj)
+        except KeyError:
+            # s/o entity type have already been deleted
+            pass
+
+
+class MemSchemaRDefAdd(MemSchemaEarlyOperation):
+    """actually add the attribute relation definition to the instance's
+    schema
+    """
+    def commit_event(self):
+        self.schema.add_relation_def(self.kobj)
+
+
+class MemSchemaRDefUpdate(MemSchemaOperation):
+    """actually update some properties of a relation definition"""
+    rschema = values = None # make pylint happy
+
+    def commit_event(self):
+        # structure should be clean, not need to remove entity's relations
+        # at this point
+        self.rschema._rproperties[self.kobj].update(self.values)
+
+
+class MemSchemaRDefDel(MemSchemaOperation):
+    """actually remove the relation definition from the instance's schema"""
+    def commit_event(self):
+        subjtype, rtype, objtype = self.kobj
+        try:
+            self.schema.del_relation_def(subjtype, rtype, objtype)
+        except KeyError:
+            # relation type may have been already deleted
+            pass
+
+
+class MemSchemaCWConstraintAdd(MemSchemaOperation):
+    """actually update constraint of a relation definition
+
+    has to be called before SourceDbCWConstraintAdd
+    """
+    cancelled = False
+
+    def precommit_event(self):
+        rdef = self.entity.reverse_constrained_by[0]
+        # when the relation is added in the same transaction, the constraint
+        # object is created by the operation adding the attribute or relation,
+        # so there is nothing to do here
+        if self.session.added_in_transaction(rdef.eid):
+            self.cancelled = True
+            return
+        subjtype, rtype, objtype = self.session.vreg.schema.schema_by_eid(rdef.eid)
+        self.prepare_constraints(subjtype, rtype, objtype)
+        cstrtype = self.entity.type
+        self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
+        self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
+        self.newcstr.eid = self.entity.eid
+
+    def commit_event(self):
+        if self.cancelled:
+            return
+        # in-place modification
+        if not self.cstr is None:
+            self.constraints.remove(self.cstr)
+        self.constraints.append(self.newcstr)
+
+
+class MemSchemaCWConstraintDel(MemSchemaOperation):
+    """actually remove a constraint of a relation definition
+
+    has to be called before SourceDbCWConstraintDel
+    """
+    rtype = subjtype = objtype = None # make pylint happy
+    def precommit_event(self):
+        self.prepare_constraints(self.subjtype, self.rtype, self.objtype)
+
+    def commit_event(self):
+        self.constraints.remove(self.cstr)
+
+
+class MemSchemaPermCWGroupAdd(MemSchemaPermOperation):
+    """synchronize schema when a *_permission relation has been added on a group
+    """
+    def __init__(self, session, perm, etype_eid, group_eid):
+        self.group = session.entity_from_eid(group_eid).name
+        super(MemSchemaPermCWGroupAdd, self).__init__(
+            session, perm, etype_eid)
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            erschema = self.schema[self.name]
+        except KeyError:
+            # duh, schema not found, log error and skip operation
+            self.error('no schema for %s', self.name)
+            return
+        groups = list(erschema.get_groups(self.perm))
+        try:
+            groups.index(self.group)
+            self.warning('group %s already have permission %s on %s',
+                         self.group, self.perm, erschema.type)
+        except ValueError:
+            groups.append(self.group)
+            erschema.set_groups(self.perm, groups)
+
+
+class MemSchemaPermCWGroupDel(MemSchemaPermCWGroupAdd):
+    """synchronize schema when a *_permission relation has been deleted from a
+    group
+    """
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            erschema = self.schema[self.name]
+        except KeyError:
+            # duh, schema not found, log error and skip operation
+            self.error('no schema for %s', self.name)
+            return
+        groups = list(erschema.get_groups(self.perm))
+        try:
+            groups.remove(self.group)
+            erschema.set_groups(self.perm, groups)
+        except ValueError:
+            self.error('can\'t remove permission %s on %s to group %s',
+                self.perm, erschema.type, self.group)
+
+
+class MemSchemaPermRQLExpressionAdd(MemSchemaPermOperation):
+    """synchronize schema when a *_permission relation has been added on a rql
+    expression
+    """
+    def __init__(self, session, perm, etype_eid, expression):
+        self.expr = expression
+        super(MemSchemaPermRQLExpressionAdd, self).__init__(
+            session, perm, etype_eid)
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            erschema = self.schema[self.name]
+        except KeyError:
+            # duh, schema not found, log error and skip operation
+            self.error('no schema for %s', self.name)
+            return
+        exprs = list(erschema.get_rqlexprs(self.perm))
+        exprs.append(erschema.rql_expression(self.expr))
+        erschema.set_rqlexprs(self.perm, exprs)
+
+
+class MemSchemaPermRQLExpressionDel(MemSchemaPermRQLExpressionAdd):
+    """synchronize schema when a *_permission relation has been deleted from an
+    rql expression
+    """
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            erschema = self.schema[self.name]
+        except KeyError:
+            # duh, schema not found, log error and skip operation
+            self.error('no schema for %s', self.name)
+            return
+        rqlexprs = list(erschema.get_rqlexprs(self.perm))
+        for i, rqlexpr in enumerate(rqlexprs):
+            if rqlexpr.expression == self.expr:
+                rqlexprs.pop(i)
+                break
+        else:
+            self.error('can\'t remove permission %s on %s for expression %s',
+                self.perm, erschema.type, self.expr)
+            return
+        erschema.set_rqlexprs(self.perm, rqlexprs)
+
+
+class MemSchemaSpecializesAdd(MemSchemaOperation):
+
+    def commit_event(self):
+        eschema = self.session.schema.schema_by_eid(self.etypeeid)
+        parenteschema = self.session.schema.schema_by_eid(self.parentetypeeid)
+        eschema._specialized_type = parenteschema.type
+        parenteschema._specialized_by.append(eschema.type)
+
+
+class MemSchemaSpecializesDel(MemSchemaOperation):
+
+    def commit_event(self):
+        try:
+            eschema = self.session.schema.schema_by_eid(self.etypeeid)
+            parenteschema = self.session.schema.schema_by_eid(self.parentetypeeid)
+        except KeyError:
+            # etype removed, nothing to do
+            return
+        eschema._specialized_type = None
+        parenteschema._specialized_by.remove(eschema.type)
+
+
+class SyncSchemaHook(hook.Hook):
+    __abstract__ = True
+    category = 'syncschema'
+
+
+# CWEType hooks ################################################################
+
+class DelCWETypeHook(SyncSchemaHook):
+    """before deleting a CWEType entity:
+    * check that we don't remove a core entity type
+    * cascade to delete related CWAttribute and CWRelation entities
+    * instantiate an operation to delete the entity type on commit
+    """
+    __id__ = 'syncdelcwetype'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWEType')
+    events = ('before_delete_entity',)
+
+    def __call__(self):
+        # final entities can't be deleted, don't care about that
+        name = self.entity.name
+        if name in CORE_ETYPES:
+            raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
+        # delete every entities of this type
+        self._cw.unsafe_execute('DELETE %s X' % name)
+        DropTable(self._cw, table=SQL_PREFIX + name)
+        MemSchemaCWETypeDel(self._cw, name)
+
+
+class AfterDelCWETypeHook(DelCWETypeHook):
+    __id__ = 'wfcleanup'
+    events = ('after_delete_entity',)
+
+    def __call__(self):
+        # workflow cleanup
+        self._cw.execute('DELETE Workflow X WHERE NOT X workflow_of Y')
+
+
+class AfterAddCWETypeHook(DelCWETypeHook):
+    """after adding a CWEType entity:
+    * create the necessary table
+    * set creation_date and modification_date by creating the necessary
+      CWAttribute entities
+    * add owned_by relation by creating the necessary CWRelation entity
+    * register an operation to add the entity type to the instance's
+      schema on commit
+    """
+    __id__ = 'syncaddcwetype'
+    events = ('before_add_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        if entity.get('final'):
+            return
+        schema = self._cw.schema
+        name = entity['name']
+        etype = EntityType(name=name, description=entity.get('description'),
+                           meta=entity.get('meta')) # don't care about final
+        # fake we add it to the schema now to get a correctly initialized schema
+        # but remove it before doing anything more dangerous...
+        schema = self._cw.schema
+        eschema = schema.add_entity_type(etype)
+        eschema.set_default_groups()
+        # generate table sql and rql to add metadata
+        tablesql = eschema2sql(self._cw.pool.source('system').dbhelper, eschema,
+                               prefix=SQL_PREFIX)
+        relrqls = []
+        for rtype in (META_RTYPES - VIRTUAL_RTYPES):
+            rschema = schema[rtype]
+            sampletype = rschema.subjects()[0]
+            desttype = rschema.objects()[0]
+            props = rschema.rproperties(sampletype, desttype)
+            relrqls += list(ss.rdef2rql(rschema, name, desttype, props))
+        # now remove it !
+        schema.del_entity_type(name)
+        # create the necessary table
+        for sql in tablesql.split(';'):
+            if sql.strip():
+                self._cw.system_sql(sql)
+        # register operation to modify the schema on commit
+        # this have to be done before adding other relations definitions
+        # or permission settings
+        etype.eid = entity.eid
+        MemSchemaCWETypeAdd(self._cw, etype)
+        # add meta relations
+        for rql, kwargs in relrqls:
+            self._cw.execute(rql, kwargs)
+
+
+class BeforeUpdateCWETypeHook(DelCWETypeHook):
+    """check name change, handle final"""
+    __id__ = 'syncupdatecwetype'
+    events = ('before_update_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        check_valid_changes(self._cw, entity, ro_attrs=('final',))
+        # don't use getattr(entity, attr), we would get the modified value if any
+        if 'name' in entity.edited_attributes:
+            newname = entity.pop('name')
+            oldname = entity.name
+            if newname.lower() != oldname.lower():
+                SourceDbCWETypeRename(self._cw, oldname=oldname, newname=newname)
+                MemSchemaCWETypeRename(self._cw, oldname=oldname, newname=newname)
+            entity['name'] = newname
+
+
+# CWRType hooks ################################################################
+
+class DelCWRTypeHook(SyncSchemaHook):
+    """before deleting a CWRType entity:
+    * check that we don't remove a core relation type
+    * cascade to delete related CWAttribute and CWRelation entities
+    * instantiate an operation to delete the relation type on commit
+    """
+    __id__ = 'syncdelcwrtype'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWRType')
+    events = ('before_delete_entity',)
+
+    def __call__(self):
+        name = self.entity.name
+        if name in CORE_ETYPES:
+            raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
+        # delete relation definitions using this relation type
+        self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
+                        {'x': self.entity.eid})
+        self._cw.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
+                        {'x': self.entity.eid})
+        MemSchemaCWRTypeDel(self._cw, name)
+
+
+class AfterAddCWRTypeHook(DelCWRTypeHook):
+    """after a CWRType entity has been added:
+    * register an operation to add the relation type to the instance's
+      schema on commit
+
+    We don't know yet this point if a table is necessary
+    """
+    __id__ = 'syncaddcwrtype'
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        rtype = RelationType(name=entity.name,
+                             description=entity.get('description'),
+                             meta=entity.get('meta', False),
+                             inlined=entity.get('inlined', False),
+                             symetric=entity.get('symetric', False),
+                             eid=entity.eid)
+        MemSchemaCWRTypeAdd(self._cw, rtype)
+
+
+class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
+    """check name change, handle final"""
+    __id__ = 'checkupdatecwrtype'
+    events = ('before_update_entity',)
+
+    def __call__(self):
+        check_valid_changes(self._cw, self.entity)
+
+
+class AfterUpdateCWRTypeHook(DelCWRTypeHook):
+    __id__ = 'syncupdatecwrtype'
+    events = ('after_update_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        rschema = self._cw.schema.rschema(entity.name)
+        newvalues = {}
+        for prop in ('meta', 'symetric', 'inlined'):
+            if prop in entity:
+                newvalues[prop] = entity[prop]
+        if newvalues:
+            MemSchemaCWRTypeUpdate(self._cw, rschema=rschema, values=newvalues)
+            SourceDbCWRTypeUpdate(self._cw, rschema=rschema, values=newvalues,
+                                  entity=entity)
+
+
+# relation_type hooks ##########################################################
+
+class AfterDelRelationTypeHook(SyncSchemaHook):
+    """before deleting a CWAttribute or CWRelation entity:
+    * if this is a final or inlined relation definition, instantiate an
+      operation to drop necessary column, else if this is the last instance
+      of a non final relation, instantiate an operation to drop necessary
+      table
+    * instantiate an operation to delete the relation definition on commit
+    * delete the associated relation type when necessary
+    """
+    __id__ = 'syncdelrelationtype'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('relation_type')
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        session = self._cw
+        subjschema, rschema, objschema = session.vreg.schema.schema_by_eid(self.eidfrom)
+        pendings = session.transaction_data.get('pendingeids', ())
+        # first delete existing relation if necessary
+        if rschema.is_final():
+            rdeftype = 'CWAttribute'
+        else:
+            rdeftype = 'CWRelation'
+            if not (subjschema.eid in pendings or objschema.eid in pendings):
+                pending = session.transaction_data.setdefault('pendingrdefs', set())
+                pending.add((subjschema, rschema, objschema))
+                session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
+                                % (rschema, subjschema, objschema))
+        execute = session.unsafe_execute
+        rteid = self.eidto
+        rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
+                       'R eid %%(x)s' % rdeftype, {'x': rteid})
+        lastrel = rset[0][0] == 0
+        # we have to update physical schema systematically for final and inlined
+        # relations, but only if it's the last instance for this relation type
+        # for other relations
+
+        if (rschema.is_final() or rschema.inlined):
+            rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
+                           'R eid %%(x)s, X from_entity E, E name %%(name)s'
+                           % rdeftype, {'x': rteid, 'name': str(subjschema)})
+            if rset[0][0] == 0 and not subjschema.eid in pendings:
+                ptypes = session.transaction_data.setdefault('pendingrtypes', set())
+                ptypes.add(rschema.type)
+                DropColumn(session, table=SQL_PREFIX + subjschema.type,
+                             column=SQL_PREFIX + rschema.type)
+        elif lastrel:
+            DropRelationTable(session, rschema.type)
+        # if this is the last instance, drop associated relation type
+        if lastrel and not rteid in pendings:
+            execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rteid}, 'x')
+        MemSchemaRDefDel(session, (subjschema, rschema, objschema))
+
+
+# CWAttribute / CWRelation hooks ###############################################
+
+class AfterAddCWAttributeHook(SyncSchemaHook):
+    __id__ = 'syncaddcwattribute'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWAttribute')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        SourceDbCWAttributeAdd(self._cw, entity=self.entity)
+
+
+class AfterAddCWRelationHook(AfterAddCWAttributeHook):
+    __id__ = 'syncaddcwrelation'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWRelation')
+
+    def __call__(self):
+        SourceDbCWRelationAdd(self._cw, entity=self.entity)
+
+
+class AfterUpdateCWRDefHook(SyncSchemaHook):
+    __id__ = 'syncaddcwattribute'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWAttribute',
+                                                               'CWRelation')
+    events = ('after_update_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        if self._cw.deleted_in_transaction(entity.eid):
+            return
+        desttype = entity.otype.name
+        rschema = self._cw.schema[entity.rtype.name]
+        newvalues = {}
+        for prop in rschema.rproperty_defs(desttype):
+            if prop == 'constraints':
+                continue
+            if prop == 'order':
+                prop = 'ordernum'
+            if prop in entity.edited_attributes:
+                newvalues[prop] = entity[prop]
+        if newvalues:
+            subjtype = entity.stype.name
+            MemSchemaRDefUpdate(self._cw, kobj=(subjtype, desttype),
+                                rschema=rschema, values=newvalues)
+            SourceDbRDefUpdate(self._cw, kobj=(subjtype, desttype),
+                               rschema=rschema, values=newvalues)
+
+
+# constraints synchronization hooks ############################################
+
+class AfterAddCWConstraintHook(SyncSchemaHook):
+    __id__ = 'syncaddcwconstraint'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWConstraint')
+    events = ('after_add_entity', 'after_update_entity')
+
+    def __call__(self):
+        MemSchemaCWConstraintAdd(self._cw, entity=self.entity)
+        SourceDbCWConstraintAdd(self._cw, entity=self.entity)
+
+
+class AfterAddConstrainedByHook(SyncSchemaHook):
+    __id__ = 'syncdelconstrainedby'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrainted_by')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        if self._cw.added_in_transaction(self.eidfrom):
+            self._cw.transaction_data.setdefault(self.eidfrom, []).append(self.eidto)
+
+
+class BeforeDeleteConstrainedByHook(AfterAddConstrainedByHook):
+    __id__ = 'syncdelconstrainedby'
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        if self._cw.deleted_in_transaction(self.eidfrom):
+            return
+        schema = self._cw.schema
+        entity = self._cw.entity_from_eid(self.eidto)
+        subjtype, rtype, objtype = schema.schema_by_eid(self.eidfrom)
+        try:
+            cstr = rtype.constraint_by_type(subjtype, objtype,
+                                            entity.cstrtype[0].name)
+        except IndexError:
+            self._cw.critical('constraint type no more accessible')
+        else:
+            SourceDbCWConstraintDel(self._cw, subjtype=subjtype, rtype=rtype,
+                                    objtype=objtype, cstr=cstr)
+            MemSchemaCWConstraintDel(self._cw, subjtype=subjtype, rtype=rtype,
+                                     objtype=objtype, cstr=cstr)
+
+
+# permissions synchronization hooks ############################################
+
+class AfterAddPermissionHook(SyncSchemaHook):
+    """added entity/relation *_permission, need to update schema"""
+    __id__ = 'syncaddperm'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype(
+        'read_permission', 'add_permission', 'delete_permission',
+        'update_permission')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        perm = self.rtype.split('_', 1)[0]
+        if self._cw.describe(self.eidto)[0] == 'CWGroup':
+            MemSchemaPermCWGroupAdd(self._cw, perm, self.eidfrom, self.eidto)
+        else: # RQLExpression
+            expr = self._cw.entity_from_eid(self.eidto).expression
+            MemSchemaPermRQLExpressionAdd(self._cw, perm, self.eidfrom, expr)
+
+
+class BeforeDelPermissionHook(AfterAddPermissionHook):
+    """delete entity/relation *_permission, need to update schema
+
+    skip the operation if the related type is being deleted
+    """
+    __id__ = 'syncdelperm'
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        if self._cw.deleted_in_transaction(self.eidfrom):
+            return
+        perm = self.rtype.split('_', 1)[0]
+        if self._cw.describe(self.eidto)[0] == 'CWGroup':
+            MemSchemaPermCWGroupDel(self._cw, perm, self.eidfrom, self.eidto)
+        else: # RQLExpression
+            expr = self._cw.entity_from_eid(self.eidto).expression
+            MemSchemaPermRQLExpressionDel(self._cw, perm, self.eidfrom, expr)
+
+
+# specializes synchronization hooks ############################################
+
+
+class AfterAddSpecializesHook(SyncSchemaHook):
+    __id__ = 'syncaddspecializes'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('specializes')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        MemSchemaSpecializesAdd(session, etypeeid=self.eidfrom,
+                                parentetypeeid=self.eidto)
+
+
+class AfterAddSpecializesHook(SyncSchemaHook):
+    __id__ = 'syncdelspecializes'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('specializes')
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        MemSchemaSpecializesDel(session, etypeeid=self.eidfrom,
+                                parentetypeeid=self.eidto)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/syncsession.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,234 @@
+"""Core hooks: synchronize living session on persistent data changes
+
+: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
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb import UnknownProperty, ValidationError, BadConnectionId
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+
+
+def get_user_sessions(repo, ueid):
+    for session in repo._sessions.values():
+        if ueid == session.user.eid:
+            yield session
+
+
+class SyncSessionHook(hook.Hook):
+    __abstract__ = True
+    category = 'syncsession'
+
+
+# user/groups synchronisation #################################################
+
+class _GroupOperation(hook.Operation):
+    """base class for group operation"""
+    geid = None
+    def __init__(self, session, *args, **kwargs):
+        """override to get the group name before actual groups manipulation:
+
+        we may temporarily loose right access during a commit event, so
+        no query should be emitted while comitting
+        """
+        rql = 'Any N WHERE G eid %(x)s, G name N'
+        result = session.execute(rql, {'x': kwargs['geid']}, 'x', build_descr=False)
+        hook.Operation.__init__(self, session, *args, **kwargs)
+        self.group = result[0][0]
+
+
+class _DeleteGroupOp(_GroupOperation):
+    """synchronize user when a in_group relation has been deleted"""
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        groups = self.cnxuser.groups
+        try:
+            groups.remove(self.group)
+        except KeyError:
+            self.error('user %s not in group %s',  self.cnxuser, self.group)
+            return
+
+
+class _AddGroupOp(_GroupOperation):
+    """synchronize user when a in_group relation has been added"""
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        groups = self.cnxuser.groups
+        if self.group in groups:
+            self.warning('user %s already in group %s', self.cnxuser,
+                         self.group)
+            return
+        groups.add(self.group)
+
+
+class SyncInGroupHook(SyncSessionHook):
+    __id__ = 'syncingroup'
+    __select__ = SyncSessionHook.__select__ & hook.match_rtype('in_group')
+    events = ('after_delete_relation', 'after_add_relation')
+
+    def __call__(self):
+        if self.event == 'after_delete_relation':
+            opcls = _DeleteGroupOp
+        else:
+            opcls = _AddGroupOp
+        for session in get_user_sessions(self._cw.repo, self.eidfrom):
+            opcls(self._cw, cnxuser=session.user, geid=self.eidto)
+
+
+class _DelUserOp(hook.Operation):
+    """close associated user's session when it is deleted"""
+    def __init__(self, session, cnxid):
+        self.cnxid = cnxid
+        hook.Operation.__init__(self, session)
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            self.repo.close(self.cnxid)
+        except BadConnectionId:
+            pass # already closed
+
+
+class CloseDeletedUserSessionsHook(SyncSessionHook):
+    __id__ = 'closession'
+    __select__ = SyncSessionHook.__select__ & entity_implements('CWUser')
+    events = ('after_delete_entity',)
+
+    def __call__(self):
+        """modify user permission, need to update users"""
+        for session in get_user_sessions(self._cw.repo, self.entity.eid):
+            _DelUserOp(self._cw, session.id)
+
+
+# CWProperty hooks #############################################################
+
+
+class _DelCWPropertyOp(hook.Operation):
+    """a user's custom properties has been deleted"""
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            del self.cwpropdict[self.key]
+        except KeyError:
+            self.error('%s has no associated value', self.key)
+
+
+class _ChangeCWPropertyOp(hook.Operation):
+    """a user's custom properties has been added/changed"""
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        self.cwpropdict[self.key] = self.value
+
+
+class _AddCWPropertyOp(hook.Operation):
+    """a user's custom properties has been added/changed"""
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        cwprop = self.cwprop
+        if not cwprop.for_user:
+            self.repo.vreg['propertyvalues'][cwprop.pkey] = cwprop.value
+        # if for_user is set, update is handled by a ChangeCWPropertyOp operation
+
+
+class AddCWPropertyHook(SyncSessionHook):
+    __id__ = 'addcwprop'
+    __select__ = SyncSessionHook.__select__ & entity_implements('CWProperty')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        key, value = self.entity.pkey, self.entity.value
+        session = self._cw
+        try:
+            value = session.vreg.typed_value(key, value)
+        except UnknownProperty:
+            raise ValidationError(self.entity.eid,
+                                  {'pkey': session._('unknown property key')})
+        except ValueError, ex:
+            raise ValidationError(self.entity.eid,
+                                  {'value': session._(str(ex))})
+        if not session.user.matching_groups('managers'):
+            session.add_relation(entity.eid, 'for_user', session.user.eid)
+        else:
+            _AddCWPropertyOp(session, cwprop=self.entity)
+
+
+class UpdateCWPropertyHook(AddCWPropertyHook):
+    __id__ = 'updatecwprop'
+    events = ('after_update_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        if not ('pkey' in entity.edited_attributes or
+                'value' in entity.edited_attributes):
+            return
+        key, value = entity.pkey, entity.value
+        session = self._cw
+        try:
+            value = session.vreg.typed_value(key, value)
+        except UnknownProperty:
+            return
+        except ValueError, ex:
+            raise ValidationError(entity.eid, {'value': session._(str(ex))})
+        if entity.for_user:
+            for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
+                _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
+                                  key=key, value=value)
+        else:
+            # site wide properties
+            _ChangeCWPropertyOp(session, cwpropdict=session.vreg['propertyvalues'],
+                              key=key, value=value)
+
+
+class DeleteCWPropertyHook(AddCWPropertyHook):
+    __id__ = 'delcwprop'
+    events = ('before_delete_entity',)
+
+    def __call__(self):
+        eid = self.entity.eid
+        session = self._cw
+        for eidfrom, rtype, eidto in session.transaction_data.get('pendingrelations', ()):
+            if rtype == 'for_user' and eidfrom == self.entity.eid:
+                # if for_user was set, delete has already been handled
+                break
+        else:
+            _DelCWPropertyOp(session, cwpropdict=session.vreg['propertyvalues'], key=entity.pkey)
+
+
+class AddForUserRelationHook(SyncSessionHook):
+    __id__ = 'addcwpropforuser'
+    __select__ = SyncSessionHook.__select__ & hook.match_rtype('for_user')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        session = self._cw
+        eidfrom = self.eidfrom
+        if not session.describe(eidfrom)[0] == 'CWProperty':
+            return
+        key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
+                                     {'x': eidfrom}, 'x')[0]
+        if session.vreg.property_info(key)['sitewide']:
+            raise ValidationError(eidfrom,
+                                  {'for_user': session._("site-wide property can't be set for user")})
+        for session_ in get_user_sessions(session.repo, self.eidto):
+            _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
+                              key=key, value=value)
+
+
+class DelForUserRelationHook(AddForUserRelationHook):
+    __id__ = 'delcwpropforuser'
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        session = self._cw
+        key = session.execute('Any K WHERE P eid %(x)s, P pkey K',
+                              {'x': self.eidfrom}, 'x')[0][0]
+        session.transaction_data.setdefault('pendingrelations', []).append(
+            (self.eidfrom, self.rtype, self.eidto))
+        for session_ in get_user_sessions(session.repo, self.eidto):
+            _DelCWPropertyOp(session, cwpropdict=session_.user.properties, key=key)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/workflow.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,337 @@
+"""Core hooks: workflow related hooks
+
+: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
+"""
+__docformat__ = "restructuredtext en"
+
+from datetime import datetime
+
+from cubicweb import RepositoryError, ValidationError
+from cubicweb.interfaces import IWorkflowable
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+from cubicweb.entities.wfobjs import WorkflowTransition
+
+
+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,
+    # unless in_state isn't stored in the system source
+    fromsource = session.describe(x)[1]
+    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)
+
+
+# operations ###################################################################
+
+class _SetInitialStateOp(hook.Operation):
+    """make initial state be a default state"""
+
+    def precommit_event(self):
+        session = self.session
+        entity = self.entity
+        # if there is an initial state and the entity's state is not set,
+        # use the initial state as a default state
+        if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
+               and entity.current_workflow:
+            state = entity.current_workflow.initial
+            if state:
+                # use super session to by-pass security checks
+                session.super_session.add_relation(entity.eid, 'in_state',
+                                                   state.eid)
+
+class _WorkflowChangedOp(hook.Operation):
+    """fix entity current state when changing its workflow"""
+
+    def precommit_event(self):
+        # 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')
+
+
+class _CheckTrExitPoint(hook.Operation):
+
+    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)
+
+
+# hooks ########################################################################
+
+class WorkflowHook(hook.Hook):
+    __abstract__ = True
+    category = 'worfklow'
+
+
+class SetInitialStateHook(WorkflowHook):
+    __id__ = 'wfsetinitial'
+    __select__ = WorkflowHook.__select__ & entity_implements(IWorkflowable)
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        _SetInitialStateOp(self._cw, entity=self.entity)
+
+
+class PrepareStateChangeHook(WorkflowHook):
+    """record previous state information"""
+    __id__ = 'cwdelstate'
+    __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        self._cw.transaction_data.setdefault('pendingrelations', []).append(
+            (self.eidfrom, self.rtype, self.eidto))
+
+
+class FireTransitionHook(WorkflowHook):
+    """check the transition is allowed, add missing information. Expect that:
+    * wf_info_for inlined relation is set
+    * by_transition or to_state (managers only) inlined relation is set
+    """
+    __id__ = 'wffiretransition'
+    __select__ = WorkflowHook.__select__ & entity_implements('TrInfo')
+    events = ('before_add_entity',)
+
+    def __call__(self):
+        session = self._cw
+        entity = self.entity
+        # first retreive entity to which the state change apply
+        try:
+            foreid = entity['wf_info_for']
+        except KeyError:
+            msg = session._('mandatory relation')
+            raise ValidationError(entity.eid, {'wf_info_for': msg})
+        forentity = session.entity_from_eid(foreid)
+        # then check it has a workflow set, 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:
+                deststateeid = tr.destination().eid
+        # everything is ok, add missing information on the trinfo entity
+        entity['from_state'] = fromstate.eid
+        entity['to_state'] = deststateeid
+        nocheck = session.transaction_data.setdefault('skip-security', set())
+        nocheck.add((entity.eid, 'from_state', fromstate.eid))
+        nocheck.add((entity.eid, 'to_state', deststateeid))
+
+
+class FiredTransitionHook(WorkflowHook):
+    """change related entity state"""
+    __id__ = 'wffiretransition'
+    __select__ = WorkflowHook.__select__ & entity_implements('TrInfo')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        session = self._cw
+        entity = self.entity
+        _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']
+        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._cw
+                forentity._cw = session.super_session
+                try:
+                    trinfo = forentity.change_state(tostate, msg, u'text/plain',
+                                                    tr=wftr)
+                finally:
+                    forentity._cw = req
+
+
+class CheckInStateChangeAllowed(WorkflowHook):
+    """check state apply, in case of direct in_state change using unsafe_execute
+    """
+    __id__ = 'wfcheckinstate'
+    __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
+    events = ('before_add_relation',)
+
+    def __call__(self):
+        session = self._cw
+        nocheck = session.transaction_data.setdefault('skip-security', ())
+        if (self.eidfrom, 'in_state', self.eidto) in nocheck:
+            # state changed through TrInfo insertion, so we already know it's ok
+            return
+        entity = session.entity_from_eid(self.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(self.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(self.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(self.eidfrom, {'in_state': msg})
+
+
+class SetModificationDateOnStateChange(WorkflowHook):
+    """update entity's modification date after changing its state"""
+    __id__ = 'wfsyncmdate'
+    __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        if self._cw.added_in_transaction(self.eidfrom):
+            # new entity, not needed
+            return
+        entity = self._cw.entity_from_eid(self.eidfrom)
+        try:
+            entity.set_attributes(modification_date=datetime.now(),
+                                  _cw_unsafe=True)
+        except RepositoryError, ex:
+            # usually occurs if entity is coming from a read-only source
+            # (eg ldap user)
+            self.warning('cant change modification date for %s: %s', entity, ex)
+
+
+class CheckWorkflowTransitionExitPoint(WorkflowHook):
+    """check that there is no multiple exits from the same state"""
+    __id__ = 'wfcheckwftrexit'
+    __select__ = WorkflowHook.__select__ & hook.match_rtype('subworkflow_exit')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        _CheckTrExitPoint(self._cw, treid=self.eidfrom)
+
+
+class SetCustomWorkflow(WorkflowHook):
+    __id__ = 'wfsetcustom'
+    __select__ = WorkflowHook.__select__ & hook.match_rtype('custom_workflow')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=self.eidto)
+
+
+class DelCustomWorkflow(SetCustomWorkflow):
+    __id__ = 'wfdelcustom'
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        entity = self._cw.entity_from_eid(self.eidfrom)
+        typewf = entity.cwetype_workflow()
+        if typewf is not None:
+            _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid)
+
+
+
+class DelWorkflowHook(WorkflowHook):
+    __id__ = 'wfdel'
+    __select__ = WorkflowHook.__select__ & entity_implements('Workflow')
+    events = ('after_delete_entity',)
+
+    def __call__(self):
+        # cleanup unused state and transition
+        self._cw.execute('DELETE State X WHERE NOT X state_of Y')
+        self._cw.execute('DELETE Transition X WHERE NOT X transition_of Y')
+
--- a/i18n/en.po	Wed Aug 05 09:15:56 2009 +0200
+++ b/i18n/en.po	Tue Sep 22 13:08:42 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"
@@ -75,31 +75,31 @@
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;days"
+msgid "%d&#160;days"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;hours"
+msgid "%d&#160;hours"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;minutes"
+msgid "%d&#160;minutes"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;months"
+msgid "%d&#160;months"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;seconds"
+msgid "%d&#160;seconds"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;weeks"
+msgid "%d&#160;weeks"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;years"
+msgid "%d&#160;years"
 msgstr ""
 
 #, python-format
@@ -193,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"
 
@@ -347,6 +356,9 @@
 msgid "Interval_plural"
 msgstr "Intervals"
 
+msgid "New BaseTransition"
+msgstr "XXX"
+
 msgid "New Bookmark"
 msgstr "New bookmark"
 
@@ -354,7 +366,7 @@
 msgstr "New attribute"
 
 msgid "New CWCache"
-msgstr ""
+msgstr "New cache"
 
 msgid "New CWConstraint"
 msgstr "New constraint"
@@ -395,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 ""
 
@@ -431,6 +452,9 @@
 msgid "Recipients:"
 msgstr ""
 
+msgid "Registry's content"
+msgstr ""
+
 msgid "Relations"
 msgstr ""
 
@@ -465,6 +489,12 @@
 msgid "String_plural"
 msgstr "Strings"
 
+msgid "SubWorkflowExitPoint"
+msgstr "Subworkflow exit-point"
+
+msgid "SubWorkflowExitPoint_plural"
+msgstr "subworkflow exit-points"
+
 msgid "Subject:"
 msgstr ""
 
@@ -485,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"
@@ -499,7 +525,7 @@
 msgstr "This attribute"
 
 msgid "This CWCache"
-msgstr ""
+msgstr "This cache"
 
 msgid "This CWConstraint"
 msgstr "This constraint"
@@ -507,9 +533,17 @@
 msgid "This CWConstraintType"
 msgstr "This constraint type"
 
+msgctxt "inlined:CWRelation.from_entity.subject"
+msgid "This CWEType"
+msgstr ""
+
 msgid "This CWEType"
 msgstr "This entity type"
 
+msgctxt "inlined:CWRelation.to_entity.subject"
+msgid "This CWEType"
+msgstr ""
+
 msgid "This CWGroup"
 msgstr "This group"
 
@@ -522,12 +556,20 @@
 msgid "This CWRType"
 msgstr "This relation type"
 
+msgctxt "inlined:CWRelation.relation_type.subject"
+msgid "This CWRType"
+msgstr "This relation type"
+
 msgid "This CWRelation"
 msgstr "This relation"
 
 msgid "This CWUser"
 msgstr "This user"
 
+msgctxt "inlined:CWUser.use_email.subject"
+msgid "This EmailAddress"
+msgstr "email address"
+
 msgid "This EmailAddress"
 msgstr "This email address"
 
@@ -540,12 +582,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"
 
@@ -577,9 +628,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 ""
 
@@ -601,7 +664,9 @@
 msgid "You can use any of the following substitutions in your text"
 msgstr ""
 
-msgid "You have no access to this view or it's not applyable to current data"
+msgid ""
+"You have no access to this view or it can not be used to display the current "
+"data."
 msgstr ""
 
 msgid ""
@@ -613,9 +678,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 "
@@ -635,12 +697,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 ""
 
@@ -653,6 +715,12 @@
 msgid "actions_addentity_description"
 msgstr ""
 
+msgid "actions_addrelated"
+msgstr ""
+
+msgid "actions_addrelated_description"
+msgstr ""
+
 msgid "actions_cancel"
 msgstr "cancel the selection"
 
@@ -839,7 +907,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"
@@ -851,63 +922,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.to_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.from_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 ""
 
@@ -922,9 +964,27 @@
 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 "add permission"
+
+msgctxt "CWRType"
+msgid "add_permission"
+msgstr "add permission"
+
+msgctxt "RQLExpression"
+msgid "add_permission_object"
+msgstr "used to define add permission on"
+
 msgid "add_permission_object"
 msgstr "has permission to add"
 
+msgctxt "CWGroup"
+msgid "add_permission_object"
+msgstr "can add"
+
 #, python-format
 msgid "added %(etype)s #%(eid)s (%(title)s)"
 msgstr ""
@@ -935,12 +995,26 @@
 "(toeid)s"
 msgstr ""
 
+msgid "addrelated"
+msgstr ""
+
 msgid "address"
 msgstr ""
 
+msgctxt "EmailAddress"
+msgid "address"
+msgstr "address"
+
+msgctxt "EmailAddress"
+msgid "alias"
+msgstr "alias"
+
 msgid "alias"
 msgstr ""
 
+msgid "allow to set a specific workflow for an entity"
+msgstr ""
+
 msgid "allowed transition from this state"
 msgstr ""
 
@@ -950,6 +1024,22 @@
 msgid "allowed_transition"
 msgstr "allowed transition"
 
+msgctxt "State"
+msgid "allowed_transition"
+msgstr "allowed transition"
+
+msgid "allowed_transition_object"
+msgstr "incoming states"
+
+msgctxt "WorkflowTransition"
+msgid "allowed_transition_object"
+msgstr "incoming states"
+
+msgctxt "Transition"
+msgid "allowed_transition_object"
+msgstr "incoming states"
+
+msgctxt "BaseTransition"
 msgid "allowed_transition_object"
 msgstr "incoming states"
 
@@ -1029,6 +1119,14 @@
 msgid "bookmarked_by"
 msgstr "bookmarked by"
 
+msgctxt "Bookmark"
+msgid "bookmarked_by"
+msgstr "bookmarked by"
+
+msgctxt "CWUser"
+msgid "bookmarked_by_object"
+msgstr "uses bookmarks"
+
 msgid "bookmarked_by_object"
 msgstr "has bookmarks"
 
@@ -1113,6 +1211,28 @@
 msgid "by relation"
 msgstr ""
 
+msgctxt "TrInfo"
+msgid "by_transition"
+msgstr "by transition"
+
+msgid "by_transition"
+msgstr "by transition"
+
+msgctxt "WorkflowTransition"
+msgid "by_transition_object"
+msgstr "transition information"
+
+msgctxt "BaseTransition"
+msgid "by_transition_object"
+msgstr "transition information"
+
+msgctxt "Transition"
+msgid "by_transition_object"
+msgstr "transition information"
+
+msgid "by_transition_object"
+msgstr "transition information"
+
 msgid "calendar"
 msgstr ""
 
@@ -1143,6 +1263,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="
@@ -1155,11 +1278,16 @@
 msgid "cancel this insert"
 msgstr ""
 
-msgid "canonical"
-msgstr ""
+msgctxt "CWRelation"
+msgid "cardinality"
+msgstr "cardinality"
 
 msgid "cardinality"
-msgstr ""
+msgstr "cardinality"
+
+msgctxt "CWAttribute"
+msgid "cardinality"
+msgstr "cardinality"
 
 msgid "category"
 msgstr ""
@@ -1183,9 +1311,14 @@
 msgid "comment"
 msgstr ""
 
-msgid "comment:"
-msgstr ""
-
+msgctxt "TrInfo"
+msgid "comment"
+msgstr "comment"
+
+msgid "comment_format"
+msgstr "format"
+
+msgctxt "TrInfo"
 msgid "comment_format"
 msgstr "format"
 
@@ -1237,6 +1370,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"
 
@@ -1246,21 +1385,53 @@
 msgid "composite"
 msgstr ""
 
+msgctxt "CWRelation"
+msgid "composite"
+msgstr "composite"
+
+msgctxt "WorkflowTransition"
 msgid "condition"
-msgstr ""
+msgstr "condition"
+
+msgctxt "Transition"
+msgid "condition"
+msgstr "condition"
+
+msgid "condition"
+msgstr "condition"
+
+msgctxt "BaseTransition"
+msgid "condition"
+msgstr "condition"
 
 msgid "condition:"
-msgstr ""
+msgstr "condtion:"
 
 msgid "condition_object"
 msgstr "condition of"
 
+msgctxt "RQLExpression"
+msgid "condition_object"
+msgstr ""
+
 msgid "confirm password"
 msgstr ""
 
 msgid "constrained_by"
 msgstr "constrained by"
 
+msgctxt "CWAttribute"
+msgid "constrained_by"
+msgstr "constrained by"
+
+msgctxt "CWRelation"
+msgid "constrained_by"
+msgstr "constrained by"
+
+msgid "constrained_by_object"
+msgstr "constraints"
+
+msgctxt "CWConstraint"
 msgid "constrained_by_object"
 msgstr "constraints"
 
@@ -1436,23 +1607,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 ""
@@ -1463,12 +1654,20 @@
 msgid "creation_date"
 msgstr "creation date"
 
+msgctxt "CWConstraint"
+msgid "cstrtype"
+msgstr "constraint type"
+
 msgid "cstrtype"
 msgstr "constraint's type"
 
 msgid "cstrtype_object"
 msgstr "used by"
 
+msgctxt "CWConstraintType"
+msgid "cstrtype_object"
+msgstr "constraint type of"
+
 msgid "csv entities export"
 msgstr ""
 
@@ -1479,6 +1678,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"
 
@@ -1515,6 +1720,30 @@
 msgid "default text format for rich text fields."
 msgstr ""
 
+msgid "default user workflow"
+msgstr ""
+
+msgid "default workflow for an entity type"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "default_workflow"
+msgstr "default workflow"
+
+msgid "default_workflow"
+msgstr "default workflow"
+
+msgid "default_workflow_object"
+msgstr "default workflow of"
+
+msgctxt "Workflow"
+msgid "default_workflow_object"
+msgstr "default workflow of"
+
+msgid "defaultval"
+msgstr "default value"
+
+msgctxt "CWAttribute"
 msgid "defaultval"
 msgstr "default value"
 
@@ -1549,6 +1778,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"
@@ -1572,6 +1804,22 @@
 msgid "delete_permission"
 msgstr "can be deleted by"
 
+msgctxt "CWRType"
+msgid "delete_permission"
+msgstr "delete permission"
+
+msgctxt "CWEType"
+msgid "delete_permission"
+msgstr "delete permission"
+
+msgid "delete_permission_object"
+msgstr "has permission to delete"
+
+msgctxt "RQLExpression"
+msgid "delete_permission_object"
+msgstr "has permission to delete"
+
+msgctxt "CWGroup"
 msgid "delete_permission_object"
 msgstr "has permission to delete"
 
@@ -1591,9 +1839,84 @@
 msgid "description"
 msgstr ""
 
+msgctxt "Transition"
+msgid "description"
+msgstr "description"
+
+msgctxt "Workflow"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWRelation"
+msgid "description"
+msgstr "description"
+
+msgctxt "WorkflowTransition"
+msgid "description"
+msgstr ""
+
+msgctxt "State"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWEType"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWRType"
+msgid "description"
+msgstr "description"
+
+msgctxt "BaseTransition"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWAttribute"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWRelation"
 msgid "description_format"
 msgstr "format"
 
+msgid "description_format"
+msgstr "format"
+
+msgctxt "CWEType"
+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 ""
+
 msgid "destination state for this transition"
 msgstr ""
 
@@ -1603,6 +1926,18 @@
 msgid "destination_state"
 msgstr "destination state"
 
+msgctxt "SubWorkflowExitPoint"
+msgid "destination_state"
+msgstr "destination state"
+
+msgctxt "Transition"
+msgid "destination_state"
+msgstr ""
+
+msgctxt "State"
+msgid "destination_state_object"
+msgstr "destination of"
+
 msgid "destination_state_object"
 msgstr "destination of"
 
@@ -1631,6 +1966,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"
@@ -1639,10 +1977,14 @@
 msgid "download"
 msgstr ""
 
+#, python-format
+msgid "download %s"
+msgstr ""
+
 msgid "download icon"
 msgstr ""
 
-msgid "download image"
+msgid "download page as pdf"
 msgstr ""
 
 msgid "download schema as owl"
@@ -1651,6 +1993,9 @@
 msgid "edit bookmarks"
 msgstr ""
 
+msgid "edit canceled"
+msgstr ""
+
 msgid "edit the index page"
 msgstr ""
 
@@ -1699,6 +2044,9 @@
 msgid "entity edited"
 msgstr ""
 
+msgid "entity has no workflow set"
+msgstr ""
+
 msgid "entity linked"
 msgstr ""
 
@@ -1710,10 +2058,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"
@@ -1733,15 +2078,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 ""
 
+msgctxt "RQLExpression"
+msgid "expression"
+msgstr ""
+
 msgid "expression"
 msgstr ""
 
 msgid "exprtype"
 msgstr "expression's type"
 
+msgctxt "RQLExpression"
+msgid "exprtype"
+msgstr "expression type"
+
 msgid "external page"
 msgstr ""
 
@@ -1790,9 +2153,21 @@
 msgid "file tree view"
 msgstr ""
 
+msgctxt "CWEType"
 msgid "final"
 msgstr ""
 
+msgid "final"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "final"
+msgstr ""
+
+msgctxt "CWUser"
+msgid "firstname"
+msgstr ""
+
 msgid "firstname"
 msgstr ""
 
@@ -1802,9 +2177,17 @@
 msgid "follow"
 msgstr ""
 
+msgctxt "CWProperty"
+msgid "for_user"
+msgstr ""
+
 msgid "for_user"
 msgstr "for user"
 
+msgctxt "CWUser"
+msgid "for_user_object"
+msgstr ""
+
 msgid "for_user_object"
 msgstr "use properties"
 
@@ -1821,6 +2204,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 "subjec relation"
+
 msgid "from_entity_object"
 msgstr "subjet relation"
 
@@ -1830,13 +2225,29 @@
 msgid "from_state"
 msgstr "from state"
 
+msgctxt "TrInfo"
+msgid "from_state"
+msgstr "from state"
+
+msgctxt "State"
+msgid "from_state_object"
+msgstr "transitions from this state"
+
 msgid "from_state_object"
 msgstr "transitions from this state"
 
 msgid "full text or RQL query"
 msgstr ""
 
+msgctxt "CWRType"
 msgid "fulltext_container"
+msgstr "fulltext container"
+
+msgid "fulltext_container"
+msgstr "fulltext container"
+
+msgctxt "CWAttribute"
+msgid "fulltextindexed"
 msgstr ""
 
 msgid "fulltextindexed"
@@ -1860,6 +2271,10 @@
 msgid "granted to groups"
 msgstr ""
 
+#, python-format
+msgid "graphical representation of %s"
+msgstr ""
+
 msgid "graphical representation of the instance'schema"
 msgstr ""
 
@@ -1930,12 +2345,6 @@
 msgid "how to format time in the ui (\"man strftime\" for format description)"
 msgstr ""
 
-msgid "html class of the component"
-msgstr ""
-
-msgid "htmlclass"
-msgstr ""
-
 msgid "i18n_login_popup"
 msgstr "login"
 
@@ -1954,6 +2363,9 @@
 msgid "id of main template used to render pages"
 msgstr ""
 
+msgid "identical to"
+msgstr ""
+
 msgid "identical_to"
 msgstr "identical to"
 
@@ -1977,9 +2389,17 @@
 msgid "in memory relation schema"
 msgstr ""
 
+msgctxt "CWUser"
 msgid "in_group"
 msgstr "in group"
 
+msgid "in_group"
+msgstr "in group"
+
+msgctxt "CWGroup"
+msgid "in_group_object"
+msgstr "contains"
+
 msgid "in_group_object"
 msgstr "contains"
 
@@ -2005,6 +2425,10 @@
 msgid "indexed"
 msgstr ""
 
+msgctxt "CWAttribute"
+msgid "indexed"
+msgstr "indexed"
+
 msgid "indicate the current state of an entity"
 msgstr ""
 
@@ -2020,18 +2444,30 @@
 msgid "initial estimation %s"
 msgstr ""
 
-msgid "initial state for entities of this type"
-msgstr ""
+msgid "initial state for this workflow"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "initial_state"
+msgstr "initial state"
 
 msgid "initial_state"
 msgstr "initial state"
 
+msgctxt "State"
+msgid "initial_state_object"
+msgstr "initial state of"
+
 msgid "initial_state_object"
 msgstr "initial state of"
 
 msgid "inlined"
 msgstr ""
 
+msgctxt "CWRType"
+msgid "inlined"
+msgstr "inlined"
+
 msgid "instance schema"
 msgstr ""
 
@@ -2041,6 +2477,10 @@
 msgid "internationalizable"
 msgstr ""
 
+msgctxt "CWAttribute"
+msgid "internationalizable"
+msgstr ""
+
 #, python-format
 msgid "invalid action %r"
 msgstr ""
@@ -2048,6 +2488,12 @@
 msgid "invalid date"
 msgstr ""
 
+msgid "invalid float value"
+msgstr ""
+
+msgid "invalid integer value"
+msgstr ""
+
 msgid "is"
 msgstr ""
 
@@ -2071,7 +2517,7 @@
 msgstr ""
 
 msgid "is_instance_of_object"
-msgstr ""
+msgstr "is instance of"
 
 msgid "is_object"
 msgstr "has instances"
@@ -2088,6 +2534,10 @@
 msgid "label"
 msgstr ""
 
+msgctxt "CWPermission"
+msgid "label"
+msgstr "label"
+
 msgid "language of the user interface"
 msgstr ""
 
@@ -2097,6 +2547,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 ""
 
@@ -2125,13 +2579,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"
@@ -2149,6 +2606,10 @@
 msgid "login"
 msgstr ""
 
+msgctxt "CWUser"
+msgid "login"
+msgstr "login"
+
 msgid "login or email"
 msgstr ""
 
@@ -2165,8 +2626,12 @@
 msgid "main informations"
 msgstr ""
 
+msgctxt "RQLExpression"
 msgid "mainvars"
-msgstr ""
+msgstr "main vars"
+
+msgid "mainvars"
+msgstr "main vars"
 
 msgid "manage"
 msgstr ""
@@ -2183,6 +2648,9 @@
 msgid "managers"
 msgstr ""
 
+msgid "mandatory relation"
+msgstr ""
+
 msgid "march"
 msgstr ""
 
@@ -2226,6 +2694,50 @@
 msgid "my custom search"
 msgstr ""
 
+msgctxt "CWPermission"
+msgid "name"
+msgstr "name"
+
+msgctxt "State"
+msgid "name"
+msgstr "name"
+
+msgctxt "BaseTransition"
+msgid "name"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "name"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "name"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "name"
+msgstr ""
+
+msgctxt "CWCache"
+msgid "name"
+msgstr ""
+
+msgid "name"
+msgstr ""
+
+msgctxt "CWConstraintType"
+msgid "name"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "name"
+msgstr ""
+
+msgctxt "Transition"
+msgid "name"
+msgstr ""
+
+msgctxt "Workflow"
 msgid "name"
 msgstr ""
 
@@ -2298,9 +2810,6 @@
 msgid "not selected"
 msgstr ""
 
-msgid "not the initial state for this entity"
-msgstr ""
-
 msgid "nothing to edit"
 msgstr ""
 
@@ -2334,6 +2843,14 @@
 msgid "order"
 msgstr ""
 
+msgctxt "CWRelation"
+msgid "ordernum"
+msgstr "order"
+
+msgctxt "CWAttribute"
+msgid "ordernum"
+msgstr "order"
+
 msgid "ordernum"
 msgstr "order"
 
@@ -2371,6 +2888,10 @@
 msgid "path"
 msgstr ""
 
+msgctxt "Bookmark"
+msgid "path"
+msgstr "path"
+
 msgid "permission"
 msgstr ""
 
@@ -2389,6 +2910,10 @@
 msgid "pick existing bookmarks"
 msgstr ""
 
+msgctxt "CWProperty"
+msgid "pkey"
+msgstr "key"
+
 msgid "pkey"
 msgstr "key"
 
@@ -2401,6 +2926,23 @@
 msgid "possible views"
 msgstr ""
 
+msgid "powered by CubicWeb"
+msgstr ""
+
+msgid "prefered_form"
+msgstr ""
+
+msgctxt "EmailAddress"
+msgid "prefered_form"
+msgstr "prefered form"
+
+msgctxt "EmailAddress"
+msgid "prefered_form_object"
+msgstr "prefered over"
+
+msgid "prefered_form_object"
+msgstr "prefered over"
+
 msgid "preferences"
 msgstr ""
 
@@ -2413,9 +2955,17 @@
 msgid "primary_email"
 msgstr "primary email"
 
+msgctxt "CWUser"
+msgid "primary_email"
+msgstr "primary email"
+
 msgid "primary_email_object"
 msgstr "primary email of"
 
+msgctxt "EmailAddress"
+msgid "primary_email_object"
+msgstr ""
+
 msgid "progress"
 msgstr ""
 
@@ -2429,7 +2979,15 @@
 msgstr ""
 
 msgid "read_perm"
-msgstr ""
+msgstr "read perm"
+
+msgctxt "CWEType"
+msgid "read_permission"
+msgstr "read permission"
+
+msgctxt "CWRType"
+msgid "read_permission"
+msgstr "read permission"
 
 msgid "read_permission"
 msgstr "can be read by"
@@ -2437,6 +2995,23 @@
 msgid "read_permission_object"
 msgstr "has permission to delete"
 
+msgctxt "CWGroup"
+msgid "read_permission_object"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "read_permission_object"
+msgstr ""
+
+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 ""
@@ -2444,9 +3019,21 @@
 msgid "relation_type"
 msgstr "relation type"
 
+msgctxt "CWAttribute"
+msgid "relation_type"
+msgstr "relation type"
+
+msgctxt "CWRelation"
+msgid "relation_type"
+msgstr "relation type"
+
 msgid "relation_type_object"
 msgstr "relation definitions"
 
+msgctxt "CWRType"
+msgid "relation_type_object"
+msgstr ""
+
 msgid "relations"
 msgstr ""
 
@@ -2456,71 +3043,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 ""
+
+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"
-
+msgstr ""
+
+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"
-
+msgstr ""
+
+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"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "require_group"
+msgstr "require group"
 
 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 "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 ""
@@ -2637,6 +3206,9 @@
 msgid "semantic description of this transition"
 msgstr ""
 
+msgid "semantic description of this workflow"
+msgstr ""
+
 msgid "send email"
 msgstr ""
 
@@ -2689,9 +3261,20 @@
 msgid "sparql xml"
 msgstr ""
 
+msgid "special transition allowing to go through a sub-workflow"
+msgstr ""
+
 msgid "specializes"
 msgstr ""
 
+msgctxt "CWEType"
+msgid "specializes"
+msgstr ""
+
+msgid "specializes_object"
+msgstr "specialized by"
+
+msgctxt "CWEType"
 msgid "specializes_object"
 msgstr ""
 
@@ -2701,9 +3284,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 ""
+
+msgctxt "State"
+msgid "state_of"
+msgstr ""
+
 msgid "state_of"
 msgstr "state of"
 
+msgctxt "Workflow"
+msgid "state_of_object"
+msgstr ""
+
 msgid "state_of_object"
 msgstr "use states"
 
@@ -2726,12 +3328,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"
+
+msgctxt "SubWorkflowExitPoint"
+msgid "subworkflow_state"
+msgstr "subworkflow state"
+
+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 ""
 
@@ -2790,6 +3445,10 @@
 msgid "timestamp"
 msgstr ""
 
+msgctxt "CWCache"
+msgid "timestamp"
+msgstr ""
+
 msgid "timestamp of the latest source synchronization."
 msgstr ""
 
@@ -2799,6 +3458,10 @@
 msgid "title"
 msgstr ""
 
+msgctxt "Bookmark"
+msgid "title"
+msgstr ""
+
 msgid "to"
 msgstr ""
 
@@ -2809,33 +3472,75 @@
 msgid "to associate with"
 msgstr ""
 
+msgctxt "CWRelation"
+msgid "to_entity"
+msgstr "to entity"
+
+msgctxt "CWAttribute"
 msgid "to_entity"
 msgstr "to entity"
 
+msgid "to_entity"
+msgstr "to entity"
+
+msgctxt "CWEType"
+msgid "to_entity_object"
+msgstr ""
+
 msgid "to_entity_object"
 msgstr "object relations"
 
 msgid "to_interval_end"
 msgstr "to"
 
+msgctxt "TrInfo"
+msgid "to_state"
+msgstr ""
+
 msgid "to_state"
 msgstr "to state"
 
 msgid "to_state_object"
 msgstr "transitions to this state"
 
+msgctxt "State"
+msgid "to_state_object"
+msgstr ""
+
 msgid "todo_by"
 msgstr "to do by"
 
 msgid "toggle check boxes"
 msgstr ""
 
-msgid "transition 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 ""
+
+msgctxt "BaseTransition"
+msgid "transition_of"
+msgstr "transition of"
+
+msgctxt "Transition"
+msgid "transition_of"
+msgstr "transition of"
+
+msgctxt "WorkflowTransition"
+msgid "transition_of"
 msgstr ""
 
 msgid "transition_of"
 msgstr "transition of"
 
+msgctxt "Workflow"
+msgid "transition_of_object"
+msgstr ""
+
 msgid "transition_of_object"
 msgstr "use transitions"
 
@@ -2905,6 +3610,10 @@
 msgid "up"
 msgstr ""
 
+msgctxt "CWUser"
+msgid "upassword"
+msgstr ""
+
 msgid "upassword"
 msgstr "password"
 
@@ -2917,13 +3626,29 @@
 msgid "update_permission"
 msgstr "can be updated by"
 
+msgctxt "CWEType"
+msgid "update_permission"
+msgstr ""
+
 msgid "update_permission_object"
 msgstr "has permission to update"
 
+msgctxt "RQLExpression"
+msgid "update_permission_object"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "update_permission_object"
+msgstr ""
+
 #, python-format
 msgid "updated %(etype)s #%(eid)s (%(title)s)"
 msgstr ""
 
+msgctxt "ExternalUri"
+msgid "uri"
+msgstr ""
+
 msgid "uri"
 msgstr ""
 
@@ -2935,9 +3660,17 @@
 "states in workflow's definitions."
 msgstr ""
 
+msgctxt "CWUser"
+msgid "use_email"
+msgstr ""
+
 msgid "use_email"
 msgstr "use email"
 
+msgctxt "EmailAddress"
+msgid "use_email_object"
+msgstr ""
+
 msgid "use_email_object"
 msgstr "used by"
 
@@ -2988,6 +3721,14 @@
 msgid "value"
 msgstr ""
 
+msgctxt "CWProperty"
+msgid "value"
+msgstr ""
+
+msgctxt "CWConstraint"
+msgid "value"
+msgstr ""
+
 msgid "value associated to this key is not editable manually"
 msgstr ""
 
@@ -3039,12 +3780,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 ""
+
+msgctxt "Workflow"
+msgid "workflow_of"
+msgstr ""
+
+msgid "workflow_of"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "workflow_of_object"
+msgstr ""
+
+msgid "workflow_of_object"
 msgstr ""
 
 msgid "xbel"
@@ -3065,47 +3835,18 @@
 msgid "you should probably delete that property"
 msgstr ""
 
-#~ msgid "Card"
-#~ msgstr "Card"
-
-#~ msgid "Card_plural"
-#~ msgstr "Cards"
-
-#~ msgid "New Card"
-#~ msgstr "New card"
-
-#~ msgid "This Card"
-#~ msgstr "This card"
-
-#~ msgid "add a Card"
-#~ msgstr "add a card"
-
-#~ msgid "components_applmessages"
-#~ msgstr "application messages"
-
-#~ msgid "components_applmessages_description"
-#~ msgstr "display the application messages"
-
-#~ msgid "content_format"
-#~ msgstr "content format"
-
-#~ msgid "hide meta-data"
-#~ msgstr "hide meta entities and relations"
-
-#~ msgid "meta"
-#~ msgstr "meta relation"
-
-#~ msgid "planned_delivery"
-#~ msgstr "planned delivery"
-
-#~ msgid "remove this Card"
-#~ msgstr "remove this card"
-
-#~ msgid "show meta-data"
-#~ msgstr "show the complete schema"
-
-#~ msgid "view_manage"
-#~ msgstr "site management"
-
-#~ msgid "wikiid"
-#~ msgstr "wiki identifier"
+#~ 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"
+
+#~ msgctxt "inlined:CWRelation:relation_type:subject"
+#~ msgid "remove this CWRType"
+#~ msgstr "remove this relation type"
+
+#~ msgctxt "inlined:CWUser:use_email:subject"
+#~ msgid "remove this EmailAddress"
+#~ msgstr "remove this email address"
--- a/i18n/es.po	Wed Aug 05 09:15:56 2009 +0200
+++ b/i18n/es.po	Tue Sep 22 13:08:42 2009 +0200
@@ -80,32 +80,32 @@
 msgstr "%d años"
 
 #, python-format
-msgid "%d&nbsp;days"
-msgstr "%d&nbsp;días"
+msgid "%d&#160;days"
+msgstr "%d&#160;días"
 
 #, python-format
-msgid "%d&nbsp;hours"
-msgstr "%d&nbsp;horas"
+msgid "%d&#160;hours"
+msgstr "%d&#160;horas"
 
 #, python-format
-msgid "%d&nbsp;minutes"
-msgstr "%d&nbsp;minutos"
+msgid "%d&#160;minutes"
+msgstr "%d&#160;minutos"
 
 #, python-format
-msgid "%d&nbsp;months"
-msgstr "%d&nbsp;meses"
+msgid "%d&#160;months"
+msgstr "%d&#160;meses"
 
 #, python-format
-msgid "%d&nbsp;seconds"
-msgstr "%d&nbsp;segundos"
+msgid "%d&#160;seconds"
+msgstr "%d&#160;segundos"
 
 #, python-format
-msgid "%d&nbsp;weeks"
-msgstr "%d&nbsp;semanas"
+msgid "%d&#160;weeks"
+msgstr "%d&#160;semanas"
 
 #, python-format
-msgid "%d&nbsp;years"
-msgstr "%d&nbsp;años"
+msgid "%d&#160;years"
+msgstr "%d&#160;años"
 
 #, python-format
 msgid "%s error report"
@@ -201,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"
 
@@ -355,6 +364,9 @@
 msgid "Interval_plural"
 msgstr "Duraciones"
 
+msgid "New BaseTransition"
+msgstr ""
+
 msgid "New Bookmark"
 msgstr "Agregar a Favoritos"
 
@@ -403,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"
 
@@ -439,6 +460,9 @@
 msgid "Recipients:"
 msgstr "Destinatarios"
 
+msgid "Registry's content"
+msgstr ""
+
 msgid "Relations"
 msgstr "Relaciones"
 
@@ -473,6 +497,12 @@
 msgid "String_plural"
 msgstr "Cadenas de caracteres"
 
+msgid "SubWorkflowExitPoint"
+msgstr ""
+
+msgid "SubWorkflowExitPoint_plural"
+msgstr ""
+
 msgid "Subject:"
 msgstr "Sujeto:"
 
@@ -493,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"
@@ -515,9 +541,17 @@
 msgid "This CWConstraintType"
 msgstr "Este tipo de Restricción"
 
+msgctxt "inlined:CWRelation.to_entity.subject"
+msgid "This CWEType"
+msgstr ""
+
 msgid "This CWEType"
 msgstr "Este tipo de Entidad"
 
+msgctxt "inlined:CWRelation.from_entity.subject"
+msgid "This CWEType"
+msgstr ""
+
 msgid "This CWGroup"
 msgstr "Este grupo"
 
@@ -527,6 +561,10 @@
 msgid "This CWProperty"
 msgstr "Esta propiedad"
 
+msgctxt "inlined:CWRelation.relation_type.subject"
+msgid "This CWRType"
+msgstr ""
+
 msgid "This CWRType"
 msgstr "Este tipo de relación"
 
@@ -539,6 +577,10 @@
 msgid "This EmailAddress"
 msgstr "Esta dirección electrónica"
 
+msgctxt "inlined:CWUser.use_email.subject"
+msgid "This EmailAddress"
+msgstr ""
+
 msgid "This ExternalUri"
 msgstr ""
 
@@ -548,12 +590,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"
 
@@ -585,9 +636,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 ""
 
@@ -618,8 +681,11 @@
 "Puede realizar cualquiera de las siguientes sustituciones en el contenido de "
 "su email."
 
-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 ""
+"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."
 
 msgid ""
 "You're not authorized to access this page. If you think you should, please "
@@ -632,9 +698,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 "
@@ -657,12 +720,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"
 
@@ -675,6 +738,12 @@
 msgid "actions_addentity_description"
 msgstr ""
 
+msgid "actions_addrelated"
+msgstr ""
+
+msgid "actions_addrelated_description"
+msgstr ""
+
 msgid "actions_cancel"
 msgstr "Anular"
 
@@ -820,28 +889,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"
@@ -850,85 +919,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.to_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.from_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"
@@ -941,12 +984,30 @@
 
 # subject and object forms for each relation type
 # (no object form for final relation types)
+msgctxt "CWEType"
+msgid "add_permission"
+msgstr ""
+
+# subject and object forms for each relation type
+# (no object form for final relation types)
 msgid "add_permission"
 msgstr "Autorización para agregar"
 
+msgctxt "CWRType"
+msgid "add_permission"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "add_permission_object"
+msgstr ""
+
 msgid "add_permission_object"
 msgstr "tiene la autorización para agregar"
 
+msgctxt "RQLExpression"
+msgid "add_permission_object"
+msgstr ""
+
 #, python-format
 msgid "added %(etype)s #%(eid)s (%(title)s)"
 msgstr "Agregado %(etype)s #%(eid)s (%(title)s)"
@@ -959,21 +1020,51 @@
 "Relación agregada %(rtype)s de %(frometype)s #%(fromeid)s hacia %(toetype)s #"
 "%(toeid)s"
 
+msgid "addrelated"
+msgstr ""
+
+msgctxt "EmailAddress"
+msgid "address"
+msgstr ""
+
 msgid "address"
 msgstr "dirección"
 
+msgctxt "EmailAddress"
+msgid "alias"
+msgstr ""
+
 msgid "alias"
 msgstr "alias"
 
+msgid "allow to set a specific workflow for an entity"
+msgstr ""
+
 msgid "allowed transition from this state"
 msgstr "transición autorizada desde este estado"
 
 msgid "allowed transitions from this state"
 msgstr "transiciones autorizadas desde este estado"
 
+msgctxt "State"
+msgid "allowed_transition"
+msgstr ""
+
 msgid "allowed_transition"
 msgstr "transición autorizada"
 
+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"
 
@@ -1055,6 +1146,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"
 
@@ -1140,6 +1239,28 @@
 msgid "by relation"
 msgstr "por relación"
 
+msgctxt "TrInfo"
+msgid "by_transition"
+msgstr ""
+
+msgid "by_transition"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "by_transition_object"
+msgstr ""
+
+msgctxt "Transition"
+msgid "by_transition_object"
+msgstr ""
+
+msgid "by_transition_object"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "by_transition_object"
+msgstr ""
+
 msgid "calendar"
 msgstr "mostrar un calendario"
 
@@ -1170,6 +1291,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="
@@ -1184,8 +1308,13 @@
 msgid "cancel this insert"
 msgstr "Cancelar esta inserción"
 
-msgid "canonical"
-msgstr "canónico"
+msgctxt "CWRelation"
+msgid "cardinality"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "cardinality"
+msgstr ""
 
 msgid "cardinality"
 msgstr "cardinalidad"
@@ -1212,8 +1341,13 @@
 msgid "comment"
 msgstr "Comentario"
 
-msgid "comment:"
-msgstr "Comentario:"
+msgctxt "TrInfo"
+msgid "comment"
+msgstr ""
+
+msgctxt "TrInfo"
+msgid "comment_format"
+msgstr ""
 
 msgid "comment_format"
 msgstr "Formato"
@@ -1269,33 +1403,71 @@
 "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"
 
 msgid "components_rqlinput_description"
 msgstr "La barra de demanda rql, en el encabezado de página"
 
+msgctxt "CWRelation"
+msgid "composite"
+msgstr ""
+
 msgid "composite"
 msgstr "composite"
 
 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"
 
 msgid "confirm password"
 msgstr "Confirmar contraseña"
 
+msgctxt "CWAttribute"
+msgid "constrained_by"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "constrained_by"
+msgstr ""
+
 msgid "constrained_by"
 msgstr "Restricción hecha por"
 
 msgid "constrained_by_object"
 msgstr "ha restringido"
 
+msgctxt "CWConstraint"
+msgid "constrained_by_object"
+msgstr ""
+
 msgid "constraint factory"
 msgstr "FAbrica de restricciones"
 
@@ -1492,23 +1664,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"
@@ -1522,9 +1714,17 @@
 msgid "cstrtype"
 msgstr "Tipo de condición"
 
+msgctxt "CWConstraint"
+msgid "cstrtype"
+msgstr ""
+
 msgid "cstrtype_object"
 msgstr "utilizado por"
 
+msgctxt "CWConstraintType"
+msgid "cstrtype_object"
+msgstr ""
+
 msgid "csv entities export"
 msgstr "Exportar entidades en csv"
 
@@ -1535,6 +1735,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"
 
@@ -1571,6 +1777,30 @@
 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 ""
+
+msgctxt "CWAttribute"
+msgid "defaultval"
+msgstr ""
+
 msgid "defaultval"
 msgstr "Valor por defecto"
 
@@ -1605,6 +1835,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"
@@ -1630,9 +1863,25 @@
 msgid "delete_permission"
 msgstr "Autorización de eliminar"
 
+msgctxt "CWEType"
+msgid "delete_permission"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "delete_permission"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "delete_permission_object"
+msgstr ""
+
 msgid "delete_permission_object"
 msgstr "posee la autorización de eliminar"
 
+msgctxt "CWGroup"
+msgid "delete_permission_object"
+msgstr ""
+
 #, python-format
 msgid "deleted %(etype)s #%(eid)s (%(title)s)"
 msgstr "Eliminación de la entidad %(etype)s #%(eid)s (%(title)s)"
@@ -1648,24 +1897,111 @@
 msgid "depends on the constraint type"
 msgstr "Depende del tipo de condición"
 
+msgctxt "State"
+msgid "description"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "description"
+msgstr ""
+
+msgctxt "Transition"
+msgid "description"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "description"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "description"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "description"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "description"
+msgstr ""
+
 msgid "description"
 msgstr "Descripción"
 
+msgctxt "CWRType"
+msgid "description"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "description"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "description_format"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "description_format"
+msgstr ""
+
+msgctxt "State"
+msgid "description_format"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "description_format"
+msgstr ""
+
+msgctxt "Transition"
+msgid "description_format"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "description_format"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "description_format"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "description_format"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "description_format"
+msgstr ""
+
 msgid "description_format"
 msgstr "Formato"
 
+msgid "destination state"
+msgstr ""
+
 msgid "destination state for this transition"
 msgstr "Estado destino para esta transición"
 
 msgid "destination state of a transition"
 msgstr "Estado destino de una transición"
 
+msgctxt "Transition"
+msgid "destination_state"
+msgstr ""
+
 msgid "destination_state"
 msgstr "Estado destino"
 
+msgctxt "SubWorkflowExitPoint"
+msgid "destination_state"
+msgstr ""
+
 msgid "destination_state_object"
 msgstr "Destino de"
 
+msgctxt "State"
+msgid "destination_state_object"
+msgstr ""
+
 msgid "detach attached file"
 msgstr "soltar el archivo existente"
 
@@ -1691,6 +2027,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"
@@ -1701,10 +2040,14 @@
 msgid "download"
 msgstr "Descargar"
 
+#, python-format
+msgid "download %s"
+msgstr ""
+
 msgid "download icon"
 msgstr "ícono de descarga"
 
-msgid "download image"
+msgid "download page as pdf"
 msgstr ""
 
 msgid "download schema as owl"
@@ -1713,6 +2056,9 @@
 msgid "edit bookmarks"
 msgstr "Editar favoritos"
 
+msgid "edit canceled"
+msgstr ""
+
 msgid "edit the index page"
 msgstr "Modificar la página de inicio"
 
@@ -1761,6 +2107,9 @@
 msgid "entity edited"
 msgstr "entidad modificada"
 
+msgid "entity has no workflow set"
+msgstr ""
+
 msgid "entity linked"
 msgstr "entidad asociada"
 
@@ -1774,11 +2123,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"
@@ -1800,15 +2146,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 :"
 
+msgctxt "RQLExpression"
+msgid "expression"
+msgstr ""
+
 msgid "expression"
 msgstr "Expresión"
 
 msgid "exprtype"
 msgstr "Tipo de la expresión"
 
+msgctxt "RQLExpression"
+msgid "exprtype"
+msgstr ""
+
 msgid "external page"
 msgstr "Página externa"
 
@@ -1860,6 +2224,18 @@
 msgid "final"
 msgstr "Final"
 
+msgctxt "CWRType"
+msgid "final"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "final"
+msgstr ""
+
+msgctxt "CWUser"
+msgid "firstname"
+msgstr ""
+
 msgid "firstname"
 msgstr "Nombre"
 
@@ -1869,9 +2245,17 @@
 msgid "follow"
 msgstr "Seguir la liga"
 
+msgctxt "CWProperty"
+msgid "for_user"
+msgstr ""
+
 msgid "for_user"
 msgstr "Para el usuario"
 
+msgctxt "CWUser"
+msgid "for_user_object"
+msgstr ""
+
 msgid "for_user_object"
 msgstr "Utiliza las propiedades"
 
@@ -1888,24 +2272,52 @@
 msgid "from_entity"
 msgstr "De la entidad"
 
+msgctxt "CWAttribute"
+msgid "from_entity"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "from_entity"
+msgstr ""
+
 msgid "from_entity_object"
 msgstr "Relación sujeto"
 
+msgctxt "CWEType"
+msgid "from_entity_object"
+msgstr ""
+
 msgid "from_interval_start"
 msgstr ""
 
+msgctxt "TrInfo"
+msgid "from_state"
+msgstr ""
+
 msgid "from_state"
 msgstr "De el estado"
 
+msgctxt "State"
+msgid "from_state_object"
+msgstr ""
+
 msgid "from_state_object"
 msgstr "Transiciones desde este estado"
 
 msgid "full text or RQL query"
 msgstr "Texto de búsqueda o demanda RQL"
 
+msgctxt "CWRType"
+msgid "fulltext_container"
+msgstr ""
+
 msgid "fulltext_container"
 msgstr "Contenedor de texto indexado"
 
+msgctxt "CWAttribute"
+msgid "fulltextindexed"
+msgstr ""
+
 msgid "fulltextindexed"
 msgstr "Indexación de texto"
 
@@ -1927,6 +2339,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 ""
 
@@ -2003,12 +2419,6 @@
 "Como formatear la hora en la interface (\"man strftime\" por la descripción "
 "del formato)"
 
-msgid "html class of the component"
-msgstr "Clase HTML de este componente"
-
-msgid "htmlclass"
-msgstr "Clase html"
-
 msgid "i18n_login_popup"
 msgstr "Identificarse"
 
@@ -2027,6 +2437,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"
 
@@ -2052,12 +2465,20 @@
 msgid "in memory relation schema"
 msgstr "Esquema de la relación en memoria"
 
+msgctxt "CWUser"
+msgid "in_group"
+msgstr ""
+
 msgid "in_group"
 msgstr "En el grupo"
 
 msgid "in_group_object"
 msgstr "Miembros"
 
+msgctxt "CWGroup"
+msgid "in_group_object"
+msgstr ""
+
 msgid "in_state"
 msgstr "estado"
 
@@ -2077,6 +2498,10 @@
 msgid "index this attribute's value in the plain text index"
 msgstr "Indexar el valor de este atributo en el índice de texto simple"
 
+msgctxt "CWAttribute"
+msgid "indexed"
+msgstr ""
+
 msgid "indexed"
 msgstr "Indexado"
 
@@ -2096,15 +2521,27 @@
 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 ""
+
 msgid "initial_state_object"
 msgstr "es el estado inicial de"
 
+msgctxt "State"
+msgid "initial_state_object"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "inlined"
+msgstr ""
+
 msgid "inlined"
 msgstr "Puesto en línea"
 
@@ -2117,6 +2554,10 @@
 msgid "internationalizable"
 msgstr "Internacionalizable"
 
+msgctxt "CWAttribute"
+msgid "internationalizable"
+msgstr ""
+
 #, python-format
 msgid "invalid action %r"
 msgstr "Acción %r invalida"
@@ -2124,6 +2565,12 @@
 msgid "invalid date"
 msgstr "Esta fecha no es válida"
 
+msgid "invalid float value"
+msgstr ""
+
+msgid "invalid integer value"
+msgstr ""
+
 msgid "is"
 msgstr "es"
 
@@ -2168,12 +2615,20 @@
 msgid "label"
 msgstr "Etiqueta"
 
+msgctxt "CWPermission"
+msgid "label"
+msgstr ""
+
 msgid "language of the user interface"
 msgstr "Idioma para la interface del usuario"
 
 msgid "last connection date"
 msgstr "Ultima fecha de conexión"
 
+msgctxt "CWUser"
+msgid "last_login_time"
+msgstr ""
+
 msgid "last_login_time"
 msgstr "Ultima fecha de conexión"
 
@@ -2210,14 +2665,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"
@@ -2234,6 +2692,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"
 
@@ -2253,6 +2715,10 @@
 msgid "mainvars"
 msgstr "Principales variables"
 
+msgctxt "RQLExpression"
+msgid "mainvars"
+msgstr ""
+
 msgid "manage"
 msgstr "Administracion del Sitio"
 
@@ -2268,6 +2734,9 @@
 msgid "managers"
 msgstr "editores"
 
+msgid "mandatory relation"
+msgstr ""
+
 msgid "march"
 msgstr "Marzo"
 
@@ -2311,9 +2780,53 @@
 msgid "my custom search"
 msgstr "Mi busqueda personalizada"
 
+msgctxt "CWRType"
+msgid "name"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "name"
+msgstr ""
+
+msgctxt "State"
+msgid "name"
+msgstr ""
+
 msgid "name"
 msgstr "Nombre"
 
+msgctxt "CWGroup"
+msgid "name"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "name"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "name"
+msgstr ""
+
+msgctxt "CWCache"
+msgid "name"
+msgstr ""
+
+msgctxt "CWConstraintType"
+msgid "name"
+msgstr ""
+
+msgctxt "Transition"
+msgid "name"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "name"
+msgstr ""
+
+msgctxt "CWPermission"
+msgid "name"
+msgstr ""
+
 msgid "name of the cache"
 msgstr "Nombre del Cache"
 
@@ -2389,9 +2902,6 @@
 msgid "not selected"
 msgstr "no seleccionado"
 
-msgid "not the initial state for this entity"
-msgstr "no el estado inicial para esta entidad"
-
 msgid "nothing to edit"
 msgstr "nada que editar"
 
@@ -2425,6 +2935,14 @@
 msgid "order"
 msgstr "orden"
 
+msgctxt "CWAttribute"
+msgid "ordernum"
+msgstr ""
+
+msgctxt "CWRelation"
+msgid "ordernum"
+msgstr ""
+
 msgid "ordernum"
 msgstr "orden"
 
@@ -2461,6 +2979,10 @@
 msgid "path"
 msgstr "Ruta"
 
+msgctxt "Bookmark"
+msgid "path"
+msgstr ""
+
 msgid "permission"
 msgstr "Permiso"
 
@@ -2479,6 +3001,10 @@
 msgid "pick existing bookmarks"
 msgstr "Seleccione los favoritos existentes"
 
+msgctxt "CWProperty"
+msgid "pkey"
+msgstr ""
+
 msgid "pkey"
 msgstr "pkey"
 
@@ -2491,6 +3017,23 @@
 msgid "possible views"
 msgstr "Vistas posibles"
 
+msgid "powered by CubicWeb"
+msgstr ""
+
+msgctxt "EmailAddress"
+msgid "prefered_form"
+msgstr ""
+
+msgid "prefered_form"
+msgstr ""
+
+msgctxt "EmailAddress"
+msgid "prefered_form_object"
+msgstr ""
+
+msgid "prefered_form_object"
+msgstr ""
+
 msgid "preferences"
 msgstr "Preferencias"
 
@@ -2503,9 +3046,17 @@
 msgid "primary_email"
 msgstr "Dirección de email principal"
 
+msgctxt "CWUser"
+msgid "primary_email"
+msgstr ""
+
 msgid "primary_email_object"
 msgstr "Dirección de email principal (objeto)"
 
+msgctxt "EmailAddress"
+msgid "primary_email_object"
+msgstr ""
+
 msgid "progress"
 msgstr "Avance"
 
@@ -2524,9 +3075,34 @@
 msgid "read_permission"
 msgstr "Permiso de lectura"
 
+msgctxt "CWEType"
+msgid "read_permission"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "read_permission"
+msgstr ""
+
 msgid "read_permission_object"
 msgstr "Objeto_permiso_lectura"
 
+msgctxt "CWGroup"
+msgid "read_permission_object"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "read_permission_object"
+msgstr ""
+
+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"
@@ -2534,6 +3110,18 @@
 msgid "relation_type"
 msgstr "tipo de relación"
 
+msgctxt "CWRelation"
+msgid "relation_type"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "relation_type"
+msgstr ""
+
+msgctxt "CWRType"
+msgid "relation_type_object"
+msgstr ""
+
 msgid "relation_type_object"
 msgstr "Definición"
 
@@ -2546,63 +3134,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.to_entity.subject"
+msgid "remove this CWEType"
+msgstr ""
+
+msgctxt "inlined:CWRelation.from_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 ""
+
+msgctxt "WorkflowTransition"
+msgid "require_group"
+msgstr ""
 
 msgid "require_group"
 msgstr "Requiere grupo"
 
+msgctxt "BaseTransition"
+msgid "require_group"
+msgstr ""
+
+msgctxt "CWPermission"
+msgid "require_group"
+msgstr ""
+
+msgctxt "Transition"
+msgid "require_group"
+msgstr ""
+
+msgctxt "CWGroup"
+msgid "require_group_object"
+msgstr ""
+
 msgid "require_group_object"
 msgstr "Requerido por grupo"
 
@@ -2731,6 +3301,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"
 
@@ -2787,9 +3360,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"
 
@@ -2799,12 +3383,31 @@
 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 ""
+
+msgctxt "State"
+msgid "state_of"
+msgstr ""
+
 msgid "state_of"
 msgstr "estado_de"
 
 msgid "state_of_object"
 msgstr "objeto_estado_de"
 
+msgctxt "Workflow"
+msgid "state_of_object"
+msgstr ""
+
 msgid "status change"
 msgstr "cambio de estatus"
 
@@ -2824,12 +3427,65 @@
 msgid "subject_plural:"
 msgstr "sujetos:"
 
+msgctxt "WorkflowTransition"
+msgid "subworkflow"
+msgstr ""
+
+msgid "subworkflow"
+msgstr ""
+
+msgid "subworkflow state"
+msgstr ""
+
+msgctxt "WorkflowTransition"
+msgid "subworkflow_exit"
+msgstr ""
+
+msgid "subworkflow_exit"
+msgstr ""
+
+msgid "subworkflow_exit_object"
+msgstr ""
+
+msgctxt "SubWorkflowExitPoint"
+msgid "subworkflow_exit_object"
+msgstr ""
+
+msgid "subworkflow_object"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "subworkflow_object"
+msgstr ""
+
+msgctxt "SubWorkflowExitPoint"
+msgid "subworkflow_state"
+msgstr ""
+
+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 ""
+
+msgctxt "CWRType"
+msgid "symetric"
+msgstr ""
+
 msgid "symetric"
 msgstr "simetrico"
 
@@ -2888,12 +3544,20 @@
 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."
 
 msgid "timetable"
 msgstr "tabla de tiempos"
 
+msgctxt "Bookmark"
+msgid "title"
+msgstr ""
+
 msgid "title"
 msgstr "titulo"
 
@@ -2907,18 +3571,38 @@
 msgid "to associate with"
 msgstr "a asociar con"
 
+msgctxt "CWRelation"
+msgid "to_entity"
+msgstr ""
+
+msgctxt "CWAttribute"
+msgid "to_entity"
+msgstr ""
+
 msgid "to_entity"
 msgstr "hacia entidad"
 
 msgid "to_entity_object"
 msgstr "hacia entidad objeto"
 
+msgctxt "CWEType"
+msgid "to_entity_object"
+msgstr ""
+
 msgid "to_interval_end"
 msgstr ""
 
+msgctxt "TrInfo"
+msgid "to_state"
+msgstr ""
+
 msgid "to_state"
 msgstr "hacia el estado"
 
+msgctxt "State"
+msgid "to_state_object"
+msgstr ""
+
 msgid "to_state_object"
 msgstr "hacia objeto estado"
 
@@ -2928,12 +3612,34 @@
 msgid "toggle check boxes"
 msgstr "cambiar valor"
 
-msgid "transition is not allowed"
-msgstr "transition no permitida"
+msgid "transition doesn't belong to entity's workflow"
+msgstr ""
+
+msgid "transition isn't allowed"
+msgstr ""
+
+msgid "transition may not be fired"
+msgstr ""
+
+msgctxt "Transition"
+msgid "transition_of"
+msgstr ""
 
 msgid "transition_of"
 msgstr "transicion de"
 
+msgctxt "WorkflowTransition"
+msgid "transition_of"
+msgstr ""
+
+msgctxt "BaseTransition"
+msgid "transition_of"
+msgstr ""
+
+msgctxt "Workflow"
+msgid "transition_of_object"
+msgstr ""
+
 msgid "transition_of_object"
 msgstr "objeto de transición"
 
@@ -3006,6 +3712,10 @@
 msgid "upassword"
 msgstr "clave de acceso"
 
+msgctxt "CWUser"
+msgid "upassword"
+msgstr ""
+
 msgid "update"
 msgstr "modificación"
 
@@ -3015,13 +3725,29 @@
 msgid "update_permission"
 msgstr "Permiso de modificación"
 
+msgctxt "CWEType"
+msgid "update_permission"
+msgstr ""
+
 msgid "update_permission_object"
 msgstr "objeto de autorización de modificaciones"
 
+msgctxt "CWGroup"
+msgid "update_permission_object"
+msgstr ""
+
+msgctxt "RQLExpression"
+msgid "update_permission_object"
+msgstr ""
+
 #, python-format
 msgid "updated %(etype)s #%(eid)s (%(title)s)"
 msgstr "actualización de la entidad %(etype)s #%(eid)s (%(title)s)"
 
+msgctxt "ExternalUri"
+msgid "uri"
+msgstr ""
+
 msgid "uri"
 msgstr ""
 
@@ -3035,9 +3761,17 @@
 "utilizado para definir una transición desde uno o multiples estados hacia "
 "uno o varios estados destino en las definiciones del workflow"
 
+msgctxt "CWUser"
+msgid "use_email"
+msgstr ""
+
 msgid "use_email"
 msgstr "correo electrónico"
 
+msgctxt "EmailAddress"
+msgid "use_email_object"
+msgstr ""
+
 msgid "use_email_object"
 msgstr "objeto email utilizado"
 
@@ -3093,9 +3827,17 @@
 msgid "validating..."
 msgstr "validando ..."
 
+msgctxt "CWProperty"
+msgid "value"
+msgstr ""
+
 msgid "value"
 msgstr "valor"
 
+msgctxt "CWConstraint"
+msgid "value"
+msgstr ""
+
 msgid "value associated to this key is not editable manually"
 msgstr "el valor asociado a este elemento no es editable manualmente"
 
@@ -3147,16 +3889,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"
@@ -3176,229 +3944,162 @@
 msgid "you should probably delete that property"
 msgstr "deberia probablamente suprimir esta propriedad"
 
-#~ msgid "%s constraint failed"
-#~ msgstr "La contrainte %s n'est pas satisfaite"
-
-#~ msgid "%s constraint failed for value %r"
-#~ msgstr "contrainte %s n'est pas respectÈe par valeur %r"
-
-#~ msgid "%s, or without time: %s"
-#~ msgstr "%s, ou bien sans prÈciser d'heure: %s"
-
-#~ msgid "Card"
-#~ msgstr "Ficha"
-
-#~ msgid "Card_plural"
-#~ msgstr "Fichas"
-
-#~ msgid "Email body: "
-#~ msgstr "Contenido del correo electrónico : "
-
-#~ msgid "From: "
-#~ msgstr "De : "
-
-#~ msgid "Loading"
-#~ msgstr "chargement"
-
-#~ msgid "New Card"
-#~ msgstr "Agregar Ficha"
-
-#~ msgid "Problem occured"
-#~ msgstr "Ha ocurrido un error"
-
-#~ msgid "Problem occured while setting new value"
-#~ msgstr "Un problËme est survenu lors de la mise ‡ jour"
-
-#~ msgid "Recipients: "
-#~ msgstr "Destinatarios : "
-
-#~ msgid "Subject: "
-#~ msgstr "Objeto : "
-
-#~ msgid "This Card"
-#~ msgstr "Esta Ficha"
-
-#~ msgid "You are not connected to an application !"
-#~ msgstr "Usted no esta conectado a una aplicación"
-
-#~ msgid ""
-#~ "a card is a textual content used as documentation, reference, procedure "
-#~ "reminder"
-#~ msgstr ""
-#~ "una ficha es un texto utilizado como documentación, referencia, memoria "
-#~ "de algún procedimiento..."
-
-#~ msgid ""
-#~ "a simple cache entity characterized by a name and a validity date. The "
-#~ "target application is responsible for updating timestamp when necessary "
-#~ "to invalidate the cache (typically in hooks). Also, checkout the "
-#~ "AppRsetObject.get_cache() method."
-#~ msgstr ""
-#~ "una entidad cache simple caracterizada por un nombre y una fecha "
-#~ "correcta. El sistema objetivo es responsable de actualizar timestamp "
-#~ "cuand se necesario para invalidar el cache (usualmente en hooks)."
-#~ "Recomendamos revisar el METODO  AppRsetObject.get_cache()."
-
-#~ msgid "add a Card"
-#~ msgstr "Agregar una ficha"
-
-#~ msgid "an abstract for this card"
-#~ msgstr "un resumen para esta ficha"
-
-#~ msgid "and"
-#~ msgstr "et"
-
-#~ msgid "application schema"
-#~ msgstr "Esquema de la aplicación"
-
-#~ msgid "at least one relation %s is required on %s(%s)"
-#~ msgstr "au moins une relation %s est nÈcessaire sur %s(%s)"
-
-#~ msgid "cancel edition"
-#~ msgstr "annuler l'Èdition"
-
-#~ msgid "components_applmessages"
-#~ msgstr "Mensajes de la aplicación"
-
-#~ msgid "components_applmessages_description"
-#~ msgstr "Muestra los mensajes de la aplicación"
-
-#~ msgid "components_rss_feed_url"
-#~ msgstr "RSS FEED URL"
-
-#~ msgid "components_rss_feed_url_description"
-#~ msgstr "El espacio para administrar RSS"
-
-#~ msgid "content"
-#~ msgstr "Contenido"
-
-#~ msgid "content_format"
-#~ msgstr "Formato"
-
-#~ msgid ""
-#~ "default language (look at the i18n directory of the application to see "
-#~ "available languages)"
-#~ msgstr ""
-#~ "langue par dÈfaut (regarder le rÈpertoire i18n de l'application pour voir "
-#~ "les langues disponibles)"
+#~ msgid "There is no workflow defined for this entity."
+#~ msgstr "No hay workflow para este entidad"
 
 #~ msgid ""
-#~ "define a final relation: link a final relation type from a non final "
-#~ "entity to a final entity type. used to build the application schema"
-#~ msgstr ""
-#~ "Define una relación no final: liga un tipo de relación no final desde una "
-#~ "entidad hacia un tipo de entidad no final. Utilizada para construir el "
-#~ "esquema de la aplicación"
+#~ "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 ""
-#~ "define a non final relation: link a non final relation type from a non "
-#~ "final entity to a non final entity type. used to build the application "
-#~ "schema"
-#~ msgstr ""
-#~ "Define una relación 'atributo', utilizada para construir el esquema dela "
-#~ "aplicación"
-
-#~ msgid "define a relation type, used to build the application schema"
-#~ msgstr ""
-#~ "Define un tipo de relación, utilizada para construir el esquema de la "
-#~ "aplicación"
-
-#~ msgid "define an entity type, used to build the application schema"
+#~ "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 ""
-#~ "Define un tipo de entidad, utilizada para construir el esquema de la "
-#~ "aplicación"
-
-#~ msgid "detailed schema view"
-#~ msgstr "Vista detallada del esquema"
-
-#~ msgid "facets_cwmeta-facet"
-#~ msgstr "faceta \"meta\""
-
-#~ msgid "facets_cwmeta-facet_description"
-#~ msgstr "faceta para las entidades \"meta\""
-
-#~ msgid "filter"
-#~ msgstr "filtrer"
-
-#~ msgid "footer"
-#~ msgstr "pied de page"
-
-#~ msgid "graphical representation of the application'schema"
-#~ msgstr "Representación gráfica del esquema de la aplicación"
-
-#~ msgid "header"
-#~ msgstr "en-tÃte de page"
-
-#~ msgid "hide meta-data"
-#~ msgstr "Esconder los meta-datos"
-
-#~ msgid "iCal"
-#~ msgstr "iCal"
-
-#~ msgid "incorrect value for type \"%s\""
-#~ msgstr "valeur incorrecte pour le type \"%s\""
-
-#~ msgid "inlined view"
-#~ msgstr "Vista incluída (en línea)"
-
-#~ msgid "is it an application entity type or not ?"
-#~ msgstr "Es un Tipo de entidad en la aplicación o no ?"
-
-#~ msgid "is it an application relation type or not ?"
-#~ msgstr "Es una relación aplicativa o no ?"
-
-#~ msgid ""
-#~ "link a transition to one or more rql expression allowing to go through "
-#~ "this transition"
-#~ msgstr ""
-#~ "liga una transición a una o mas expresiones RQL permitiendo que funcione"
-
-#~ msgid "linked"
-#~ msgstr "liÈ"
-
-#~ msgid "loop in %s relation (%s)"
-#~ msgstr "boucle dans la relation %s (%s)"
-
-#~ msgid ""
-#~ "maximum number of related entities to display in in the restriction view"
-#~ msgstr ""
-#~ "nombre maximum d'entitÈs liÈes ‡ afficher dans la vue de restriction"
-
-#~ msgid "meta"
-#~ msgstr "Meta"
-
-#~ msgid "no associated epermissions"
-#~ msgstr "permisos no asociados"
-
-#~ msgid "no possible transition"
-#~ msgstr "transición no posible"
-
-#~ msgid "not specified"
-#~ msgstr "no especificado"
-
-#~ msgid "owned by"
-#~ msgstr "appartient ‡"
-
-#~ msgid "planned_delivery"
-#~ msgstr "entrega planeada"
-
-#~ msgid "remove this Card"
-#~ msgstr "Eliminar esta Ficha"
-
-#~ msgid "see also"
-#~ msgstr "voir aussi"
-
-#~ msgid "show meta-data"
-#~ msgstr "mostrar meta-data"
-
-#~ msgid "status will change from %s to %s"
-#~ msgstr "l'Ètat va passer de %s ‡ %s"
-
-#~ msgid "synopsis"
-#~ msgstr "sinopsis"
-
-#~ msgid "wikiid"
-#~ msgstr "identificador wiki"
-
-#~ msgid "workflow history"
-#~ msgstr "historique du workflow"
+#~ "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	Wed Aug 05 09:15:56 2009 +0200
+++ b/i18n/fr.po	Tue Sep 22 13:08:42 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"
@@ -80,32 +80,32 @@
 msgstr "%d années"
 
 #, python-format
-msgid "%d&nbsp;days"
-msgstr "%d&nbsp;jours"
+msgid "%d&#160;days"
+msgstr "%d&#160;jours"
 
 #, python-format
-msgid "%d&nbsp;hours"
-msgstr "%d&nbsp;heures"
+msgid "%d&#160;hours"
+msgstr "%d&#160;heures"
 
 #, python-format
-msgid "%d&nbsp;minutes"
-msgstr "%d&nbsp;minutes"
+msgid "%d&#160;minutes"
+msgstr "%d&#160;minutes"
 
 #, python-format
-msgid "%d&nbsp;months"
-msgstr "%d&nbsp;mois"
+msgid "%d&#160;months"
+msgstr "%d&#160;mois"
 
 #, python-format
-msgid "%d&nbsp;seconds"
-msgstr "%d&nbsp;secondes"
+msgid "%d&#160;seconds"
+msgstr "%d&#160;secondes"
 
 #, python-format
-msgid "%d&nbsp;weeks"
-msgstr "%d&nbsp;semaines"
+msgid "%d&#160;weeks"
+msgstr "%d&#160;semaines"
 
 #, python-format
-msgid "%d&nbsp;years"
-msgstr "%d&nbsp;années"
+msgid "%d&#160;years"
+msgstr "%d&#160;années"
 
 #, python-format
 msgid "%s error report"
@@ -200,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"
 
@@ -354,6 +363,9 @@
 msgid "Interval_plural"
 msgstr "Durées"
 
+msgid "New BaseTransition"
+msgstr "XXX"
+
 msgid "New Bookmark"
 msgstr "Nouveau signet"
 
@@ -402,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é"
 
@@ -438,6 +459,9 @@
 msgid "Recipients:"
 msgstr "Destinataires :"
 
+msgid "Registry's content"
+msgstr "Contenu du registre"
+
 msgid "Relations"
 msgstr "Relations"
 
@@ -472,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 :"
 
@@ -492,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"
@@ -514,9 +540,17 @@
 msgid "This CWConstraintType"
 msgstr "Ce type de contrainte"
 
+msgctxt "inlined:CWRelation.from_entity.subject"
+msgid "This CWEType"
+msgstr "type d'entité sujet"
+
 msgid "This CWEType"
 msgstr "Ce type d'entité"
 
+msgctxt "inlined:CWRelation.to_entity.subject"
+msgid "This CWEType"
+msgstr "type d'entité objet"
+
 msgid "This CWGroup"
 msgstr "Ce groupe"
 
@@ -529,12 +563,20 @@
 msgid "This CWRType"
 msgstr "Ce type de relation"
 
+msgctxt "inlined:CWRelation.relation_type.subject"
+msgid "This CWRType"
+msgstr "Ce type de relation"
+
 msgid "This CWRelation"
-msgstr "Cette définition de relation non finale"
+msgstr "Cette définition de relation"
 
 msgid "This CWUser"
 msgstr "Cet utilisateur"
 
+msgctxt "inlined:CWUser.use_email.subject"
+msgid "This EmailAddress"
+msgstr "adresse électronique"
+
 msgid "This EmailAddress"
 msgstr "Cette adresse électronique"
 
@@ -547,12 +589,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"
 
@@ -584,11 +635,23 @@
 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 !"
+msgstr "Vous n'êtes pas connecté à une instance"
 
 #, python-format
 msgid "You are now connected to %s"
@@ -617,9 +680,11 @@
 "Vous pouvez utiliser n'importe quelle substitution parmi la liste suivante "
 "dans le contenu de votre courriel."
 
-msgid "You have no access to this view or it's not applyable to current data"
+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 s'applique pas aux données"
+"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 "
@@ -632,9 +697,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 "
@@ -663,12 +725,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"
 
@@ -681,6 +743,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"
 
@@ -790,7 +858,7 @@
 msgstr ""
 
 msgid "actions_siteinfo"
-msgstr ""
+msgstr "information sur ce site"
 
 msgid "actions_siteinfo_description"
 msgstr ""
@@ -859,82 +927,56 @@
 msgstr "utilisateur"
 
 msgid "add CWUser use_email EmailAddress subject"
-msgstr "ajouter une addresse email"
+msgstr "adresse 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.to_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é objet"
+
+msgctxt "inlined:CWRelation.from_entity.subject"
+msgid "add a CWEType"
+msgstr "ajouter un type d'entité sujet"
+
+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"
@@ -948,8 +990,26 @@
 # 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 "RQLExpression"
+msgid "add_permission_object"
+msgstr "a la permission d'ajouter"
+
+msgid "add_permission_object"
+msgstr "a la permission d'ajouter"
+
+msgctxt "CWGroup"
 msgid "add_permission_object"
 msgstr "a la permission d'ajouter"
 
@@ -965,12 +1025,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"
+
+msgctxt "EmailAddress"
+msgid "alias"
+msgstr "alias"
+
 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"
 
@@ -978,11 +1052,27 @@
 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"
 
 msgid "allowed_transition_object"
 msgstr "états en entrée"
 
+msgctxt "WorkflowTransition"
+msgid "allowed_transition_object"
+msgstr "transition autorisée de"
+
+msgctxt "Transition"
+msgid "allowed_transition_object"
+msgstr "transition autorisée de"
+
+msgctxt "BaseTransition"
+msgid "allowed_transition_object"
+msgstr "transition autorisée de"
+
 msgid "am/pm calendar (month)"
 msgstr "calendrier am/pm (mois)"
 
@@ -996,7 +1086,7 @@
 msgstr "calendrier am/pm (année)"
 
 msgid "an electronic mail address associated to a short alias"
-msgstr "une addresse électronique associée à un alias"
+msgstr "une adresse électronique associée à un alias"
 
 msgid "an error occured"
 msgstr "une erreur est survenue"
@@ -1061,6 +1151,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"
 
@@ -1147,6 +1245,28 @@
 msgid "by relation"
 msgstr "via la relation"
 
+msgctxt "TrInfo"
+msgid "by_transition"
+msgstr "transition"
+
+msgid "by_transition"
+msgstr "transition"
+
+msgctxt "WorkflowTransition"
+msgid "by_transition_object"
+msgstr "a pour information"
+
+msgctxt "BaseTransition"
+msgid "by_transition_object"
+msgstr "a pour information"
+
+msgctxt "Transition"
+msgid "by_transition_object"
+msgstr "a pour information"
+
+msgid "by_transition_object"
+msgstr "changement d'états"
+
 msgid "calendar"
 msgstr "afficher un calendrier"
 
@@ -1177,6 +1297,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="
@@ -1191,9 +1314,14 @@
 msgid "cancel this insert"
 msgstr "annuler cette insertion"
 
-msgid "canonical"
-msgstr "canonique"
-
+msgctxt "CWRelation"
+msgid "cardinality"
+msgstr "cardinalité"
+
+msgid "cardinality"
+msgstr "cardinalité"
+
+msgctxt "CWAttribute"
 msgid "cardinality"
 msgstr "cardinalité"
 
@@ -1219,9 +1347,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"
 
@@ -1276,6 +1409,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"
 
@@ -1285,6 +1424,22 @@
 msgid "composite"
 msgstr "composite"
 
+msgctxt "CWRelation"
+msgid "composite"
+msgstr "composite"
+
+msgctxt "WorkflowTransition"
+msgid "condition"
+msgstr "condition"
+
+msgctxt "Transition"
+msgid "condition"
+msgstr "condition"
+
+msgid "condition"
+msgstr "condition"
+
+msgctxt "BaseTransition"
 msgid "condition"
 msgstr "condition"
 
@@ -1294,12 +1449,28 @@
 msgid "condition_object"
 msgstr "condition de"
 
+msgctxt "RQLExpression"
+msgid "condition_object"
+msgstr "condition de"
+
 msgid "confirm password"
 msgstr "confirmer le mot de passe"
 
 msgid "constrained_by"
 msgstr "contraint par"
 
+msgctxt "CWAttribute"
+msgid "constrained_by"
+msgstr "contraint par"
+
+msgctxt "CWRelation"
+msgid "constrained_by"
+msgstr "contraint par"
+
+msgid "constrained_by_object"
+msgstr "contrainte de"
+
+msgctxt "CWConstraint"
 msgid "constrained_by_object"
 msgstr "contrainte de"
 
@@ -1500,23 +1671,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"
@@ -1527,12 +1718,20 @@
 msgid "creation_date"
 msgstr "date de création"
 
+msgctxt "CWConstraint"
+msgid "cstrtype"
+msgstr "type"
+
 msgid "cstrtype"
 msgstr "type de constrainte"
 
 msgid "cstrtype_object"
 msgstr "utilisé par"
 
+msgctxt "CWConstraintType"
+msgid "cstrtype_object"
+msgstr "type des contraintes"
+
 msgid "csv entities export"
 msgstr "export d'entités en CSV"
 
@@ -1543,6 +1742,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"
 
@@ -1579,6 +1784,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é"
+
+msgctxt "CWEType"
+msgid "default_workflow"
+msgstr "workflow par défaut"
+
+msgid "default_workflow"
+msgstr "workflow par défaut"
+
+msgid "default_workflow_object"
+msgstr "workflow par défaut de"
+
+msgctxt "Workflow"
+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"
 
@@ -1592,11 +1821,16 @@
 "define a final relation: link a final relation type from a non final entity "
 "to a final entity type. used to build the instance schema"
 msgstr ""
+"définit une relation non finale: lie un type de relation non finaledepuis "
+"une entité vers un type d'entité non final. Utilisé pour construire le "
+"schéma de l'instance"
 
 msgid ""
 "define a non final relation: link a non final relation type from a non final "
 "entity to a non final entity type. used to build the instance schema"
 msgstr ""
+"définit une relation 'attribut', utilisé pour construire le schéma de "
+"l'instance"
 
 msgid "define a relation type, used to build the instance schema"
 msgstr "définit un type de relation"
@@ -1611,7 +1845,10 @@
 msgstr "définit un type de contrainte de schema"
 
 msgid "define an entity type, used to build the instance schema"
-msgstr ""
+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 "
@@ -1638,9 +1875,25 @@
 msgid "delete_permission"
 msgstr "permission de supprimer"
 
+msgctxt "CWRType"
+msgid "delete_permission"
+msgstr "permission de supprimer"
+
+msgctxt "CWEType"
+msgid "delete_permission"
+msgstr "permission de supprimer"
+
 msgid "delete_permission_object"
 msgstr "a la permission de supprimer"
 
+msgctxt "RQLExpression"
+msgid "delete_permission_object"
+msgstr "peut supprimer"
+
+msgctxt "CWGroup"
+msgid "delete_permission_object"
+msgstr "peut supprimer"
+
 #, python-format
 msgid "deleted %(etype)s #%(eid)s (%(title)s)"
 msgstr "suppression de l'entité %(etype)s #%(eid)s (%(title)s)"
@@ -1659,9 +1912,84 @@
 msgid "description"
 msgstr "description"
 
+msgctxt "Transition"
+msgid "description"
+msgstr "description"
+
+msgctxt "Workflow"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWRelation"
+msgid "description"
+msgstr "description"
+
+msgctxt "WorkflowTransition"
+msgid "description"
+msgstr "description"
+
+msgctxt "State"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWEType"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWRType"
+msgid "description"
+msgstr "description"
+
+msgctxt "BaseTransition"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWAttribute"
+msgid "description"
+msgstr "description"
+
+msgctxt "CWRelation"
 msgid "description_format"
 msgstr "format"
 
+msgid "description_format"
+msgstr "format"
+
+msgctxt "CWEType"
+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"
 
@@ -1671,6 +1999,18 @@
 msgid "destination_state"
 msgstr "état de destination"
 
+msgctxt "SubWorkflowExitPoint"
+msgid "destination_state"
+msgstr "état de destination"
+
+msgctxt "Transition"
+msgid "destination_state"
+msgstr "état de destination"
+
+msgctxt "State"
+msgid "destination_state_object"
+msgstr "état final de"
+
 msgid "destination_state_object"
 msgstr "destination de"
 
@@ -1699,6 +2039,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"
@@ -1709,11 +2052,15 @@
 msgid "download"
 msgstr "télécharger"
 
+#, python-format
+msgid "download %s"
+msgstr "télécharger %s"
+
 msgid "download icon"
 msgstr "icône de téléchargement"
 
-msgid "download image"
-msgstr "image 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"
@@ -1721,6 +2068,9 @@
 msgid "edit bookmarks"
 msgstr "éditer les signets"
 
+msgid "edit canceled"
+msgstr "édition annulée"
+
 msgid "edit the index page"
 msgstr "éditer la page d'accueil"
 
@@ -1769,6 +2119,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"
 
@@ -1781,11 +2134,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"
@@ -1807,15 +2157,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 :"
 
+msgctxt "RQLExpression"
+msgid "expression"
+msgstr "rql de l'expression"
+
 msgid "expression"
 msgstr "expression"
 
 msgid "exprtype"
 msgstr "type de l'expression"
 
+msgctxt "RQLExpression"
+msgid "exprtype"
+msgstr "type"
+
 msgid "external page"
 msgstr "page externe"
 
@@ -1864,9 +2232,21 @@
 msgid "file tree view"
 msgstr "arborescence (fichiers)"
 
+msgctxt "CWEType"
+msgid "final"
+msgstr "final"
+
 msgid "final"
 msgstr "final"
 
+msgctxt "CWRType"
+msgid "final"
+msgstr "final"
+
+msgctxt "CWUser"
+msgid "firstname"
+msgstr "prénom"
+
 msgid "firstname"
 msgstr "prénom"
 
@@ -1876,9 +2256,17 @@
 msgid "follow"
 msgstr "suivre le lien"
 
+msgctxt "CWProperty"
+msgid "for_user"
+msgstr "propriété de l'utilisateur"
+
 msgid "for_user"
 msgstr "pour l'utilisateur"
 
+msgctxt "CWUser"
+msgid "for_user_object"
+msgstr "a pour préférence"
+
 msgid "for_user_object"
 msgstr "utilise les propriétés"
 
@@ -1895,6 +2283,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"
 
@@ -1904,15 +2304,31 @@
 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"
 
 msgid "full text or RQL query"
 msgstr "texte à rechercher ou requête RQL"
 
+msgctxt "CWRType"
+msgid "fulltext_container"
+msgstr "objet à indexer"
+
 msgid "fulltext_container"
 msgstr "conteneur du texte indexé"
 
+msgctxt "CWAttribute"
+msgid "fulltextindexed"
+msgstr "texte indexé"
+
 msgid "fulltextindexed"
 msgstr "indexation du texte"
 
@@ -1934,6 +2350,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"
 
@@ -2011,12 +2431,6 @@
 "comment formater l'heure dans l'interface (\"man strftime\" pour la "
 "description du format)"
 
-msgid "html class of the component"
-msgstr "classe HTML de ce composant"
-
-msgid "htmlclass"
-msgstr "classe html"
-
 msgid "i18n_login_popup"
 msgstr "s'authentifier"
 
@@ -2035,6 +2449,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 à"
 
@@ -2060,9 +2477,17 @@
 msgid "in memory relation schema"
 msgstr "schéma de la relation en mémoire"
 
+msgctxt "CWUser"
+msgid "in_group"
+msgstr "fait partie du groupe"
+
 msgid "in_group"
 msgstr "dans le groupe"
 
+msgctxt "CWGroup"
+msgid "in_group_object"
+msgstr "contient les utilisateurs"
+
 msgid "in_group_object"
 msgstr "membres"
 
@@ -2088,6 +2513,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é"
 
@@ -2104,18 +2533,30 @@
 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"
+
+msgctxt "Workflow"
+msgid "initial_state"
+msgstr "état initial"
 
 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"
+
 msgid "instance schema"
 msgstr "schéma de l'instance"
 
@@ -2125,6 +2566,10 @@
 msgid "internationalizable"
 msgstr "internationalisable"
 
+msgctxt "CWAttribute"
+msgid "internationalizable"
+msgstr "internationalisable"
+
 #, python-format
 msgid "invalid action %r"
 msgstr "action %r invalide"
@@ -2132,6 +2577,12 @@
 msgid "invalid date"
 msgstr "cette date n'est pas valide"
 
+msgid "invalid float value"
+msgstr "nombre flottant non valide"
+
+msgid "invalid integer value"
+msgstr "nombre entier non valide"
+
 msgid "is"
 msgstr "de type"
 
@@ -2177,6 +2628,10 @@
 msgid "label"
 msgstr "libellé"
 
+msgctxt "CWPermission"
+msgid "label"
+msgstr "libellé"
+
 msgid "language of the user interface"
 msgstr "langue pour l'interface utilisateur"
 
@@ -2186,6 +2641,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é"
 
@@ -2219,14 +2678,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"
@@ -2243,6 +2705,10 @@
 msgid "login"
 msgstr "identifiant"
 
+msgctxt "CWUser"
+msgid "login"
+msgstr "identifiant"
+
 msgid "login or email"
 msgstr "identifiant ou email"
 
@@ -2259,6 +2725,10 @@
 msgid "main informations"
 msgstr "Informations générales"
 
+msgctxt "RQLExpression"
+msgid "mainvars"
+msgstr "variables principales"
+
 msgid "mainvars"
 msgstr "variables principales"
 
@@ -2277,6 +2747,9 @@
 msgid "managers"
 msgstr "administrateurs"
 
+msgid "mandatory relation"
+msgstr "relation obligatoire"
+
 msgid "march"
 msgstr "mars"
 
@@ -2320,6 +2793,50 @@
 msgid "my custom search"
 msgstr "ma recherche personnalisée"
 
+msgctxt "CWPermission"
+msgid "name"
+msgstr "nom"
+
+msgctxt "State"
+msgid "name"
+msgstr "nom"
+
+msgctxt "BaseTransition"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWRType"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWGroup"
+msgid "name"
+msgstr "nom"
+
+msgctxt "WorkflowTransition"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWCache"
+msgid "name"
+msgstr "nom"
+
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWConstraintType"
+msgid "name"
+msgstr "nom"
+
+msgctxt "CWEType"
+msgid "name"
+msgstr "nom"
+
+msgctxt "Transition"
+msgid "name"
+msgstr "nom"
+
+msgctxt "Workflow"
 msgid "name"
 msgstr "nom"
 
@@ -2394,9 +2911,6 @@
 msgid "not selected"
 msgstr "non sélectionné"
 
-msgid "not the initial state for this entity"
-msgstr "n'est pas l'état initial pour cette entité"
-
 msgid "nothing to edit"
 msgstr "rien à éditer"
 
@@ -2430,6 +2944,14 @@
 msgid "order"
 msgstr "ordre"
 
+msgctxt "CWRelation"
+msgid "ordernum"
+msgstr "numéro d'ordre"
+
+msgctxt "CWAttribute"
+msgid "ordernum"
+msgstr "numéro d'ordre"
+
 msgid "ordernum"
 msgstr "ordre"
 
@@ -2468,6 +2990,10 @@
 msgid "path"
 msgstr "chemin"
 
+msgctxt "Bookmark"
+msgid "path"
+msgstr "chemin"
+
 msgid "permission"
 msgstr "permission"
 
@@ -2486,6 +3012,10 @@
 msgid "pick existing bookmarks"
 msgstr "récupérer des signets existants"
 
+msgctxt "CWProperty"
+msgid "pkey"
+msgstr "code de la propriété"
+
 msgid "pkey"
 msgstr "clé"
 
@@ -2498,6 +3028,23 @@
 msgid "possible views"
 msgstr "vues possibles"
 
+msgid "powered by CubicWeb"
+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"
 
@@ -2510,9 +3057,17 @@
 msgid "primary_email"
 msgstr "adresse email principale"
 
+msgctxt "CWUser"
+msgid "primary_email"
+msgstr "email principal"
+
 msgid "primary_email_object"
 msgstr "adresse email principale (object)"
 
+msgctxt "EmailAddress"
+msgid "primary_email_object"
+msgstr "adresse principale de"
+
 msgid "progress"
 msgstr "avancement"
 
@@ -2528,12 +3083,37 @@
 msgid "read_perm"
 msgstr "lecture"
 
+msgctxt "CWEType"
+msgid "read_permission"
+msgstr "permission d'ajouter"
+
+msgctxt "CWRType"
+msgid "read_permission"
+msgstr "permission d'ajouter"
+
 msgid "read_permission"
 msgstr "permission de lire"
 
 msgid "read_permission_object"
 msgstr "a la permission de lire"
 
+msgctxt "CWGroup"
+msgid "read_permission_object"
+msgstr "peut lire"
+
+msgctxt "RQLExpression"
+msgid "read_permission_object"
+msgstr "peut 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"
@@ -2541,6 +3121,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"
+
+msgid "relation_type_object"
+msgstr "définition"
+
+msgctxt "CWRType"
 msgid "relation_type_object"
 msgstr "définition"
 
@@ -2553,63 +3145,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 sujet de relation"
+
+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é"
-
+msgstr "supprimer cet objet de la relation"
+
+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"
-
+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"
+
+msgctxt "WorkflowTransition"
+msgid "require_group"
+msgstr "restreinte au groupe"
 
 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 "CWGroup"
+msgid "require_group_object"
+msgstr "dé"
+
 msgid "require_group_object"
 msgstr "à les droits"
 
@@ -2743,6 +3317,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"
 
@@ -2798,9 +3375,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"
+
+msgid "specializes_object"
+msgstr "parent de"
+
+msgctxt "CWEType"
 msgid "specializes_object"
 msgstr "parent de"
 
@@ -2810,9 +3398,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."
+
+msgctxt "State"
 msgid "state_of"
 msgstr "état de"
 
+msgid "state_of"
+msgstr "état de"
+
+msgctxt "Workflow"
+msgid "state_of_object"
+msgstr "contient les états"
+
 msgid "state_of_object"
 msgstr "a pour état"
 
@@ -2835,12 +3444,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 "sous workflow de"
+
+msgid "subworkflow_object"
+msgstr "utilisé par la transition"
+
+msgctxt "SubWorkflowExitPoint"
+msgid "subworkflow_state"
+msgstr "état"
+
+msgid "subworkflow_state"
+msgstr "état du sous-workflow"
+
+msgctxt "State"
+msgid "subworkflow_state_object"
+msgstr "état de sortie de"
+
+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"
 
@@ -2900,6 +3562,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."
 
@@ -2909,6 +3575,10 @@
 msgid "title"
 msgstr "titre"
 
+msgctxt "Bookmark"
+msgid "title"
+msgstr "libellé"
+
 msgid "to"
 msgstr "à"
 
@@ -2919,20 +3589,40 @@
 msgid "to associate with"
 msgstr "pour associer à"
 
+msgctxt "CWRelation"
+msgid "to_entity"
+msgstr "pour l'entité"
+
+msgctxt "CWAttribute"
+msgid "to_entity"
+msgstr "pour l'entité"
+
 msgid "to_entity"
 msgstr "vers l'entité"
 
+msgctxt "CWEType"
+msgid "to_entity_object"
+msgstr "relation objet"
+
 msgid "to_entity_object"
 msgstr "relation objet"
 
 msgid "to_interval_end"
 msgstr "à"
 
+msgctxt "TrInfo"
+msgid "to_state"
+msgstr "état de destination"
+
 msgid "to_state"
 msgstr "vers l'état"
 
 msgid "to_state_object"
-msgstr "transitions vers cette état"
+msgstr "transitions vers cet état"
+
+msgctxt "State"
+msgid "to_state_object"
+msgstr "transition vers cet état"
 
 msgid "todo_by"
 msgstr "à faire par"
@@ -2940,12 +3630,34 @@
 msgid "toggle check boxes"
 msgstr "inverser les cases à cocher"
 
-msgid "transition is not allowed"
-msgstr "transition non 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"
+
+msgctxt "BaseTransition"
+msgid "transition_of"
+msgstr "transition de"
+
+msgctxt "Transition"
+msgid "transition_of"
+msgstr "transition de"
+
+msgctxt "WorkflowTransition"
+msgid "transition_of"
+msgstr "transition de"
 
 msgid "transition_of"
 msgstr "transition de"
 
+msgctxt "Workflow"
+msgid "transition_of_object"
+msgstr "a pour transition"
+
 msgid "transition_of_object"
 msgstr "a pour transition"
 
@@ -3015,6 +3727,10 @@
 msgid "up"
 msgstr "haut"
 
+msgctxt "CWUser"
+msgid "upassword"
+msgstr "mot de passe"
+
 msgid "upassword"
 msgstr "mot de passe"
 
@@ -3027,13 +3743,29 @@
 msgid "update_permission"
 msgstr "permission de modification"
 
+msgctxt "CWEType"
+msgid "update_permission"
+msgstr "permission de modifier"
+
 msgid "update_permission_object"
 msgstr "à la permission de modifier"
 
+msgctxt "RQLExpression"
+msgid "update_permission_object"
+msgstr "peut modifier"
+
+msgctxt "CWGroup"
+msgid "update_permission_object"
+msgstr "peut modifier"
+
 #, python-format
 msgid "updated %(etype)s #%(eid)s (%(title)s)"
 msgstr "modification de l'entité %(etype)s #%(eid)s (%(title)s)"
 
+msgctxt "ExternalUri"
+msgid "uri"
+msgstr "uri"
+
 msgid "uri"
 msgstr "uri"
 
@@ -3047,9 +3779,17 @@
 "utiliser dans une définition de processus pour ajouter une transition depuis "
 "un ou plusieurs états vers un état de destination."
 
+msgctxt "CWUser"
+msgid "use_email"
+msgstr "utilise l'adresse électronique"
+
 msgid "use_email"
 msgstr "adresse électronique"
 
+msgctxt "EmailAddress"
+msgid "use_email_object"
+msgstr "utilisée par"
+
 msgid "use_email_object"
 msgstr "adresse utilisée par"
 
@@ -3106,6 +3846,14 @@
 msgid "value"
 msgstr "valeur"
 
+msgctxt "CWProperty"
+msgid "value"
+msgstr "valeur"
+
+msgctxt "CWConstraint"
+msgid "value"
+msgstr "contrainte"
+
 msgid "value associated to this key is not editable manually"
 msgstr "la valeur associée à cette clé n'est pas éditable manuellement"
 
@@ -3158,16 +3906,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"
+
+msgctxt "Workflow"
+msgid "workflow_of"
+msgstr "workflow de"
+
+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"
@@ -3187,223 +3961,18 @@
 msgid "you should probably delete that property"
 msgstr "vous devriez probablement supprimer cette propriété"
 
-#~ msgid "%s constraint failed"
-#~ msgstr "La contrainte %s n'est pas satisfaite"
-
-#~ msgid "%s constraint failed for value %r"
-#~ msgstr "contrainte %s n'est pas respectée par valeur %r"
-
-#~ msgid "%s, or without time: %s"
-#~ msgstr "%s, ou bien sans préciser d'heure: %s"
-
-#~ msgid "Card"
-#~ msgstr "Fiche"
-
-#~ msgid "Card_plural"
-#~ msgstr "Fiches"
-
-#~ msgid "Email body: "
-#~ msgstr "Contenu du courriel : "
-
-#~ msgid "From: "
-#~ msgstr "De : "
-
-#~ msgid "Loading"
-#~ msgstr "chargement"
-
-#~ msgid "New Card"
-#~ msgstr "Nouvelle fiche"
-
-#~ msgid "Problem occured"
-#~ msgstr "Une erreur est survenue"
-
-#~ msgid "Problem occured while setting new value"
-#~ msgstr "Un problème est survenu lors de la mise à jour"
-
-#~ msgid "Recipients: "
-#~ msgstr "Destinataires : "
-
-#~ msgid "Subject: "
-#~ msgstr "Sujet : "
-
-#~ msgid "This Card"
-#~ msgstr "Cette fiche"
-
-#~ msgid "You are not connected to an application !"
-#~ msgstr "Vous n'êtes pas connecté à une application"
-
-#~ msgid ""
-#~ "a card is a textual content used as documentation, reference, procedure "
-#~ "reminder"
-#~ msgstr ""
-#~ "une fiche est un texte utilisé comme documentation, référence, rappel de "
-#~ "procédure..."
-
-#~ msgid ""
-#~ "a simple cache entity characterized by a name and a validity date. The "
-#~ "target application is responsible for updating timestamp when necessary "
-#~ "to invalidate the cache (typically in hooks). Also, checkout the "
-#~ "AppRsetObject.get_cache() method."
-#~ msgstr ""
-#~ "une simple entité de cache, caractérisées par un nom et une date de "
-#~ "validité. L'application est responsable de la mise à jour de la date "
-#~ "quand il est nécessaire d'invalider le cache (typiquement dans les "
-#~ "crochets). Voir aussi la méthode get_cache() sur la classe AppRsetObject."
-
-#~ msgid "actions_addpermission"
-#~ msgstr "ajouter une permission"
-
-#~ msgid "add a Card"
-#~ msgstr "ajouter une fiche"
-
-#~ msgid "an abstract for this card"
-#~ msgstr "un résumé pour cette fiche"
-
-#~ msgid "and"
-#~ msgstr "et"
-
-#~ msgid "application schema"
-#~ msgstr "Schéma de l'application"
-
-#~ msgid "at least one relation %s is required on %s(%s)"
-#~ msgstr "au moins une relation %s est nécessaire sur %s(%s)"
-
-#~ msgid "cancel edition"
-#~ msgstr "annuler l'édition"
-
-#~ msgid "close all"
-#~ msgstr "tout fermer"
-
-#~ msgid "components_applmessages"
-#~ msgstr "messages applicatifs"
-
-#~ msgid "components_applmessages_description"
-#~ msgstr "affiche les messages applicatifs"
-
-#~ msgid "components_rss_feed_url"
-#~ msgstr "syndication rss"
-
-#~ msgid "content"
-#~ msgstr "contenu"
-
-#~ msgid "content_format"
-#~ msgstr "format"
-
-#~ msgid ""
-#~ "default language (look at the i18n directory of the application to see "
-#~ "available languages)"
-#~ msgstr ""
-#~ "langue par défaut (regarder le répertoire i18n de l'application pour voir "
-#~ "les langues disponibles)"
-
-#~ msgid ""
-#~ "define a final relation: link a final relation type from a non final "
-#~ "entity to a final entity type. used to build the application schema"
-#~ msgstr ""
-#~ "définit une relation non finale: lie un type de relation non finaledepuis "
-#~ "une entité vers un type d'entité non final. Utilisé pour construire le "
-#~ "schéma de l'application"
-
-#~ msgid ""
-#~ "define a non final relation: link a non final relation type from a non "
-#~ "final entity to a non final entity type. used to build the application "
-#~ "schema"
-#~ msgstr ""
-#~ "définit une relation 'attribut', utilisé pour construire le schéma de "
-#~ "l'application"
-
-#~ msgid "define a relation type, used to build the application schema"
-#~ msgstr ""
-#~ "définit un type de relation, utilisé pour construire le schéma de "
-#~ "l'application"
-
-#~ msgid "define an entity type, used to build the application schema"
-#~ msgstr ""
-#~ "définit un type d'entité, utilisé pour construire le schéma de "
-#~ "l'application"
-
-#~ msgid "detailed schema view"
-#~ msgstr "vue détaillée du schéma"
-
-#~ msgid "filter"
-#~ msgstr "filtrer"
-
-#~ msgid "footer"
-#~ msgstr "pied de page"
-
-#~ msgid "graphical representation of the application'schema"
-#~ msgstr "représentation graphique du schéma de l'application"
-
-#~ msgid "header"
-#~ msgstr "en-tête de page"
-
-#~ msgid "hide meta-data"
-#~ msgstr "cacher les entités et relations \"méta\""
-
-#~ msgid "iCal"
-#~ msgstr "iCal"
-
-#~ msgid "incorrect value for type \"%s\""
-#~ msgstr "valeur incorrecte pour le type \"%s\""
-
-#~ msgid "inlined view"
-#~ msgstr "vue embarquée (en ligne)"
-
-#~ msgid "is it an application entity type or not ?"
-#~ msgstr "est-ce une entité applicative ou non ?"
-
-#~ msgid "is it an application relation type or not ?"
-#~ msgstr "est-ce une relation applicative ou non ?"
-
-#~ msgid "linked"
-#~ msgstr "lié"
-
-#~ msgid "loop in %s relation (%s)"
-#~ msgstr "boucle dans la relation %s (%s)"
-
-#~ msgid ""
-#~ "maximum number of related entities to display in in the restriction view"
-#~ msgstr ""
-#~ "nombre maximum d'entités liées à afficher dans la vue de restriction"
-
-#~ msgid "meta"
-#~ msgstr "méta"
-
-#~ msgid "no associated epermissions"
-#~ msgstr "aucune permission spécifique n'est définie"
-
-#~ msgid "no possible transition"
-#~ msgstr "aucune transition possible"
-
-#~ msgid "not specified"
-#~ msgstr "non spécifié"
-
-#~ msgid "owned by"
-#~ msgstr "appartient à"
-
-#~ msgid "planned_delivery"
-#~ msgstr "livraison prévue"
-
-#~ msgid "remove this Card"
-#~ msgstr "supprimer cette fiche"
-
-#~ msgid "see also"
-#~ msgstr "voir aussi"
-
-#~ msgid "show meta-data"
-#~ msgstr "afficher le schéma complet"
-
-#~ msgid "status will change from %s to %s"
-#~ msgstr "l'état va passer de %s à %s"
-
-#~ msgid "synopsis"
-#~ msgstr "synopsis"
-
-#~ msgid "view_manage"
-#~ msgstr "gestion du site"
-
-#~ msgid "wikiid"
-#~ msgstr "identifiant wiki"
-
-#~ msgid "workflow history"
-#~ msgstr "historique du workflow"
+#~ 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é"
+
+#~ msgctxt "inlined:CWRelation:relation_type:subject"
+#~ msgid "remove this CWRType"
+#~ msgstr "supprimer ce type de relation"
+
+#~ msgctxt "inlined:CWUser:use_email:subject"
+#~ msgid "remove this EmailAddress"
+#~ msgstr "supprimer cette adresse électronique"
--- a/interfaces.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/interfaces.py	Tue Sep 22 13:08:42 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
 
@@ -127,6 +124,9 @@
     def children(self):
         """returns the item's children"""
 
+    def children_rql(self):
+        """XXX returns RQL to get children"""
+
     def __iter__(self):
         """iterates over the item's children"""
 
@@ -137,7 +137,7 @@
         """returns true if this node has no parent"""
 
     def root(self):
-        """return the root object"""
+        """returns the root object"""
 
 
 ## web specific interfaces ####################################################
--- a/md5crypt.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/md5crypt.py	Tue Sep 22 13:08:42 2009 +0200
@@ -1,10 +1,8 @@
 #########################################################
 """
-
+XXX clarify this header
 :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
 """
 # md5crypt.py
 #
@@ -58,8 +56,9 @@
         v = v >> 6
     return ret
 
-
 def crypt(pw, salt, magic=None):
+    if isinstance(pw, unicode):
+        pw = pw.encode('utf-8')
     if magic is None:
         magic = MAGIC
     # Take care of the magic string if present
--- a/misc/migration/2.42.0_Any.py	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 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.4.3_Any.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,2 @@
+# sync and restart to make sure cwuri does not appear in forms
+sync_schema_props_perms()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.5.0_Any.py	Tue Sep 22 13:08:42 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	Wed Aug 05 09:15:56 2009 +0200
+++ b/misc/migration/bootstrapmigration_repository.py	Tue Sep 22 13:08:42 2009 +0200
@@ -11,26 +11,63 @@
 applcubicwebversion, cubicwebversion = versions_map['cubicweb']
 
 if applcubicwebversion < (3, 4, 0) and cubicwebversion >= (3, 4, 0):
-    from cubicweb import RepositoryError
-    from cubicweb.server.hooks import uniquecstrcheck_before_modification
+
     session.set_shared_data('do-not-insert-cwuri', True)
-    repo.hm.unregister_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
-    repo.hm.unregister_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
+    deactivate_verification_hooks()
     add_relation_type('cwuri')
     base_url = session.base_url()
     # use an internal session since some entity might forbid modifications to admin
     isession = repo.internal_session()
     for eid, in rql('Any X', ask_confirm=False):
-        try:
+        type, source, extid = session.describe(eid)
+        if source == 'system':
             isession.execute('SET X cwuri %(u)s WHERE X eid %(x)s',
                              {'x': eid, 'u': base_url + u'eid/%s' % eid})
-        except RepositoryError:
-            print 'unable to set cwuri for', eid, session.describe(eid)
     isession.commit()
-    repo.hm.register_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
-    repo.hm.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
+    reactivate_verification_hooks()
     session.set_shared_data('do-not-insert-cwuri', False)
 
+if applcubicwebversion < (3, 5, 0) and cubicwebversion >= (3, 5, 0):
+    add_entity_type('Workflow')
+    add_entity_type('BaseTransition')
+    add_entity_type('WorkflowTransition')
+    add_entity_type('SubWorkflowExitPoint')
+    # drop 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'):
@@ -42,37 +79,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/misc/migration/postcreate.py	Tue Sep 22 13:08:42 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/req.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,324 @@
+"""Base class for request/session
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: Library General Public License version 2 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from urllib import quote as urlquote, unquote as urlunquote
+from datetime import time, datetime, timedelta
+
+from logilab.common.decorators import cached
+
+from cubicweb import Unauthorized, typed_eid
+from cubicweb.rset import ResultSet
+from cubicweb.utils import ustrftime, strptime, todate, todatetime
+
+ONESECOND = timedelta(0, 1, 0)
+CACHE_REGISTRY = {}
+
+
+class Cache(dict):
+    def __init__(self):
+        super(Cache, self).__init__()
+        _now = datetime.now()
+        self.cache_creation_date = _now
+        self.latest_cache_lookup = _now
+
+
+class RequestSessionBase(object):
+    """base class containing stuff shared by server session and web request
+
+    request/session is the main resources accessor, mainly through it's vreg
+    attribute:
+    :vreg:
+      the instance's registry
+    :vreg.schema:
+      the instance's schema
+    :vreg.config:
+      the instance's configuration
+    """
+    def __init__(self, vreg):
+        self.vreg = vreg
+        try:
+            encoding = vreg.property_value('ui.encoding')
+        except: # no vreg or property not registered
+            encoding = 'utf-8'
+        self.encoding = encoding
+        # cache result of execution for (rql expr / eids),
+        # should be emptied on commit/rollback of the server session / web
+        # connection
+        self.local_perm_cache = {}
+        self._ = unicode
+
+    def property_value(self, key):
+        """return value of the property with the given key, giving priority to
+        user specific value if any, else using site value
+        """
+        if self.user:
+            return self.user.property_value(key)
+        return self.vreg.property_value(key)
+
+    def etype_rset(self, etype, size=1):
+        """return a fake result set for a particular entity type"""
+        rset = ResultSet([('A',)]*size, '%s X' % etype,
+                         description=[(etype,)]*size)
+        def get_entity(row, col=0, etype=etype, req=self, rset=rset):
+            return req.vreg.etype_class(etype)(req, rset, row, col)
+        rset.get_entity = get_entity
+        return self.decorate_rset(rset)
+
+    def eid_rset(self, eid, etype=None):
+        """return a result set for the given eid without doing actual query
+        (we have the eid, we can suppose it exists and user has access to the
+        entity)
+        """
+        eid = typed_eid(eid)
+        if etype is None:
+            etype = self.describe(eid)[0]
+        rset = ResultSet([(eid,)], 'Any X WHERE X eid %(x)s', {'x': eid},
+                         [(etype,)])
+        return self.decorate_rset(rset)
+
+    def empty_rset(self):
+        """return a result set for the given eid without doing actual query
+        (we have the eid, we can suppose it exists and user has access to the
+        entity)
+        """
+        return self.decorate_rset(ResultSet([], 'Any X WHERE X eid -1'))
+
+    def entity_from_eid(self, eid, etype=None):
+        """return an entity instance for the given eid. No query is done"""
+        try:
+            return self.entity_cache(eid)
+        except KeyError:
+            rset = self.eid_rset(eid, etype)
+            entity = rset.get_entity(0, 0)
+            self.set_entity_cache(entity)
+            return entity
+
+    def entity_cache(self, eid):
+        raise KeyError
+
+    def set_entity_cache(self, entity):
+        pass
+
+    # XXX move to CWEntityManager or even better as factory method (unclear
+    # where yet...)
+    def create_entity(self, etype, *args, **kwargs):
+        """add a new entity of the given type"""
+        rql = 'INSERT %s X' % etype
+        relations = []
+        restrictions = []
+        cachekey = []
+        for rtype, rvar in args:
+            relations.append('X %s %s' % (rtype, rvar))
+            restrictions.append('%s eid %%(%s)s' % (rvar, rvar))
+            cachekey.append(rvar)
+        for attr in kwargs:
+            if attr in cachekey:
+                continue
+            relations.append('X %s %%(%s)s' % (attr, attr))
+        if relations:
+            rql = '%s: %s' % (rql, ', '.join(relations))
+        if restrictions:
+            rql = '%s WHERE %s' % (rql, ', '.join(restrictions))
+        return self.execute(rql, kwargs, cachekey).get_entity(0, 0)
+
+    def ensure_ro_rql(self, rql):
+        """raise an exception if the given rql is not a select query"""
+        first = rql.split(' ', 1)[0].lower()
+        if first in ('insert', 'set', 'delete'):
+            raise Unauthorized(self._('only select queries are authorized'))
+
+    def get_cache(self, cachename):
+        """
+        NOTE: cachename should be dotted names as in :
+        - cubicweb.mycache
+        - cubes.blog.mycache
+        - etc.
+        """
+        if cachename in CACHE_REGISTRY:
+            cache = CACHE_REGISTRY[cachename]
+        else:
+            cache = CACHE_REGISTRY[cachename] = Cache()
+        _now = datetime.now()
+        if _now > cache.latest_cache_lookup + ONESECOND:
+            ecache = self.execute(
+                'Any C,T WHERE C is CWCache, C name %(name)s, C timestamp T',
+                {'name':cachename}).get_entity(0,0)
+            cache.latest_cache_lookup = _now
+            if not ecache.valid(cache.cache_creation_date):
+                cache.clear()
+                cache.cache_creation_date = _now
+        return cache
+
+    # url generation methods ##################################################
+
+    def build_url(self, *args, **kwargs):
+        """return an absolute URL using params dictionary key/values as URL
+        parameters. Values are automatically URL quoted, and the
+        publishing method to use may be specified or will be guessed.
+        """
+        # use *args since we don't want first argument to be "anonymous" to
+        # avoid potential clash with kwargs
+        if args:
+            assert len(args) == 1, 'only 0 or 1 non-named-argument expected'
+            method = args[0]
+        else:
+            method = None
+        # XXX I (adim) think that if method is passed explicitly, we should
+        #     not try to process it and directly call req.build_url()
+        if method is None:
+            if self.from_controller() == 'view' and not '_restpath' in kwargs:
+                method = self.relative_path(includeparams=False) or 'view'
+            else:
+                method = 'view'
+        base_url = kwargs.pop('base_url', None)
+        if base_url is None:
+            base_url = self.base_url()
+        if '_restpath' in kwargs:
+            assert method == 'view', method
+            path = kwargs.pop('_restpath')
+        else:
+            path = method
+        if not kwargs:
+            return u'%s%s' % (base_url, path)
+        return u'%s%s?%s' % (base_url, path, self.build_url_params(**kwargs))
+
+
+    def build_url_params(self, **kwargs):
+        """return encoded params to incorporate them in an URL"""
+        args = []
+        for param, values in kwargs.items():
+            if not isinstance(values, (list, tuple)):
+                values = (values,)
+            for value in values:
+                args.append(u'%s=%s' % (param, self.url_quote(value)))
+        return '&'.join(args)
+
+    def url_quote(self, value, safe=''):
+        """urllib.quote is not unicode safe, use this method to do the
+        necessary encoding / decoding. Also it's designed to quote each
+        part of a url path and so the '/' character will be encoded as well.
+        """
+        if isinstance(value, unicode):
+            quoted = urlquote(value.encode(self.encoding), safe=safe)
+            return unicode(quoted, self.encoding)
+        return urlquote(str(value), safe=safe)
+
+    def url_unquote(self, quoted):
+        """returns a unicode unquoted string
+
+        decoding is based on `self.encoding` which is the encoding
+        used in `url_quote`
+        """
+        if isinstance(quoted, unicode):
+            quoted = quoted.encode(self.encoding)
+        try:
+            return unicode(urlunquote(quoted), self.encoding)
+        except UnicodeDecodeError: # might occurs on manually typed URLs
+            return unicode(urlunquote(quoted), 'iso-8859-1')
+
+    # bound user related methods ###############################################
+
+    @cached
+    def user_data(self):
+        """returns a dictionnary with this user's information"""
+        userinfo = {}
+        if self.is_internal_session:
+            userinfo['login'] = "cubicweb"
+            userinfo['name'] = "cubicweb"
+            userinfo['email'] = ""
+            return userinfo
+        user = self.actual_session().user
+        userinfo['login'] = user.login
+        userinfo['name'] = user.name()
+        userinfo['email'] = user.get_email()
+        return userinfo
+
+    def is_internal_session(self):
+        """overrided on the server-side"""
+        return False
+
+    # formating methods #######################################################
+
+    def view(self, __vid, rset=None, __fallback_oid=None, __registry='views',
+             **kwargs):
+        """shortcut to self.vreg.view method avoiding to pass the request"""
+        return self.vreg[__registry].render(__vid, self, __fallback_oid,
+                                            rset=rset, **kwargs)
+
+    def format_date(self, date, date_format=None, time=False):
+        """return a string for a date time according to instance's
+        configuration
+        """
+        if date:
+            if date_format is None:
+                if time:
+                    date_format = self.property_value('ui.datetime-format')
+                else:
+                    date_format = self.property_value('ui.date-format')
+            return ustrftime(date, date_format)
+        return u''
+
+    def format_time(self, time):
+        """return a string for a time according to instance's
+        configuration
+        """
+        if time:
+            return ustrftime(time, self.property_value('ui.time-format'))
+        return u''
+
+    def format_float(self, num):
+        """return a string for floating point number according to instance's
+        configuration
+        """
+        if num:
+            return self.property_value('ui.float-format') % num
+        return u''
+
+    def parse_datetime(self, value, etype='Datetime'):
+        """get a datetime or time from a string (according to etype)
+        Datetime formatted as Date are accepted
+        """
+        assert etype in ('Datetime', 'Date', 'Time'), etype
+        # XXX raise proper validation error
+        if etype == 'Datetime':
+            format = self.property_value('ui.datetime-format')
+            try:
+                return todatetime(strptime(value, format))
+            except ValueError:
+                pass
+        elif etype == 'Time':
+            format = self.property_value('ui.time-format')
+            try:
+                # (adim) I can't find a way to parse a Time with a custom format
+                date = strptime(value, format) # this returns a DateTime
+                return time(date.hour, date.minute, date.second)
+            except ValueError:
+                raise ValueError('can\'t parse %r (expected %s)' % (value, format))
+        try:
+            format = self.property_value('ui.date-format')
+            dt = strptime(value, format)
+            if etype == 'Datetime':
+                return todatetime(dt)
+            return todate(dt)
+        except ValueError:
+            raise ValueError('can\'t parse %r (expected %s)' % (value, format))
+
+    # abstract methods to override according to the web front-end #############
+
+    def base_url(self):
+        """return the root url of the instance"""
+        raise NotImplementedError
+
+    def decorate_rset(self, rset):
+        """add vreg/req (at least) attributes to the given result set """
+        raise NotImplementedError
+
+    def describe(self, eid):
+        """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
+        raise NotImplementedError
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rqlrewrite.py	Tue Sep 22 13:08:42 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/rset.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/rset.py	Tue Sep 22 13:08:42 2009 +0200
@@ -9,7 +9,7 @@
 
 from logilab.common.decorators import cached, clear_cache, copy_cache
 
-from rql import nodes
+from rql import nodes, stmts
 
 from cubicweb import NotAnEntity
 
@@ -83,7 +83,7 @@
         try:
             return self._rsetactions[key]
         except KeyError:
-            actions = self.vreg['actions'].possible_vobjects(
+            actions = self.vreg['actions'].poss_visible_objects(
                 self.req, rset=self, **kwargs)
             self._rsetactions[key] = actions
             return actions
@@ -240,8 +240,6 @@
                 rset = mapping[key]
             rset.rows.append(self.rows[idx])
             rset.description.append(self.description[idx])
-
-
         for rset in result:
             rset.rowcount = len(rset.rows)
         if return_dict:
@@ -249,6 +247,51 @@
         else:
             return result
 
+    def limited_rql(self):
+        """return a printable rql for the result set associated to the object,
+        with limit/offset correctly set according to maximum page size and
+        currently displayed page when necessary
+        """
+        # try to get page boundaries from the navigation component
+        # XXX we should probably not have a ref to this component here (eg in
+        #     cubicweb.common)
+        nav = self.vreg['components'].select_or_none('navigation', self.req,
+                                                     rset=self)
+        if nav:
+            start, stop = nav.page_boundaries()
+            rql = self._limit_offset_rql(stop - start, start)
+        # result set may have be limited manually in which case navigation won't
+        # apply
+        elif self.limited:
+            rql = self._limit_offset_rql(*self.limited)
+        # navigation component doesn't apply and rset has not been limited, no
+        # need to limit query
+        else:
+            rql = self.printable_rql()
+        return rql
+
+    def _limit_offset_rql(self, limit, offset):
+        rqlst = self.syntax_tree()
+        if len(rqlst.children) == 1:
+            select = rqlst.children[0]
+            olimit, ooffset = select.limit, select.offset
+            select.limit, select.offset = limit, offset
+            rql = rqlst.as_string(kwargs=self.args)
+            # restore original limit/offset
+            select.limit, select.offset = olimit, ooffset
+        else:
+            newselect = stmts.Select()
+            newselect.limit = limit
+            newselect.offset = offset
+            aliases = [nodes.VariableRef(newselect.get_variable(vref.name, i))
+                       for i, vref in enumerate(rqlst.selection)]
+            newselect.set_with([nodes.SubQuery(aliases, rqlst)], check=False)
+            newunion = stmts.Union()
+            newunion.append(newselect)
+            rql = rqlst.as_string(kwargs=self.args)
+            rqlst.parent = None
+        return rql
+
     def limit(self, limit, offset=0, inplace=False):
         """limit the result set to the given number of rows optionaly starting
         from an index different than 0
@@ -318,6 +361,14 @@
             if self.rows[i][col] is not None:
                 yield self.get_entity(i, col)
 
+    def complete_entity(self, row, col=0, skip_bytes=True):
+        """short cut to get an completed entity instance for a particular
+        row (all instance's attributes have been fetched)
+        """
+        entity = self.get_entity(row, col)
+        entity.complete(skip_bytes=skip_bytes)
+        return entity
+
     @cached
     def get_entity(self, row, col=None):
         """special method for query retreiving a single entity, returns a
@@ -371,16 +422,17 @@
         #     new attributes found in this resultset ?
         try:
             entity = req.entity_cache(eid)
-            if entity.rset is None:
-                # entity has no rset set, this means entity has been cached by
-                # the repository (req is a repository session) which had no rset
-                # info. Add id.
-                entity.rset = self
-                entity.row = row
-                entity.col = col
-            return entity
         except KeyError:
             pass
+        else:
+            if entity.rset is None:
+                # entity has no rset set, this means entity has been created by
+                # the querier (req is a repository session) and so jas no rset
+                # info. Add it.
+                entity.cw_rset = self
+                entity.cw_row = row
+                entity.cw_col = col
+            return entity
         # build entity instance
         etype = self.description[row][col]
         entity = self.vreg['etypes'].etype_class(etype)(req, rset=self,
@@ -395,7 +447,7 @@
             if rqlst.TYPE == 'select':
                 # UNION query, find the subquery from which this entity has been
                 # found
-                rqlst = rqlst.locate_subquery(col, etype, self.args)
+                rqlst, col = rqlst.locate_subquery(col, etype, self.args)
             # take care, due to outer join support, we may find None
             # values for non final relation
             for i, attr, x in attr_desc_iterator(rqlst, col):
@@ -495,7 +547,8 @@
                     if len(self.column_types(i)) > 1:
                         break
         # UNION query, find the subquery from which this entity has been found
-        select = rqlst.locate_subquery(locate_query_col, etype, self.args)
+        select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0]
+        col = rqlst.subquery_selection_index(select, col)
         try:
             myvar = select.selection[col].variable
         except AttributeError:
@@ -503,7 +556,7 @@
             return None, None
         rel = myvar.main_relation()
         if rel is not None:
-            index = rel.children[0].variable.selected_index()
+            index = rel.children[0].root_selection_index()
             if index is not None and self.rows[row][index]:
                 return self.get_entity(row, index), rel.r_type
         return None, None
--- a/schema.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/schema.py	Tue Sep 22 13:08:42 2009 +0200
@@ -14,7 +14,9 @@
 from warnings import warn
 
 from logilab.common.decorators import cached, clear_cache, monkeypatch
+from logilab.common.logging_ext import set_log_methods
 from logilab.common.deprecation import deprecated
+from logilab.common.graph import get_cycles
 from logilab.common.compat import any
 
 from yams import BadSchemaDefinition, buildobjs as ybo
@@ -26,8 +28,8 @@
 
 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
 
+import cubicweb
 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
-from cubicweb import set_log_methods
 
 # XXX <3.2 bw compat
 from yams import schema
@@ -42,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((
@@ -58,6 +61,38 @@
 ybo.RTYPE_PROPERTIES += ('eid',)
 ybo.RDEF_PROPERTIES += ('eid',)
 
+
+# XXX same algorithm as in reorder_cubes and probably other place,
+# may probably extract a generic function
+def order_eschemas(eschemas):
+    """return entity schemas ordered such that entity types which specializes an
+    other one appears after that one
+    """
+    graph = {}
+    for eschema in eschemas:
+        if eschema.specializes():
+            graph[eschema] = set((eschema.specializes(),))
+        else:
+            graph[eschema] = set()
+    cycles = get_cycles(graph)
+    if cycles:
+        cycles = '\n'.join(' -> '.join(cycle) for cycle in cycles)
+        raise Exception('cycles in entity schema specialization: %s'
+                        % cycles)
+    eschemas = []
+    while graph:
+        # sorted to get predictable results
+        for eschema, deps in sorted(graph.items()):
+            if not deps:
+                eschemas.append(eschema)
+                del graph[eschema]
+                for deps in graph.itervalues():
+                    try:
+                        deps.remove(eschema)
+                    except KeyError:
+                        continue
+    return eschemas
+
 def bw_normalize_etype(etype):
     if etype in ETYPE_NAME_MAP:
         msg = '%s has been renamed to %s, please update your code' % (
@@ -66,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
     """
@@ -76,9 +111,13 @@
     if form:
         key = key + '_' + form
     # ensure unicode
-    # added .lower() in case no translation are available
-    return unicode(req._(key)).lower()
-__builtins__['display_name'] = deprecated('display_name should be imported from cubicweb.schema')(display_name)
+    # .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('[3.4] display_name should be imported from cubicweb.schema')(display_name)
 
 def ERSchema_display_name(self, req, form=''):
     """return a internationalized string for the entity/relation type name in
@@ -413,6 +452,7 @@
     reading_from_database = False
     entity_class = CubicWebEntitySchema
     relation_class = CubicWebRelationSchema
+    no_specialization_inference = ('identity',)
 
     def __init__(self, *args, **kwargs):
         self._eid_index = {}
@@ -485,6 +525,7 @@
         for k, v in self._eid_index.items():
             if v == (subjtype, rtype, objtype):
                 del self._eid_index[k]
+                break
         super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype)
 
     def del_entity_type(self, etype):
@@ -516,7 +557,7 @@
     def __init__(self, restriction):
         self.restriction = restriction
 
-    def check_consistency(self, subjschema, objschema):
+    def check_consistency(self, subjschema, objschema, rdef):
         if objschema.is_final():
             raise BadSchemaDefinition("unique constraint doesn't apply to "
                                       "final entity type")
@@ -606,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
@@ -731,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):
@@ -802,6 +843,7 @@
 PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression)
 
 # workflow extensions #########################################################
+
 from yams.buildobjs import _add_relation as yams_add_relation
 
 class workflowable_definition(ybo.metadefinition):
@@ -810,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
 
@@ -848,13 +897,12 @@
         """return a Schema instance from the schema definition read
         from <directory>
         """
-        self.lib_directory = config.schemas_lib_dir()
         return super(BootstrapSchemaLoader, self).load(
             path, config.appid, register_base_types=False, **kwargs)
 
     def _load_definition_files(self, cubes=None):
         # bootstraping, ignore cubes
-        filepath = join(self.lib_directory, 'bootstrap.py')
+        filepath = join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py')
         self.info('loading %s', filepath)
         self.handle_file(filepath)
 
@@ -873,6 +921,10 @@
         from <directory>
         """
         self.info('loading %s schemas', ', '.join(config.cubes()))
+        self.extrapath = {}
+        for cubesdir in config.cubes_search_path():
+            if cubesdir != config.CUBES_DIR:
+                self.extrapath[cubesdir] = 'cubes'
         if config.apphome:
             path = tuple(reversed([config.apphome] + config.cubes_path()))
         else:
@@ -881,13 +933,13 @@
             return super(CubicWebSchemaLoader, self).load(config, path=path, **kwargs)
         finally:
             # we've to cleanup modules imported from cubicweb.schemas as well
-            cleanup_sys_modules([self.lib_directory])
+            cleanup_sys_modules([join(cubicweb.CW_SOFTWARE_ROOT, 'schemas')])
 
     def _load_definition_files(self, cubes):
-        for filepath in (join(self.lib_directory, 'bootstrap.py'),
-                         join(self.lib_directory, 'base.py'),
-                         join(self.lib_directory, 'workflow.py'),
-                         join(self.lib_directory, 'Bookmark.py')):
+        for filepath in (join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py'),
+                         join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'base.py'),
+                         join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'workflow.py'),
+                         join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'Bookmark.py')):
             self.info('loading %s', filepath)
             self.handle_file(filepath)
         for cube in cubes:
@@ -906,9 +958,12 @@
 NEED_PERM_FORMATS = [_('text/cubicweb-page-template')]
 
 @monkeypatch(FormatConstraint)
-def vocabulary(self, entity=None, req=None):
-    if req is None and entity is not None:
+def vocabulary(self, entity=None, form=None):
+    req = None
+    if form is None and entity is not None:
         req = entity.req
+    elif form is not None:
+        req = form.req
     if req is not None and req.user.has_permission(PERM_USE_TEMPLATE_FORMAT):
         return self.regular_formats + tuple(NEED_PERM_FORMATS)
     return self.regular_formats
@@ -941,3 +996,9 @@
 def bw_set_statement_type(self, etype):
     return orig_set_statement_type(self, bw_normalize_etype(etype))
 stmts.Select.set_statement_type = bw_set_statement_type
+
+# XXX deprecated
+from yams.constraints import format_constraint
+format_constraint = deprecated('[3.4] use RichString instead of format_constraint')(format_constraint)
+from yams.buildobjs import RichString
+PyFileReader.context['format_constraint'] = format_constraint
--- a/schemas/base.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/schemas/base.py	Tue Sep 22 13:08:42 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	Wed Aug 05 09:15:56 2009 +0200
+++ b/schemas/workflow.py	Tue Sep 22 13:08:42 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/schemaviewer.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/schemaviewer.py	Tue Sep 22 13:08:42 2009 +0200
@@ -104,7 +104,7 @@
         """get a layout for an entity schema"""
         etype = eschema.type
         layout = Section(children=' ', klass='clear')
-        layout.append(Link(etype,'&nbsp;' , id=etype)) # anchor
+        layout.append(Link(etype,'&#160;' , id=etype)) # anchor
         title = Link(self.eschema_link_url(eschema), etype)
         boxchild = [Section(children=(title, ' (%s)'% eschema.display_name(self.req)), klass='title')]
         table = Table(cols=4, rheaders=1,
--- a/selectors.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/selectors.py	Tue Sep 22 13:08:42 2009 +0200
@@ -46,7 +46,6 @@
 from warnings import warn
 
 from logilab.common.compat import all
-from logilab.common.deprecation import deprecated
 from logilab.common.interface import implements as implements_iface
 
 from yams import BASE_TYPES
@@ -55,6 +54,7 @@
                       role, typed_eid)
 # even if not used, let yes here so it's importable through this module
 from cubicweb.appobject import Selector, objectify_selector, yes
+from cubicweb.vregistry import class_regid
 from cubicweb.cwconfig import CubicWebConfiguration
 from cubicweb.schema import split_expression
 
@@ -75,7 +75,7 @@
         else:
             selname = selector.__name__
             vobj = cls
-        oid = vobj.id
+        oid = class_regid(vobj)
         ret = selector(cls, *args, **kwargs)
         if TRACED_OIDS == 'all' or oid in TRACED_OIDS:
             #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
@@ -113,13 +113,13 @@
         return traceback is None
 
 
-def score_interface(cls_or_inst, cls, iface):
+def score_interface(etypesreg, cls_or_inst, cls, iface):
     """Return XXX if the give object (maybe an instance or class) implements
     the interface.
     """
     if getattr(iface, '__registry__', None) == 'etypes':
         # adjust score if the interface is an entity class
-        parents = cls_or_inst.parent_classes()
+        parents = etypesreg.parent_classes(cls_or_inst.id)
         if iface is cls:
             return len(parents) + 4
         if iface is parents[-1]: # Any
@@ -158,17 +158,18 @@
         return '%s(%s)' % (self.__class__.__name__,
                            ','.join(str(s) for s in self.expected_ifaces))
 
-    def score_interfaces(self, cls_or_inst, cls):
+    def score_interfaces(self, req, cls_or_inst, cls):
         score = 0
-        vreg, eschema = cls_or_inst.vreg, cls_or_inst.e_schema
+        etypesreg = req.vreg['etypes']
+        eschema = cls_or_inst.e_schema
         for iface in self.expected_ifaces:
             if isinstance(iface, basestring):
                 # entity type
                 try:
-                    iface = vreg['etypes'].etype_class(iface)
+                    iface = etypesreg.etype_class(iface)
                 except KeyError:
                     continue # entity type not in the schema
-            score += score_interface(cls_or_inst, cls, iface)
+            score += score_interface(etypesreg, cls_or_inst, cls, iface)
         return score
 
 
@@ -212,7 +213,7 @@
     def score(self, cls, req, etype):
         if etype in BASE_TYPES:
             return 0
-        return self.score_class(cls.vreg['etypes'].etype_class(etype), req)
+        return self.score_class(req.vreg['etypes'].etype_class(etype), req)
 
     def score_class(self, eclass, req):
         raise NotImplementedError()
@@ -527,12 +528,10 @@
     @lltrace
     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
         try:
-            trname = req.execute('Any XN WHERE X is Transition, X eid %(x)s, X name XN',
-                                 {'x': typed_eid(req.form['treid'])})[0][0]
-        except (KeyError, IndexError):
-            return 0
-        # XXX check this is a transition that apply to the object?
-        if not trname in self.expected:
+            # XXX check this is a transition that apply to the object?
+            if not kwargs['transition'].name in self.expected:
+                return 0
+        except KeyError:
             return 0
         return 1
 
@@ -561,7 +560,7 @@
 
     def __call__(self, cls, req, **kwargs):
         try:
-            cls.vreg[self.registry].select(self.oid, req, **kwargs)
+            req.vreg[self.registry].select(self.oid, req, **kwargs)
             return 1
         except NoSelectableObject:
             return 0
@@ -585,7 +584,7 @@
           proximity so the most specific object'll be selected
     """
     def score_class(self, eclass, req):
-        return self.score_interfaces(eclass, eclass)
+        return self.score_interfaces(req, eclass, eclass)
 
 
 class specified_etype_implements(implements):
@@ -611,14 +610,14 @@
                 etype = req.form['etype']
             except KeyError:
                 return 0
-        else:
-            # only check this is a known type if etype comes from req.form,
-            # else we want the error to propagate
-            try:
-                etype = cls.vreg.case_insensitive_etypes[etype.lower()]
-                req.form['etype'] = etype
-            except KeyError:
-                return 0
+            else:
+                # only check this is a known type if etype comes from req.form,
+                # else we want the error to propagate
+                try:
+                    etype = cls.vreg.case_insensitive_etypes[etype.lower()]
+                    req.form['etype'] = etype
+                except KeyError:
+                    return 0
         return self.score_class(cls.vreg['etypes'].etype_class(etype), req)
 
 
@@ -638,7 +637,7 @@
           proximity so the most specific object'll be selected
     """
     def score_entity(self, entity):
-        return self.score_interfaces(entity, entity.__class__)
+        return self.score_interfaces(entity.req, entity, entity.__class__)
 
 
 class relation_possible(EClassSelector):
@@ -665,10 +664,14 @@
 
     @lltrace
     def __call__(self, cls, req, *args, **kwargs):
-        rschema = cls.schema.rschema(self.rtype)
+        rschema = req.vreg.schema.rschema(self.rtype)
         if not (rschema.has_perm(req, self.action)
                 or rschema.has_local_role(self.action)):
             return 0
+        if self.action != 'read':
+            if not (rschema.has_perm(req, 'read')
+                    or rschema.has_local_role('read')):
+                return 0
         score = super(relation_possible, self).__call__(cls, req, *args, **kwargs)
         return score
 
@@ -854,7 +857,7 @@
         if row is None:
             score = 0
             need_local_check = []
-            geteschema = cls.schema.eschema
+            geteschema = req.vreg.schema.eschema
             for etype in rset.column_types(0):
                 if etype in BASE_TYPES:
                     return 0
@@ -892,7 +895,7 @@
     See `EClassSelector` documentation for behaviour when row is not specified.
     """
     def score(self, cls, req, etype):
-        eschema = cls.schema.eschema(etype)
+        eschema = req.vreg.schema.eschema(etype)
         if not (eschema.is_final() or eschema.is_subobject(strict=True)) \
                and eschema.has_perm(req, 'add'):
             return 1
@@ -960,201 +963,11 @@
     """
     def __init__(self, scorefunc, once_is_enough=False):
         super(score_entity, self).__init__(once_is_enough)
-        self.score_entity = scorefunc
-
-
-# XXX DEPRECATED ##############################################################
-from cubicweb.vregistry import chainall
-
-yes_selector = deprecated()(yes)
-norset_selector = deprecated()(none_rset)
-rset_selector = deprecated()(any_rset)
-anyrset_selector = deprecated()(nonempty_rset)
-emptyrset_selector = deprecated()(empty_rset)
-onelinerset_selector = deprecated()(one_line_rset)
-twolinerset_selector = deprecated()(two_lines_rset)
-twocolrset_selector = deprecated()(two_cols_rset)
-largerset_selector = deprecated()(paginated_rset)
-sortedrset_selector = deprecated()(sorted_rset)
-oneetyperset_selector = deprecated()(one_etype_rset)
-multitype_selector = deprecated()(two_etypes_rset)
-anonymous_selector = deprecated()(anonymous_user)
-not_anonymous_selector = deprecated()(authenticated_user)
-primaryview_selector = deprecated()(primary_view)
-contextprop_selector = deprecated()(match_context_prop)
-
-@deprecated('use non_final_entity instead of %s')
-def nfentity_selector(cls, req, rset=None, row=None, col=0, **kwargs):
-    return non_final_entity()(cls, req, rset, row, col)
-
-@deprecated('use implements instead of %s')
-def implement_interface(cls, req, rset=None, row=None, col=0, **kwargs):
-    return implements(*cls.accepts_interfaces)(cls, req, rset, row, col)
-_interface_selector = deprecated()(implement_interface)
-interface_selector = deprecated()(implement_interface)
-
-@deprecated('use specified_etype_implements instead of %s')
-def accept_etype(cls, req, *args, **kwargs):
-    """check etype presence in request form *and* accepts conformance"""
-    return specified_etype_implements(*cls.accepts)(cls, req, *args)
-etype_form_selector = accept_etype
-
-@deprecated('use match_search_state instead of %s')
-def searchstate_selector(cls, req, rset=None, row=None, col=0, **kwargs):
-    return match_search_state(cls.search_states)(cls, req, rset, row, col)
-
-@deprecated('use match_user_groups instead of %s')
-def match_user_group(cls, req, rset=None, row=None, col=0, **kwargs):
-    return match_user_groups(*cls.require_groups)(cls, req, rset, row, col, **kwargs)
-in_group_selector = match_user_group
-
-@deprecated('use relation_possible instead of %s')
-def has_relation(cls, req, rset=None, row=None, col=0, **kwargs):
-    return relation_possible(cls.rtype, role(cls), cls.etype,
-                             getattr(cls, 'require_permission', 'read'))(cls, req, rset, row, col, **kwargs)
-
-@deprecated('use relation_possible instead of %s')
-def one_has_relation(cls, req, rset=None, row=None, col=0, **kwargs):
-    return relation_possible(cls.rtype, role(cls), cls.etype,
-                             getattr(cls, 'require_permission', 'read',
-                                     once_is_enough=True))(cls, req, rset, row, col, **kwargs)
-
-@deprecated('use implements instead of %s')
-def accept_rset(cls, req, rset=None, row=None, col=0, **kwargs):
-    """simply delegate to cls.accept_rset method"""
-    return implements(*cls.accepts)(cls, req, rset, row=row, col=col)
-accept_rset_selector = accept_rset
-
-accept = chainall(non_final_entity(), accept_rset, name='accept')
-accept = deprecated('use implements selector')(accept)
-accept_selector = deprecated()(accept)
-
-accept_one = deprecated()(chainall(one_line_rset, accept,
-                                   name='accept_one'))
-accept_one_selector = deprecated()(accept_one)
-
-
-def _rql_condition(cls, req, rset=None, row=None, col=0, **kwargs):
-    if cls.condition:
-        return rql_condition(cls.condition)(cls, req, rset, row, col)
-    return 1
-_rqlcondition_selector = deprecated()(_rql_condition)
-
-rqlcondition_selector = deprecated()(chainall(non_final_entity(), one_line_rset, _rql_condition,
-                         name='rql_condition'))
-
-@deprecated('use but_etype instead of %s')
-def but_etype_selector(cls, req, rset=None, row=None, col=0, **kwargs):
-    return but_etype(cls.etype)(cls, req, rset, row, col)
-
-@lltrace
-def etype_rtype_selector(cls, req, rset=None, row=None, col=0, **kwargs):
-    schema = cls.schema
-    perm = getattr(cls, 'require_permission', 'read')
-    if hasattr(cls, 'etype'):
-        eschema = schema.eschema(cls.etype)
-        if not (eschema.has_perm(req, perm) or eschema.has_local_role(perm)):
-            return 0
-    if hasattr(cls, 'rtype'):
-        rschema = schema.rschema(cls.rtype)
-        if not (rschema.has_perm(req, perm) or rschema.has_local_role(perm)):
-            return 0
-    return 1
-etype_rtype_selector = deprecated()(etype_rtype_selector)
-
-#req_form_params_selector = deprecated()(match_form_params) # form_params
-#kwargs_selector = deprecated()(match_kwargs) # expected_kwargs
-
-# compound selectors ##########################################################
-
-searchstate_accept = chainall(nonempty_rset(), accept,
-                              name='searchstate_accept')
-searchstate_accept_selector = deprecated()(searchstate_accept)
-
-searchstate_accept_one = chainall(one_line_rset, accept, _rql_condition,
-                                  name='searchstate_accept_one')
-searchstate_accept_one_selector = deprecated()(searchstate_accept_one)
-
-searchstate_accept = deprecated()(searchstate_accept)
-searchstate_accept_one = deprecated()(searchstate_accept_one)
-
-# end of deprecation section ##################################################
-
-def unbind_method(selector):
-    def new_selector(registered):
-        # get the unbound method
-        if hasattr(registered, 'im_func'):
-            registered = registered.im_func
-        # don't rebind since it will be done automatically during
-        # the assignment, inside the destination class body
-        return selector(registered)
-    new_selector.__name__ = selector.__name__
-    return new_selector
-
-
-def deprecate(registered, msg):
-    # get the unbound method
-    if hasattr(registered, 'im_func'):
-        registered = registered.im_func
-    def _deprecate(cls, vreg):
-        warn(msg, DeprecationWarning)
-        return registered(cls, vreg)
-    return _deprecate
-
-@unbind_method
-def require_group_compat(registered):
-    def plug_selector(cls, vreg):
-        cls = registered(cls, vreg)
-        if getattr(cls, 'require_groups', None):
-            warn('use "match_user_groups(group1, group2)" instead of using require_groups',
-                 DeprecationWarning)
-            cls.__select__ &= match_user_groups(cls.require_groups)
-        return cls
-    return plug_selector
-
-@unbind_method
-def accepts_compat(registered):
-    def plug_selector(cls, vreg):
-        cls = registered(cls, vreg)
-        if getattr(cls, 'accepts', None):
-            warn('use "implements("EntityType", IFace)" instead of using accepts on %s'
-                 % cls,
-                 DeprecationWarning)
-            cls.__select__ &= implements(*cls.accepts)
-        return cls
-    return plug_selector
-
-@unbind_method
-def accepts_etype_compat(registered):
-    def plug_selector(cls, vreg):
-        cls = registered(cls, vreg)
-        if getattr(cls, 'accepts', None):
-            warn('use "specified_etype_implements("EntityType", IFace)" instead of using accepts',
-                 DeprecationWarning)
-            cls.__select__ &= specified_etype_implements(*cls.accepts)
-        return cls
-    return plug_selector
-
-@unbind_method
-def condition_compat(registered):
-    def plug_selector(cls, vreg):
-        cls = registered(cls, vreg)
-        if getattr(cls, 'condition', None):
-            warn('use "use rql_condition(expression)" instead of using condition',
-                 DeprecationWarning)
-            cls.__select__ &= rql_condition(cls.condition)
-        return cls
-    return plug_selector
-
-@unbind_method
-def has_relation_compat(registered):
-    def plug_selector(cls, vreg):
-        cls = registered(cls, vreg)
-        if getattr(cls, 'etype', None):
-            warn('use relation_possible selector instead of using etype_rtype',
-                 DeprecationWarning)
-            cls.__select__ &= relation_possible(cls.rtype, role(cls),
-                                                getattr(cls, 'etype', None),
-                                                action=getattr(cls, 'require_permission', 'read'))
-        return cls
-    return plug_selector
+        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
--- a/server/__init__.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/__init__.py	Tue Sep 22 13:08:42 2009 +0200
@@ -12,18 +12,24 @@
 
 import sys
 from os.path import join, exists
+from glob import glob
 
 from logilab.common.modutils import LazyObject
 from logilab.common.textutils import splitstrip
 
+from yams import BASE_GROUPS
+
+from cubicweb import CW_SOFTWARE_ROOT
+
 # server-side debugging #########################################################
 
 # server debugging flags. They may be combined using binary operators.
-DBG_NONE = 0 # no debug information
-DBG_RQL = 1  # rql execution information
-DBG_SQL = 2  # executed sql
-DBG_REPO = 4 # repository events
-DBG_MORE = 8 # repository events
+DBG_NONE = 0  # no debug information
+DBG_RQL = 1   # rql execution information
+DBG_SQL = 2   # executed sql
+DBG_REPO = 4  # repository events
+DBG_MS = 8    # multi-sources
+DBG_MORE = 16 # repository events
 
 # current debug mode
 DEBUG = 0
@@ -92,8 +98,6 @@
     with the minimal set of entities (ie at least the schema, base groups and
     a initial user)
     """
-    from glob import glob
-    from yams import BASE_GROUPS
     from cubicweb.dbapi import in_memory_cnx
     from cubicweb.server.repository import Repository
     from cubicweb.server.utils import manager_userpasswd
@@ -140,7 +144,7 @@
         #               if not repo.system_source.support_entity(str(e))])
     sqlexec(schemasql, execute, pbtitle=_title)
     # install additional driver specific sql files
-    for fpath in glob(join(config.schemas_lib_dir(), '*.sql.%s' % driver)):
+    for fpath in glob(join(CW_SOFTWARE_ROOT, 'schemas', '*.sql.%s' % driver)):
         print '-> installing', fpath
         sqlexec(open(fpath).read(), execute, False, delimiter=';;')
     for directory in config.cubes_path():
@@ -176,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
@@ -223,7 +211,8 @@
     for path in reversed(paths):
         mhandler.exec_event_script('pre%s' % event, path)
     # enter instance'schema into the database
-    serialize_schema(mhandler.rqlcursor, schema)
+    mhandler.session.set_pool()
+    serialize_schema(mhandler.session, schema)
     # execute cubicweb's post<event> script
     mhandler.exec_event_script('post%s' % event)
     # execute cubes'post<event> script if any
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/hook.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,440 @@
+"""Hooks management
+
+This module defined the `Hook` class and registry and a set of abstract classes
+for operations.
+
+
+Hooks are called before / after any individual update of entities / relations
+in the repository and on special events such as server startup or shutdown.
+
+
+Operations may be registered by hooks during a transaction, which will  be
+fired when the pool is commited or rollbacked.
+
+
+Entity hooks (eg before_add_entity, after_add_entity, before_update_entity,
+after_update_entity, before_delete_entity, after_delete_entity) all have an
+`entity` attribute
+
+Relation (eg before_add_relation, after_add_relation, before_delete_relation,
+after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes.
+
+Server start/stop hooks (eg server_startup, server_shutdown) have a `repo`
+attribute, but *their `_cw` attribute is None*.
+
+Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a
+`timestamp` attributes, but *their `_cw` attribute is None*.
+
+Session hooks (eg session_open, session_close) have no special attribute.
+
+
+: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
+"""
+__docformat__ = "restructuredtext en"
+
+from warnings import warn
+from logging import getLogger
+
+from logilab.common.decorators import classproperty
+from logilab.common.deprecation import deprecated
+from logilab.common.logging_ext import set_log_methods
+
+from cubicweb.cwvreg import CWRegistry, VRegistry
+from cubicweb.selectors import (objectify_selector, lltrace, match_search_state,
+                                entity_implements)
+from cubicweb.appobject import AppObject
+
+
+ENTITIES_HOOKS = set(('before_add_entity',    'after_add_entity',
+                      'before_update_entity', 'after_update_entity',
+                      'before_delete_entity', 'after_delete_entity'))
+RELATIONS_HOOKS = set(('before_add_relation',   'after_add_relation' ,
+                       'before_delete_relation','after_delete_relation'))
+SYSTEM_HOOKS = set(('server_backup', 'server_restore',
+                    'server_startup', 'server_shutdown',
+                    'session_open', 'session_close'))
+ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
+
+
+class HooksRegistry(CWRegistry):
+
+    def register(self, obj, **kwargs):
+        try:
+            iter(obj.events)
+        except:
+            raise Exception('bad .events attribute %s on %s' % (obj.event, obj))
+        for event in obj.events:
+            if event not in ALL_HOOKS:
+                raise Exception('bad event %s on %s' % (event, obj))
+        super(HooksRegistry, self).register(obj, **kwargs)
+
+    def call_hooks(self, event, req=None, **kwargs):
+        kwargs['event'] = event
+        for hook in sorted(self.possible_objects(req, **kwargs), key=lambda x: x.order):
+            if hook.enabled:
+                hook()
+            else:
+                warn('[3.6] %s: enabled is deprecated' % cls)
+
+VRegistry.REGISTRY_FACTORY['hooks'] = HooksRegistry
+
+
+# some hook specific selectors #################################################
+
+@objectify_selector
+@lltrace
+def match_event(cls, req, **kwargs):
+    if kwargs.get('event') in cls.events:
+        return 1
+    return 0
+
+@objectify_selector
+@lltrace
+def enabled_category(cls, req, **kwargs):
+    if req is None:
+        # server startup / shutdown event
+        config = kwargs['repo'].config
+    else:
+        config = req.vreg.config
+    if enabled_category in config.disabled_hooks_categories:
+        return 0
+    return 1
+
+@objectify_selector
+@lltrace
+def regular_session(cls, req, **kwargs):
+    if req is None or req.is_super_session:
+        return 0
+    return 1
+
+class match_rtype(match_search_state):
+    """accept if parameters specified as initializer arguments are specified
+    in named arguments given to the selector
+
+    :param *expected: parameters (eg `basestring`) which are expected to be
+                      found in named arguments (kwargs)
+    """
+
+    @lltrace
+    def __call__(self, cls, req, *args, **kwargs):
+        return kwargs.get('rtype') in self.expected
+
+
+# base class for hook ##########################################################
+
+class Hook(AppObject):
+    __registry__ = 'hooks'
+    __select__ = match_event() & enabled_category()
+    # set this in derivated classes
+    events = None
+    category = None
+    order = 0
+    # XXX deprecated
+    enabled = True
+
+    @classproperty
+    def __id__(cls):
+        warn('[3.6] %s: please specify an id for your hook' % cls)
+        return str(id(cls))
+
+    @classmethod
+    def __registered__(cls, vreg):
+        super(Hook, cls).__registered__(vreg)
+        if getattr(cls, 'accepts', None):
+            warn('[3.6] %s: accepts is deprecated, define proper __select__' % cls)
+            rtypes = []
+            for ertype in cls.accepts:
+                if ertype.islower():
+                    rtypes.append(ertype)
+                else:
+                    cls.__select__ = cls.__select__ & entity_implements(ertype)
+            if rtypes:
+                cls.__select__ = cls.__select__ & match_rtype(*rtypes)
+        return cls
+
+    known_args = set(('entity', 'rtype', 'eidfrom', 'eidto', 'repo', 'timestamp'))
+    def __init__(self, req, event, **kwargs):
+        for arg in self.known_args:
+            if arg in kwargs:
+                setattr(self, arg, kwargs.pop(arg))
+        super(Hook, self).__init__(req, **kwargs)
+        self.event = event
+
+    def __call__(self):
+        if hasattr(self, 'call'):
+            warn('[3.6] %s: call is deprecated, implements __call__' % self.__class__,
+                 DeprecationWarning)
+            if self.event.endswith('_relation'):
+                self.call(self._cw, self.eidfrom, self.rtype, self.eidto)
+            elif 'delete' in self.event:
+                self.call(self._cw, self.entity.eid)
+            elif self.event.startswith('server_'):
+                self.call(self.repo)
+            elif self.event.startswith('session_'):
+                self.call(self._cw)
+            else:
+                self.call(self._cw, self.entity)
+
+set_log_methods(Hook, getLogger('cubicweb.hook'))
+
+
+# base classes for relation propagation ########################################
+
+class PropagateSubjectRelationHook(Hook):
+    """propagate permissions and nosy list when new entity are added"""
+    events = ('after_add_relation',)
+    # to set in concrete class
+    rtype = None
+    subject_relations = None
+    object_relations = None
+    accepts = None # subject_relations + object_relations
+
+    def call(self, session, fromeid, rtype, toeid):
+        for eid in (fromeid, toeid):
+            etype = session.describe(eid)[0]
+            if not self.schema.eschema(etype).has_subject_relation(self.rtype):
+                return
+        if rtype in self.subject_relations:
+            meid, seid = fromeid, toeid
+        else:
+            assert rtype in self.object_relations
+            meid, seid = toeid, fromeid
+        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')
+
+
+# abstract classes for operation ###############################################
+
+class Operation(object):
+    """an operation is triggered on connections pool events related to
+    commit / rollback transations. Possible events are:
+
+    precommit:
+      the pool is preparing to commit. You shouldn't do anything things which
+      has to be reverted if the commit fail at this point, but you can freely
+      do any heavy computation or raise an exception if the commit can't go.
+      You can add some new operation during this phase but their precommit
+      event won't be triggered
+
+    commit:
+      the pool is preparing to commit. You should avoid to do to expensive
+      stuff or something that may cause an exception in this event
+
+    revertcommit:
+      if an operation failed while commited, this event is triggered for
+      all operations which had their commit event already to let them
+      revert things (including the operation which made fail the commit)
+
+    rollback:
+      the transaction has been either rollbacked either
+      * intentionaly
+      * a precommit event failed, all operations are rollbacked
+      * a commit event failed, all operations which are not been triggered for
+        commit are rollbacked
+
+    order of operations may be important, and is controlled according to:
+    * operation's class
+    """
+
+    def __init__(self, session, **kwargs):
+        self.session = session
+        self.__dict__.update(kwargs)
+        self.register(session)
+        # execution information
+        self.processed = None # 'precommit', 'commit'
+        self.failed = False
+
+    def register(self, session):
+        session.add_operation(self, self.insert_index())
+
+    def insert_index(self):
+        """return the index of  the lastest instance which is not a
+        LateOperation instance
+        """
+        for i, op in enumerate(self.session.pending_operations):
+            if isinstance(op, (LateOperation, SingleLastOperation)):
+                return i
+        return None
+
+    def handle_event(self, event):
+        """delegate event handling to the opertaion"""
+        getattr(self, event)()
+
+    def precommit_event(self):
+        """the observed connections pool is preparing a commit"""
+
+    def revertprecommit_event(self):
+        """an error went when pre-commiting this operation or a later one
+
+        should revert pre-commit's changes but take care, they may have not
+        been all considered if it's this operation which failed
+        """
+
+    def commit_event(self):
+        """the observed connections pool is commiting"""
+
+    def revertcommit_event(self):
+        """an error went when commiting this operation or a later one
+
+        should revert commit's changes but take care, they may have not
+        been all considered if it's this operation which failed
+        """
+
+    def rollback_event(self):
+        """the observed connections pool has been rollbacked
+
+        do nothing by default, the operation will just be removed from the pool
+        operation list
+        """
+
+    @property
+    @deprecated('[3.6] use self.session.user')
+    def user(self):
+        return self.session.user
+
+    @property
+    @deprecated('[3.6] use self.session.repo')
+    def repo(self):
+        return self.session.repo
+
+    @property
+    @deprecated('[3.6] use self.session.vreg.schema')
+    def schema(self):
+        return self.session.repo.schema
+
+    @property
+    @deprecated('[3.6] use self.session.vreg.config')
+    def config(self):
+        return self.session.repo.config
+
+set_log_methods(Operation, getLogger('cubicweb.session'))
+
+
+class LateOperation(Operation):
+    """special operation which should be called after all possible (ie non late)
+    operations
+    """
+    def insert_index(self):
+        """return the index of  the lastest instance which is not a
+        SingleLastOperation instance
+        """
+        for i, op in enumerate(self.session.pending_operations):
+            if isinstance(op, SingleLastOperation):
+                return i
+        return None
+
+
+class SingleOperation(Operation):
+    """special operation which should be called once"""
+    def register(self, session):
+        """override register to handle cases where this operation has already
+        been added
+        """
+        operations = session.pending_operations
+        index = self.equivalent_index(operations)
+        if index is not None:
+            equivalent = operations.pop(index)
+        else:
+            equivalent = None
+        session.add_operation(self, self.insert_index())
+        return equivalent
+
+    def equivalent_index(self, operations):
+        """return the index of the equivalent operation if any"""
+        equivalents = [i for i, op in enumerate(operations)
+                       if op.__class__ is self.__class__]
+        if equivalents:
+            return equivalents[0]
+        return None
+
+
+class SingleLastOperation(SingleOperation):
+    """special operation which should be called once and after all other
+    operations
+    """
+    def insert_index(self):
+        return None
+
+
+class SendMailOp(SingleLastOperation):
+    def __init__(self, session, msg=None, recipients=None, **kwargs):
+        # may not specify msg yet, as
+        # `cubicweb.sobjects.supervision.SupervisionMailOp`
+        if msg is not None:
+            assert recipients
+            self.to_send = [(msg, recipients)]
+        else:
+            assert recipients is None
+            self.to_send = []
+        super(SendMailOp, self).__init__(session, **kwargs)
+
+    def register(self, session):
+        previous = super(SendMailOp, self).register(session)
+        if previous:
+            self.to_send = previous.to_send + self.to_send
+
+    def commit_event(self):
+        self.repo.threaded_task(self.sendmails)
+
+    def sendmails(self):
+        self.config.sendmails(self.to_send)
+
+
+class RQLPrecommitOperation(Operation):
+    def precommit_event(self):
+        execute = self.session.unsafe_execute
+        for rql in self.rqls:
+            execute(*rql)
--- a/server/hookhelper.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/hookhelper.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,82 +7,19 @@
 """
 __docformat__ = "restructuredtext en"
 
-from cubicweb import RepositoryError
-from cubicweb.server.pool import SingleLastOperation
-
-
-def entity_name(session, eid):
-    """return the "name" attribute of the entity with the given eid"""
-    return entity_attr(session, eid, 'name')
-
-def entity_attr(session, eid, attr):
-    """return an arbitrary attribute of the entity with the given eid"""
-    return getattr(session.entity_from_eid(eid), attr)
+from logilab.common.deprecation import deprecated, class_moved
 
-def rproperty(session, rtype, eidfrom, eidto, rprop):
-    rschema = session.repo.schema[rtype]
-    subjtype = session.describe(eidfrom)[0]
-    objtype = session.describe(eidto)[0]
-    return rschema.rproperty(subjtype, objtype, rprop)
-
-def check_internal_entity(session, eid, internal_names):
-    """check that the entity's name is not in the internal_names list.
-    raise a RepositoryError if so, else return the entity's name
-    """
-    name = entity_name(session, eid)
-    if name in internal_names:
-        raise RepositoryError('%s entity can\'t be deleted' % name)
-    return name
-
-def get_user_sessions(repo, ueid):
-    for session in repo._sessions.values():
-        if ueid == session.user.eid:
-            yield session
+from cubicweb import RepositoryError
 
 
-# mail related ################################################################
-
-class SendMailOp(SingleLastOperation):
-    def __init__(self, session, msg=None, recipients=None, **kwargs):
-        # may not specify msg yet, as
-        # `cubicweb.sobjects.supervision.SupervisionMailOp`
-        if msg is not None:
-            assert recipients
-            self.to_send = [(msg, recipients)]
-        else:
-            assert recipients is None
-            self.to_send = []
-        super(SendMailOp, self).__init__(session, **kwargs)
-
-    def register(self, session):
-        previous = super(SendMailOp, self).register(session)
-        if previous:
-            self.to_send = previous.to_send + self.to_send
-
-    def commit_event(self):
-        self.repo.threaded_task(self.sendmails)
+@deprecated('[3.6] entity_name is deprecated, use entity.name')
+def entity_name(session, eid):
+    """return the "name" attribute of the entity with the given eid"""
+    return session.entity_from_eid(eid).name
 
-    def sendmails(self):
-        self.config.sendmails(self.to_send)
-
-
-# state related ###############################################################
+@deprecated('[3.6] rproperty is deprecated, use session.schema_rproperty')
+def rproperty(session, rtype, eidfrom, eidto, rprop):
+    return session.rproperty(rtype, eidfrom, eidto, rprop)
 
-def previous_state(session, eid):
-    """return the state of the entity with the given eid,
-    usually since it's changing in the current transaction. Due to internal
-    relation hooks, the relation may has been deleted at this point, so
-    we have handle that
-    """
-    if eid in session.transaction_data.get('neweids', ()):
-        return
-    pending = session.transaction_data.get('pendingrelations', ())
-    for eidfrom, rtype, eidto in reversed(pending):
-        if rtype == 'in_state' and eidfrom == eid:
-            rset = session.execute('Any S,N WHERE S eid %(x)s, S name N',
-                                   {'x': eidto}, 'x')
-            return rset.get_entity(0, 0)
-    rset = session.execute('Any S,N WHERE X eid %(x)s, X in_state S, S name N',
-                           {'x': eid}, 'x')
-    if rset:
-        return rset.get_entity(0, 0)
+from cubicweb.server.hook import SendMailOp
+SendMailOp = class_moved(SendMailOp)
--- a/server/hooks.py	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,594 +0,0 @@
-"""Core hooks: check schema validity, unsure we are not deleting necessary
-entities...
-
-: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
-"""
-__docformat__ = "restructuredtext en"
-
-from datetime import datetime
-
-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.repository import FTIndexEntityOp
-
-# special relations that don't have to be checked for integrity, usually
-# because they are handled internally by hooks (so we trust ourselves)
-DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
-                                'is', 'is_instance_of',
-                                'wf_info_for', 'from_state', 'to_state'))
-DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of',
-                                'wf_info_for', 'from_state', 'to_state'))
-
-
-def relation_deleted(session, eidfrom, rtype, eidto):
-    session.transaction_data.setdefault('pendingrelations', []).append(
-        (eidfrom, rtype, eidto))
-
-def eschema_type_eid(session, etype):
-    """get eid of the CWEType entity for the given yams type"""
-    eschema = session.repo.schema.eschema(etype)
-    # eschema.eid is None if schema has been readen from the filesystem, not
-    # from the database (eg during tests)
-    if eschema.eid is None:
-        eschema.eid = session.unsafe_execute(
-            'Any X WHERE X is CWEType, X name %(name)s', {'name': etype})[0][0]
-    return eschema.eid
-
-
-# base meta-data handling ######################################################
-
-def setctime_before_add_entity(session, entity):
-    """before create a new entity -> set creation and modification date
-
-    this is a conveniency hook, you shouldn't have to disable it
-    """
-    timestamp = datetime.now()
-    entity.setdefault('creation_date', timestamp)
-    entity.setdefault('modification_date', timestamp)
-    if not session.get_shared_data('do-not-insert-cwuri'):
-        entity.setdefault('cwuri', u'%seid/%s' % (session.base_url(), entity.eid))
-
-
-def setmtime_before_update_entity(session, entity):
-    """update an entity -> set modification date"""
-    entity.setdefault('modification_date', datetime.now())
-
-
-class SetCreatorOp(PreCommitOperation):
-
-    def precommit_event(self):
-        session = self.session
-        if self.entity.eid in session.transaction_data.get('pendingeids', ()):
-            # entity have been created and deleted in the same transaction
-            return
-        if not self.entity.created_by:
-            session.add_relation(self.entity.eid, 'created_by', session.user.eid)
-
-
-def setowner_after_add_entity(session, entity):
-    """create a new entity -> set owner and creator metadata"""
-    asession = session.actual_session()
-    if not asession.is_internal_session:
-        session.add_relation(entity.eid, 'owned_by', asession.user.eid)
-        SetCreatorOp(asession, entity=entity)
-
-
-def setis_after_add_entity(session, entity):
-    """create a new entity -> set is relation"""
-    if hasattr(entity, '_cw_recreating'):
-        return
-    try:
-        session.add_relation(entity.eid, 'is',
-                             eschema_type_eid(session, entity.id))
-    except IndexError:
-        # during schema serialization, skip
-        return
-    # XXX < 2.50 bw compat
-    if not session.get_shared_data('do-not-insert-is_instance_of'):
-        for etype in entity.e_schema.ancestors() + [entity.e_schema]:
-            session.add_relation(entity.eid, 'is_instance_of',
-                                 eschema_type_eid(session, etype))
-
-
-def setowner_after_add_user(session, entity):
-    """when a user has been created, add owned_by relation on itself"""
-    session.add_relation(entity.eid, 'owned_by', entity.eid)
-
-
-def fti_update_after_add_relation(session, eidfrom, rtype, eidto):
-    """sync fulltext index when relevant relation is added. Reindexing the
-    contained entity is enough since it will implicitly reindex the container
-    entity.
-    """
-    ftcontainer = session.repo.schema.rschema(rtype).fulltext_container
-    if ftcontainer == 'subject':
-        FTIndexEntityOp(session, entity=session.entity_from_eid(eidto))
-    elif ftcontainer == 'object':
-        FTIndexEntityOp(session, entity=session.entity_from_eid(eidfrom))
-
-
-def fti_update_after_delete_relation(session, eidfrom, rtype, eidto):
-    """sync fulltext index when relevant relation is deleted. Reindexing both
-    entities is necessary.
-    """
-    if session.repo.schema.rschema(rtype).fulltext_container:
-        FTIndexEntityOp(session, entity=session.entity_from_eid(eidto))
-        FTIndexEntityOp(session, entity=session.entity_from_eid(eidfrom))
-
-
-class SyncOwnersOp(PreCommitOperation):
-
-    def precommit_event(self):
-        self.session.unsafe_execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,'
-                                    'NOT EXISTS(X owned_by U, X eid %(x)s)',
-                                    {'c': self.compositeeid, 'x': self.composedeid},
-                                    ('c', 'x'))
-
-
-def sync_owner_after_add_composite_relation(session, eidfrom, rtype, eidto):
-    """when adding composite relation, the composed should have the same owners
-    has the composite
-    """
-    if rtype == 'wf_info_for':
-        # skip this special composite relation # XXX (syt) why?
-        return
-    composite = rproperty(session, rtype, eidfrom, eidto, 'composite')
-    if composite == 'subject':
-        SyncOwnersOp(session, compositeeid=eidfrom, composedeid=eidto)
-    elif composite == 'object':
-        SyncOwnersOp(session, compositeeid=eidto, composedeid=eidfrom)
-
-
-def _register_metadata_hooks(hm):
-    """register meta-data related hooks on the hooks manager"""
-    hm.register_hook(setctime_before_add_entity, 'before_add_entity', '')
-    hm.register_hook(setmtime_before_update_entity, 'before_update_entity', '')
-    hm.register_hook(setowner_after_add_entity, 'after_add_entity', '')
-    hm.register_hook(sync_owner_after_add_composite_relation, 'after_add_relation', '')
-    hm.register_hook(fti_update_after_add_relation, 'after_add_relation', '')
-    hm.register_hook(fti_update_after_delete_relation, 'after_delete_relation', '')
-    if 'is' in hm.schema:
-        hm.register_hook(setis_after_add_entity, 'after_add_entity', '')
-    if 'CWUser' in hm.schema:
-        hm.register_hook(setowner_after_add_user, 'after_add_entity', 'CWUser')
-
-
-# core hooks ##################################################################
-
-class DelayedDeleteOp(PreCommitOperation):
-    """delete the object of composite relation except if the relation
-    has actually been redirected to another composite
-    """
-
-    def precommit_event(self):
-        session = self.session
-        # don't do anything if the entity is being created or deleted
-        if not (self.eid in session.transaction_data.get('pendingeids', ()) or
-                self.eid in session.transaction_data.get('neweids', ())):
-            etype = session.describe(self.eid)[0]
-            session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
-                                   % (etype, self.relation),
-                                   {'x': self.eid}, 'x')
-
-
-def handle_composite_before_del_relation(session, eidfrom, rtype, eidto):
-    """delete the object of composite relation"""
-    composite = rproperty(session, rtype, eidfrom, eidto, 'composite')
-    if composite == 'subject':
-        DelayedDeleteOp(session, eid=eidto, relation='Y %s X' % rtype)
-    elif composite == 'object':
-        DelayedDeleteOp(session, eid=eidfrom, relation='X %s Y' % rtype)
-
-
-def before_del_group(session, eid):
-    """check that we don't remove the owners group"""
-    check_internal_entity(session, eid, ('owners',))
-
-
-# schema validation hooks #####################################################
-
-class CheckConstraintsOperation(LateOperation):
-    """check a new relation satisfy its constraints
-    """
-    def precommit_event(self):
-        eidfrom, rtype, eidto = self.rdef
-        # first check related entities have not been deleted in the same
-        # transaction
-        pending = self.session.transaction_data.get('pendingeids', ())
-        if eidfrom in pending:
-            return
-        if eidto in pending:
-            return
-        for constraint in self.constraints:
-            try:
-                constraint.repo_check(self.session, eidfrom, rtype, eidto)
-            except NotImplementedError:
-                self.critical('can\'t check constraint %s, not supported',
-                              constraint)
-
-    def commit_event(self):
-        pass
-
-
-def cstrcheck_after_add_relation(session, eidfrom, rtype, eidto):
-    """check the relation satisfy its constraints
-
-    this is delayed to a precommit time operation since other relation which
-    will make constraint satisfied may be added later.
-    """
-    constraints = rproperty(session, rtype, eidfrom, eidto, 'constraints')
-    if constraints:
-        CheckConstraintsOperation(session, constraints=constraints,
-                                  rdef=(eidfrom, rtype, eidto))
-
-def uniquecstrcheck_before_modification(session, entity):
-    eschema = entity.e_schema
-    for attr, val in entity.items():
-        if val is None:
-            continue
-        if eschema.subject_relation(attr).is_final() and \
-               eschema.has_unique_values(attr):
-            rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
-            rset = session.unsafe_execute(rql, {'val': val})
-            if rset and rset[0][0] != entity.eid:
-                msg = session._('the value "%s" is already used, use another one')
-                raise ValidationError(entity.eid, {attr: msg % val})
-
-
-class CheckRequiredRelationOperation(LateOperation):
-    """checking relation cardinality has to be done after commit in
-    case the relation is being replaced
-    """
-    eid, rtype = None, None
-
-    def precommit_event(self):
-        # recheck pending eids
-        if self.eid in self.session.transaction_data.get('pendingeids', ()):
-            return
-        if self.session.unsafe_execute(*self._rql()).rowcount < 1:
-            etype = self.session.describe(self.eid)[0]
-            _ = self.session._
-            msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')
-            msg %= {'rtype': _(self.rtype), 'etype': _(etype), 'eid': self.eid}
-            raise ValidationError(self.eid, {self.rtype: msg})
-
-    def commit_event(self):
-        pass
-
-    def _rql(self):
-        raise NotImplementedError()
-
-
-class CheckSRelationOp(CheckRequiredRelationOperation):
-    """check required subject relation"""
-    def _rql(self):
-        return 'Any O WHERE S eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
-
-
-class CheckORelationOp(CheckRequiredRelationOperation):
-    """check required object relation"""
-    def _rql(self):
-        return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
-
-
-def checkrel_if_necessary(session, opcls, rtype, eid):
-    """check an equivalent operation has not already been added"""
-    for op in session.pending_operations:
-        if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid:
-            break
-    else:
-        opcls(session, rtype=rtype, eid=eid)
-
-
-def cardinalitycheck_after_add_entity(session, entity):
-    """check cardinalities are satisfied"""
-    eid = entity.eid
-    for rschema, targetschemas, x in entity.e_schema.relation_definitions():
-        # skip automatically handled relations
-        if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
-            continue
-        if x == 'subject':
-            subjtype = entity.e_schema
-            objtype = targetschemas[0].type
-            cardindex = 0
-            opcls = CheckSRelationOp
-        else:
-            subjtype = targetschemas[0].type
-            objtype = entity.e_schema
-            cardindex = 1
-            opcls = CheckORelationOp
-        card = rschema.rproperty(subjtype, objtype, 'cardinality')
-        if card[cardindex] in '1+':
-            checkrel_if_necessary(session, opcls, rschema.type, eid)
-
-
-def cardinalitycheck_before_del_relation(session, eidfrom, rtype, eidto):
-    """check cardinalities are satisfied"""
-    if rtype in DONT_CHECK_RTYPES_ON_DEL:
-        return
-    card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
-    pendingeids = session.transaction_data.get('pendingeids', ())
-    if card[0] in '1+' and not eidfrom in pendingeids:
-        checkrel_if_necessary(session, CheckSRelationOp, rtype, eidfrom)
-    if card[1] in '1+' and not eidto in pendingeids:
-        checkrel_if_necessary(session, CheckORelationOp, rtype, eidto)
-
-
-def _register_core_hooks(hm):
-    hm.register_hook(handle_composite_before_del_relation, 'before_delete_relation', '')
-    hm.register_hook(before_del_group, 'before_delete_entity', 'CWGroup')
-
-    #hm.register_hook(cstrcheck_before_update_entity, 'before_update_entity', '')
-    hm.register_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
-    hm.register_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
-    hm.register_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
-    hm.register_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
-    hm.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
-
-
-# user/groups synchronisation #################################################
-
-class GroupOperation(Operation):
-    """base class for group operation"""
-    geid = None
-    def __init__(self, session, *args, **kwargs):
-        """override to get the group name before actual groups manipulation:
-
-        we may temporarily loose right access during a commit event, so
-        no query should be emitted while comitting
-        """
-        rql = 'Any N WHERE G eid %(x)s, G name N'
-        result = session.execute(rql, {'x': kwargs['geid']}, 'x', build_descr=False)
-        Operation.__init__(self, session, *args, **kwargs)
-        self.group = result[0][0]
-
-
-class DeleteGroupOp(GroupOperation):
-    """synchronize user when a in_group relation has been deleted"""
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        groups = self.cnxuser.groups
-        try:
-            groups.remove(self.group)
-        except KeyError:
-            self.error('user %s not in group %s',  self.cnxuser, self.group)
-            return
-
-
-def after_del_in_group(session, fromeid, rtype, toeid):
-    """modify user permission, need to update users"""
-    for session_ in get_user_sessions(session.repo, fromeid):
-        DeleteGroupOp(session, cnxuser=session_.user, geid=toeid)
-
-
-class AddGroupOp(GroupOperation):
-    """synchronize user when a in_group relation has been added"""
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        groups = self.cnxuser.groups
-        if self.group in groups:
-            self.warning('user %s already in group %s', self.cnxuser,
-                         self.group)
-            return
-        groups.add(self.group)
-
-
-def after_add_in_group(session, fromeid, rtype, toeid):
-    """modify user permission, need to update users"""
-    for session_ in get_user_sessions(session.repo, fromeid):
-        AddGroupOp(session, cnxuser=session_.user, geid=toeid)
-
-
-class DelUserOp(Operation):
-    """synchronize user when a in_group relation has been added"""
-    def __init__(self, session, cnxid):
-        self.cnxid = cnxid
-        Operation.__init__(self, session)
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            self.repo.close(self.cnxid)
-        except BadConnectionId:
-            pass # already closed
-
-
-def after_del_user(session, eid):
-    """modify user permission, need to update users"""
-    for session_ in get_user_sessions(session.repo, eid):
-        DelUserOp(session, session_.id)
-
-
-def _register_usergroup_hooks(hm):
-    """register user/group related hooks on the hooks manager"""
-    hm.register_hook(after_del_user, 'after_delete_entity', 'CWUser')
-    hm.register_hook(after_add_in_group, 'after_add_relation', 'in_group')
-    hm.register_hook(after_del_in_group, 'after_delete_relation', 'in_group')
-
-
-# workflow handling ###########################################################
-
-def before_add_in_state(session, fromeid, rtype, toeid):
-    """check the transition is allowed and record transition information
-    """
-    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:
-                msg = session._('transition is not allowed')
-                raise ValidationError(fromeid, {'in_state': msg})
-        else:
-            # not a transition
-            # check state is initial state if the workflow defines one
-            isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s',
-                                            {'etype': etype})
-            if isrset and not toeid == isrset[0][0]:
-                msg = session._('not the initial state for this entity')
-                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')
-
-
-class SetInitialStateOp(PreCommitOperation):
-    """make initial state be a default state"""
-
-    def precommit_event(self):
-        session = self.session
-        entity = self.entity
-        # if there is an initial state and the entity's state is not set,
-        # use the initial state as a default state
-        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])
-
-
-def set_initial_state_after_add(session, entity):
-    SetInitialStateOp(session, entity=entity)
-
-
-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')
-        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))
-
-
-# CWProperty hooks #############################################################
-
-
-class DelCWPropertyOp(Operation):
-    """a user's custom properties has been deleted"""
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            del self.epropdict[self.key]
-        except KeyError:
-            self.error('%s has no associated value', self.key)
-
-
-class ChangeCWPropertyOp(Operation):
-    """a user's custom properties has been added/changed"""
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        self.epropdict[self.key] = self.value
-
-
-class AddCWPropertyOp(Operation):
-    """a user's custom properties has been added/changed"""
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        eprop = self.eprop
-        if not eprop.for_user:
-            self.repo.vreg.eprop_values[eprop.pkey] = eprop.value
-        # if for_user is set, update is handled by a ChangeCWPropertyOp operation
-
-
-def after_add_eproperty(session, entity):
-    key, value = entity.pkey, entity.value
-    try:
-        value = session.vreg.typed_value(key, value)
-    except UnknownProperty:
-        raise ValidationError(entity.eid, {'pkey': session._('unknown property key')})
-    except ValueError, ex:
-        raise ValidationError(entity.eid, {'value': session._(str(ex))})
-    if not session.user.matching_groups('managers'):
-        session.add_relation(entity.eid, 'for_user', session.user.eid)
-    else:
-        AddCWPropertyOp(session, eprop=entity)
-
-
-def after_update_eproperty(session, entity):
-    key, value = entity.pkey, entity.value
-    try:
-        value = session.vreg.typed_value(key, value)
-    except UnknownProperty:
-        return
-    except ValueError, ex:
-        raise ValidationError(entity.eid, {'value': session._(str(ex))})
-    if entity.for_user:
-        for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
-            ChangeCWPropertyOp(session, epropdict=session_.user.properties,
-                              key=key, value=value)
-    else:
-        # site wide properties
-        ChangeCWPropertyOp(session, epropdict=session.vreg.eprop_values,
-                          key=key, value=value)
-
-
-def before_del_eproperty(session, eid):
-    for eidfrom, rtype, eidto in session.transaction_data.get('pendingrelations', ()):
-        if rtype == 'for_user' and eidfrom == eid:
-            # if for_user was set, delete has already been handled
-            break
-    else:
-        key = session.execute('Any K WHERE P eid %(x)s, P pkey K',
-                              {'x': eid}, 'x')[0][0]
-        DelCWPropertyOp(session, epropdict=session.vreg.eprop_values, key=key)
-
-
-def after_add_for_user(session, fromeid, rtype, toeid):
-    if not session.describe(fromeid)[0] == 'CWProperty':
-        return
-    key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
-                                 {'x': fromeid}, 'x')[0]
-    if session.vreg.property_info(key)['sitewide']:
-        raise ValidationError(fromeid,
-                              {'for_user': session._("site-wide property can't be set for user")})
-    for session_ in get_user_sessions(session.repo, toeid):
-        ChangeCWPropertyOp(session, epropdict=session_.user.properties,
-                          key=key, value=value)
-
-
-def before_del_for_user(session, fromeid, rtype, toeid):
-    key = session.execute('Any K WHERE P eid %(x)s, P pkey K',
-                          {'x': fromeid}, 'x')[0][0]
-    relation_deleted(session, fromeid, rtype, toeid)
-    for session_ in get_user_sessions(session.repo, toeid):
-        DelCWPropertyOp(session, epropdict=session_.user.properties, key=key)
-
-
-def _register_eproperty_hooks(hm):
-    """register workflow related hooks on the hooks manager"""
-    hm.register_hook(after_add_eproperty, 'after_add_entity', 'CWProperty')
-    hm.register_hook(after_update_eproperty, 'after_update_entity', 'CWProperty')
-    hm.register_hook(before_del_eproperty, 'before_delete_entity', 'CWProperty')
-    hm.register_hook(after_add_for_user, 'after_add_relation', 'for_user')
-    hm.register_hook(before_del_for_user, 'before_delete_relation', 'for_user')
--- a/server/hooksmanager.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/hooksmanager.py	Tue Sep 22 13:08:42 2009 +0200
@@ -1,270 +1,8 @@
-"""Hooks management
-
-Hooks are called before / after any individual update of entities / relations
-in the repository.
-
-Here is the prototype of the different hooks:
-
-* filtered on the entity's type:
-
-  before_add_entity    (session, entity)
-  after_add_entity     (session, entity)
-  before_update_entity (session, entity)
-  after_update_entity  (session, entity)
-  before_delete_entity (session, eid)
-  after_delete_entity  (session, eid)
-
-* filtered on the relation's type:
-
-  before_add_relation    (session, fromeid, rtype, toeid)
-  after_add_relation     (session, fromeid, rtype, toeid)
-  before_delete_relation (session, fromeid, rtype, toeid)
-  after_delete_relation  (session, fromeid, rtype, toeid)
-
-
-: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
-"""
-__docformat__ = "restructuredtext en"
-
-ENTITIES_HOOKS = ('before_add_entity',    'after_add_entity',
-                  'before_update_entity', 'after_update_entity',
-                  'before_delete_entity', 'after_delete_entity')
-RELATIONS_HOOKS = ('before_add_relation',   'after_add_relation' ,
-                   'before_delete_relation','after_delete_relation')
-SYSTEM_HOOKS = ('server_backup', 'server_restore',
-                'server_startup', 'server_shutdown',
-                'session_open', 'session_close')
-
-ALL_HOOKS = frozenset(ENTITIES_HOOKS + RELATIONS_HOOKS + SYSTEM_HOOKS)
-
-class HooksManager(object):
-    """handle hooks registration and calls
-    """
-    verification_hooks_activated = True
-
-    def __init__(self, schema):
-        self.set_schema(schema)
-
-    def set_schema(self, schema):
-        self._hooks = {}
-        self.schema = schema
-        self._init_hooks(schema)
-
-    def register_hooks(self, hooks):
-        """register a dictionary of hooks :
-
-             {'event': {'entity or relation type': [callbacks list]}}
-        """
-        for event, subevents in hooks.items():
-            for subevent, callbacks in subevents.items():
-                for callback in callbacks:
-                    self.register_hook(callback, event, subevent)
-
-    def register_hook(self, function, event, etype=''):
-        """register a function to call when <event> occurs
-
-        <etype> is an entity/relation type or an empty string.
-
-        If etype is the empty string, the function will be called at each event,
-        else the function will be called only when event occurs on an entity or
-        relation of the given type.
-        """
-        assert event in ALL_HOOKS, '%r NOT IN %r' % (event, ALL_HOOKS)
-        assert (not event in SYSTEM_HOOKS or not etype), (event, etype)
-        etype = etype or ''
-        try:
-            self._hooks[event][etype].append(function)
-            self.debug('registered hook %s on %s (%s)', event, etype or 'any',
-                       function.func_name)
-
-        except KeyError:
-            self.error('can\'t register hook %s on %s (%s)',
-                       event, etype or 'any', function.func_name)
-
-    def unregister_hook(self, function_or_cls, event=None, etype=''):
-        """unregister a function to call when <event> occurs, or a Hook subclass.
-        In the later case, event/type information are extracted from the given
-        class.
-        """
-        if isinstance(function_or_cls, type) and issubclass(function_or_cls, Hook):
-            for event, ertype in function_or_cls.register_to():
-                for hook in self._hooks[event][ertype]:
-                    if getattr(hook, 'im_self', None).__class__ is function_or_cls:
-                        self._hooks[event][ertype].remove(hook)
-                        self.info('unregister hook %s on %s (%s)', event, etype,
-                                  function_or_cls.__name__)
-                        break
-                else:
-                    self.warning("can't unregister hook %s on %s (%s), not found",
-                                 event, etype, function_or_cls.__name__)
-        else:
-            assert event in ALL_HOOKS, event
-            etype = etype or ''
-            self.info('unregister hook %s on %s (%s)', event, etype,
-                      function_or_cls.func_name)
-            self._hooks[event][etype].remove(function_or_cls)
-
-    def call_hooks(self, __event, __type='', *args, **kwargs):
-        """call hook matching event and optional type"""
-        if __type:
-            self.info('calling hooks for event %s (%s)', __event, __type)
-        else:
-            self.info('calling hooks for event %s', __event)
-        # call generic hooks first
-        for hook in self._hooks[__event]['']:
-            #print '[generic]', hook.__name__
-            hook(*args, **kwargs)
-        if __type:
-            for hook in self._hooks[__event][__type]:
-                #print '[%s]'%__type, hook.__name__
-                hook(*args, **kwargs)
+from logilab.common.deprecation import class_renamed, class_moved
+from cubicweb.server.hook import Hook
+SystemHook = class_renamed('SystemHook', Hook)
+PropagateSubjectRelationHook = class_renamed('PropagateSubjectRelationHook', Hook)
+PropagateSubjectRelationAddHook = class_renamed('PropagateSubjectRelationAddHook', Hook)
+PropagateSubjectRelationDelHook = class_renamed('PropagateSubjectRelationDelHook', Hook)
+Hook = class_moved(Hook)
 
-    def _init_hooks(self, schema):
-        """initialize the hooks map"""
-        for hook_event in ENTITIES_HOOKS:
-            self._hooks[hook_event] = {'': []}
-            for etype in schema.entities():
-                self._hooks[hook_event][etype] = []
-        for hook_event in RELATIONS_HOOKS:
-            self._hooks[hook_event] = {'': []}
-            for r_type in schema.relations():
-                self._hooks[hook_event][r_type] = []
-        for hook_event in SYSTEM_HOOKS:
-            self._hooks[hook_event] = {'': []}
-
-    def register_system_hooks(self, config):
-        """register system hooks according to the configuration"""
-        self.info('register core hooks')
-        from cubicweb.server.hooks import _register_metadata_hooks, _register_wf_hooks
-        _register_metadata_hooks(self)
-        self.info('register workflow hooks')
-        _register_wf_hooks(self)
-        if config.core_hooks:
-            from cubicweb.server.hooks import _register_core_hooks
-            _register_core_hooks(self)
-        if config.schema_hooks:
-            from cubicweb.server.schemahooks import _register_schema_hooks
-            self.info('register schema hooks')
-            _register_schema_hooks(self)
-        if config.usergroup_hooks:
-            from cubicweb.server.hooks import _register_usergroup_hooks
-            from cubicweb.server.hooks import _register_eproperty_hooks
-            self.info('register user/group hooks')
-            _register_usergroup_hooks(self)
-            _register_eproperty_hooks(self)
-        if config.security_hooks:
-            from cubicweb.server.securityhooks import register_security_hooks
-            self.info('register security hooks')
-            register_security_hooks(self)
-        if not self.verification_hooks_activated:
-            self.deactivate_verification_hooks()
-
-    def deactivate_verification_hooks(self):
-        from cubicweb.server.hooks import (cardinalitycheck_after_add_entity,
-                                        cardinalitycheck_before_del_relation,
-                                        cstrcheck_after_add_relation,
-                                        uniquecstrcheck_before_modification)
-        self.warning('deactivating verification hooks')
-        self.verification_hooks_activated = False
-        self.unregister_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
-        self.unregister_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
-        self.unregister_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
-        self.unregister_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
-        self.unregister_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
-#         self.unregister_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
-#         self.unregister_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')
-
-    def reactivate_verification_hooks(self):
-        from cubicweb.server.hooks import (cardinalitycheck_after_add_entity,
-                                        cardinalitycheck_before_del_relation,
-                                        cstrcheck_after_add_relation,
-                                        uniquecstrcheck_before_modification)
-        self.warning('reactivating verification hooks')
-        self.verification_hooks_activated = True
-        self.register_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
-        self.register_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
-        self.register_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
-        self.register_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
-        self.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
-#         self.register_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
-#         self.register_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')
-
-from cubicweb.selectors import yes
-from cubicweb.appobject import AppObject
-
-class autoid(type):
-    """metaclass to create an unique 'id' attribute on the class using it"""
-    # XXX is this metaclass really necessary ?
-    def __new__(mcs, name, bases, classdict):
-        cls = super(autoid, mcs).__new__(mcs, name, bases, classdict)
-        cls.id = str(id(cls))
-        return cls
-
-class Hook(AppObject):
-    __metaclass__ = autoid
-    __registry__ = 'hooks'
-    __select__ = yes()
-    # set this in derivated classes
-    events = None
-    accepts = None
-    enabled = True
-
-    def __init__(self, event=None):
-        super(Hook, self).__init__()
-        self.event = event
-
-    @classmethod
-    def registered(cls, vreg):
-        super(Hook, cls).registered(vreg)
-        return cls()
-
-    @classmethod
-    def register_to(cls):
-        if not cls.enabled:
-            cls.warning('%s hook has been disabled', cls)
-            return
-        done = set()
-        assert isinstance(cls.events, (tuple, list)), \
-               '%s: events is expected to be a tuple, not %s' % (
-            cls, type(cls.events))
-        for event in cls.events:
-            if event in SYSTEM_HOOKS:
-                assert not cls.accepts or cls.accepts == ('Any',), \
-                       '%s doesnt make sense on %s' % (cls.accepts, event)
-                cls.accepts = ('Any',)
-            for ertype in cls.accepts:
-                if (event, ertype) in done:
-                    continue
-                yield event, ertype
-                done.add((event, ertype))
-                try:
-                    eschema = cls.schema.eschema(ertype)
-                except KeyError:
-                    # relation schema
-                    pass
-                else:
-                    for eetype in eschema.specialized_by():
-                        if (event, eetype) in done:
-                            continue
-                        yield event, str(eetype)
-                        done.add((event, eetype))
-
-
-    def make_callback(self, event):
-        if len(self.events) == 1:
-            return self.call
-        return self.__class__(event=event).call
-
-    def call(self):
-        raise NotImplementedError
-
-class SystemHook(Hook):
-    accepts = ()
-
-from logging import getLogger
-from cubicweb import set_log_methods
-set_log_methods(HooksManager, getLogger('cubicweb.hooksmanager'))
-set_log_methods(Hook, getLogger('cubicweb.hooks'))
--- a/server/migractions.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/migractions.py	Tue Sep 22 13:08:42 2009 +0200
@@ -19,7 +19,10 @@
 
 import sys
 import os
-from os.path import join, exists
+import tarfile
+import tempfile
+import shutil
+import os.path as osp
 from datetime import datetime
 
 from logilab.common.deprecation import deprecated
@@ -30,7 +33,8 @@
 from yams.schema2sql import eschema2sql, rschema2sql
 
 from cubicweb import AuthenticationError, ETYPE_NAME_MAP
-from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CubicWebRelationSchema
+from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES,
+                             CubicWebRelationSchema, order_eschemas)
 from cubicweb.dbapi import get_repository, repo_connect
 from cubicweb.common.migration import MigrationHelper, yes
 
@@ -57,6 +61,7 @@
             assert repo
             self._cnx = cnx
             self.repo = repo
+            self.session.data['rebuild-infered'] = False
         elif connect:
             self.repo_connect()
         if not schema:
@@ -110,25 +115,88 @@
     def backup_database(self, backupfile=None, askconfirm=True):
         config = self.config
         repo = self.repo_connect()
-        timestamp = datetime.now().strftime('%Y-%m-%d_%H:%M:%S')
-        for source in repo.sources:
-            source.backup(self.confirm, backupfile, timestamp,
-                          askconfirm=askconfirm)
-        repo.hm.call_hooks('server_backup', repo=repo, timestamp=timestamp)
+        # paths
+        timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
+        instbkdir = osp.join(config.appdatahome, 'backup')
+        if not osp.exists(instbkdir):
+            os.makedirs(instbkdir)
+        backupfile = backupfile or osp.join(instbkdir, '%s-%s.tar.gz'
+                                            % (config.appid, timestamp))
+        # check backup has to be done
+        if osp.exists(backupfile) and not \
+                self.confirm('Backup file %s exists, overwrite it?' % backupfile):
+            print '-> no backup done.'
+            return
+        elif askconfirm and not self.confirm('Backup %s database?' % config.appid):
+            print '-> no backup done.'
+            return
+        open(backupfile,'w').close() # kinda lock
+        os.chmod(backupfile, 0600)
+        # backup
+        tmpdir = tempfile.mkdtemp(dir=instbkdir)
+        try:
+            for source in repo.sources:
+                try:
+                    source.backup(osp.join(tmpdir, source.uri))
+                except Exception, exc:
+                    print '-> error trying to backup [%s]' % exc
+                    if not self.confirm('Continue anyway?', default='n'):
+                        raise SystemExit(1)
+                    else:
+                        break
+            else:
+                bkup = tarfile.open(backupfile, 'w|gz')
+                for filename in os.listdir(tmpdir):
+                    bkup.add(osp.join(tmpdir,filename), filename)
+                bkup.close()
+                # call hooks
+                repo.hm.call_hooks('server_backup', repo=repo, timestamp=timestamp)
+                # done
+                print '-> backup file',  backupfile
+        finally:
+            shutil.rmtree(tmpdir)
 
     def restore_database(self, backupfile, drop=True, systemonly=True,
                          askconfirm=True):
-        config = self.config
-        repo = self.repo_connect()
-        if systemonly:
-            repo.system_source.restore(self.confirm, backupfile=backupfile,
-                                       drop=drop, askconfirm=askconfirm)
+        # check
+        if not osp.exists(backupfile):
+            raise Exception("Backup file %s doesn't exist" % backupfile)
+            return
+        if askconfirm and not self.confirm('Restore %s database from %s ?'
+                                           % (self.config.appid, backupfile)):
+            return
+        # unpack backup
+        tmpdir = tempfile.mkdtemp()
+        try:
+            bkup = tarfile.open(backupfile, 'r|gz')
+        except tarfile.ReadError:
+            # assume restoring old backup
+            shutil.copy(backupfile, osp.join(tmpdir, 'system'))  
         else:
-            # in that case, backup file is expected to be a time stamp
-            for source in repo.sources:
-                source.backup(self.confirm, timestamp=backupfile, drop=drop,
-                              askconfirm=askconfirm)
-            repo.hm.call_hooks('server_restore', repo=repo, timestamp=backupfile)
+            for name in bkup.getnames():
+                if name[0] in '/.':
+                    raise Exception('Security check failed, path starts with "/" or "."')
+            bkup.close() # XXX seek error if not close+open !?!
+            bkup = tarfile.open(backupfile, 'r|gz')
+            bkup.extractall(path=tmpdir)
+            bkup.close()
+
+        self.config.open_connections_pools = False
+        repo = self.repo_connect()
+        for source in repo.sources:
+            if systemonly and source.uri != 'system':
+                continue
+            try:
+                source.restore(osp.join(tmpdir, source.uri), drop=drop)
+            except Exception, exc:
+                print '-> error trying to restore [%s]' % exc
+                if not self.confirm('Continue anyway?', default='n'):
+                    raise SystemExit(1)
+        shutil.rmtree(tmpdir)
+        # call hooks
+        repo.open_connections_pools()
+        repo.hm.call_hooks('server_restore', repo=repo, timestamp=backupfile)
+        print '-> database restored.'
 
     @property
     def cnx(self):
@@ -160,21 +228,13 @@
                     print 'aborting...'
                     sys.exit(0)
             self.session.keep_pool_mode('transaction')
+            self.session.data['rebuild-infered'] = False
             return self._cnx
 
     @property
     def session(self):
         return self.repo._get_session(self.cnx.sessionid)
 
-    @property
-    @cached
-    def rqlcursor(self):
-        """lazy rql cursor"""
-        # should not give session as cnx.cursor(), else we may try to execute
-        # some query while no pool is set on the session (eg on entity attribute
-        # access for instance)
-        return self.cnx.cursor()
-
     def commit(self):
         if hasattr(self, '_cnx'):
             self._cnx.commit()
@@ -199,29 +259,30 @@
                         'fsschema': self.fs_schema,
                         'session' : self.session,
                         'repo' : self.repo,
-                        'synchronize_schema': deprecated()(self.cmd_sync_schema_props_perms),
-                        'synchronize_eschema': deprecated()(self.cmd_sync_schema_props_perms),
-                        'synchronize_rschema': deprecated()(self.cmd_sync_schema_props_perms),
+                        'synchronize_schema': deprecated()(self.cmd_sync_schema_props_perms), # 3.4
+                        'synchronize_eschema': deprecated()(self.cmd_sync_schema_props_perms), # 3.4
+                        'synchronize_rschema': deprecated()(self.cmd_sync_schema_props_perms), # 3.4
                         })
         return context
 
     @cached
     def group_mapping(self):
         """cached group mapping"""
-        return ss.group_mapping(self.rqlcursor)
+        self.session.set_pool()
+        return ss.group_mapping(self.session)
 
     def exec_event_script(self, event, cubepath=None, funcname=None,
                           *args, **kwargs):
         if cubepath:
-            apc = join(cubepath, 'migration', '%s.py' % event)
+            apc = osp.join(cubepath, 'migration', '%s.py' % event)
         else:
-            apc = join(self.config.migration_scripts_dir(), '%s.py' % event)
-        if exists(apc):
+            apc = osp.join(self.config.migration_scripts_dir(), '%s.py' % event)
+        if osp.exists(apc):
             if self.config.free_wheel:
                 from cubicweb.server.hooks import setowner_after_add_entity
                 self.repo.hm.unregister_hook(setowner_after_add_entity,
                                              'after_add_entity', '')
-                self.deactivate_verification_hooks()
+                self.cmd_deactivate_verification_hooks()
             self.info('executing %s', apc)
             confirm = self.confirm
             execscript_confirm = self.execscript_confirm
@@ -235,7 +296,7 @@
                 if self.config.free_wheel:
                     self.repo.hm.register_hook(setowner_after_add_entity,
                                                'after_add_entity', '')
-                    self.reactivate_verification_hooks()
+                    self.cmd_reactivate_verification_hooks()
 
     # schema synchronization internals ########################################
 
@@ -354,19 +415,20 @@
         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, x in eschema.relation_definitions(True):
-            if x == 'subject':
+        for rschema, targettypes, role in eschema.relation_definitions(True):
+            if role == 'subject':
                 if not rschema in repoeschema.subject_relations():
                     continue
                 subjtypes, objtypes = [etype], targettypes
-            else: # x == 'object'
+            else: # role == 'object'
                 if not rschema in repoeschema.object_relations():
                     continue
                 subjtypes, objtypes = targettypes, [etype]
@@ -472,10 +534,11 @@
             if not rschema in self.repo.schema:
                 self.cmd_add_relation_type(rschema.type)
                 new.add(rschema.type)
-        for eschema in newcubes_schema.entities():
-            if not eschema in self.repo.schema:
-                self.cmd_add_entity_type(eschema.type)
-                new.add(eschema.type)
+        toadd = [eschema for eschema in newcubes_schema.entities()
+                 if not eschema in self.repo.schema]
+        for eschema in order_eschemas(toadd):
+            self.cmd_add_entity_type(eschema.type)
+            new.add(eschema.type)
         # check if attributes has been added to existing entities
         for rschema in newcubes_schema.relations():
             existingschema = self.repo.schema.rschema(rschema.type)
@@ -507,9 +570,11 @@
         for rschema in fsschema.relations():
             if not rschema in removedcubes_schema and rschema in reposchema:
                 self.cmd_drop_relation_type(rschema.type)
-        for eschema in fsschema.entities():
-            if not eschema in removedcubes_schema and eschema in reposchema:
-                self.cmd_drop_entity_type(eschema.type)
+        toremove = [eschema for eschema in fsschema.entities()
+                    if not eschema in removedcubes_schema
+                    and eschema in reposchema]
+        for eschema in reversed(order_eschemas(toremove)):
+            self.cmd_drop_entity_type(eschema.type)
         for rschema in fsschema.relations():
             if rschema in removedcubes_schema and rschema in reposchema:
                 # check if attributes/relations has been added to entities from
@@ -569,11 +634,13 @@
         in auto mode, automatically register entity's relation where the
         targeted type is known
         """
-        applschema = self.repo.schema
-        if etype in applschema:
-            eschema = applschema[etype]
+        instschema = self.repo.schema
+        if etype in instschema:
+            # XXX (syt) plz explain: if we're adding an entity type, it should
+            # not be there...
+            eschema = instschema[etype]
             if eschema.is_final():
-                applschema.del_entity_type(etype)
+                instschema.del_entity_type(etype)
         else:
             eschema = self.fs_schema.eschema(etype)
         confirm = self.verbosity >= 2
@@ -589,13 +656,46 @@
             # ignore those meta relations, they will be automatically added
             if rschema.type in META_RTYPES:
                 continue
-            if not rschema.type in applschema:
+            if not rschema.type in instschema:
                 # need to add the relation type and to commit to get it
                 # actually in the schema
                 self.cmd_add_relation_type(rschema.type, False, commit=True)
             # register relation definition
             self.rqlexecall(ss.rdef2rql(rschema, etype, attrschema.type),
                             ask_confirm=confirm)
+        # take care to newly introduced base class
+        # XXX some part of this should probably be under the "if auto" block
+        for spschema in eschema.specialized_by(recursive=False):
+            try:
+                instspschema = instschema[spschema]
+            except KeyError:
+                # specialized entity type not in schema, ignore
+                continue
+            if instspschema.specializes() != eschema:
+                self.rqlexec('SET D specializes P WHERE D eid %(d)s, P name %(pn)s',
+                              {'d': instspschema.eid,
+                               'pn': eschema.type}, ask_confirm=confirm)
+                for rschema, tschemas, role in spschema.relation_definitions(True):
+                    for tschema in tschemas:
+                        if not tschema in instschema:
+                            continue
+                        if role == 'subject':
+                            subjschema = spschema
+                            objschema = tschema
+                            if rschema.final and instspschema.has_subject_relation(rschema):
+                                # attribute already set, has_rdef would check if
+                                # it's of the same type, we don't want this so
+                                # simply skip here
+                                continue
+                        elif role == 'object':
+                            subjschema = tschema
+                            objschema = spschema
+                        if (rschema.rproperty(subjschema, objschema, 'infered')
+                            or (instschema.has_relation(rschema) and
+                                instschema[rschema].has_rdef(subjschema, objschema))):
+                                continue
+                        self.cmd_add_relation_definition(
+                            subjschema.type, rschema.type, objschema.type)
         if auto:
             # we have commit here to get relation types actually in the schema
             self.commit()
@@ -605,12 +705,12 @@
                 # 'owned_by'/'created_by' will be automatically added
                 if rschema.final or rschema.type in META_RTYPES:
                     continue
-                rtypeadded = rschema.type in applschema
+                rtypeadded = rschema.type in instschema
                 for targetschema in rschema.objects(etype):
                     # ignore relations where the targeted type is not in the
                     # current instance schema
                     targettype = targetschema.type
-                    if not targettype in applschema and targettype != etype:
+                    if not targettype in instschema and targettype != etype:
                         continue
                     if not rtypeadded:
                         # need to add the relation type and to commit to get it
@@ -625,14 +725,14 @@
                     self.rqlexecall(ss.rdef2rql(rschema, etype, targettype),
                                     ask_confirm=confirm)
             for rschema in eschema.object_relations():
-                rtypeadded = rschema.type in applschema or rschema.type in added
+                rtypeadded = rschema.type in instschema or rschema.type in added
                 for targetschema in rschema.subjects(etype):
                     # ignore relations where the targeted type is not in the
                     # current instance schema
                     targettype = targetschema.type
                     # don't check targettype != etype since in this case the
                     # relation has already been added as a subject relation
-                    if not targettype in applschema:
+                    if not targettype in instschema:
                         continue
                     if not rtypeadded:
                         # need to add the relation type and to commit to get it
@@ -694,6 +794,22 @@
             self.commit()
             self.rqlexecall(ss.rdef2rql(rschema),
                             ask_confirm=self.verbosity>=2)
+            if rtype in META_RTYPES:
+                # if the relation is in META_RTYPES, ensure we're adding it for
+                # all entity types *in the persistent schema*, not only those in
+                # the fs schema
+                for etype in self.repo.schema.entities():
+                    if not etype in self.fs_schema:
+                        # get sample object type and rproperties
+                        objtypes = rschema.objects()
+                        assert len(objtypes) == 1
+                        objtype = objtypes[0]
+                        props = rschema.rproperties(
+                            rschema.subjects(objtype)[0], objtype)
+                        assert props
+                        self.rqlexecall(ss.rdef2rql(rschema, etype, objtype, props),
+                                        ask_confirm=self.verbosity>=2)
+
         if commit:
             self.commit()
 
@@ -838,84 +954,84 @@
         if commit:
             self.commit()
 
-    @deprecated('use sync_schema_props_perms(ertype, syncprops=False)')
+    @deprecated('[3.4] use sync_schema_props_perms(ertype, syncprops=False)')
     def cmd_synchronize_permissions(self, ertype, commit=True):
         self.cmd_sync_schema_props_perms(ertype, syncprops=False, commit=commit)
 
     # 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()
 
@@ -932,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
@@ -985,6 +1095,7 @@
         if not isinstance(rql, (tuple, list)):
             rql = ( (rql, kwargs), )
         res = None
+        self.session.set_pool()
         for rql, kwargs in rql:
             if kwargs:
                 msg = '%s (%s)' % (rql, kwargs)
@@ -992,7 +1103,7 @@
                 msg = rql
             if not ask_confirm or self.confirm('execute rql: %s ?' % msg):
                 try:
-                    res = self.rqlcursor.execute(rql, kwargs, cachekey)
+                    res = self.session.execute(rql, kwargs, cachekey)
                 except Exception, ex:
                     if self.confirm('error: %s\nabort?' % ex):
                         raise
@@ -1002,10 +1113,10 @@
         return ForRqlIterator(self, rql, None, ask_confirm)
 
     def cmd_deactivate_verification_hooks(self):
-        self.repo.hm.deactivate_verification_hooks()
+        self.config.disabled_hooks_categories.add('integrity')
 
     def cmd_reactivate_verification_hooks(self):
-        self.repo.hm.reactivate_verification_hooks()
+        self.config.disabled_hooks_categories.remove('integrity')
 
     # broken db commands ######################################################
 
@@ -1081,8 +1192,9 @@
         if self.ask_confirm:
             if not self._h.confirm('execute rql: %s ?' % msg):
                 raise StopIteration
+        self._h.session.set_pool()
         try:
-            rset = self._h.rqlcursor.execute(rql, kwargs)
+            rset = self._h.session.execute(rql, kwargs)
         except Exception, ex:
             if self._h.confirm('error: %s\nabort?' % ex):
                 raise
--- a/server/msplanner.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/msplanner.py	Tue Sep 22 13:08:42 2009 +0200
@@ -97,8 +97,15 @@
 # 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
+    # check vargraph contains some other relation than the identity relation
+    # test of key nature since it may be a variable name (don't care about that)
+    # or a 2-uple (var1, var2) associated to the relation to traverse to go from
+    # var1 to var2
+    return any(key for key, val in vargraph.iteritems()
+               if isinstance(key, tuple) and val != 'identity')
 
 def need_aggr_step(select, sources, stepdefs=None):
     """return True if a temporary table is necessary to store some partial
@@ -257,7 +264,7 @@
         self._conflicts = []
         if rqlhelper is not None: # else test
             self._insert_identity_variable = rqlhelper._annotator.rewrite_shared_optional
-        if server.DEBUG:
+        if server.DEBUG & server.DBG_MS:
             print 'sourcesterms:'
             self._debug_sourcesterms()
 
@@ -337,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
@@ -559,7 +566,8 @@
             # testing for rqlst with nothing in vargraph nor defined_vars is the
             # simplest way the check the condition explained below
             if not self.system_source in self._sourcesterms and \
-                   not self.rqlst.vargraph and not self.rqlst.defined_vars:
+                   not self.rqlst.defined_vars and \
+                   not need_source_access_relation(self.rqlst.vargraph):
                 self._sourcesterms = {self.system_source: {}}
         elif not self.needsplit:
             if not allequals(self._sourcesterms.itervalues()):
@@ -1012,7 +1020,7 @@
 
         the rqlst should not be tagged at this point
         """
-        if server.DEBUG:
+        if server.DEBUG & server.DBG_MS:
             print '-'*80
             print 'PLANNING', rqlst
         for select in rqlst.children:
@@ -1029,7 +1037,7 @@
         ppis = [PartPlanInformation(plan, select, self.rqlhelper)
                 for select in rqlst.children]
         steps = self._union_plan(plan, rqlst, ppis)
-        if server.DEBUG:
+        if server.DEBUG & server.DBG_MS:
             from pprint import pprint
             for step in plan.steps:
                 pprint(step.test_repr())
@@ -1224,7 +1232,7 @@
         return rqlst
 
     def filter(self, sources, terms, rqlst, solindices, needsel, final):
-        if server.DEBUG:
+        if server.DEBUG & server.DBG_MS:
             print 'filter', final and 'final' or '', sources, terms, rqlst, solindices, needsel
         newroot = Select()
         self.sources = sorted(sources)
@@ -1318,7 +1326,7 @@
                     elif ored:
                         newroot.remove_node(rel)
         add_types_restriction(self.schema, rqlst, newroot, solutions)
-        if server.DEBUG:
+        if server.DEBUG & server.DBG_MS:
             print '--->', newroot
         return newroot, self.insertedvars
 
--- a/server/pool.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/pool.py	Tue Sep 22 13:08:42 2009 +0200
@@ -1,13 +1,7 @@
-"""CubicWeb server connections pool :
-
-* the rql repository has a limited number of connections pools, each of them
-  dealing with a set of connections on each source used by the repository
-
-* operation may be registered by hooks during a transaction, which will  be
-  fired when the pool is commited or rollbacked
-
-This module defined the `ConnectionsPool` class and a set of abstract classes
-for operation.
+"""CubicWeb server connections pool : the repository has a limited number of
+connections pools, each of them dealing with a set of connections on each source
+used by the repository. A connections pools (`ConnectionsPool`) is an
+abstraction for a group of connection to each source.
 
 
 :organization: Logilab
@@ -33,6 +27,17 @@
             self.source_cnxs['system'] = self.source_cnxs[sources[0].uri]
         self._cursors = {}
 
+    def __getitem__(self, uri):
+        """subscription notation provide access to sources'cursors"""
+        try:
+            cursor = self._cursors[uri]
+        except KeyError:
+            cursor = self.source_cnxs[uri][1].cursor()
+            if cursor is not None:
+                # None possible on sources without cursor support such as ldap
+                self._cursors[uri] = cursor
+        return cursor
+
     def commit(self):
         """commit the current transaction for this user"""
         # FIXME: what happends if a commit fail
@@ -77,22 +82,11 @@
         for source, cnx in self.source_cnxs.values():
             source.pool_reset(cnx)
 
-    def __getitem__(self, uri):
-        """subscription notation provide access to sources'cursors"""
-        try:
-            cursor = self._cursors[uri]
-        except KeyError:
-            cursor = self.source_cnxs[uri][1].cursor()
-            if cursor is not None:
-                # None possible on sources without cursor support such as ldap
-                self._cursors[uri] = cursor
-        return cursor
-
     def sources(self):
         """return the source objects handled by this pool"""
         # implementation details of flying insert requires the system source
         # first
-        yield self.source_cnxs['system']
+        yield self.source_cnxs['system'][0]
         for uri, (source, cursor) in self.source_cnxs.items():
             if uri == 'system':
                 continue
@@ -107,11 +101,17 @@
         """return the connection on the source object with the given uri"""
         return self.source_cnxs[uid][1]
 
-    def reconnect(self, source):
-        """reopen a connection for this source"""
-        source.info('trying to reconnect')
-        self.source_cnxs[source.uri] = (source, source.get_connection())
-        del self._cursors[source.uri]
+    def reconnect(self, source=None):
+        """reopen a connection for this source or all sources if none specified
+        """
+        if source is None:
+            sources = self.sources()
+        else:
+            sources = (source,)
+        for source in sources:
+            source.info('trying to reconnect')
+            self.source_cnxs[source.uri] = (source, source.get_connection())
+            self._cursors.pop(source.uri, None)
 
     def check_connections(self):
         for source, cnx in self.source_cnxs.itervalues():
@@ -123,152 +123,11 @@
         self.source_cnxs[source.uri] = (source, cnx)
         self._cursors.pop(source.uri, None)
 
-
-class Operation(object):
-    """an operation is triggered on connections pool events related to
-    commit / rollback transations. Possible events are:
-
-    precommit:
-      the pool is preparing to commit. You shouldn't do anything things which
-      has to be reverted if the commit fail at this point, but you can freely
-      do any heavy computation or raise an exception if the commit can't go.
-      You can add some new operation during this phase but their precommit
-      event won't be triggered
-
-    commit:
-      the pool is preparing to commit. You should avoid to do to expensive
-      stuff or something that may cause an exception in this event
-
-    revertcommit:
-      if an operation failed while commited, this event is triggered for
-      all operations which had their commit event already to let them
-      revert things (including the operation which made fail the commit)
-
-    rollback:
-      the transaction has been either rollbacked either
-      * intentionaly
-      * a precommit event failed, all operations are rollbacked
-      * a commit event failed, all operations which are not been triggered for
-        commit are rollbacked
-
-    order of operations may be important, and is controlled according to:
-    * operation's class
-    """
-
-    def __init__(self, session, **kwargs):
-        self.session = session
-        self.user = session.user
-        self.repo = session.repo
-        self.schema = session.repo.schema
-        self.config = session.repo.config
-        self.__dict__.update(kwargs)
-        self.register(session)
-        # execution information
-        self.processed = None # 'precommit', 'commit'
-        self.failed = False
-
-    def register(self, session):
-        session.add_operation(self, self.insert_index())
-
-    def insert_index(self):
-        """return the index of  the lastest instance which is not a
-        LateOperation instance
-        """
-        for i, op in enumerate(self.session.pending_operations):
-            if isinstance(op, (LateOperation, SingleLastOperation)):
-                return i
-        return None
-
-    def handle_event(self, event):
-        """delegate event handling to the opertaion"""
-        getattr(self, event)()
-
-    def precommit_event(self):
-        """the observed connections pool is preparing a commit"""
-
-    def revertprecommit_event(self):
-        """an error went when pre-commiting this operation or a later one
-
-        should revert pre-commit's changes but take care, they may have not
-        been all considered if it's this operation which failed
-        """
-
-    def commit_event(self):
-        """the observed connections pool is commiting"""
-        raise NotImplementedError()
-
-    def revertcommit_event(self):
-        """an error went when commiting this operation or a later one
-
-        should revert commit's changes but take care, they may have not
-        been all considered if it's this operation which failed
-        """
-
-    def rollback_event(self):
-        """the observed connections pool has been rollbacked
-
-        do nothing by default, the operation will just be removed from the pool
-        operation list
-        """
-
-
-class PreCommitOperation(Operation):
-    """base class for operation only defining a precommit operation
-    """
-
-    def precommit_event(self):
-        """the observed connections pool is preparing a commit"""
-        raise NotImplementedError()
-
-    def commit_event(self):
-        """the observed connections pool is commiting"""
-
-
-class LateOperation(Operation):
-    """special operation which should be called after all possible (ie non late)
-    operations
-    """
-    def insert_index(self):
-        """return the index of  the lastest instance which is not a
-        SingleLastOperation instance
-        """
-        for i, op in enumerate(self.session.pending_operations):
-            if isinstance(op, SingleLastOperation):
-                return i
-        return None
-
-
-class SingleOperation(Operation):
-    """special operation which should be called once"""
-    def register(self, session):
-        """override register to handle cases where this operation has already
-        been added
-        """
-        operations = session.pending_operations
-        index = self.equivalent_index(operations)
-        if index is not None:
-            equivalent = operations.pop(index)
-        else:
-            equivalent = None
-        session.add_operation(self, self.insert_index())
-        return equivalent
-
-    def equivalent_index(self, operations):
-        """return the index of the equivalent operation if any"""
-        equivalents = [i for i, op in enumerate(operations)
-                       if op.__class__ is self.__class__]
-        if equivalents:
-            return equivalents[0]
-        return None
-
-
-class SingleLastOperation(SingleOperation):
-    """special operation which should be called once and after all other
-    operations
-    """
-    def insert_index(self):
-        return None
-
-from logging import getLogger
-from cubicweb import set_log_methods
-set_log_methods(Operation, getLogger('cubicweb.session'))
+from cubicweb.server.hook import (Operation, LateOperation, SingleOperation,
+                                  SingleLastOperation)
+from logilab.common.deprecation import class_moved, class_renamed
+Operation = class_moved(Operation)
+PreCommitOperation = class_renamed('PreCommitOperation', Operation)
+LateOperation = class_moved(LateOperation)
+SingleOperation = class_moved(SingleOperation)
+SingleLastOperation = class_moved(SingleLastOperation)
--- a/server/querier.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/querier.py	Tue Sep 22 13:08:42 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:
@@ -508,7 +510,7 @@
             if repo.schema.rschema(rtype).inlined:
                 entity = session.entity_from_eid(subj)
                 entity[rtype] = obj
-                repo.glob_update_entity(session, entity)
+                repo.glob_update_entity(session, entity, set((rtype,)))
             else:
                 repo.glob_add_relation(session, subj, rtype, obj)
 
@@ -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':
@@ -608,6 +606,8 @@
                     # return an empty result instead of raising UnknownEid
                     return empty_rset(session, rql, args)
                 cachekey.append(etype)
+                # ensure eid is correctly typed in args
+                args[key] = typed_eid(args[key])
             cachekey = tuple(cachekey)
         else:
             cachekey = rql
@@ -640,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/repository.py	Tue Sep 22 13:08:42 2009 +0200
@@ -28,24 +28,17 @@
 from yams import BadSchemaDefinition
 from rql import RQLSyntaxError
 
-from cubicweb import (CW_SOFTWARE_ROOT, UnknownEid, AuthenticationError,
-                      ETypeNotSupportedBySources, RTypeNotSupportedBySources,
+from cubicweb import (CW_SOFTWARE_ROOT, CW_MIGRATION_MAP, CW_EVENT_MANAGER,
+                      UnknownEid, AuthenticationError, ExecutionError,
+                      ETypeNotSupportedBySources, MultiSourcesError,
                       BadConnectionId, Unauthorized, ValidationError,
-                      ExecutionError, typed_eid,
-                      CW_MIGRATION_MAP)
-from cubicweb.cwvreg import CubicWebVRegistry
-from cubicweb.schema import VIRTUAL_RTYPES, CubicWebSchema
-from cubicweb import server
-from cubicweb.server.utils import RepoThread, LoopTask
-from cubicweb.server.pool import ConnectionsPool, LateOperation, SingleLastOperation
+                      typed_eid)
+from cubicweb import cwvreg, schema, server
+from cubicweb.server import utils, hook, pool, querier, sources
 from cubicweb.server.session import Session, InternalSession
-from cubicweb.server.querier import QuerierHelper
-from cubicweb.server.sources import get_source
-from cubicweb.server.hooksmanager import HooksManager
-from cubicweb.server.hookhelper import rproperty
 
 
-class CleanupEidTypeCacheOp(SingleLastOperation):
+class CleanupEidTypeCacheOp(hook.SingleLastOperation):
     """on rollback of a insert query or commit of delete query, we have to
     clear repository's cache from no more valid entries
 
@@ -75,7 +68,7 @@
             pass
 
 
-class FTIndexEntityOp(LateOperation):
+class FTIndexEntityOp(hook.LateOperation):
     """operation to delay entity full text indexation to commit
 
     since fti indexing may trigger discovery of other entities, it should be
@@ -108,7 +101,10 @@
     # hooks responsability to ensure they do not violate relation's cardinality
     if session.is_super_session:
         return
-    card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
+    ensure_card_respected(session.unsafe_execute, session, eidfrom, rtype, eidto)
+
+def ensure_card_respected(execute, session, eidfrom, rtype, eidto):
+    card = session.schema_rproperty(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
     # same transaction where the entity is being created. This never occurs from
@@ -118,14 +114,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
@@ -137,7 +130,7 @@
     def __init__(self, config, vreg=None, debug=False):
         self.config = config
         if vreg is None:
-            vreg = CubicWebVRegistry(config, debug)
+            vreg = cwvreg.CubicWebVRegistry(config, debug)
         self.vreg = vreg
         self.pyro_registered = False
         self.info('starting repository from %s', self.config.apphome)
@@ -148,9 +141,10 @@
         # list of running threads
         self._running_threads = []
         # initial schema, should be build or replaced latter
-        self.schema = CubicWebSchema(config.appid)
+        self.schema = 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)
+        self.querier = querier.QuerierHelper(self, self.schema)
         # should we reindex in changes?
         self.do_fti = not config['delay-full-text-indexation']
         # sources
@@ -173,11 +167,14 @@
         self._type_source_cache = {}
         # cache (extid, source uri) -> eid
         self._extid_cache = {}
-        # create the hooks manager
-        self.hm = HooksManager(self.schema)
         # open some connections pools
+        if config.open_connections_pools:
+            self.open_connections_pools()
+
+    def open_connections_pools(self):
+        config = self.config
         self._available_pools = Queue.Queue()
-        self._available_pools.put_nowait(ConnectionsPool(self.sources))
+        self._available_pools.put_nowait(pool.ConnectionsPool(self.sources))
         if config.read_instance_schema:
             # normal start: load the instance schema from the database
             self.fill_schema()
@@ -185,20 +182,21 @@
             # usually during repository creation
             self.warning("set fs instance'schema as bootstrap schema")
             config.bootstrap_cubes()
-            self.set_bootstrap_schema(self.config.load_schema())
+            self.set_schema(config.load_schema(), resetvreg=False)
             # need to load the Any and CWUser entity types
-            self.vreg.schema = self.schema
             etdirectory = join(CW_SOFTWARE_ROOT, 'entities')
             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")
             config.bootstrap_cubes()
-            self.set_schema(self.config.load_schema())
+            self.set_schema(config.load_schema())
         if not config.creating:
             if 'CWProperty' in self.schema:
                 self.vreg.init_properties(self.properties())
@@ -219,47 +217,45 @@
         # list of available pools (we can't iterated on Queue instance)
         self.pools = []
         for i in xrange(config['connections-pool-size']):
-            self.pools.append(ConnectionsPool(self.sources))
+            self.pools.append(pool.ConnectionsPool(self.sources))
             self._available_pools.put_nowait(self.pools[-1])
         self._shutting_down = False
+        self.hm = self.vreg['hooks']
         if not (config.creating or config.repairing):
             # call instance level initialisation hooks
             self.hm.call_hooks('server_startup', repo=self)
             # register a task to cleanup expired session
-            self.looping_task(self.config['session-time']/3.,
-                              self.clean_sessions)
+            self.looping_task(config['session-time']/3., self.clean_sessions)
 
     # internals ###############################################################
 
     def get_source(self, uri, source_config):
         source_config['uri'] = uri
-        return get_source(source_config, self.schema, self)
+        return sources.get_source(source_config, self.schema, self)
 
-    def set_schema(self, schema, resetvreg=True):
-        schema.rebuild_infered_relations()
+    def set_schema(self, schema, resetvreg=True, rebuildinfered=True):
+        if rebuildinfered:
+            schema.rebuild_infered_relations()
         self.info('set schema %s %#x', schema.name, id(schema))
-        self.debug(', '.join(sorted(str(e) for e in schema.entities())))
+        if resetvreg:
+            if self.config._cubes is None:
+                self.config.init_cubes(self.get_cubes())
+            # 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.hm.set_schema(schema)
-        self.hm.register_system_hooks(self.config)
-        # instance specific hooks
-        if self.config.instance_hooks:
-            self.info('loading instance hooks')
-            self.hm.register_hooks(self.config.load_hooks(self.vreg))
 
     def fill_schema(self):
         """lod schema from the repository"""
         from cubicweb.server.schemaserial import deserialize_schema
         self.info('loading schema from the repository')
-        appschema = CubicWebSchema(self.config.appid)
-        self.set_bootstrap_schema(self.config.load_bootstrap_schema())
+        appschema = schema.CubicWebSchema(self.config.appid)
+        self.set_schema(self.config.load_bootstrap_schema(), resetvreg=False)
         self.debug('deserializing db schema into %s %#x', appschema.name, id(appschema))
         session = self.internal_session()
         try:
@@ -273,64 +269,34 @@
                 raise Exception('Is the database initialised ? (cause: %s)' %
                                 (ex.args and ex.args[0].strip() or 'unknown')), \
                                 None, sys.exc_info()[-1]
-            self.info('set the actual schema')
-            # XXX have to do this since CWProperty isn't in the bootstrap schema
-            #     it'll be redone in set_schema
-            self.set_bootstrap_schema(appschema)
-            # 2.49 migration
-            if exists(join(self.config.apphome, 'vc.conf')):
-                session.set_pool()
-                if not 'template' in file(join(self.config.apphome, 'vc.conf')).read():
-                    # remaning from cubicweb < 2.38...
-                    session.execute('DELETE CWProperty X WHERE X pkey "system.version.template"')
-                    session.commit()
         finally:
             session.close()
-        self.config.init_cubes(self.get_cubes())
         self.set_schema(appschema)
 
-    def set_bootstrap_schema(self, schema):
-        """disable hooks when setting a bootstrap schema, but restore
-        the configuration for the next time
-        """
-        config = self.config
-        # XXX refactor
-        config.core_hooks = False
-        config.usergroup_hooks = False
-        config.schema_hooks = False
-        config.notification_hooks = False
-        config.instance_hooks = False
-        self.set_schema(schema, resetvreg=False)
-        config.core_hooks = True
-        config.usergroup_hooks = True
-        config.schema_hooks = True
-        config.notification_hooks = True
-        config.instance_hooks = True
-
     def start_looping_tasks(self):
         assert isinstance(self._looping_tasks, list), 'already started'
-        for i, (interval, func) in enumerate(self._looping_tasks):
-            self._looping_tasks[i] = task = LoopTask(interval, func)
+        for i, (interval, func, args) in enumerate(self._looping_tasks):
+            self._looping_tasks[i] = task = utils.LoopTask(interval, func, args)
             self.info('starting task %s with interval %.2fs', task.name,
                       interval)
             task.start()
         # ensure no tasks will be further added
         self._looping_tasks = tuple(self._looping_tasks)
 
-    def looping_task(self, interval, func):
+    def looping_task(self, interval, func, *args):
         """register a function to be called every `interval` seconds.
 
         looping tasks can only be registered during repository initialization,
         once done this method will fail.
         """
         try:
-            self._looping_tasks.append( (interval, func) )
+            self._looping_tasks.append( (interval, func, args) )
         except AttributeError:
             raise RuntimeError("can't add looping task once the repository is started")
 
     def threaded_task(self, func):
         """start function in a separated thread"""
-        t = RepoThread(func, self._running_threads)
+        t = utils.RepoThread(func, self._running_threads)
         t.start()
 
     #@locked
@@ -570,11 +536,11 @@
         finally:
             session.close()
         session = Session(user, self, cnxprops)
-        user.req = user.rset.req = session
+        user._cw = user.rset.req = session
         user.clear_related_cache()
         self._sessions[session.id] = session
         self.info('opened %s', session)
-        self.hm.call_hooks('session_open', session=session)
+        self.hm.call_hooks('session_open', session)
         # commit session at this point in case write operation has been done
         # during `session_open` hooks
         session.commit()
@@ -665,7 +631,7 @@
                                     checkshuttingdown=checkshuttingdown)
         # operation uncommited before close are rollbacked before hook is called
         session.rollback()
-        self.hm.call_hooks('session_close', session=session)
+        self.hm.call_hooks('session_close', session)
         # commit session at this point in case write operation has been done
         # during `session_close` hooks
         session.commit()
@@ -846,11 +812,11 @@
                 entity = source.before_entity_insertion(session, extid, etype, eid)
                 entity._cw_recreating = True
                 if source.should_call_hooks:
-                    self.hm.call_hooks('before_add_entity', etype, session, entity)
+                    self.hm.call_hooks('before_add_entity', session, entity=entity)
                 # XXX add fti op ?
                 source.after_entity_insertion(session, extid, entity)
                 if source.should_call_hooks:
-                    self.hm.call_hooks('after_add_entity', etype, session, entity)
+                    self.hm.call_hooks('after_add_entity', session, entity=entity)
             if reset_pool:
                 session.reset_pool()
             return eid
@@ -871,12 +837,13 @@
             self._type_source_cache[eid] = (etype, source.uri, extid)
             entity = source.before_entity_insertion(session, extid, etype, eid)
             if source.should_call_hooks:
-                self.hm.call_hooks('before_add_entity', etype, session, entity)
+                entity.edited_attributes = set(entity)
+                self.hm.call_hooks('before_add_entity', session, entity=entity)
             # XXX call add_info with complete=False ?
             self.add_info(session, entity, source, extid)
             source.after_entity_insertion(session, extid, entity)
             if source.should_call_hooks:
-                self.hm.call_hooks('after_add_entity', etype, session, entity)
+                self.hm.call_hooks('after_add_entity', session, entity=entity)
             else:
                 # minimal meta-data
                 session.execute('SET X is E WHERE X eid %(x)s, E name %(name)s',
@@ -935,19 +902,19 @@
         pendingrtypes = session.transaction_data.get('pendingrtypes', ())
         for rschema, targetschemas, x in eschema.relation_definitions():
             rtype = rschema.type
-            if rtype in VIRTUAL_RTYPES or rtype in pendingrtypes:
+            if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
                 continue
             var = '%s%s' % (rtype.upper(), x.upper())
             if x == 'subject':
                 # don't skip inlined relation so they are regularly
                 # deleted and so hooks are correctly called
-                rql.append('X %s %s' % (rtype, var))
+                selection = 'X %s %s' % (rtype, var)
             else:
-                rql.append('%s %s X' % (var, rtype))
-        rql = 'DELETE %s WHERE X eid %%(x)s' % ','.join(rql)
-        # unsafe_execute since we suppose that if user can delete the entity,
-        # he can delete all its relations without security checking
-        session.unsafe_execute(rql, {'x': eid}, 'x', build_descr=False)
+                selection = '%s %s X' % (var, rtype)
+            rql = 'DELETE %s WHERE X eid %%(x)s' % selection
+            # unsafe_execute since we suppose that if user can delete the entity,
+            # he can delete all its relations without security checking
+            session.unsafe_execute(rql, {'x': eid}, 'x', build_descr=False)
 
     def index_entity(self, session, entity):
         """full text index a modified entity"""
@@ -961,12 +928,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):
@@ -992,15 +968,16 @@
             print 'ADD entity', etype, entity.eid, dict(entity)
         entity._is_saved = False # entity has an eid but is not yet saved
         relations = []
-        # if inlined relations are specified, fill entity's related cache to
-        # avoid unnecessary queries
+        # init edited_attributes before calling before_add_entity hooks
+        entity.edited_attributes = set(entity)
+        if source.should_call_hooks:
+            self.hm.call_hooks('before_add_entity', session, entity=entity)
+        # XXX use entity.keys here since edited_attributes is not updated for
+        # inline relations
         for attr in entity.keys():
             rschema = eschema.subject_relation(attr)
             if not rschema.is_final(): # inlined relation
                 relations.append((attr, entity[attr]))
-        if source.should_call_hooks:
-            self.hm.call_hooks('before_add_entity', etype, session, entity)
-        entity.edited_attributes = entity.keys()
         entity.set_defaults()
         entity.check(creation=True)
         source.add_entity(session, entity)
@@ -1015,7 +992,7 @@
         session.set_entity_cache(entity)
         for rschema in eschema.subject_relations():
             rtype = str(rschema)
-            if rtype in VIRTUAL_RTYPES:
+            if rtype in schema.VIRTUAL_RTYPES:
                 continue
             if rschema.is_final():
                 entity.setdefault(rtype, None)
@@ -1023,19 +1000,21 @@
                 entity.set_related_cache(rtype, 'subject', session.empty_rset())
         for rschema in eschema.object_relations():
             rtype = str(rschema)
-            if rtype in VIRTUAL_RTYPES:
+            if rtype in schema.VIRTUAL_RTYPES:
                 continue
             entity.set_related_cache(rtype, 'object', session.empty_rset())
+        # set inline relation cache before call to after_add_entity
+        for attr, value in relations:
+            session.update_rel_cache_add(entity.eid, attr, value)
         # trigger after_add_entity after after_add_relation
         if source.should_call_hooks:
-            self.hm.call_hooks('after_add_entity', etype, session, entity)
+            self.hm.call_hooks('after_add_entity', session, entity=entity)
             # call hooks for inlined relations
             for attr, value in relations:
-                self.hm.call_hooks('before_add_relation', attr, session,
-                                    entity.eid, attr, value)
-                session.update_rel_cache_add(entity.eid, attr, value)
-                self.hm.call_hooks('after_add_relation', attr, session,
-                                    entity.eid, attr, value)
+                self.hm.call_hooks('before_add_relation', session,
+                                    eidfrom=entity.eid, rtype=attr, eidto=value)
+                self.hm.call_hooks('after_add_relation', session,
+                                    eidfrom=entity.eid, rtype=attr, eidto=value)
         return entity.eid
 
     def glob_update_entity(self, session, entity, edited_attributes):
@@ -1052,7 +1031,7 @@
         session.set_entity_cache(entity)
         only_inline_rels, need_fti_update = True, False
         relations = []
-        for attr in entity.keys():
+        for attr in edited_attributes:
             if attr == 'eid':
                 continue
             rschema = eschema.subject_relation(attr)
@@ -1062,25 +1041,24 @@
                 only_inline_rels = False
             else:
                 # inlined relation
-                previous_value = entity.related(attr)
-                if previous_value:
+                previous_value = entity.related(attr) or None
+                if previous_value is not None:
                     previous_value = previous_value[0][0] # got a result set
                     if previous_value == entity[attr]:
                         previous_value = None
                     else:
-                        self.hm.call_hooks('before_delete_relation', attr,
-                                           session, entity.eid, attr,
-                                           previous_value)
+                        self.hm.call_hooks('before_delete_relation', session,
+                                           eidfrom=entity.eid, rtype=attr,
+                                           eidto=previous_value)
                 relations.append((attr, entity[attr], previous_value))
         source = self.source_from_eid(entity.eid, session)
         if source.should_call_hooks:
             # call hooks for inlined relations
             for attr, value, _ in relations:
-                self.hm.call_hooks('before_add_relation', attr, session,
-                                    entity.eid, attr, value)
+                self.hm.call_hooks('before_add_relation', session,
+                                    eidfrom=entity.eid, rtype=attr, eidto=value)
             if not only_inline_rels:
-                self.hm.call_hooks('before_update_entity', etype, session,
-                                    entity)
+                self.hm.call_hooks('before_update_entity', session, entity=entity)
         source.update_entity(session, entity)
         if not only_inline_rels:
             if need_fti_update and self.do_fti:
@@ -1088,15 +1066,14 @@
                 # one indexable attribute
                 FTIndexEntityOp(session, entity=entity)
             if source.should_call_hooks:
-                self.hm.call_hooks('after_update_entity', etype, session,
-                                    entity)
+                self.hm.call_hooks('after_update_entity', session, entity=entity)
         if source.should_call_hooks:
             for attr, value, prevvalue in relations:
                 # if the relation is already cached, update existant cache
                 relcache = entity.relation_cached(attr, 'subject')
-                if prevvalue:
-                    self.hm.call_hooks('after_delete_relation', attr, session,
-                                       entity.eid, attr, prevvalue)
+                if prevvalue is not None:
+                    self.hm.call_hooks('after_delete_relation', session,
+                                       eidfrom=entity.eid, rtype=attr, eidto=prevvalue)
                     if relcache is not None:
                         session.update_rel_cache_del(entity.eid, attr, prevvalue)
                 del_existing_rel_if_needed(session, entity.eid, attr, value)
@@ -1105,8 +1082,8 @@
                 else:
                     entity.set_related_cache(attr, 'subject',
                                              session.eid_rset(value))
-                self.hm.call_hooks('after_add_relation', attr, session,
-                                    entity.eid, attr, value)
+                self.hm.call_hooks('after_add_relation', session,
+                                    eidfrom=entity.eid, rtype=attr, eidto=value)
 
     def glob_delete_entity(self, session, eid):
         """delete an entity and all related entities from the repository"""
@@ -1119,11 +1096,12 @@
                 server.DEBUG |= (server.DBG_SQL | server.DBG_RQL | server.DBG_MORE)
         source = self.sources_by_uri[uri]
         if source.should_call_hooks:
-            self.hm.call_hooks('before_delete_entity', etype, session, eid)
+            entity = session.entity_from_eid(eid)
+            self.hm.call_hooks('before_delete_entity', session, entity=entity)
         self._delete_info(session, eid)
         source.delete_entity(session, etype, eid)
         if source.should_call_hooks:
-            self.hm.call_hooks('after_delete_entity', etype, session, eid)
+            self.hm.call_hooks('after_delete_entity', session, entity=entity)
         # don't clear cache here this is done in a hook on commit
 
     def glob_add_relation(self, session, subject, rtype, object):
@@ -1133,14 +1111,14 @@
         source = self.locate_relation_source(session, subject, rtype, object)
         if source.should_call_hooks:
             del_existing_rel_if_needed(session, subject, rtype, object)
-            self.hm.call_hooks('before_add_relation', rtype, session,
-                               subject, rtype, object)
+            self.hm.call_hooks('before_add_relation', session,
+                               eidfrom=subject, rtype=rtype, eidto=object)
         source.add_relation(session, subject, rtype, object)
         rschema = self.schema.rschema(rtype)
         session.update_rel_cache_add(subject, rtype, object, rschema.symetric)
         if source.should_call_hooks:
-            self.hm.call_hooks('after_add_relation', rtype, session,
-                               subject, rtype, object)
+            self.hm.call_hooks('after_add_relation', session,
+                               eidfrom=subject, rtype=rtype, eidto=object)
 
     def glob_delete_relation(self, session, subject, rtype, object):
         """delete a relation from the repository"""
@@ -1148,8 +1126,8 @@
             print 'DELETE relation', subject, rtype, object
         source = self.locate_relation_source(session, subject, rtype, object)
         if source.should_call_hooks:
-            self.hm.call_hooks('before_delete_relation', rtype, session,
-                               subject, rtype, object)
+            self.hm.call_hooks('before_delete_relation', session,
+                               eidfrom=subject, rtype=rtype, eidto=object)
         source.delete_relation(session, subject, rtype, object)
         rschema = self.schema.rschema(rtype)
         session.update_rel_cache_del(subject, rtype, object, rschema.symetric)
@@ -1158,8 +1136,8 @@
             # stored so try to delete both
             source.delete_relation(session, object, rtype, subject)
         if source.should_call_hooks:
-            self.hm.call_hooks('after_delete_relation', rtype, session,
-                               subject, rtype, object)
+            self.hm.call_hooks('after_delete_relation', session,
+                               eidfrom=subject, rtype=rtype, eidto=object)
 
 
     # pyro handling ###########################################################
--- a/server/rqlannotation.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/rqlannotation.py	Tue Sep 22 13:08:42 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	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1053 +0,0 @@
-"""schema hooks:
-
-- synchronize the living schema object with the persistent schema
-- perform physical update on the source when necessary
-
-checking for schema consistency is done in hooks.py
-
-: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
-"""
-__docformat__ = "restructuredtext en"
-
-from yams.schema import BASE_TYPES
-from yams.buildobjs import EntityType, RelationType, RelationDefinition
-from yams.schema2sql import eschema2sql, rschema2sql, type_from_constraints
-
-
-from cubicweb import ValidationError, RepositoryError
-from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS
-from cubicweb.server import schemaserial as ss
-from cubicweb.server.sqlutils import SQL_PREFIX
-from cubicweb.server.pool import Operation, SingleLastOperation, PreCommitOperation
-from cubicweb.server.hookhelper import (entity_attr, entity_name,
-                                        check_internal_entity)
-
-
-TYPE_CONVERTER = { # XXX
-    'Boolean': bool,
-    'Int': int,
-    'Float': float,
-    'Password': str,
-    'String': unicode,
-    'Date' : unicode,
-    'Datetime' : unicode,
-    'Time' : unicode,
-    }
-
-# core entity and relation types which can't be removed
-CORE_ETYPES = list(BASE_TYPES) + ['CWEType', 'CWRType', 'CWUser', 'CWGroup',
-                                  'CWConstraint', 'CWAttribute', 'CWRelation']
-CORE_RTYPES = ['eid', 'creation_date', 'modification_date', 'cwuri',
-               'login', 'upassword', 'name',
-               'is', 'instanceof', 'owned_by', 'created_by', 'in_group',
-               'relation_type', 'from_entity', 'to_entity',
-               'constrainted_by',
-               'read_permission', 'add_permission',
-               'delete_permission', 'updated_permission',
-               ]
-
-def get_constraints(session, entity):
-    constraints = []
-    for cstreid in session.transaction_data.get(entity.eid, ()):
-        cstrent = session.entity_from_eid(cstreid)
-        cstr = CONSTRAINTS[cstrent.type].deserialize(cstrent.value)
-        cstr.eid = cstreid
-        constraints.append(cstr)
-    return constraints
-
-def add_inline_relation_column(session, etype, rtype):
-    """add necessary column and index for an inlined relation"""
-    table = SQL_PREFIX + etype
-    column = SQL_PREFIX + rtype
-    try:
-        session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer'
-                               % (table, column)), rollback_on_failure=False)
-        session.info('added column %s to table %s', column, table)
-    except:
-        # silent exception here, if this error has not been raised because the
-        # column already exists, index creation will fail anyway
-        session.exception('error while adding column %s to table %s',
-                          table, column)
-    # create index before alter table which may expectingly fail during test
-    # (sqlite) while index creation should never fail (test for index existence
-    # is done by the dbhelper)
-    session.pool.source('system').create_index(session, table, column)
-    session.info('added index on %s(%s)', table, column)
-    session.transaction_data.setdefault('createdattrs', []).append(
-        '%s.%s' % (etype, rtype))
-
-
-# operations for low-level database alteration  ################################
-
-class DropTable(PreCommitOperation):
-    """actually remove a database from the instance's schema"""
-    table = None # make pylint happy
-    def precommit_event(self):
-        dropped = self.session.transaction_data.setdefault('droppedtables',
-                                                           set())
-        if self.table in dropped:
-            return # already processed
-        dropped.add(self.table)
-        self.session.system_sql('DROP TABLE %s' % self.table)
-        self.info('dropped table %s', self.table)
-
-
-class DropRelationTable(DropTable):
-    def __init__(self, session, rtype):
-        super(DropRelationTable, self).__init__(
-            session, table='%s_relation' % rtype)
-        session.transaction_data.setdefault('pendingrtypes', set()).add(rtype)
-
-
-class DropColumn(PreCommitOperation):
-    """actually remove the attribut's column from entity table in the system
-    database
-    """
-    table = column = None # make pylint happy
-    def precommit_event(self):
-        session, table, column = self.session, self.table, self.column
-        # drop index if any
-        session.pool.source('system').drop_index(session, table, column)
-        try:
-            session.system_sql('ALTER TABLE %s DROP COLUMN %s'
-                               % (table, column), rollback_on_failure=False)
-            self.info('dropped column %s from table %s', column, table)
-        except Exception, ex:
-            # not supported by sqlite for instance
-            self.error('error while altering table %s: %s', table, ex)
-
-
-# base operations for in-memory schema synchronization  ########################
-
-class MemSchemaNotifyChanges(SingleLastOperation):
-    """the update schema operation:
-
-    special operation which should be called once and after all other schema
-    operations. It will trigger internal structures rebuilding to consider
-    schema changes
-    """
-
-    def __init__(self, session):
-        self.repo = session.repo
-        SingleLastOperation.__init__(self, session)
-
-    def commit_event(self):
-        self.repo.set_schema(self.repo.schema)
-
-
-class MemSchemaOperation(Operation):
-    """base class for schema operations"""
-    def __init__(self, session, kobj=None, **kwargs):
-        self.schema = session.schema
-        self.kobj = kobj
-        # once Operation.__init__ has been called, event may be triggered, so
-        # do this last !
-        Operation.__init__(self, session, **kwargs)
-        # every schema operation is triggering a schema update
-        MemSchemaNotifyChanges(session)
-
-    def prepare_constraints(self, subjtype, rtype, objtype):
-        constraints = rtype.rproperty(subjtype, objtype, 'constraints')
-        self.constraints = list(constraints)
-        rtype.set_rproperty(subjtype, objtype, 'constraints', self.constraints)
-
-
-class MemSchemaEarlyOperation(MemSchemaOperation):
-    def insert_index(self):
-        """schema operation which are inserted at the begining of the queue
-        (typically to add/remove entity or relation types)
-        """
-        i = -1
-        for i, op in enumerate(self.session.pending_operations):
-            if not isinstance(op, MemSchemaEarlyOperation):
-                return i
-        return i + 1
-
-
-class MemSchemaPermissionOperation(MemSchemaOperation):
-    """base class to synchronize schema permission definitions"""
-    def __init__(self, session, perm, etype_eid):
-        self.perm = perm
-        try:
-            self.name = entity_name(session, etype_eid)
-        except IndexError:
-            self.error('changing permission of a no more existant type #%s',
-                etype_eid)
-        else:
-            Operation.__init__(self, session)
-
-
-# operations for high-level source database alteration  ########################
-
-class SourceDbCWETypeRename(PreCommitOperation):
-    """this operation updates physical storage accordingly"""
-    oldname = newname = None # make pylint happy
-
-    def precommit_event(self):
-        # we need sql to operate physical changes on the system database
-        sqlexec = self.session.system_sql
-        sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname,
-                                                     SQL_PREFIX, self.newname))
-        self.info('renamed table %s to %s', self.oldname, self.newname)
-        sqlexec('UPDATE entities SET type=%s WHERE type=%s',
-                (self.newname, self.oldname))
-        sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
-                (self.newname, self.oldname))
-
-
-class SourceDbCWRTypeUpdate(PreCommitOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = entity = None # make pylint happy
-
-    def precommit_event(self):
-        session = self.session
-        rschema = self.rschema
-        if rschema.is_final() or not 'inlined' in self.values:
-            return # nothing to do
-        inlined = self.values['inlined']
-        entity = self.entity
-        # check in-lining is necessary / possible
-        if not entity.inlined_changed(inlined):
-            return # nothing to do
-        # inlined changed, make necessary physical changes!
-        sqlexec = self.session.system_sql
-        rtype = rschema.type
-        eidcolumn = SQL_PREFIX + 'eid'
-        if not inlined:
-            # need to create the relation if it has not been already done by
-            # another event of the same transaction
-            if not rschema.type in session.transaction_data.get('createdtables', ()):
-                tablesql = rschema2sql(rschema)
-                # create the necessary table
-                for sql in tablesql.split(';'):
-                    if sql.strip():
-                        sqlexec(sql)
-                session.transaction_data.setdefault('createdtables', []).append(
-                    rschema.type)
-            # copy existant data
-            column = SQL_PREFIX + rtype
-            for etype in rschema.subjects():
-                table = SQL_PREFIX + str(etype)
-                sqlexec('INSERT INTO %s_relation SELECT %s, %s FROM %s WHERE NOT %s IS NULL'
-                        % (rtype, eidcolumn, column, table, column))
-            # drop existant columns
-            for etype in rschema.subjects():
-                DropColumn(session, table=SQL_PREFIX + str(etype),
-                             column=SQL_PREFIX + rtype)
-        else:
-            for etype in rschema.subjects():
-                try:
-                    add_inline_relation_column(session, str(etype), rtype)
-                except Exception, ex:
-                    # the column probably already exists. this occurs when the
-                    # entity's type has just been added or if the column has not
-                    # been previously dropped
-                    self.error('error while altering table %s: %s', etype, ex)
-                # copy existant data.
-                # XXX don't use, it's not supported by sqlite (at least at when i tried it)
-                #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '
-                #        'FROM %(rtype)s_relation '
-                #        'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from'
-                #        % locals())
-                table = SQL_PREFIX + str(etype)
-                cursor = sqlexec('SELECT eid_from, eid_to FROM %(table)s, '
-                                 '%(rtype)s_relation WHERE %(table)s.%(eidcolumn)s='
-                                 '%(rtype)s_relation.eid_from' % locals())
-                args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()]
-                if args:
-                    column = SQL_PREFIX + rtype
-                    cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
-                                       % (table, column, eidcolumn), args)
-                # drop existant table
-                DropRelationTable(session, rtype)
-
-
-class SourceDbCWAttributeAdd(PreCommitOperation):
-    """an attribute relation (CWAttribute) has been added:
-    * add the necessary column
-    * set default on this column if any and possible
-    * register an operation to add the relation definition to the
-      instance's schema on commit
-
-    constraints are handled by specific hooks
-    """
-    entity = None # make pylint happy
-
-    def init_rdef(self, **kwargs):
-        entity = self.entity
-        fromentity = entity.stype
-        self.session.execute('SET X ordernum Y+1 '
-                             'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
-                             'X ordernum >= %(order)s, NOT X eid %(x)s',
-                             {'x': entity.eid, 'se': fromentity.eid,
-                              'order': entity.ordernum or 0})
-        subj = str(fromentity.name)
-        rtype = entity.rtype.name
-        obj = str(entity.otype.name)
-        constraints = get_constraints(self.session, entity)
-        rdef = RelationDefinition(subj, rtype, obj,
-                                  description=entity.description,
-                                  cardinality=entity.cardinality,
-                                  constraints=constraints,
-                                  order=entity.ordernum,
-                                  eid=entity.eid,
-                                  **kwargs)
-        MemSchemaRDefAdd(self.session, rdef)
-        return rdef
-
-    def precommit_event(self):
-        session = self.session
-        entity = self.entity
-        # entity.defaultval is a string or None, but we need a correctly typed
-        # value
-        default = entity.defaultval
-        if default is not None:
-            default = TYPE_CONVERTER[entity.otype.name](default)
-        rdef = self.init_rdef(default=default,
-                              indexed=entity.indexed,
-                              fulltextindexed=entity.fulltextindexed,
-                              internationalizable=entity.internationalizable)
-        sysource = session.pool.source('system')
-        attrtype = type_from_constraints(sysource.dbhelper, rdef.object,
-                                         rdef.constraints)
-        # XXX should be moved somehow into lgc.adbh: sqlite doesn't support to
-        # add a new column with UNIQUE, it should be added after the ALTER TABLE
-        # using ADD INDEX
-        if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
-            extra_unique_index = True
-            attrtype = attrtype.replace(' UNIQUE', '')
-        else:
-            extra_unique_index = False
-        # added some str() wrapping query since some backend (eg psycopg) don't
-        # allow unicode queries
-        table = SQL_PREFIX + rdef.subject
-        column = SQL_PREFIX + rdef.name
-        try:
-            session.system_sql(str('ALTER TABLE %s ADD COLUMN %s %s'
-                                   % (table, column, attrtype)),
-                               rollback_on_failure=False)
-            self.info('added column %s to table %s', table, column)
-        except Exception, ex:
-            # the column probably already exists. this occurs when
-            # the entity's type has just been added or if the column
-            # has not been previously dropped
-            self.error('error while altering table %s: %s', table, ex)
-        if extra_unique_index or entity.indexed:
-            try:
-                sysource.create_index(session, table, column,
-                                      unique=extra_unique_index)
-            except Exception, ex:
-                self.error('error while creating index for %s.%s: %s',
-                           table, column, ex)
-
-
-class SourceDbCWRelationAdd(SourceDbCWAttributeAdd):
-    """an actual relation has been added:
-    * if this is an inlined relation, add the necessary column
-      else if it's the first instance of this relation type, add the
-      necessary table and set default permissions
-    * register an operation to add the relation definition to the
-      instance's schema on commit
-
-    constraints are handled by specific hooks
-    """
-    entity = None # make pylint happy
-
-    def precommit_event(self):
-        session = self.session
-        entity = self.entity
-        rdef = self.init_rdef(composite=entity.composite)
-        schema = session.schema
-        rtype = rdef.name
-        rschema = session.schema.rschema(rtype)
-        # this have to be done before permissions setting
-        if rschema.inlined:
-            # need to add a column if the relation is inlined and if this is the
-            # first occurence of "Subject relation Something" whatever Something
-            # and if it has not been added during other event of the same
-            # transaction
-            key = '%s.%s' % (rdef.subject, rtype)
-            try:
-                alreadythere = bool(rschema.objects(rdef.subject))
-            except KeyError:
-                alreadythere = False
-            if not (alreadythere or
-                    key in session.transaction_data.get('createdattrs', ())):
-                add_inline_relation_column(session, rdef.subject, rtype)
-        else:
-            # need to create the relation if no relation definition in the
-            # schema and if it has not been added during other event of the same
-            # transaction
-            if not (rschema.subjects() or
-                    rtype in session.transaction_data.get('createdtables', ())):
-                try:
-                    rschema = session.schema.rschema(rtype)
-                    tablesql = rschema2sql(rschema)
-                except KeyError:
-                    # fake we add it to the schema now to get a correctly
-                    # initialized schema but remove it before doing anything
-                    # more dangerous...
-                    rschema = session.schema.add_relation_type(rdef)
-                    tablesql = rschema2sql(rschema)
-                    session.schema.del_relation_type(rtype)
-                # create the necessary table
-                for sql in tablesql.split(';'):
-                    if sql.strip():
-                        session.system_sql(sql)
-                session.transaction_data.setdefault('createdtables', []).append(
-                    rtype)
-
-
-class SourceDbRDefUpdate(PreCommitOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def precommit_event(self):
-        etype = self.kobj[0]
-        table = SQL_PREFIX + etype
-        column = SQL_PREFIX + self.rschema.type
-        if 'indexed' in self.values:
-            sysource = self.session.pool.source('system')
-            if self.values['indexed']:
-                sysource.create_index(self.session, table, column)
-            else:
-                sysource.drop_index(self.session, table, column)
-        if 'cardinality' in self.values and self.rschema.is_final():
-            adbh = self.session.pool.source('system').dbhelper
-            if not adbh.alter_column_support:
-                # not supported (and NOT NULL not set by yams in that case, so
-                # no worry)
-                return
-            atype = self.rschema.objects(etype)[0]
-            constraints = self.rschema.rproperty(etype, atype, 'constraints')
-            coltype = type_from_constraints(adbh, atype, constraints,
-                                            creating=False)
-            # XXX check self.values['cardinality'][0] actually changed?
-            sql = adbh.sql_set_null_allowed(table, column, coltype,
-                                            self.values['cardinality'][0] != '1')
-            self.session.system_sql(sql)
-
-
-class SourceDbCWConstraintAdd(PreCommitOperation):
-    """actually update constraint of a relation definition"""
-    entity = None # make pylint happy
-    cancelled = False
-
-    def precommit_event(self):
-        rdef = self.entity.reverse_constrained_by[0]
-        session = self.session
-        # when the relation is added in the same transaction, the constraint
-        # object is created by the operation adding the attribute or relation,
-        # so there is nothing to do here
-        if rdef.eid in session.transaction_data.get('neweids', ()):
-            return
-        subjtype, rtype, objtype = session.schema.schema_by_eid(rdef.eid)
-        cstrtype = self.entity.type
-        cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
-        prevcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
-        table = SQL_PREFIX + str(subjtype)
-        column = SQL_PREFIX + str(rtype)
-        # alter the physical schema on size constraint changes
-        if prevcstr.type() == 'SizeConstraint' and (
-            cstr is None or cstr.max != prevcstr.max):
-            adbh = self.session.pool.source('system').dbhelper
-            card = rtype.rproperty(subjtype, objtype, 'cardinality')
-            coltype = type_from_constraints(adbh, objtype, [prevcstr],
-                                            creating=False)
-            sql = adbh.sql_change_col_type(table, column, coltype, card != '1')
-            try:
-                session.system_sql(sql, rollback_on_failure=False)
-                self.info('altered column %s of table %s: now VARCHAR(%s)',
-                          column, table, prevcstr.max)
-            except Exception, ex:
-                # not supported by sqlite for instance
-                self.error('error while altering table %s: %s', table, ex)
-        elif cstrtype == 'UniqueConstraint':
-            session.pool.source('system').create_index(
-                self.session, table, column, unique=True)
-
-
-class SourceDbCWConstraintDel(PreCommitOperation):
-    """actually remove a constraint of a relation definition"""
-    rtype = subjtype = objtype = None # make pylint happy
-
-    def precommit_event(self):
-        cstrtype = self.cstr.type()
-        table = SQL_PREFIX + str(self.subjtype)
-        column = SQL_PREFIX + str(self.rtype)
-        # alter the physical schema on size/unique constraint changes
-        if cstrtype == 'SizeConstraint':
-            try:
-                self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT'
-                                        % (table, column),
-                                        rollback_on_failure=False)
-                self.info('altered column %s of table %s: now TEXT',
-                          column, table)
-            except Exception, ex:
-                # not supported by sqlite for instance
-                self.error('error while altering table %s: %s', table, ex)
-        elif cstrtype == 'UniqueConstraint':
-            self.session.pool.source('system').drop_index(
-                self.session, table, column, unique=True)
-
-
-# operations for in-memory schema synchronization  #############################
-
-class MemSchemaCWETypeAdd(MemSchemaEarlyOperation):
-    """actually add the entity type to the instance's schema"""
-    eid = None # make pylint happy
-    def commit_event(self):
-        self.schema.add_entity_type(self.kobj)
-
-
-class MemSchemaCWETypeRename(MemSchemaOperation):
-    """this operation updates physical storage accordingly"""
-    oldname = newname = None # make pylint happy
-
-    def commit_event(self):
-        self.session.schema.rename_entity_type(self.oldname, self.newname)
-
-
-class MemSchemaCWETypeDel(MemSchemaOperation):
-    """actually remove the entity type from the instance's schema"""
-    def commit_event(self):
-        try:
-            # del_entity_type also removes entity's relations
-            self.schema.del_entity_type(self.kobj)
-        except KeyError:
-            # s/o entity type have already been deleted
-            pass
-
-
-class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation):
-    """actually add the relation type to the instance's schema"""
-    eid = None # make pylint happy
-    def commit_event(self):
-        rschema = self.schema.add_relation_type(self.kobj)
-        rschema.set_default_groups()
-
-
-class MemSchemaCWRTypeUpdate(MemSchemaOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def commit_event(self):
-        # structure should be clean, not need to remove entity's relations
-        # at this point
-        self.rschema.__dict__.update(self.values)
-
-
-class MemSchemaCWRTypeDel(MemSchemaOperation):
-    """actually remove the relation type from the instance's schema"""
-    def commit_event(self):
-        try:
-            self.schema.del_relation_type(self.kobj)
-        except KeyError:
-            # s/o entity type have already been deleted
-            pass
-
-
-class MemSchemaRDefAdd(MemSchemaEarlyOperation):
-    """actually add the attribute relation definition to the instance's
-    schema
-    """
-    def commit_event(self):
-        self.schema.add_relation_def(self.kobj)
-
-
-class MemSchemaRDefUpdate(MemSchemaOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def commit_event(self):
-        # structure should be clean, not need to remove entity's relations
-        # at this point
-        self.rschema._rproperties[self.kobj].update(self.values)
-
-
-class MemSchemaRDefDel(MemSchemaOperation):
-    """actually remove the relation definition from the instance's schema"""
-    def commit_event(self):
-        subjtype, rtype, objtype = self.kobj
-        try:
-            self.schema.del_relation_def(subjtype, rtype, objtype)
-        except KeyError:
-            # relation type may have been already deleted
-            pass
-
-
-class MemSchemaCWConstraintAdd(MemSchemaOperation):
-    """actually update constraint of a relation definition
-
-    has to be called before SourceDbCWConstraintAdd
-    """
-    cancelled = False
-
-    def precommit_event(self):
-        rdef = self.entity.reverse_constrained_by[0]
-        # when the relation is added in the same transaction, the constraint
-        # object is created by the operation adding the attribute or relation,
-        # so there is nothing to do here
-        if rdef.eid in self.session.transaction_data.get('neweids', ()):
-            self.cancelled = True
-            return
-        subjtype, rtype, objtype = self.session.schema.schema_by_eid(rdef.eid)
-        self.prepare_constraints(subjtype, rtype, objtype)
-        cstrtype = self.entity.type
-        self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
-        self.prevcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
-        self.prevcstr.eid = self.entity.eid
-
-    def commit_event(self):
-        if self.cancelled:
-            return
-        # in-place modification
-        if not self.cstr is None:
-            self.constraints.remove(self.cstr)
-        self.constraints.append(self.prevcstr)
-
-
-class MemSchemaCWConstraintDel(MemSchemaOperation):
-    """actually remove a constraint of a relation definition
-
-    has to be called before SourceDbCWConstraintDel
-    """
-    rtype = subjtype = objtype = None # make pylint happy
-    def precommit_event(self):
-        self.prepare_constraints(self.subjtype, self.rtype, self.objtype)
-
-    def commit_event(self):
-        self.constraints.remove(self.cstr)
-
-
-class MemSchemaPermissionCWGroupAdd(MemSchemaPermissionOperation):
-    """synchronize schema when a *_permission relation has been added on a group
-    """
-    def __init__(self, session, perm, etype_eid, group_eid):
-        self.group = entity_name(session, group_eid)
-        super(MemSchemaPermissionCWGroupAdd, self).__init__(
-            session, perm, etype_eid)
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            erschema = self.schema[self.name]
-        except KeyError:
-            # duh, schema not found, log error and skip operation
-            self.error('no schema for %s', self.name)
-            return
-        groups = list(erschema.get_groups(self.perm))
-        try:
-            groups.index(self.group)
-            self.warning('group %s already have permission %s on %s',
-                         self.group, self.perm, erschema.type)
-        except ValueError:
-            groups.append(self.group)
-            erschema.set_groups(self.perm, groups)
-
-
-class MemSchemaPermissionCWGroupDel(MemSchemaPermissionCWGroupAdd):
-    """synchronize schema when a *_permission relation has been deleted from a
-    group
-    """
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            erschema = self.schema[self.name]
-        except KeyError:
-            # duh, schema not found, log error and skip operation
-            self.error('no schema for %s', self.name)
-            return
-        groups = list(erschema.get_groups(self.perm))
-        try:
-            groups.remove(self.group)
-            erschema.set_groups(self.perm, groups)
-        except ValueError:
-            self.error('can\'t remove permission %s on %s to group %s',
-                self.perm, erschema.type, self.group)
-
-
-class MemSchemaPermissionRQLExpressionAdd(MemSchemaPermissionOperation):
-    """synchronize schema when a *_permission relation has been added on a rql
-    expression
-    """
-    def __init__(self, session, perm, etype_eid, expression):
-        self.expr = expression
-        super(MemSchemaPermissionRQLExpressionAdd, self).__init__(
-            session, perm, etype_eid)
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            erschema = self.schema[self.name]
-        except KeyError:
-            # duh, schema not found, log error and skip operation
-            self.error('no schema for %s', self.name)
-            return
-        exprs = list(erschema.get_rqlexprs(self.perm))
-        exprs.append(erschema.rql_expression(self.expr))
-        erschema.set_rqlexprs(self.perm, exprs)
-
-
-class MemSchemaPermissionRQLExpressionDel(MemSchemaPermissionRQLExpressionAdd):
-    """synchronize schema when a *_permission relation has been deleted from an
-    rql expression
-    """
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            erschema = self.schema[self.name]
-        except KeyError:
-            # duh, schema not found, log error and skip operation
-            self.error('no schema for %s', self.name)
-            return
-        rqlexprs = list(erschema.get_rqlexprs(self.perm))
-        for i, rqlexpr in enumerate(rqlexprs):
-            if rqlexpr.expression == self.expr:
-                rqlexprs.pop(i)
-                break
-        else:
-            self.error('can\'t remove permission %s on %s for expression %s',
-                self.perm, erschema.type, self.expr)
-            return
-        erschema.set_rqlexprs(self.perm, rqlexprs)
-
-
-# deletion hooks ###############################################################
-
-def before_del_eetype(session, eid):
-    """before deleting a CWEType entity:
-    * check that we don't remove a core entity type
-    * cascade to delete related CWAttribute and CWRelation entities
-    * instantiate an operation to delete the entity type on commit
-    """
-    # final entities can't be deleted, don't care about that
-    name = check_internal_entity(session, eid, CORE_ETYPES)
-    # delete every entities of this type
-    session.unsafe_execute('DELETE %s X' % name)
-    DropTable(session, table=SQL_PREFIX + name)
-    MemSchemaCWETypeDel(session, name)
-
-
-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')
-
-
-def before_del_ertype(session, eid):
-    """before deleting a CWRType entity:
-    * check that we don't remove a core relation type
-    * cascade to delete related CWAttribute and CWRelation entities
-    * instantiate an operation to delete the relation type on commit
-    """
-    name = check_internal_entity(session, eid, CORE_RTYPES)
-    # delete relation definitions using this relation type
-    session.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
-                    {'x': eid})
-    session.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
-                    {'x': eid})
-    MemSchemaCWRTypeDel(session, name)
-
-
-def after_del_relation_type(session, rdefeid, rtype, rteid):
-    """before deleting a CWAttribute or CWRelation entity:
-    * if this is a final or inlined relation definition, instantiate an
-      operation to drop necessary column, else if this is the last instance
-      of a non final relation, instantiate an operation to drop necessary
-      table
-    * instantiate an operation to delete the relation definition on commit
-    * delete the associated relation type when necessary
-    """
-    subjschema, rschema, objschema = session.schema.schema_by_eid(rdefeid)
-    pendings = session.transaction_data.get('pendingeids', ())
-    # first delete existing relation if necessary
-    if rschema.is_final():
-        rdeftype = 'CWAttribute'
-    else:
-        rdeftype = 'CWRelation'
-        if not (subjschema.eid in pendings or objschema.eid in pendings):
-            session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
-                            % (rschema, subjschema, objschema))
-    execute = session.unsafe_execute
-    rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
-                   'R eid %%(x)s' % rdeftype, {'x': rteid})
-    lastrel = rset[0][0] == 0
-    # we have to update physical schema systematically for final and inlined
-    # relations, but only if it's the last instance for this relation type
-    # for other relations
-
-    if (rschema.is_final() or rschema.inlined):
-        rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
-                       'R eid %%(x)s, X from_entity E, E name %%(name)s'
-                       % rdeftype, {'x': rteid, 'name': str(subjschema)})
-        if rset[0][0] == 0 and not subjschema.eid in pendings:
-            ptypes = session.transaction_data.setdefault('pendingrtypes', set())
-            ptypes.add(rschema.type)
-            DropColumn(session, table=SQL_PREFIX + subjschema.type,
-                         column=SQL_PREFIX + rschema.type)
-    elif lastrel:
-        DropRelationTable(session, rschema.type)
-    # if this is the last instance, drop associated relation type
-    if lastrel and not rteid in pendings:
-        execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rteid}, 'x')
-    MemSchemaRDefDel(session, (subjschema, rschema, objschema))
-
-
-# addition hooks ###############################################################
-
-def before_add_eetype(session, entity):
-    """before adding a CWEType entity:
-    * check that we are not using an existing entity type,
-    """
-    name = entity['name']
-    schema = session.schema
-    if name in schema and schema[name].eid is not None:
-        raise RepositoryError('an entity type %s already exists' % name)
-
-def after_add_eetype(session, entity):
-    """after adding a CWEType entity:
-    * create the necessary table
-    * set creation_date and modification_date by creating the necessary
-      CWAttribute entities
-    * add owned_by relation by creating the necessary CWRelation entity
-    * register an operation to add the entity type to the instance's
-      schema on commit
-    """
-    if entity.get('final'):
-        return
-    schema = session.schema
-    name = entity['name']
-    etype = EntityType(name=name, description=entity.get('description'),
-                       meta=entity.get('meta')) # don't care about final
-    # fake we add it to the schema now to get a correctly initialized schema
-    # but remove it before doing anything more dangerous...
-    schema = session.schema
-    eschema = schema.add_entity_type(etype)
-    eschema.set_default_groups()
-    # generate table sql and rql to add metadata
-    tablesql = eschema2sql(session.pool.source('system').dbhelper, eschema,
-                           prefix=SQL_PREFIX)
-    relrqls = []
-    for rtype in (META_RTYPES - VIRTUAL_RTYPES):
-        rschema = schema[rtype]
-        sampletype = rschema.subjects()[0]
-        desttype = rschema.objects()[0]
-        props = rschema.rproperties(sampletype, desttype)
-        relrqls += list(ss.rdef2rql(rschema, name, desttype, props))
-    # now remove it !
-    schema.del_entity_type(name)
-    # create the necessary table
-    for sql in tablesql.split(';'):
-        if sql.strip():
-            session.system_sql(sql)
-    # register operation to modify the schema on commit
-    # this have to be done before adding other relations definitions
-    # or permission settings
-    etype.eid = entity.eid
-    MemSchemaCWETypeAdd(session, etype)
-    # add meta relations
-    for rql, kwargs in relrqls:
-        session.execute(rql, kwargs)
-
-
-def before_add_ertype(session, entity):
-    """before adding a CWRType entity:
-    * check that we are not using an existing relation type,
-    * register an operation to add the relation type to the instance's
-      schema on commit
-
-    We don't know yeat this point if a table is necessary
-    """
-    name = entity['name']
-    if name in session.schema.relations():
-        raise RepositoryError('a relation type %s already exists' % name)
-
-
-def after_add_ertype(session, entity):
-    """after a CWRType entity has been added:
-    * register an operation to add the relation type to the instance's
-      schema on commit
-    We don't know yeat this point if a table is necessary
-    """
-    rtype = RelationType(name=entity['name'],
-                         description=entity.get('description'),
-                         meta=entity.get('meta', False),
-                         inlined=entity.get('inlined', False),
-                         symetric=entity.get('symetric', False))
-    rtype.eid = entity.eid
-    MemSchemaCWRTypeAdd(session, rtype)
-
-
-def after_add_efrdef(session, entity):
-    SourceDbCWAttributeAdd(session, entity=entity)
-
-def after_add_enfrdef(session, entity):
-    SourceDbCWRelationAdd(session, entity=entity)
-
-
-# update hooks #################################################################
-
-def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
-    errors = {}
-    # don't use getattr(entity, attr), we would get the modified value if any
-    for attr in ro_attrs:
-        origval = entity_attr(session, entity.eid, attr)
-        if entity.get(attr, origval) != origval:
-            errors[attr] = session._("can't change the %s attribute") % \
-                           display_name(session, attr)
-    if errors:
-        raise ValidationError(entity.eid, errors)
-
-def before_update_eetype(session, entity):
-    """check name change, handle final"""
-    check_valid_changes(session, entity, ro_attrs=('final',))
-    # don't use getattr(entity, attr), we would get the modified value if any
-    oldname = entity_attr(session, entity.eid, 'name')
-    newname = entity.get('name', oldname)
-    if newname.lower() != oldname.lower():
-        SourceDbCWETypeRename(session, oldname=oldname, newname=newname)
-        MemSchemaCWETypeRename(session, oldname=oldname, newname=newname)
-
-def before_update_ertype(session, entity):
-    """check name change, handle final"""
-    check_valid_changes(session, entity)
-
-
-def after_update_erdef(session, entity):
-    desttype = entity.otype.name
-    rschema = session.schema[entity.rtype.name]
-    newvalues = {}
-    for prop in rschema.rproperty_defs(desttype):
-        if prop == 'constraints':
-            continue
-        if prop == 'order':
-            prop = 'ordernum'
-        if prop in entity:
-            newvalues[prop] = entity[prop]
-    if newvalues:
-        subjtype = entity.stype.name
-        MemSchemaRDefUpdate(session, kobj=(subjtype, desttype),
-                            rschema=rschema, values=newvalues)
-        SourceDbRDefUpdate(session, kobj=(subjtype, desttype),
-                           rschema=rschema, values=newvalues)
-
-def after_update_ertype(session, entity):
-    rschema = session.schema.rschema(entity.name)
-    newvalues = {}
-    for prop in ('meta', 'symetric', 'inlined'):
-        if prop in entity:
-            newvalues[prop] = entity[prop]
-    if newvalues:
-        MemSchemaCWRTypeUpdate(session, rschema=rschema, values=newvalues)
-        SourceDbCWRTypeUpdate(session, rschema=rschema, values=newvalues,
-                              entity=entity)
-
-# constraints synchronization hooks ############################################
-
-def after_add_econstraint(session, entity):
-    MemSchemaCWConstraintAdd(session, entity=entity)
-    SourceDbCWConstraintAdd(session, entity=entity)
-
-
-def after_update_econstraint(session, entity):
-    MemSchemaCWConstraintAdd(session, entity=entity)
-    SourceDbCWConstraintAdd(session, entity=entity)
-
-
-def before_delete_constrained_by(session, fromeid, rtype, toeid):
-    if not fromeid in session.transaction_data.get('pendingeids', ()):
-        schema = session.schema
-        entity = session.entity_from_eid(toeid)
-        subjtype, rtype, objtype = schema.schema_by_eid(fromeid)
-        try:
-            cstr = rtype.constraint_by_type(subjtype, objtype,
-                                            entity.cstrtype[0].name)
-        except IndexError:
-            session.critical('constraint type no more accessible')
-        else:
-            SourceDbCWConstraintDel(session, subjtype=subjtype, rtype=rtype,
-                                    objtype=objtype, cstr=cstr)
-            MemSchemaCWConstraintDel(session, subjtype=subjtype, rtype=rtype,
-                                     objtype=objtype, cstr=cstr)
-
-
-def after_add_constrained_by(session, fromeid, rtype, toeid):
-    if fromeid in session.transaction_data.get('neweids', ()):
-        session.transaction_data.setdefault(fromeid, []).append(toeid)
-
-
-# permissions synchronization hooks ############################################
-
-def after_add_permission(session, subject, rtype, object):
-    """added entity/relation *_permission, need to update schema"""
-    perm = rtype.split('_', 1)[0]
-    if session.describe(object)[0] == 'CWGroup':
-        MemSchemaPermissionCWGroupAdd(session, perm, subject, object)
-    else: # RQLExpression
-        expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR',
-                               {'x': object}, 'x')[0][0]
-        MemSchemaPermissionRQLExpressionAdd(session, perm, subject, expr)
-
-
-def before_del_permission(session, subject, rtype, object):
-    """delete entity/relation *_permission, need to update schema
-
-    skip the operation if the related type is being deleted
-    """
-    if subject in session.transaction_data.get('pendingeids', ()):
-        return
-    perm = rtype.split('_', 1)[0]
-    if session.describe(object)[0] == 'CWGroup':
-        MemSchemaPermissionCWGroupDel(session, perm, subject, object)
-    else: # RQLExpression
-        expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR',
-                               {'x': object}, 'x')[0][0]
-        MemSchemaPermissionRQLExpressionDel(session, perm, subject, expr)
-
-
-def rebuild_infered_relations(session, subject, rtype, object):
-    # registering a schema operation will trigger a call to
-    # repo.set_schema() on commit which will in turn rebuild
-    # infered relation definitions
-    MemSchemaNotifyChanges(session)
-
-
-def _register_schema_hooks(hm):
-    """register schema related hooks on the hooks manager"""
-    # schema synchronisation #####################
-    # before/after add
-    hm.register_hook(before_add_eetype, 'before_add_entity', 'CWEType')
-    hm.register_hook(before_add_ertype, 'before_add_entity', 'CWRType')
-    hm.register_hook(after_add_eetype, 'after_add_entity', 'CWEType')
-    hm.register_hook(after_add_ertype, 'after_add_entity', 'CWRType')
-    hm.register_hook(after_add_efrdef, 'after_add_entity', 'CWAttribute')
-    hm.register_hook(after_add_enfrdef, 'after_add_entity', 'CWRelation')
-    # before/after update
-    hm.register_hook(before_update_eetype, 'before_update_entity', 'CWEType')
-    hm.register_hook(before_update_ertype, 'before_update_entity', 'CWRType')
-    hm.register_hook(after_update_ertype, 'after_update_entity', 'CWRType')
-    hm.register_hook(after_update_erdef, 'after_update_entity', 'CWAttribute')
-    hm.register_hook(after_update_erdef, 'after_update_entity', 'CWRelation')
-    # before/after delete
-    hm.register_hook(before_del_eetype, 'before_delete_entity', 'CWEType')
-    hm.register_hook(after_del_eetype, 'after_delete_entity', 'CWEType')
-    hm.register_hook(before_del_ertype, 'before_delete_entity', 'CWRType')
-    hm.register_hook(after_del_relation_type, 'after_delete_relation', 'relation_type')
-    hm.register_hook(rebuild_infered_relations, 'after_add_relation', 'specializes')
-    hm.register_hook(rebuild_infered_relations, 'after_delete_relation', 'specializes')
-    # constraints synchronization hooks
-    hm.register_hook(after_add_econstraint, 'after_add_entity', 'CWConstraint')
-    hm.register_hook(after_update_econstraint, 'after_update_entity', 'CWConstraint')
-    hm.register_hook(before_delete_constrained_by, 'before_delete_relation', 'constrained_by')
-    hm.register_hook(after_add_constrained_by, 'after_add_relation', 'constrained_by')
-    # permissions synchronisation ################
-    for perm in ('read_permission', 'add_permission',
-                 'delete_permission', 'update_permission'):
-        hm.register_hook(after_add_permission, 'after_add_relation', perm)
-        hm.register_hook(before_del_permission, 'before_delete_relation', perm)
--- a/server/schemaserial.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/schemaserial.py	Tue Sep 22 13:08:42 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	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-"""Security hooks: check permissions to add/delete/update entities according to
-the user connected to a session
-
-: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
-"""
-__docformat__ = "restructuredtext en"
-
-from cubicweb import Unauthorized
-from cubicweb.server.pool import LateOperation
-from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS
-
-def check_entity_attributes(session, entity):
-    eid = entity.eid
-    eschema = entity.e_schema
-    # ._default_set is only there on entity creation to indicate unspecified
-    # attributes which has been set to a default value defined in the schema
-    defaults = getattr(entity, '_default_set', ())
-    try:
-        editedattrs = entity.edited_attributes
-    except AttributeError:
-        editedattrs = entity.keys()
-    for attr in editedattrs:
-        if attr in defaults:
-            continue
-        rschema = eschema.subject_relation(attr)
-        if rschema.is_final(): # non final relation are checked by other hooks
-            # add/delete should be equivalent (XXX: unify them into 'update' ?)
-            rschema.check_perm(session, 'add', eid)
-
-
-class CheckEntityPermissionOp(LateOperation):
-    def precommit_event(self):
-        #print 'CheckEntityPermissionOp', self.session.user, self.entity, self.action
-        self.entity.check_perm(self.action)
-        check_entity_attributes(self.session, self.entity)
-
-    def commit_event(self):
-        pass
-
-
-class CheckRelationPermissionOp(LateOperation):
-    def precommit_event(self):
-        self.rschema.check_perm(self.session, self.action, self.fromeid, self.toeid)
-
-    def commit_event(self):
-        pass
-
-def after_add_entity(session, entity):
-    if not session.is_super_session:
-        CheckEntityPermissionOp(session, entity=entity, action='add')
-
-def after_update_entity(session, entity):
-    if not session.is_super_session:
-        try:
-            # check user has permission right now, if not retry at commit time
-            entity.check_perm('update')
-            check_entity_attributes(session, entity)
-        except Unauthorized:
-            entity.clear_local_perm_cache('update')
-            CheckEntityPermissionOp(session, entity=entity, action='update')
-
-def before_del_entity(session, eid):
-    if not session.is_super_session:
-        eschema = session.repo.schema[session.describe(eid)[0]]
-        eschema.check_perm(session, 'delete', eid)
-
-
-def before_add_relation(session, fromeid, rtype, toeid):
-    if rtype in BEFORE_ADD_RELATIONS and not session.is_super_session:
-        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:
-        rschema = session.repo.schema[rtype]
-        if rtype in ON_COMMIT_ADD_RELATIONS:
-            CheckRelationPermissionOp(session, action='add', rschema=rschema,
-                                      fromeid=fromeid, toeid=toeid)
-        else:
-            rschema.check_perm(session, 'add', fromeid, toeid)
-
-def before_del_relation(session, fromeid, rtype, toeid):
-    if not session.is_super_session:
-        session.repo.schema[rtype].check_perm(session, 'delete', fromeid, toeid)
-
-def register_security_hooks(hm):
-    """register meta-data related hooks on the hooks manager"""
-    hm.register_hook(after_add_entity, 'after_add_entity', '')
-    hm.register_hook(after_update_entity, 'after_update_entity', '')
-    hm.register_hook(before_del_entity, 'before_delete_entity', '')
-    hm.register_hook(before_add_relation, 'before_add_relation', '')
-    hm.register_hook(after_add_relation, 'after_add_relation', '')
-    hm.register_hook(before_del_relation, 'before_delete_relation', '')
-
--- a/server/serverconfig.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/serverconfig.py	Tue Sep 22 13:08:42 2009 +0200
@@ -77,15 +77,12 @@
     name = 'repository'
     if os.environ.get('APYCOT_ROOT'):
         root = os.environ['APYCOT_ROOT']
-        SCHEMAS_LIB_DIR = '%s/local/share/cubicweb/schemas/' % root
     elif CubicWebConfiguration.mode == 'dev':
-        SCHEMAS_LIB_DIR = join(CW_SOFTWARE_ROOT, 'schemas')
         BACKUP_DIR = CubicWebConfiguration.RUNTIME_DIR
     else:
-        SCHEMAS_LIB_DIR = '/usr/share/cubicweb/schemas/'
         BACKUP_DIR = '/var/lib/cubicweb/backup/'
 
-    cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set(['sobjects'])
+    cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set(['sobjects', 'hooks'])
     cube_appobject_path = CubicWebConfiguration.cube_appobject_path | set(['sobjects', 'hooks'])
 
     options = merge_options((
@@ -181,6 +178,10 @@
           }),
         ) + CubicWebConfiguration.options)
 
+    # should we open connections pools (eg connect to sources). This is usually
+    # necessary...
+    open_connections_pools = True
+
     # read the schema from the database
     read_instance_schema = True
     bootstrap_schema = True
@@ -188,14 +189,9 @@
     # check user's state at login time
     consider_user_state = True
 
-    # hooks registration configuration
+    # hooks activation configuration
     # all hooks should be activated during normal execution
-    core_hooks = True
-    usergroup_hooks = True
-    schema_hooks = True
-    notification_hooks = True
-    security_hooks = True
-    instance_hooks = True
+    disabled_hooks_categories = set()
 
     # should some hooks be deactivated during [pre|post]create script execution
     free_wheel = False
@@ -208,11 +204,6 @@
         self._enabled_sources = sourceuris
         clear_cache(self, 'sources')
 
-    @classmethod
-    def schemas_lib_dir(cls):
-        """instance schema directory"""
-        return env_path('CW_SCHEMA_LIB', cls.SCHEMAS_LIB_DIR, 'schemas')
-
     def bootstrap_cubes(self):
         from logilab.common.textutils import splitstrip
         for line in file(join(self.apphome, 'bootstrap_cubes')):
@@ -264,20 +255,6 @@
         """pyro is always enabled in standalone repository configuration"""
         return True
 
-    def load_hooks(self, vreg):
-        hooks = {}
-        try:
-            apphookdefs = vreg['hooks'].all_objects()
-        except RegistryNotFound:
-            return hooks
-        for hookdef in apphookdefs:
-            for event, ertype in hookdef.register_to():
-                if ertype == 'Any':
-                    ertype = ''
-                cb = hookdef.make_callback(event)
-                hooks.setdefault(event, {}).setdefault(ertype, []).append(cb)
-        return hooks
-
     def load_schema(self, expand_cubes=False, **kwargs):
         from cubicweb.schema import CubicWebSchemaLoader
         if expand_cubes:
--- a/server/serverctl.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/serverctl.py	Tue Sep 22 13:08:42 2009 +0200
@@ -14,8 +14,8 @@
 from logilab.common.clcommands import register_commands, cmd_run, pop_arg
 from logilab.common.shellutils import ASK
 
-from cubicweb import AuthenticationError, ExecutionError, ConfigurationError, underline_title
-from cubicweb.toolsutils import Command, CommandHandler
+from cubicweb import AuthenticationError, ExecutionError, ConfigurationError
+from cubicweb.toolsutils import Command, CommandHandler, underline_title
 from cubicweb.server import SOURCE_TYPES
 from cubicweb.server.utils import ask_source_config
 from cubicweb.server.serverconfig import USER_OPTIONS, ServerConfiguration
@@ -210,12 +210,12 @@
     cmdname = 'start'
     cfgname = 'repository'
 
-    def start_command(self, ctlconf, debug):
+    def start_server(self, ctlconf, debug):
         command = ['cubicweb-ctl start-repository ']
         if debug:
             command.append('--debug')
         command.append(self.config.appid)
-        return ' '.join(command)
+        os.system(' '.join(command))
 
 
 class RepositoryStopHandler(CommandHandler):
@@ -257,12 +257,19 @@
           'help': 'verbose mode: will ask all possible configuration questions',
           }
          ),
+        ('automatic',
+         {'short': 'a', 'type' : 'yn', 'metavar': '<auto>',
+          'default': 'n',
+          'help': 'automatic mode: never ask and use default answer to every question',
+          }
+         ),
         )
     def run(self, args):
         """run the command with its specific arguments"""
         from logilab.common.adbh import get_adv_func_helper
         from indexer import get_indexer
         verbose = self.get('verbose')
+        automatic = self.get('automatic')
         appid = pop_arg(args, msg='No instance specified !')
         config = ServerConfiguration.config_for(appid)
         create_db = self.config.create_db
@@ -277,13 +284,13 @@
             try:
                 if helper.users_support:
                     user = source['db-user']
-                    if not helper.user_exists(cursor, user) and \
-                           ASK.confirm('Create db user %s ?' % user, default_is_yes=False):
+                    if not helper.user_exists(cursor, user) and (automatic or \
+                           ASK.confirm('Create db user %s ?' % user, default_is_yes=False)):
                         helper.create_user(source['db-user'], source['db-password'])
                         print '-> user %s created.' % user
                 dbname = source['db-name']
                 if dbname in helper.list_databases(cursor):
-                    if ASK.confirm('Database %s already exists -- do you want to drop it ?' % dbname):
+                    if automatic or ASK.confirm('Database %s already exists -- do you want to drop it ?' % dbname):
                         cursor.execute('DROP DATABASE %s' % dbname)
                     else:
                         return
@@ -305,13 +312,14 @@
         # 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()
         print '-> database for instance %s created and necessary extensions installed.' % appid
         print
-        if ASK.confirm('Run db-init to initialize the system database ?'):
+        if automatic or ASK.confirm('Run db-init to initialize the system database ?'):
             cmd_run('db-init', config.appid)
         else:
             print ('-> nevermind, you can do it later with '
@@ -458,7 +466,13 @@
         # create the server
         server = RepositoryServer(config, debug)
         # go ! (don't daemonize in debug mode)
-        if not debug and server.daemonize(config['pid-file']) == -1:
+        pidfile = config['pid-file']
+        # ensure the directory where the pid-file should be set exists (for
+        # instance /var/run/cubicweb may be deleted on computer restart)
+        piddir = os.path.dirname(pidfile)
+        if not os.path.exists(piddir):
+            os.makedirs(piddir)
+        if not debug and server.daemonize(pidfile) == -1:
             return
         uid = config['uid']
         if uid is not None:
@@ -475,7 +489,9 @@
 
 def _remote_dump(host, appid, output, sudo=False):
     # XXX generate unique/portable file name
-    dmpcmd = 'cubicweb-ctl db-dump -o /tmp/%s.dump %s' % (appid, appid)
+    from datetime import date
+    filename = '%s-%s.tgz' % (appid, date.today().strftime('%Y-%m-%d'))
+    dmpcmd = 'cubicweb-ctl db-dump -o /tmp/%s %s' % (filename, appid)
     if sudo:
         dmpcmd = 'sudo %s' % (dmpcmd)
     dmpcmd = 'ssh -t %s "%s"' % (host, dmpcmd)
@@ -483,18 +499,17 @@
     if os.system(dmpcmd):
         raise ExecutionError('Error while dumping the database')
     if output is None:
-        from datetime import date
-        date = date.today().strftime('%Y-%m-%d')
-        output = '%s-%s.dump' % (appid, date)
-    cmd = 'scp %s:/tmp/%s.dump %s' % (host, appid, output)
+        output = filename
+    cmd = 'scp %s:/tmp/%s %s' % (host, filename, output)
     print cmd
     if os.system(cmd):
-        raise ExecutionError('Error while retrieving the dump')
-    rmcmd = 'ssh -t %s "rm -f /tmp/%s.dump"' % (host, appid)
+        raise ExecutionError('Error while retrieving the dump at /tmp/%s' % filename)
+    rmcmd = 'ssh -t %s "rm -f /tmp/%s"' % (host, filename)
     print rmcmd
     if os.system(rmcmd) and not ASK.confirm(
-        'An error occured while deleting remote dump. Continue anyway?'):
-        raise ExecutionError('Error while deleting remote dump')
+        'An error occured while deleting remote dump at /tmp/%s. '
+        'Continue anyway?' % filename):
+        raise ExecutionError('Error while deleting remote dump at /tmp/%s' % filename)
 
 def _local_dump(appid, output):
     config = ServerConfiguration.config_for(appid)
@@ -522,21 +537,20 @@
     status = instance_status(config, eversion, dbversions)
     # * database version > installed software
     if status == 'needsoftupgrade':
-        print "database is using some earlier version than installed software!"
-        print "please upgrade your software and then upgrade the instance"
-        print "using command 'cubicweb-ctl upgrade %s'" % config.appid
+        print "** The database of %s is more recent than the installed software!" % config.appid
+        print "** Upgrade your software, then migrate the database by running the command"
+        print "** 'cubicweb-ctl upgrade %s'" % config.appid
         return
     # * database version < installed software, an upgrade will be necessary
     #   anyway, just rewrite vc.conf and warn user he has to upgrade
-    if status == 'needapplupgrade':
-        print "database is using some older version than installed software."
-        print "You'll have to upgrade the instance using command"
-        print "'cubicweb-ctl upgrade %s'" % config.appid
+    elif status == 'needapplupgrade':
+        print "** The database of %s is older than the installed software." % config.appid
+        print "** Migrate the database by running the command"
+        print "** 'cubicweb-ctl upgrade %s'" % config.appid
         return
     # * database version = installed software, database version = instance fs version
     #   ok!
 
-
 def instance_status(config, cubicwebapplversion, vcconf):
     cubicwebversion = config.cubicweb_version()
     if cubicwebapplversion > cubicwebversion:
@@ -663,9 +677,8 @@
         import tempfile
         srcappid = pop_arg(args, 1, msg='No source instance specified !')
         destappid = pop_arg(args, msg='No destination instance specified !')
-        # XXX -system necessary to match file name modified on source restore.
-        # should not have to expect this.
-        _, output = tempfile.mkstemp('-system.sql')
+        fd, output = tempfile.mkstemp()
+        os.close(fd)
         if ':' in srcappid:
             host, srcappid = srcappid.split(':')
             _remote_dump(host, srcappid, output, self.config.sudo)
--- a/server/session.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/session.py	Tue Sep 22 13:08:42 2009 +0200
@@ -15,10 +15,11 @@
 from rql.nodes import VariableRef, Function, ETYPE_PYOBJ_MAP, etype_from_pyobj
 from yams import BASE_TYPES
 
-from cubicweb import RequestSessionMixIn, Binary, UnknownEid
+from cubicweb import Binary, UnknownEid
+from cubicweb.req import RequestSessionBase
 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'
 
@@ -41,7 +42,7 @@
     return description
 
 
-class Session(RequestSessionMixIn):
+class Session(RequestSessionBase):
     """tie session id, user, connections pool and other session data all
     together
     """
@@ -74,20 +75,64 @@
         return '<%ssession %s (%s 0x%x)>' % (self.cnxtype, self.user.login,
                                              self.id, id(self))
 
-    @property
-    def schema(self):
-        return self.repo.schema
+    def hijack_user(self, user):
+        """return a fake request/session using specified user"""
+        session = Session(user, self.repo)
+        session._threaddata = self.actual_session()._threaddata
+        return session
 
-    def add_relation(self, fromeid, rtype, toeid):
+    def _super_call(self, __cb, *args, **kwargs):
         if self.is_super_session:
-            self.repo.glob_add_relation(self, fromeid, rtype, toeid)
+            __cb(self, *args, **kwargs)
             return
         self.is_super_session = True
         try:
-            self.repo.glob_add_relation(self, fromeid, rtype, toeid)
+            __cb(self, *args, **kwargs)
         finally:
             self.is_super_session = False
 
+    def add_relation(self, fromeid, rtype, toeid):
+        """provide direct access to the repository method to add a relation.
+
+        This is equivalent to the following rql query:
+
+          SET X rtype Y WHERE X eid  fromeid, T eid toeid
+
+        without read security check but also all the burden of rql execution.
+        You may use this in hooks when you know both eids of the relation you
+        want to add.
+        """
+        if self.vreg.schema[rtype].inlined:
+            entity = self.entity_from_eid(fromeid)
+            entity[rtype] = toeid
+            self._super_call(self.repo.glob_update_entity,
+                             entity, set((rtype,)))
+        else:
+            self._super_call(self.repo.glob_add_relation,
+                             fromeid, rtype, toeid)
+
+    def delete_relation(self, fromeid, rtype, toeid):
+        """provide direct access to the repository method to delete a relation.
+
+        This is equivalent to the following rql query:
+
+          DELETE X rtype Y WHERE X eid  fromeid, T eid toeid
+
+        without read security check but also all the burden of rql execution.
+        You may use this in hooks when you know both eids of the relation you
+        want to delete.
+        """
+        if self.vreg.schema[rtype].inlined:
+            entity = self.entity_from_eid(fromeid)
+            entity[rtype] = None
+            self._super_call(self.repo.glob_update_entity,
+                             entity, set((rtype,)))
+        else:
+            self._super_call(self.repo.glob_delete_relation,
+                             fromeid, rtype, toeid)
+
+    # relations cache handling #################################################
+
     def update_rel_cache_add(self, subject, rtype, object, symetric=False):
         self._update_entity_rel_cache_add(subject, rtype, 'subject', object)
         if symetric:
@@ -114,10 +159,15 @@
         if rcache is not None:
             rset, entities = rcache
             rset.rows.append([targeteid])
-            if isinstance(rset.description, list): # else description not set
-                rset.description.append([self.describe(targeteid)[0]])
+            if not isinstance(rset.description, list): # else description not set
+                rset.description = list(rset.description)
+            rset.description.append([self.describe(targeteid)[0]])
+            targetentity = self.entity_from_eid(targeteid)
+            if targetentity.rset is None:
+                targetentity.rset = rset
+                targetentity.row = rset.rowcount
+                targetentity.col = 0
             rset.rowcount += 1
-            targetentity = self.entity_from_eid(targeteid)
             entities.append(targetentity)
 
     def _update_entity_rel_cache_del(self, eid, rtype, role, targeteid):
@@ -128,8 +178,11 @@
                 if row[0] == targeteid:
                     break
             else:
-                raise Exception('cache inconsistency for %s %s %s %s' %
-                                (eid, rtype, role, targeteid))
+                # this may occurs if the cache has been filed by a hook
+                # after the database update
+                self.debug('cache inconsistency for %s %s %s %s', eid, rtype,
+                           role, targeteid)
+                return
             del rset.rows[idx]
             if isinstance(rset.description, list): # else description not set
                 del rset.description[idx]
@@ -154,19 +207,36 @@
         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):
         assert prop == 'lang' # this is the only one changeable property for now
         self.set_language(value)
 
+    def deleted_in_transaction(self, eid):
+        return eid in self.transaction_data.get('pendingeids', ())
+
+    def added_in_transaction(self, eid):
+        return eid in self.transaction_data.get('neweids', ())
+
+    def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
+        rschema = self.repo.schema[rtype]
+        subjtype = self.describe(eidfrom)[0]
+        objtype = self.describe(eidto)[0]
+        return rschema.rproperty(subjtype, objtype, rprop)
+
     # connection management ###################################################
 
     def keep_pool_mode(self, mode):
@@ -208,9 +278,9 @@
         """connections pool, set according to transaction mode for each query"""
         return getattr(self._threaddata, 'pool', None)
 
-    def set_pool(self):
+    def set_pool(self, checkclosed=True):
         """the session need a pool to execute some queries"""
-        if self._closed:
+        if checkclosed and self._closed:
             raise Exception('try to set pool on a closed session')
         if self.pool is None:
             # get pool first to avoid race-condition
@@ -264,6 +334,11 @@
 
     # request interface #######################################################
 
+    @property
+    def cursor(self):
+        """return a rql cursor"""
+        return self
+
     def set_entity_cache(self, entity):
         # XXX session level caching may be a pb with multiple repository
         #     instances, but 1. this is probably not the only one :$ and 2. it
@@ -329,13 +404,13 @@
         try:
             csession = self._threaddata.childsession
         except AttributeError:
-            if self.is_super_session:
+            if isinstance(self, (ChildSession, InternalSession)):
                 csession = self
             else:
                 csession = ChildSession(self)
             self._threaddata.childsession = csession
         # need shared pool set
-        self.set_pool()
+        self.set_pool(checkclosed=False)
         return csession
 
     def unsafe_execute(self, rql, kwargs=None, eid_key=None, build_descr=True,
@@ -350,11 +425,6 @@
         return self.super_session.execute(rql, kwargs, eid_key, build_descr,
                                           propagate)
 
-    @property
-    def cursor(self):
-        """return a rql cursor"""
-        return self
-
     def execute(self, rql, kwargs=None, eid_key=None, build_descr=True,
                 propagate=False):
         """db-api like method directly linked to the querier execute method
@@ -471,7 +541,6 @@
             self._threaddata.pending_operations = []
             return self._threaddata.pending_operations
 
-
     def add_operation(self, operation, index=None):
         """add an observer"""
         assert self.commit_state != 'commit'
@@ -487,7 +556,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):
@@ -551,12 +620,19 @@
             description.append(tuple(row_descr))
         return description
 
-    @deprecated("use vreg['etypes'].etype_class(etype)")
+    # deprecated ###############################################################
+
+    @property
+    @deprecated("[3.6] use session.vreg.schema")
+    def schema(self):
+        return self.repo.schema
+
+    @deprecated("[3.4] use vreg['etypes'].etype_class(etype)")
     def etype_class(self, etype):
         """return an entity class for the given entity type"""
         return self.vreg['etypes'].etype_class(etype)
 
-    @deprecated('use direct access to session.transaction_data')
+    @deprecated('[3.4] use direct access to session.transaction_data')
     def query_data(self, key, default=None, setdefault=False, pop=False):
         if setdefault:
             assert not pop
@@ -566,7 +642,7 @@
         else:
             return self.transaction_data.get(key, default)
 
-    @deprecated('use entity_from_eid(eid, etype=None)')
+    @deprecated('[3.4] use entity_from_eid(eid, etype=None)')
     def entity(self, eid):
         """return a result set for the given eid"""
         return self.entity_from_eid(eid)
@@ -584,6 +660,7 @@
         # session which has created this one
         self.parent_session = parent_session
         self.user = InternalManager()
+        self.user.req = self # XXX remove when "vreg = user.req.vreg" hack in entity.py is gone
         self.repo = parent_session.repo
         self.vreg = parent_session.vreg
         self.data = parent_session.data
@@ -653,8 +730,9 @@
     """special session created internaly by the repository"""
 
     def __init__(self, repo, cnxprops=None):
-        super(InternalSession, self).__init__(_IMANAGER, repo, cnxprops,
+        super(InternalSession, self).__init__(InternalManager(), repo, cnxprops,
                                               _id='internal')
+        self.user.req = self # XXX remove when "vreg = user.req.vreg" hack in entity.py is gone
         self.cnxtype = 'inmemory'
         self.is_internal_session = True
         self.is_super_session = True
@@ -691,7 +769,6 @@
             return 'en'
         return None
 
-_IMANAGER = InternalManager()
 
 from logging import getLogger
 from cubicweb import set_log_methods
--- a/server/sources/__init__.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/sources/__init__.py	Tue Sep 22 13:08:42 2009 +0200
@@ -18,14 +18,14 @@
 
 def dbg_st_search(uri, union, varmap, args, cachekey=None, prefix='rql for'):
     if server.DEBUG & server.DBG_RQL:
-        print '%s %s source: %s' % (prefix, uri, union.as_string())
+        print '  %s %s source: %s' % (prefix, uri, union.as_string())
         if varmap:
-            print '  using varmap', varmap
+            print '    using varmap', varmap
         if server.DEBUG & server.DBG_MORE:
-            print '  args', args
-            print '  cache key', cachekey
-            print '  solutions', ','.join(str(s.solutions)
-                                          for s in union.children)
+            print '    args', args
+            print '    cache key', cachekey
+            print '    solutions', ','.join(str(s.solutions)
+                                            for s in union.children)
     # return true so it can be used as assertion (and so be killed by python -O)
     return True
 
@@ -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']
@@ -95,30 +100,11 @@
         """method called by the repository once ready to handle request"""
         pass
 
-    def backup_file(self, backupfile=None, timestamp=None):
-        """return a unique file name for a source's dump
-
-        either backupfile or timestamp (used to generated a backup file name if
-        needed) should be specified.
-        """
-        if backupfile is None:
-            config = self.repo.config
-            return join(config.appdatahome, 'backup',
-                        '%s-%s-%s.dump' % (config.appid, timestamp, self.uri))
-        # backup file is the system database backup file, add uri to it if not
-        # already there
-        base, ext = splitext(backupfile)
-        if not base.endswith('-%s' % self.uri):
-            return '%s-%s%s' % (base, self.uri, ext)
-        return backupfile
-
-    def backup(self, confirm, backupfile=None, timestamp=None,
-               askconfirm=False):
+    def backup(self, backupfile):
         """method called to create a backup of source's data"""
         pass
 
-    def restore(self, confirm, backupfile=None, timestamp=None, drop=True,
-               askconfirm=False):
+    def restore(self, backupfile):
         """method called to restore a backup of source's data"""
         pass
 
@@ -196,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/extlite.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/sources/extlite.py	Tue Sep 22 13:08:42 2009 +0200
@@ -11,8 +11,7 @@
 from os.path import join, exists
 
 from cubicweb import server
-from cubicweb.server.sqlutils import (SQL_PREFIX, SQLAdapterMixIn, sqlexec,
-                                      sql_source_backup, sql_source_restore)
+from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn, sqlexec
 from cubicweb.server.sources import native, rql2sql
 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
 
@@ -36,13 +35,13 @@
 
     def commit(self):
         if self._cnx is not None:
-            if server.DEBUG & server.DBG_SQL:
+            if server.DEBUG & (server.DBG_SQL | server.DBG_RQL):
                 print 'sql cnx COMMIT', self._cnx
             self._cnx.commit()
 
     def rollback(self):
         if self._cnx is not None:
-            if server.DEBUG & server.DBG_SQL:
+            if server.DEBUG & (server.DBG_SQL | server.DBG_RQL):
                 print 'sql cnx ROLLBACK', self._cnx
             self._cnx.rollback()
 
@@ -94,18 +93,21 @@
         AbstractSource.__init__(self, repo, appschema, source_config,
                                 *args, **kwargs)
 
-    def backup(self, confirm, backupfile=None, timestamp=None, askconfirm=False):
-        """method called to create a backup of source's data"""
-        backupfile = self.backup_file(backupfile, timestamp)
-        sql_source_backup(self, self.sqladapter, confirm, backupfile,
-                          askconfirm)
+    def backup(self, backupfile):
+        """method called to create a backup of the source's data"""
+        self.close_pool_connections()
+        try:
+            self.sqladapter.backup_to_file(backupfile)
+        finally:
+            self.open_pool_connections()
 
-    def restore(self, confirm, backupfile=None, timestamp=None, drop=True,
-               askconfirm=False):
+    def restore(self, backupfile, drop):
         """method called to restore a backup of source's data"""
-        backupfile = self.backup_file(backupfile, timestamp)
-        sql_source_restore(self, self.sqladapter, confirm, backupfile, drop,
-                           askconfirm)
+        self.close_pool_connections()
+        try:
+            self.sqladapter.restore_from_file(backupfile, drop)
+        finally:
+            self.open_pool_connections()
 
     @property
     def _sqlcnx(self):
--- a/server/sources/ldapuser.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/sources/ldapuser.py	Tue Sep 22 13:08:42 2009 +0200
@@ -41,35 +41,61 @@
 ONELEVEL = ldap.SCOPE_ONELEVEL
 SUBTREE = ldap.SCOPE_SUBTREE
 
-# XXX only for edition ??
-## password encryption possibilities
-#ENCRYPTIONS = ('SHA', 'CRYPT', 'MD5', 'CLEAR') # , 'SSHA'
-
-# mode identifier : (port, protocol)
-MODES = {
-    0: (389, 'ldap'),
-    1: (636, 'ldaps'),
-    2: (0,   'ldapi'),
-    }
+# map ldap protocol to their standard port
+PROTO_PORT = {'ldap': 389,
+              'ldaps': 636,
+              'ldapi': None,
+              }
 
 
 class LDAPUserSource(AbstractSource):
     """LDAP read-only CWUser source"""
     support_entities = {'CWUser': False}
 
-    port = None
-
-    cnx_mode = 0
-    cnx_dn = ''
-    cnx_pwd = ''
-
     options = (
         ('host',
          {'type' : 'string',
           'default': 'ldap',
-          'help': 'ldap host',
+          'help': 'ldap host. It may contains port information using \
+<host>:<port> notation.',
+          'group': 'ldap-source', 'inputlevel': 1,
+          }),
+        ('protocol',
+         {'type' : 'choice',
+          'default': 'ldap',
+          'choices': ('ldap', 'ldaps', 'ldapi'),
+          'help': 'ldap protocol',
+          'group': 'ldap-source', 'inputlevel': 1,
+          }),
+
+        ('auth-mode',
+         {'type' : 'choice',
+          'default': 'simple',
+          'choices': ('simple', 'cram_md5', 'digest_md5', 'gssapi'),
+          'help': 'authentication mode used to authenticate user to the ldap.',
           'group': 'ldap-source', 'inputlevel': 1,
           }),
+        ('auth-realm',
+         {'type' : 'string',
+          'default': None,
+          'help': 'realm to use when using gssapp/kerberos authentication.',
+          'group': 'ldap-source', 'inputlevel': 1,
+          }),
+
+        ('data-cnx-dn',
+         {'type' : 'string',
+          'default': '',
+          'help': 'user dn to use to open data connection to the ldap (eg used \
+to respond to rql queries).',
+          'group': 'ldap-source', 'inputlevel': 1,
+          }),
+        ('data-cnx-password',
+         {'type' : 'string',
+          'default': '',
+          'help': 'password to use to open data connection to the ldap (eg used to respond to rql queries).',
+          'group': 'ldap-source', 'inputlevel': 1,
+          }),
+
         ('user-base-dn',
          {'type' : 'string',
           'default': 'ou=People,dc=logilab,dc=fr',
@@ -129,6 +155,12 @@
         AbstractSource.__init__(self, repo, appschema, source_config,
                                 *args, **kwargs)
         self.host = source_config['host']
+        self.protocol = source_config.get('protocol', 'ldap')
+        self.authmode = source_config.get('auth-mode', 'simple')
+        self._authenticate = getattr(self, '_auth_%s' % self.authmode)
+        self.cnx_dn = source_config.get('data-cnx-dn') or ''
+        self.cnx_pwd = source_config.get('data-cnx-password') or ''
+        self.user_base_scope = globals()[source_config['user-scope']]
         self.user_base_dn = source_config['user-base-dn']
         self.user_base_scope = globals()[source_config['user-scope']]
         self.user_classes = splitstrip(source_config['user-classes'])
@@ -149,6 +181,7 @@
 
     def reset_caches(self):
         """method called during test to reset potential source caches"""
+        self._cache = {}
         self._query_cache = TimedCache(2*60)
 
     def init(self):
@@ -225,8 +258,9 @@
             raise AuthenticationError()
         # check password by establishing a (unused) connection
         try:
-            self._connect(user['dn'], password)
-        except:
+            self._connect(user, password)
+        except Exception, ex:
+            self.info('while trying to authenticate %s: %s', user, ex)
             # Something went wrong, most likely bad credentials
             raise AuthenticationError()
         return self.extid2eid(user['dn'], 'CWUser', session)
@@ -302,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 = []
@@ -368,15 +402,16 @@
         return result
 
 
-    def _connect(self, userdn=None, userpwd=None):
-        port, protocol = MODES[self.cnx_mode]
-        if protocol == 'ldapi':
+    def _connect(self, user=None, userpwd=None):
+        if self.protocol == 'ldapi':
             hostport = self.host
+        elif not ':' in self.host:
+            hostport = '%s:%s' % (self.host, PROTO_PORT[self.protocol])
         else:
-            hostport = '%s:%s' % (self.host, self.port or port)
-        self.info('connecting %s://%s as %s', protocol, hostport,
-                  userdn or 'anonymous')
-        url = LDAPUrl(urlscheme=protocol, hostport=hostport)
+            hostport = self.host
+        self.info('connecting %s://%s as %s', self.protocol, hostport,
+                  user and user['dn'] or 'anonymous')
+        url = LDAPUrl(urlscheme=self.protocol, hostport=hostport)
         conn = ReconnectLDAPObject(url.initializeUrl())
         # Set the protocol version - version 3 is preferred
         try:
@@ -391,14 +426,42 @@
         #conn.set_option(ldap.OPT_NETWORK_TIMEOUT, conn_timeout)
         #conn.timeout = op_timeout
         # Now bind with the credentials given. Let exceptions propagate out.
-        if userdn is None:
+        if user is None:
+            # no user specified, we want to initialize the 'data' connection,
             assert self._conn is None
             self._conn = conn
-            userdn = self.cnx_dn
-            userpwd = self.cnx_pwd
-        conn.simple_bind_s(userdn, userpwd)
+            # XXX always use simple bind for data connection
+            if not self.cnx_dn:
+                conn.simple_bind_s(self.cnx_dn, self.cnx_pwd)
+            else:
+                self._authenticate(conn, {'dn': self.cnx_dn}, self.cnx_pwd)
+        else:
+            # user specified, we want to check user/password, no need to return
+            # the connection which will be thrown out
+            self._authenticate(conn, user, userpwd)
         return conn
 
+    def _auth_simple(self, conn, user, userpwd):
+        conn.simple_bind_s(user['dn'], userpwd)
+
+    def _auth_cram_md5(self, conn, user, userpwd):
+        from ldap import sasl
+        auth_token = sasl.cram_md5(user['dn'], userpwd)
+        conn.sasl_interactive_bind_s('', auth_tokens)
+
+    def _auth_digest_md5(self, conn, user, userpwd):
+        from ldap import sasl
+        auth_token = sasl.digest_md5(user['dn'], userpwd)
+        conn.sasl_interactive_bind_s('', auth_tokens)
+
+    def _auth_gssapi(self, conn, user, userpwd):
+        # print XXX not proper sasl/gssapi
+        import kerberos
+        if not kerberos.checkPassword(user[self.user_login_attr], userpwd):
+            raise Exception('BAD login / mdp')
+        #from ldap import sasl
+        #conn.sasl_interactive_bind_s('', sasl.gssapi())
+
     def _search(self, session, base, scope,
                 searchstr='(objectClass=*)', attrs=()):
         """make an ldap query"""
--- a/server/sources/native.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/sources/native.py	Tue Sep 22 13:08:42 2009 +0200
@@ -25,8 +25,7 @@
 
 from cubicweb import UnknownEid, AuthenticationError, Binary, server
 from cubicweb.server.utils import crypt_password
-from cubicweb.server.sqlutils import (SQL_PREFIX, SQLAdapterMixIn,
-                                      sql_source_backup, sql_source_restore)
+from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
 from cubicweb.server.rqlannotation import set_qdata
 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
 from cubicweb.server.sources.rql2sql import SQLGenerator
@@ -95,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"
@@ -207,29 +204,26 @@
         pool.pool_reset()
         self.repo._free_pool(pool)
 
-    def backup(self, confirm, backupfile=None, timestamp=None,
-               askconfirm=False):
-        """method called to create a backup of source's data"""
-        backupfile = self.backup_file(backupfile, timestamp)
-        sql_source_backup(self, self, confirm, backupfile, askconfirm)
+    def backup(self, backupfile):
+        """method called to create a backup of the source's data"""
+        self.close_pool_connections()
+        try:
+            self.backup_to_file(backupfile)
+        finally:
+            self.open_pool_connections()
 
-    def restore(self, confirm, backupfile=None, timestamp=None, drop=True,
-               askconfirm=False):
+    def restore(self, backupfile, drop):
         """method called to restore a backup of source's data"""
-        backupfile = self.backup_file(backupfile, timestamp)
-        sql_source_restore(self, self, confirm, backupfile, drop, askconfirm)
+        if self.repo.config.open_connections_pools:
+            self.close_pool_connections()
+        try:
+            self.restore_from_file(backupfile, drop)
+        finally:
+            if self.repo.config.open_connections_pools:
+                self.open_pool_connections()
 
     def init(self):
         self.init_creating()
-        pool = self.repo._get_pool()
-        pool.pool_set()
-        # XXX cubicweb < 2.42 compat
-        if 'deleted_entities' in self.dbhelper.list_tables(pool['system']):
-            self.has_deleted_entitites_table = True
-        else:
-            self.has_deleted_entitites_table = False
-        pool.pool_reset()
-        self.repo._free_pool(pool)
 
     def map_attribute(self, etype, attr, cb):
         self._rql_sqlgen.attr_map['%s.%s' % (etype, attr)] = cb
@@ -237,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)
@@ -274,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`
@@ -544,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/sources/pyrorql.py	Tue Sep 22 13:08:42 2009 +0200
@@ -26,6 +26,12 @@
 from cubicweb.server.sources import (AbstractSource, ConnectionWrapper,
                                      TimedCache, dbg_st_search, dbg_results)
 
+
+def uidtype(union, col, etype, args):
+    select, col = union.locate_subquery(col, etype, args)
+    return getattr(select.selection[col], 'uidtype', None)
+
+
 class ReplaceByInOperator(Exception):
     def __init__(self, eids):
         self.eids = eids
@@ -127,6 +133,10 @@
         register_persistent_options(myoptions)
         self._query_cache = TimedCache(30)
 
+    def reset_caches(self):
+        """method called during test to reset potential source caches"""
+        self._query_cache = TimedCache(30)
+
     def last_update_time(self):
         pkey = u'sources.%s.latest-update-time' % self.uri
         rql = 'Any V WHERE X is CWProperty, X value V, X pkey %(k)s'
@@ -291,8 +301,8 @@
             needtranslation = []
             rows = rset.rows
             for i, etype in enumerate(descr[0]):
-                if (etype is None or not self.schema.eschema(etype).is_final() or
-                    getattr(union.locate_subquery(i, etype, args).selection[i], 'uidtype', None)):
+                if (etype is None or not self.schema.eschema(etype).is_final()
+                    or uidtype(union, i, etype, args)):
                     needtranslation.append(i)
             if needtranslation:
                 cnx = session.pool.connection(self.uri)
@@ -335,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"""
@@ -350,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"""
@@ -358,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/sources/rql2sql.py	Tue Sep 22 13:08:42 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,7 +1152,9 @@
             key = table
         if key in self._state.tables:
             return
-        self._state.tables[key] = (len(self._state.actual_tables) - 1, table)
+        if scope < 0:
+            scope = len(self._state.actual_tables) + scope
+        self._state.tables[key] = (scope, table)
         self._state.actual_tables[scope].append(table)
 
     def replace_tables_by_outer_join(self, substitute, lefttable, *tables):
--- a/server/sqlutils.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/sqlutils.py	Tue Sep 22 13:08:42 2009 +0200
@@ -120,39 +120,6 @@
                      skip_relations=skip_relations))
     return '\n'.join(output)
 
-
-def sql_source_backup(source, sqladapter, confirm, backupfile,
-                      askconfirm=False):
-    if exists(backupfile):
-        if not confirm('Backup file %s exists, overwrite it?' % backupfile):
-            return
-    elif askconfirm and not confirm('Backup %s database?'
-                                    % source.repo.config.appid):
-        print '-> no backup done.'
-        return
-    # should close opened connection before backuping
-    source.close_pool_connections()
-    try:
-        sqladapter.backup_to_file(backupfile, confirm)
-    finally:
-        source.open_pool_connections()
-
-def sql_source_restore(source, sqladapter, confirm, backupfile, drop=True,
-                       askconfirm=False):
-    if not exists(backupfile):
-        raise Exception("backup file %s doesn't exist" % backupfile)
-    app = source.repo.config.appid
-    if askconfirm and not confirm('Restore %s %s database from %s ?'
-                                  % (app, source.uri, backupfile)):
-        return
-    # should close opened connection before restoring
-    source.close_pool_connections()
-    try:
-        sqladapter.restore_from_file(backupfile, confirm, drop=drop)
-    finally:
-        source.open_pool_connections()
-
-
 try:
     from mx.DateTime import DateTimeType, DateTimeDeltaType
 except ImportError:
@@ -196,25 +163,12 @@
         #self.dbapi_module.type_code_test(cnx.cursor())
         return cnx
 
-    def backup_to_file(self, backupfile, confirm):
+    def backup_to_file(self, backupfile):
         cmd = self.dbhelper.backup_command(self.dbname, self.dbhost,
                                            self.dbuser, backupfile,
                                            keepownership=False)
-        backupdir = os.path.dirname(backupfile)
-        if not os.path.exists(backupdir):
-            if confirm('%s does not exist. Create it?' % backupdir,
-                       abort=False, shell=False):
-                os.mkdir(backupdir)
-            else:
-                print '-> failed to backup instance'
-                return
         if os.system(cmd):
-            print '-> error trying to backup with command', cmd
-            if not confirm('Continue anyway?', default_is_yes=False):
-                raise SystemExit(1)
-        else:
-            print '-> backup file',  backupfile
-            restrict_perms_to_user(backupfile, self.info)
+            raise Exception('Failed command: %s' % cmd)
 
     def restore_from_file(self, backupfile, confirm, drop=True):
         for cmd in self.dbhelper.restore_commands(self.dbname, self.dbhost,
@@ -222,19 +176,8 @@
                                                   self.encoding,
                                                   keepownership=False,
                                                   drop=drop):
-            while True:
-                print cmd
-                if os.system(cmd):
-                    print '-> error while restoring the base'
-                    answer = confirm('Continue anyway?',
-                                     shell=False, abort=False, retry=True)
-                    if not answer:
-                        raise SystemExit(1)
-                    if answer == 1: # 1: continue, 2: retry
-                        break
-                else:
-                    break
-        print '-> database restored.'
+            if os.system(cmd):
+                raise Exception('Failed command: %s' % cmd)
 
     def merge_args(self, args, query_args):
         if args is not None:
--- a/server/ssplanner.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/ssplanner.py	Tue Sep 22 13:08:42 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):
@@ -482,8 +484,9 @@
         repo = session.repo
         edefs = {}
         # insert relations
-        attributes = [relation.r_type for relation in self.attribute_relations]
-        for row in self.execute_child():
+        attributes = set([relation.r_type for relation in self.attribute_relations])
+        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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/data/migratedapp/schema.py	Tue Sep 22 13:08:42 2009 +0200
@@ -33,7 +33,15 @@
         'delete': ('managers', RRQLExpression('O owned_by U')),
         }
 
-class Note(EntityType):
+class Para(EntityType):
+    para = String(maxsize=512)
+    newattr = String()
+    newinlined = SubjectRelation('Affaire', cardinality='?*', inlined=True)
+    newnotinlined = SubjectRelation('Affaire', cardinality='?*')
+
+class Note(Para):
+    __specializes_schema__ = True
+
     permissions = {'read':   ('managers', 'users', 'guests',),
                    'update': ('managers', 'owners',),
                    'delete': ('managers', ),
@@ -42,15 +50,18 @@
                                              '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')
-    para = String(maxsize=512)
     shortpara = String(maxsize=64)
     ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
     attachment = SubjectRelation(('File', 'Image'))
 
+class Text(Para):
+    __specializes_schema__ = True
+    summary = String(maxsize=512)
+
 class ecrit_par(RelationType):
     permissions = {'read':   ('managers', 'users', 'guests',),
                    'delete': ('managers', ),
@@ -90,10 +101,10 @@
 
     travaille = SubjectRelation('Societe')
     concerne = SubjectRelation('Affaire')
-    concerne2 = SubjectRelation('Affaire')
+    concerne2 = SubjectRelation(('Affaire', 'Note'), cardinality='1*')
     connait = SubjectRelation('Personne', symetric=True)
 
-class Societe(EntityType):
+class Societe(WorkflowableEntityType):
     permissions = {
         'read': ('managers', 'users', 'guests'),
         'update': ('managers', 'owners'),
@@ -112,7 +123,6 @@
     cp   = String(maxsize=12)
     ville= String(maxsize=32)
 
-    in_state = SubjectRelation('State', cardinality='?*')
 
 class evaluee(RelationDefinition):
     subject = ('Personne', 'CWUser', 'Societe')
--- a/server/test/data/migration/postcreate.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/data/migration/postcreate.py	Tue Sep 22 13:08:42 2009 +0200
@@ -6,18 +6,20 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 
-todoeid = add_state(u'todo', 'Note', initial=True)
-doneeid = add_state(u'done', 'Note')
-add_transition(u'redoit', 'Note', (doneeid,), todoeid)
-add_transition(u'markasdone', 'Note', (todoeid,), doneeid)
+wf = add_workflow(u'note workflow', 'Note')
+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)
 checkpoint()
 
-pitetre = add_state(u'pitetre', 'Affaire', initial=True)
-encours = add_state(u'en cours', 'Affaire')
-finie = add_state(u'finie', 'Affaire')
-bennon = add_state(u'ben non', 'Affaire')
-add_transition(u'abort', 'Affaire', (pitetre,), bennon)
-add_transition(u'start', 'Affaire', (pitetre,), encours)
-add_transition(u'end', 'Affaire', (encours,), finie)
+wf = add_workflow(u'affaire workflow', 'Affaire')
+pitetre = wf.add_state(u'pitetre', initial=True)
+encours = wf.add_state(u'en cours')
+finie = wf.add_state(u'finie')
+bennon = wf.add_state(u'ben non')
+wf.add_transition(u'abort', pitetre, bennon)
+wf.add_transition(u'start', pitetre, encours)
+wf.add_transition(u'end', encours, finie)
 checkpoint()
 
--- a/server/test/data/schema.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/data/schema.py	Tue Sep 22 13:08:42 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_checkintegrity.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_checkintegrity.py	Tue Sep 22 13:08:42 2009 +0200
@@ -13,7 +13,7 @@
 
 from cubicweb.server.checkintegrity import check
 
-repo, cnx = init_test_database('sqlite')
+repo, cnx = init_test_database()
 
 class CheckIntegrityTC(TestCase):
     def test(self):
--- a/server/test/unittest_extlite.py	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +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 setUp(self):
-        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):
-        try:
-            os.remove(self.sqlite_file)
-        except:
-            pass
-
-    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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_hookhelper.py	Tue Sep 22 13:08:42 2009 +0200
@@ -8,18 +8,28 @@
 """
 
 from logilab.common.testlib import unittest_main
-from cubicweb.devtools.apptest import RepositoryBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 
 from cubicweb.server.pool import LateOperation, Operation, SingleLastOperation
 from cubicweb.server.hookhelper import *
+from cubicweb.server import hooks, schemahooks
 
 
-class HookHelpersTC(RepositoryBasedTC):
+def clean_session_ops(func):
+    def wrapper(self, *args, **kwargs):
+        try:
+            return func(self, *args, **kwargs)
+        finally:
+            self.session.pending_operations[:] = []
+    return wrapper
+
+class HookHelpersTC(CubicWebTC):
 
     def setUp(self):
-        RepositoryBasedTC.setUp(self)
+        CubicWebTC.setUp(self)
         self.hm = self.repo.hm
 
+    @clean_session_ops
     def test_late_operation(self):
         session = self.session
         l1 = LateOperation(session)
@@ -27,6 +37,7 @@
         l3 = Operation(session)
         self.assertEquals(session.pending_operations, [l3, l1, l2])
 
+    @clean_session_ops
     def test_single_last_operation(self):
         session = self.session
         l0 = SingleLastOperation(session)
@@ -37,8 +48,8 @@
         l4 = SingleLastOperation(session)
         self.assertEquals(session.pending_operations, [l3, l1, l2, l4])
 
+    @clean_session_ops
     def test_global_operation_order(self):
-        from cubicweb.server import hooks, schemahooks
         session = self.session
         op1 = hooks.DelayedDeleteOp(session)
         op2 = schemahooks.MemSchemaRDefDel(session)
@@ -49,41 +60,5 @@
         op5 = hooks.CheckORelationOp(session)
         self.assertEquals(session.pending_operations, [op1, op2, op4, op5, op3])
 
-
-    def test_in_state_notification(self):
-        result = []
-        # test both email notification and transition_information
-        # whatever if we can connect to the default stmp server, transaction
-        # should not fail
-        def in_state_changed(session, eidfrom, rtype, eidto):
-            tr = previous_state(session, eidfrom)
-            if tr is None:
-                result.append(tr)
-                return
-            content = u'trÀnsition from %s to %s' % (tr.name, entity_name(session, eidto))
-            result.append(content)
-            SendMailOp(session, msg=content, recipients=['test@logilab.fr'])
-        self.hm.register_hook(in_state_changed,
-                             'before_add_relation', 'in_state')
-        self.execute('INSERT CWUser X: X login "paf", X upassword "wouf", X in_state S, X in_group G WHERE S name "activated", G name "users"')
-        self.assertEquals(result, [None])
-        searchedops = [op for op in self.session.pending_operations
-                       if isinstance(op, SendMailOp)]
-        self.assertEquals(len(searchedops), 0,
-                          self.session.pending_operations)
-        self.commit()
-        self.execute('SET X in_state S WHERE X login "paf", S name "deactivated"')
-        self.assertEquals(result, [None, u'trÀnsition from activated to deactivated'])
-        # one to send the mail, one to close the smtp connection
-        searchedops = [op for op in self.session.pending_operations
-                       if isinstance(op, SendMailOp)]
-        self.assertEquals(len(searchedops), 1,
-                          self.session.pending_operations)
-        self.commit()
-        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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_hooks.py	Tue Sep 22 13:08:42 2009 +0200
@@ -11,7 +11,7 @@
 
 from cubicweb import (ConnectionError, RepositoryError, ValidationError,
                       AuthenticationError, BadConnectionId)
-from cubicweb.devtools.apptest import RepositoryBasedTC, get_versions
+from cubicweb.devtools.testlib import CubicWebTC, get_versions
 
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.repository import Repository
@@ -26,7 +26,7 @@
 
 
 
-class CoreHooksTC(RepositoryBasedTC):
+class CoreHooksTC(CubicWebTC):
 
     def test_delete_internal_entities(self):
         self.assertRaises(RepositoryError, self.execute,
@@ -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,8 +143,43 @@
         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)
 
-class UserGroupHooksTC(RepositoryBasedTC):
+    def test_metadata_creation_modification_date(self):
+        _now = datetime.now()
+        eid = self.execute('INSERT Note X')[0][0]
+        creation_date, modification_date = self.execute('Any CD, MD WHERE X eid %s, '
+                                                        'X creation_date CD, '
+                                                        'X modification_date MD' % eid)[0]
+        self.assertEquals((creation_date - _now).seconds, 0)
+        self.assertEquals((modification_date - _now).seconds, 0)
+
+    def test_metadata__date(self):
+        _now = datetime.now()
+        eid = self.execute('INSERT Note X')[0][0]
+        creation_date = self.execute('Any D WHERE X eid %s, X creation_date D' % eid)[0][0]
+        self.assertEquals((creation_date - _now).seconds, 0)
+
+    def test_metadata_created_by(self):
+        eid = self.execute('INSERT Note X')[0][0]
+        self.commit() # fire operations
+        rset = self.execute('Any U WHERE X eid %s, X created_by U' % eid)
+        self.assertEquals(len(rset), 1) # make sure we have only one creator
+        self.assertEquals(rset[0][0], self.session.user.eid)
+
+    def test_metadata_owned_by(self):
+        eid = self.execute('INSERT Note X')[0][0]
+        self.commit() # fire operations
+        rset = self.execute('Any U WHERE X eid %s, X owned_by U' % eid)
+        self.assertEquals(len(rset), 1) # make sure we have only one owner
+        self.assertEquals(rset[0][0], self.session.user.eid)
+
+
+
+class UserGroupHooksTC(CubicWebTC):
 
     def test_user_synchronization(self):
         self.create_user('toto', password='hop', commit=False)
@@ -164,7 +187,7 @@
                           self.repo.connect, u'toto', 'hop')
         self.commit()
         cnxid = self.repo.connect(u'toto', 'hop')
-        self.failIfEqual(cnxid, self.cnxid)
+        self.failIfEqual(cnxid, self.session.id)
         self.execute('DELETE CWUser X WHERE X login "toto"')
         self.repo.execute(cnxid, 'State X')
         self.commit()
@@ -184,7 +207,7 @@
         self.assertEquals(user.groups, set(('managers',)))
 
     def test_user_composite_owner(self):
-        ueid = self.create_user('toto')
+        ueid = self.create_user('toto').eid
         # composite of euser should be owned by the euser regardless of who created it
         self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", U use_email X '
                      'WHERE U login "toto"')
@@ -199,7 +222,8 @@
         self.commit()
         self.failIf(self.execute('Any X WHERE X created_by Y, X eid >= %(x)s', {'x': eid}))
 
-class CWPropertyHooksTC(RepositoryBasedTC):
+
+class CWPropertyHooksTC(CubicWebTC):
 
     def test_unexistant_eproperty(self):
         ex = self.assertRaises(ValidationError,
@@ -223,7 +247,7 @@
         self.assertEquals(ex.errors, {'value': u'unauthorized value'})
 
 
-class SchemaHooksTC(RepositoryBasedTC):
+class SchemaHooksTC(CubicWebTC):
 
     def test_duplicate_etype_error(self):
         # check we can't add a CWEType or CWRType entity if it already exists one
@@ -245,24 +269,23 @@
             self.assertEquals(ex.errors, {'login': 'the value "admin" is already used, use another one'})
 
 
-class SchemaModificationHooksTC(RepositoryBasedTC):
+class SchemaModificationHooksTC(CubicWebTC):
 
-    def setUp(self):
-        if not hasattr(self, '_repo'):
-            # first initialization
-            repo = self.repo # set by the RepositoryBasedTC metaclass
-            # force to read schema from the database to get proper eid set on schema instances
-            repo.config._cubes = None
-            repo.fill_schema()
-        RepositoryBasedTC.setUp(self)
+    @classmethod
+    def init_config(cls, config):
+        super(SchemaModificationHooksTC, cls).init_config(config)
+        config._cubes = None
+        cls.repo.fill_schema()
 
     def index_exists(self, etype, attr, unique=False):
+        self.session.set_pool()
         dbhelper = self.session.pool.source('system').dbhelper
         sqlcursor = self.session.pool['system']
         return dbhelper.index_exists(sqlcursor, SQL_PREFIX + etype, SQL_PREFIX + attr, unique=unique)
 
     def test_base(self):
         schema = self.repo.schema
+        self.session.set_pool()
         dbhelper = self.session.pool.source('system').dbhelper
         sqlcursor = self.session.pool['system']
         self.failIf(schema.has_entity('Societe2'))
@@ -380,6 +403,7 @@
     # schema modification hooks tests #########################################
 
     def test_uninline_relation(self):
+        self.session.set_pool()
         dbhelper = self.session.pool.source('system').dbhelper
         sqlcursor = self.session.pool['system']
         # Personne inline2 Affaire inline
@@ -414,6 +438,7 @@
             self.assertEquals(rset.rows[0], [peid, aeid])
 
     def test_indexed_change(self):
+        self.session.set_pool()
         dbhelper = self.session.pool.source('system').dbhelper
         sqlcursor = self.session.pool['system']
         try:
@@ -432,6 +457,7 @@
             self.failIf(self.index_exists('Affaire', 'sujet'))
 
     def test_unique_change(self):
+        self.session.set_pool()
         dbhelper = self.session.pool.source('system').dbhelper
         sqlcursor = self.session.pool['system']
         try:
@@ -479,177 +505,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_hooksmanager.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_hooksmanager.py	Tue Sep 22 13:08:42 2009 +0200
@@ -6,7 +6,7 @@
 
 from cubicweb.server.hooksmanager import HooksManager, Hook
 from cubicweb.devtools import TestServerConfiguration
-from cubicweb.devtools.apptest import RepositoryBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 
 class HookCalled(Exception): pass
 
@@ -144,7 +144,7 @@
         self.called.append((subject, r_type, object))
 
 
-class SystemHooksTC(RepositoryBasedTC):
+class SystemHooksTC(CubicWebTC):
 
     def test_startup_shutdown(self):
         import hooks # cubicweb/server/test/data/hooks.py
@@ -168,9 +168,9 @@
     events = ('whatever', 'another')
     accepts = ('Societe', 'Division')
 
-class HookTC(RepositoryBasedTC):
+class HookTC(CubicWebTC):
     def test_inheritance(self):
-        self.assertEquals(list(MyHook.register_to()),
+        self.assertEquals(list(MyHook.register_to(self.schema)),
                           zip(repeat('whatever'), ('Societe', 'Division', 'SubDivision'))
                           + zip(repeat('another'), ('Societe', 'Division', 'SubDivision')))
 
--- a/server/test/unittest_ldapuser.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_ldapuser.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,8 +7,8 @@
 """
 
 from logilab.common.testlib import TestCase, unittest_main, mock_object
-from cubicweb.devtools import init_test_database, TestServerConfiguration
-from cubicweb.devtools.apptest import RepositoryBasedTC
+from cubicweb.devtools import TestServerConfiguration
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.repotest import RQLGeneratorTC
 
 from cubicweb.server.sources.ldapuser import *
@@ -34,36 +34,31 @@
 
 
 
-config = TestServerConfiguration('data')
-config.sources_file = lambda : 'data/sourcesldap'
-repo, cnx = init_test_database('sqlite', config=config)
-
-class LDAPUserSourceTC(RepositoryBasedTC):
-    repo, cnx = repo, cnx
+class LDAPUserSourceTC(CubicWebTC):
+    config = TestServerConfiguration('data')
+    config.sources_file = lambda : 'data/sourcesldap'
 
     def patch_authenticate(self):
         self._orig_authenticate = LDAPUserSource.authenticate
         LDAPUserSource.authenticate = nopwd_authenticate
 
-    def setUp(self):
-        self._prepare()
+    def setup_database(self):
         # XXX: need this first query else we get 'database is locked' from
         # sqlite since it doesn't support multiple connections on the same
         # database
         # so doing, ldap inserted users don't get removed between each test
-        rset = self.execute('CWUser X')
-        self.commit()
+        rset = self.sexecute('CWUser X')
         # check we get some users from ldap
         self.assert_(len(rset) > 1)
-        self.maxeid = self.execute('Any MAX(X)')[0][0]
 
     def tearDown(self):
         if hasattr(self, '_orig_authenticate'):
             LDAPUserSource.authenticate = self._orig_authenticate
-        RepositoryBasedTC.tearDown(self)
+        CubicWebTC.tearDown(self)
 
     def test_authenticate(self):
         source = self.repo.sources_by_uri['ldapuser']
+        self.session.set_pool()
         self.assertRaises(AuthenticationError,
                           source.authenticate, self.session, 'toto', 'toto')
 
@@ -73,7 +68,7 @@
 
     def test_base(self):
         # check a known one
-        e = self.execute('CWUser X WHERE X login "syt"').get_entity(0, 0)
+        e = self.sexecute('CWUser X WHERE X login "syt"').get_entity(0, 0)
         self.assertEquals(e.login, 'syt')
         e.complete()
         self.assertEquals(e.creation_date, None)
@@ -85,86 +80,87 @@
         self.assertEquals(e.created_by, [])
         self.assertEquals(e.primary_email[0].address, 'Sylvain Thenault')
         # email content should be indexed on the user
-        rset = self.execute('CWUser X WHERE X has_text "thenault"')
+        rset = self.sexecute('CWUser X WHERE X has_text "thenault"')
         self.assertEquals(rset.rows, [[e.eid]])
 
     def test_not(self):
-        eid = self.execute('CWUser X WHERE X login "syt"')[0][0]
-        rset = self.execute('CWUser X WHERE NOT X eid %s' % eid)
+        eid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
+        rset = self.sexecute('CWUser X WHERE NOT X eid %s' % eid)
         self.assert_(rset)
         self.assert_(not eid in (r[0] for r in rset))
 
     def test_multiple(self):
-        seid = self.execute('CWUser X WHERE X login "syt"')[0][0]
-        aeid = self.execute('CWUser X WHERE X login "adim"')[0][0]
-        rset = self.execute('CWUser X, Y WHERE X login "syt", Y login "adim"')
+        seid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
+        aeid = self.sexecute('CWUser X WHERE X login "adim"')[0][0]
+        rset = self.sexecute('CWUser X, Y WHERE X login "syt", Y login "adim"')
         self.assertEquals(rset.rows, [[seid, aeid]])
-        rset = self.execute('Any X,Y,L WHERE X login L, X login "syt", Y login "adim"')
+        rset = self.sexecute('Any X,Y,L WHERE X login L, X login "syt", Y login "adim"')
         self.assertEquals(rset.rows, [[seid, aeid, 'syt']])
 
     def test_in(self):
-        seid = self.execute('CWUser X WHERE X login "syt"')[0][0]
-        aeid = self.execute('CWUser X WHERE X login "adim"')[0][0]
-        rset = self.execute('Any X,L ORDERBY L WHERE X login IN("syt", "adim"), X login L')
+        seid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
+        aeid = self.sexecute('CWUser X WHERE X login "adim"')[0][0]
+        rset = self.sexecute('Any X,L ORDERBY L WHERE X login IN("syt", "adim"), X login L')
         self.assertEquals(rset.rows, [[aeid, 'adim'], [seid, 'syt']])
 
     def test_relations(self):
-        eid = self.execute('CWUser X WHERE X login "syt"')[0][0]
-        rset = self.execute('Any X,E WHERE X is CWUser, X login L, X primary_email E')
+        eid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
+        rset = self.sexecute('Any X,E WHERE X is CWUser, X login L, X primary_email E')
         self.assert_(eid in (r[0] for r in rset))
-        rset = self.execute('Any X,L,E WHERE X is CWUser, X login L, X primary_email E')
+        rset = self.sexecute('Any X,L,E WHERE X is CWUser, X login L, X primary_email E')
         self.assert_('syt' in (r[1] for r in rset))
 
     def test_count(self):
-        nbusers = self.execute('Any COUNT(X) WHERE X is CWUser')[0][0]
+        nbusers = self.sexecute('Any COUNT(X) WHERE X is CWUser')[0][0]
         # just check this is a possible number
         self.assert_(nbusers > 1, nbusers)
         self.assert_(nbusers < 30, nbusers)
 
     def test_upper(self):
-        eid = self.execute('CWUser X WHERE X login "syt"')[0][0]
-        rset = self.execute('Any UPPER(L) WHERE X eid %s, X login L' % eid)
+        eid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
+        rset = self.sexecute('Any UPPER(L) WHERE X eid %s, X login L' % eid)
         self.assertEquals(rset[0][0], 'SYT')
 
     def test_unknown_attr(self):
-        eid = self.execute('CWUser X WHERE X login "syt"')[0][0]
-        rset = self.execute('Any L,C,M WHERE X eid %s, X login L, '
+        eid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
+        rset = self.sexecute('Any L,C,M WHERE X eid %s, X login L, '
                             'X creation_date C, X modification_date M' % eid)
         self.assertEquals(rset[0][0], 'syt')
         self.assertEquals(rset[0][1], None)
         self.assertEquals(rset[0][2], None)
 
     def test_sort(self):
-        logins = [l for l, in self.execute('Any L ORDERBY L WHERE X login L')]
+        logins = [l for l, in self.sexecute('Any L ORDERBY L WHERE X login L')]
         self.assertEquals(logins, sorted(logins))
 
     def test_lower_sort(self):
-        logins = [l for l, in self.execute('Any L ORDERBY lower(L) WHERE X login L')]
+        logins = [l for l, in self.sexecute('Any L ORDERBY lower(L) WHERE X login L')]
         self.assertEquals(logins, sorted(logins))
 
     def test_or(self):
-        rset = self.execute('DISTINCT Any X WHERE X login "syt" OR (X in_group G, G name "managers")')
+        rset = self.sexecute('DISTINCT Any X WHERE X login "syt" OR (X in_group G, G name "managers")')
         self.assertEquals(len(rset), 2, rset.rows) # syt + admin
 
     def test_nonregr_set_owned_by(self):
         # test that when a user coming from ldap is triggering a transition
         # the related TrInfo has correct owner information
-        self.execute('SET X in_group G WHERE X login "syt", G name "managers"')
+        self.sexecute('SET X in_group G WHERE X login "syt", G name "managers"')
         self.commit()
-        syt = self.execute('CWUser X WHERE X login "syt"').get_entity(0, 0)
+        syt = self.sexecute('CWUser X WHERE X login "syt"').get_entity(0, 0)
         self.assertEquals([g.name for g in syt.in_group], ['managers', 'users'])
         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)
+            alf = self.sexecute('CWUser X WHERE X login "alf"').get_entity(0, 0)
             self.assertEquals(alf.in_state[0].name, 'deactivated')
             trinfo = alf.latest_trinfo()
             self.assertEquals(trinfo.owned_by[0].login, 'syt')
             # select from_state to skip the user's creation TrInfo
-            rset = self.execute('Any U ORDERBY D DESC WHERE WF wf_info_for X,'
+            rset = self.sexecute('Any U ORDERBY D DESC WHERE WF wf_info_for X,'
                                 'WF creation_date D, WF from_state FS,'
                                 'WF owned_by U?, X eid %(x)s',
                                 {'x': alf.eid}, 'x')
@@ -172,76 +168,77 @@
         finally:
             # restore db state
             self.restore_connection()
-            self.execute('SET X in_state S WHERE X login "alf", S name "activated"')
-            self.execute('DELETE X in_group G WHERE X login "syt", G name "managers"')
+            alf = self.sexecute('Any X WHERE X login "alf"').get_entity(0, 0)
+            alf.fire_transition('activate')
+            self.sexecute('DELETE X in_group G WHERE X login "syt", G name "managers"')
 
     def test_same_column_names(self):
-        self.execute('Any X, Y WHERE X copain Y, X login "comme", Y login "cochon"')
+        self.sexecute('Any X, Y WHERE X copain Y, X login "comme", Y login "cochon"')
 
     def test_multiple_entities_from_different_sources(self):
-        self.create_user('cochon')
-        self.failUnless(self.execute('Any X,Y WHERE X login "syt", Y login "cochon"'))
+        self.create_user('cochon', req=self.session)
+        self.failUnless(self.sexecute('Any X,Y WHERE X login "syt", Y login "cochon"'))
 
     def test_exists1(self):
-        self.add_entity('CWGroup', name=u'bougloup1')
-        self.add_entity('CWGroup', name=u'bougloup2')
-        self.execute('SET U in_group G WHERE G name ~= "bougloup%", U login "admin"')
-        self.execute('SET U in_group G WHERE G name = "bougloup1", U login "syt"')
-        rset = self.execute('Any L,SN ORDERBY L WHERE X in_state S, S name SN, X login L, EXISTS(X in_group G, G name ~= "bougloup%")')
+        self.add_entity('CWGroup', name=u'bougloup1', req=self.session)
+        self.add_entity('CWGroup', name=u'bougloup2', req=self.session)
+        self.sexecute('SET U in_group G WHERE G name ~= "bougloup%", U login "admin"')
+        self.sexecute('SET U in_group G WHERE G name = "bougloup1", U login "syt"')
+        rset = self.sexecute('Any L,SN ORDERBY L WHERE X in_state S, S name SN, X login L, EXISTS(X in_group G, G name ~= "bougloup%")')
         self.assertEquals(rset.rows, [['admin', 'activated'], ['syt', 'activated']])
 
     def test_exists2(self):
-        self.create_user('comme')
-        self.create_user('cochon')
-        self.execute('SET X copain Y WHERE X login "comme", Y login "cochon"')
-        rset = self.execute('Any GN ORDERBY GN WHERE X in_group G, G name GN, (G name "managers" OR EXISTS(X copain T, T login in ("comme", "cochon")))')
+        self.create_user('comme', req=self.session)
+        self.create_user('cochon', req=self.session)
+        self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
+        rset = self.sexecute('Any GN ORDERBY GN WHERE X in_group G, G name GN, (G name "managers" OR EXISTS(X copain T, T login in ("comme", "cochon")))')
         self.assertEquals(rset.rows, [['managers'], ['users']])
 
     def test_exists3(self):
-        self.create_user('comme')
-        self.create_user('cochon')
-        self.execute('SET X copain Y WHERE X login "comme", Y login "cochon"')
-        self.failUnless(self.execute('Any X, Y WHERE X copain Y, X login "comme", Y login "cochon"'))
-        self.execute('SET X copain Y WHERE X login "syt", Y login "cochon"')
-        self.failUnless(self.execute('Any X, Y WHERE X copain Y, X login "syt", Y login "cochon"'))
-        rset = self.execute('Any GN,L WHERE X in_group G, X login L, G name GN, G name "managers" OR EXISTS(X copain T, T login in ("comme", "cochon"))')
+        self.create_user('comme', req=self.session)
+        self.create_user('cochon', req=self.session)
+        self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
+        self.failUnless(self.sexecute('Any X, Y WHERE X copain Y, X login "comme", Y login "cochon"'))
+        self.sexecute('SET X copain Y WHERE X login "syt", Y login "cochon"')
+        self.failUnless(self.sexecute('Any X, Y WHERE X copain Y, X login "syt", Y login "cochon"'))
+        rset = self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, G name "managers" OR EXISTS(X copain T, T login in ("comme", "cochon"))')
         self.assertEquals(sorted(rset.rows), [['managers', 'admin'], ['users', 'comme'], ['users', 'syt']])
 
     def test_exists4(self):
-        self.create_user('comme')
-        self.create_user('cochon', groups=('users', 'guests'))
-        self.create_user('billy')
-        self.execute('SET X copain Y WHERE X login "comme", Y login "cochon"')
-        self.execute('SET X copain Y WHERE X login "cochon", Y login "cochon"')
-        self.execute('SET X copain Y WHERE X login "comme", Y login "billy"')
-        self.execute('SET X copain Y WHERE X login "syt", Y login "billy"')
+        self.create_user('comme', req=self.session)
+        self.create_user('cochon', groups=('users', 'guests'), req=self.session)
+        self.create_user('billy', req=self.session)
+        self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
+        self.sexecute('SET X copain Y WHERE X login "cochon", Y login "cochon"')
+        self.sexecute('SET X copain Y WHERE X login "comme", Y login "billy"')
+        self.sexecute('SET X copain Y WHERE X login "syt", Y login "billy"')
         # search for group name, login where
         #   CWUser copain with "comme" or "cochon" AND same login as the copain
         # OR
         #   CWUser in_state activated AND not copain with billy
         #
         # SO we expect everybody but "comme" and "syt"
-        rset= self.execute('Any GN,L WHERE X in_group G, X login L, G name GN, '
+        rset= self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, '
                            'EXISTS(X copain T, T login L, T login in ("comme", "cochon")) OR '
                            'EXISTS(X in_state S, S name "activated", NOT X copain T2, T2 login "billy")')
-        all = self.execute('Any GN, L WHERE X in_group G, X login L, G name GN')
+        all = self.sexecute('Any GN, L WHERE X in_group G, X login L, G name GN')
         all.rows.remove(['users', 'comme'])
         all.rows.remove(['users', 'syt'])
         self.assertEquals(sorted(rset.rows), sorted(all.rows))
 
     def test_exists5(self):
-        self.create_user('comme')
-        self.create_user('cochon', groups=('users', 'guests'))
-        self.create_user('billy')
-        self.execute('SET X copain Y WHERE X login "comme", Y login "cochon"')
-        self.execute('SET X copain Y WHERE X login "cochon", Y login "cochon"')
-        self.execute('SET X copain Y WHERE X login "comme", Y login "billy"')
-        self.execute('SET X copain Y WHERE X login "syt", Y login "cochon"')
-        rset= self.execute('Any L WHERE X login L, '
+        self.create_user('comme', req=self.session)
+        self.create_user('cochon', groups=('users', 'guests'), req=self.session)
+        self.create_user('billy', req=self.session)
+        self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
+        self.sexecute('SET X copain Y WHERE X login "cochon", Y login "cochon"')
+        self.sexecute('SET X copain Y WHERE X login "comme", Y login "billy"')
+        self.sexecute('SET X copain Y WHERE X login "syt", Y login "cochon"')
+        rset= self.sexecute('Any L WHERE X login L, '
                            'EXISTS(X copain T, T login in ("comme", "cochon")) AND '
                            'NOT EXISTS(X copain T2, T2 login "billy")')
         self.assertEquals(sorted(rset.rows), [['cochon'], ['syt']])
-        rset= self.execute('Any GN,L WHERE X in_group G, X login L, G name GN, '
+        rset= self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, '
                            'EXISTS(X copain T, T login in ("comme", "cochon")) AND '
                            'NOT EXISTS(X copain T2, T2 login "billy")')
         self.assertEquals(sorted(rset.rows), [['guests', 'cochon'],
@@ -249,18 +246,18 @@
                                               ['users', 'syt']])
 
     def test_cd_restriction(self):
-        rset = self.execute('CWUser X WHERE X creation_date > "2009-02-01"')
+        rset = self.sexecute('CWUser X WHERE X creation_date > "2009-02-01"')
         self.assertEquals(len(rset), 2) # admin/anon but no ldap user since it doesn't support creation_date
 
     def test_union(self):
-        afeids = self.execute('State X')
-        ueids = self.execute('CWUser X')
-        rset = self.execute('(Any X WHERE X is State) UNION (Any X WHERE X is CWUser)')
+        afeids = self.sexecute('State X')
+        ueids = self.sexecute('CWUser X')
+        rset = self.sexecute('(Any X WHERE X is State) UNION (Any X WHERE X is CWUser)')
         self.assertEquals(sorted(r[0] for r in rset.rows),
                           sorted(r[0] for r in afeids + ueids))
 
     def _init_security_test(self):
-        self.create_user('iaminguestsgrouponly', groups=('guests',))
+        self.create_user('iaminguestsgrouponly', groups=('guests',), req=self.session)
         cnx = self.login('iaminguestsgrouponly')
         return cnx.cursor()
 
@@ -286,33 +283,33 @@
         self.assertEquals(rset.rows, [[None]])
 
     def test_nonregr1(self):
-        self.execute('Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, E owned_by X, '
+        self.sexecute('Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, E owned_by X, '
                      'X modification_date AA',
-                     {'x': cnx.user(self.session).eid})
+                     {'x': self.session.user.eid})
 
     def test_nonregr2(self):
-        self.execute('Any X,L,AA WHERE E eid %(x)s, E owned_by X, '
+        self.sexecute('Any X,L,AA WHERE E eid %(x)s, E owned_by X, '
                      'X login L, X modification_date AA',
-                     {'x': cnx.user(self.session).eid})
+                     {'x': self.session.user.eid})
 
     def test_nonregr3(self):
-        self.execute('Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, '
+        self.sexecute('Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, '
                      'X modification_date AA',
-                     {'x': cnx.user(self.session).eid})
+                     {'x': self.session.user.eid})
 
     def test_nonregr4(self):
-        emaileid = self.execute('INSERT EmailAddress X: X address "toto@logilab.org"')[0][0]
-        self.execute('Any X,AA WHERE X use_email Y, Y eid %(x)s, X modification_date AA',
+        emaileid = self.sexecute('INSERT EmailAddress X: X address "toto@logilab.org"')[0][0]
+        self.sexecute('Any X,AA WHERE X use_email Y, Y eid %(x)s, X modification_date AA',
                      {'x': emaileid})
 
     def test_nonregr5(self):
         # original jpl query:
         # Any X, NOW - CD, P WHERE P is Project, U interested_in P, U is CWUser, U login "sthenault", X concerns P, X creation_date CD ORDERBY CD DESC LIMIT 5
         rql = 'Any X, NOW - CD, P ORDERBY CD DESC LIMIT 5 WHERE P bookmarked_by U, U login "%s", P is X, X creation_date CD' % self.session.user.login
-        self.execute(rql, )#{'x': })
+        self.sexecute(rql, )#{'x': })
 
     def test_nonregr6(self):
-        self.execute('Any B,U,UL GROUPBY B,U,UL WHERE B created_by U?, B is File '
+        self.sexecute('Any B,U,UL GROUPBY B,U,UL WHERE B created_by U?, B is File '
                      'WITH U,UL BEING (Any U,UL WHERE ME eid %(x)s, (EXISTS(U identity ME) '
                      'OR (EXISTS(U in_group G, G name IN("managers", "staff")))) '
                      'OR (EXISTS(U in_group H, ME in_group H, NOT H name "users")), U login UL, U is CWUser)',
@@ -353,6 +350,9 @@
         res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]])
         self.assertEquals(res, [[1, 5], [2, 4], [3, 6]])
 
+# XXX
+LDAPUserSourceTC._init_repo()
+repo = LDAPUserSourceTC.repo
 
 class RQL2LDAPFilterTC(RQLGeneratorTC):
     schema = repo.schema
--- a/server/test/unittest_migractions.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_migractions.py	Tue Sep 22 13:08:42 2009 +0200
@@ -2,13 +2,14 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 
+from copy import deepcopy
 from datetime import date
 from os.path import join
 
 from logilab.common.testlib import TestCase, unittest_main
 
 from cubicweb import ConfigurationError
-from cubicweb.devtools.apptest import RepositoryBasedTC, get_versions
+from cubicweb.devtools.testlib import CubicWebTC, get_versions
 from cubicweb.schema import CubicWebSchemaLoader
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.repository import Repository
@@ -23,42 +24,55 @@
     Repository.get_versions = orig_get_versions
 
 
-class MigrationCommandsTC(RepositoryBasedTC):
+class MigrationCommandsTC(CubicWebTC):
+
+    @classmethod
+    def init_config(cls, config):
+        super(MigrationCommandsTC, cls).init_config(config)
+        config._cubes = None
+        cls.repo.fill_schema()
+        cls.origschema = deepcopy(cls.repo.schema)
+        # hack to read the schema from data/migrschema
+        config.appid = join('data', 'migratedapp')
+        global migrschema
+        migrschema = config.load_schema()
+        config.appid = 'data'
+        assert 'Folder' in migrschema
+
+    @classmethod
+    def _refresh_repo(cls):
+        super(MigrationCommandsTC, cls)._refresh_repo()
+        cls.repo.schema = cls.vreg.schema = deepcopy(cls.origschema)
 
     def setUp(self):
-        if not hasattr(self, '_repo'):
-            # first initialization
-            repo = self.repo # set by the RepositoryBasedTC metaclass
-            # force to read schema from the database
-            repo.config._cubes = None
-            repo.fill_schema()
-            # hack to read the schema from data/migrschema
-            self.repo.config.appid = join('data', 'migratedapp')
-            global migrschema
-            migrschema = self.repo.config.load_schema()
-            self.repo.config.appid = 'data'
-            assert 'Folder' in migrschema
-            self.repo.hm.deactivate_verification_hooks()
-        RepositoryBasedTC.setUp(self)
+        CubicWebTC.setUp(self)
         self.mh = ServerMigrationHelper(self.repo.config, migrschema,
                                         repo=self.repo, cnx=self.cnx,
                                         interactive=False)
         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)
@@ -107,23 +121,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)
@@ -133,6 +138,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',
@@ -151,22 +157,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()))
@@ -178,8 +187,19 @@
         self.mh.cmd_add_relation_definition('Personne', 'concerne2', 'Affaire')
         self.assertEquals(self.schema['concerne2'].subjects(),
                           ('Personne',))
-        self.assertEquals(self.schema['concerne2'].objects(), ('Affaire',))
+        self.assertEquals(self.schema['concerne2'].objects(),
+                          ('Affaire', ))
+        self.assertEquals(self.schema['concerne2'].rproperty('Personne', 'Affaire', 'cardinality'),
+                          '1*')
+        self.mh.cmd_add_relation_definition('Personne', 'concerne2', 'Note')
+        self.assertEquals(sorted(self.schema['concerne2'].objects()), ['Affaire', 'Note'])
+        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')
+        self.failUnless('concerne2' in self.schema)
+        self.mh.cmd_drop_relation_definition('Personne', 'concerne2', 'Note')
         self.failIf('concerne2' in self.schema)
 
     def test_drop_relation_definition_existant_rtype(self):
@@ -209,11 +229,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]
@@ -248,7 +274,7 @@
                                               fulltextindexed=False)
 
     def test_sync_schema_props_perms(self):
-        cursor = self.mh.rqlcursor
+        cursor = self.mh.session
         nbrqlexpr_start = len(cursor.execute('RQLExpression X'))
         migrschema['titre']._rproperties[('Personne', 'String')]['order'] = 7
         migrschema['adel']._rproperties[('Personne', 'String')]['order'] = 6
@@ -270,7 +296,7 @@
             'Any N ORDERBY O WHERE X is CWAttribute, X relation_type RT, RT name N,'
             'X from_entity FE, FE name "Personne",'
             'X ordernum O')]
-        expected = [u'nom', u'prenom', u'promo', u'ass', u'adel', u'titre',
+        expected = [u'nom', u'prenom', u'sexe', u'promo', u'ass', u'adel', u'titre',
                     u'web', u'tel', u'fax', u'datenaiss', u'test', 'description', u'firstname',
                     u'creation_date', 'cwuri', u'modification_date']
         self.assertEquals(rinorder, expected)
@@ -327,14 +353,14 @@
 
     def _erqlexpr_rset(self, action, ertype):
         rql = 'RQLExpression X WHERE ET is CWEType, ET %s_permission X, ET name %%(name)s' % action
-        return self.mh.rqlcursor.execute(rql, {'name': ertype})
+        return self.mh.session.execute(rql, {'name': ertype})
     def _erqlexpr_entity(self, action, ertype):
         rset = self._erqlexpr_rset(action, ertype)
         self.assertEquals(len(rset), 1)
         return rset.get_entity(0, 0)
     def _rrqlexpr_rset(self, action, ertype):
         rql = 'RQLExpression X WHERE ET is CWRType, ET %s_permission X, ET name %%(name)s' % action
-        return self.mh.rqlcursor.execute(rql, {'name': ertype})
+        return self.mh.session.execute(rql, {'name': ertype})
     def _rrqlexpr_entity(self, action, ertype):
         rset = self._rrqlexpr_rset(action, ertype)
         self.assertEquals(len(rset), 1)
@@ -364,7 +390,9 @@
                 self.mh.cmd_remove_cube('email', removedeps=True)
                 # file was there because it's an email dependancy, should have been removed
                 self.failIf('email' in self.config.cubes())
+                self.failIf(self.config.cube_dir('email') in self.config.cubes_path())
                 self.failIf('file' in self.config.cubes())
+                self.failIf(self.config.cube_dir('file') in self.config.cubes_path())
                 for ertype in ('Email', 'EmailThread', 'EmailPart', 'File', 'Image',
                                'sender', 'in_thread', 'reply_to', 'data_format'):
                     self.failIf(ertype in schema, ertype)
@@ -385,7 +413,9 @@
         finally:
             self.mh.cmd_add_cube('email')
             self.failUnless('email' in self.config.cubes())
+            self.failUnless(self.config.cube_dir('email') in self.config.cubes_path())
             self.failUnless('file' in self.config.cubes())
+            self.failUnless(self.config.cube_dir('file') in self.config.cubes_path())
             for ertype in ('Email', 'EmailThread', 'EmailPart', 'File', 'Image',
                            'sender', 'in_thread', 'reply_to', 'data_format'):
                 self.failUnless(ertype in schema, ertype)
@@ -441,11 +471,47 @@
         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()
+        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+                          ['Note'])
+        self.assertEquals(self.schema['Note'].specializes().type, 'Para')
+        self.mh.cmd_add_entity_type('Text')
+        self.mh.repo.schema.rebuild_infered_relations()
+        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+                          ['Note', 'Text'])
+        self.assertEquals(self.schema['Text'].specializes().type, 'Para')
+        # test columns have been actually added
+        text = self.execute('INSERT Text X: X para "hip", X summary "hop", X newattr "momo"').get_entity(0, 0)
+        note = self.execute('INSERT Note X: X para "hip", X shortpara "hop", X newattr "momo"').get_entity(0, 0)
+        aff = self.execute('INSERT Affaire X').get_entity(0, 0)
+        self.failUnless(self.execute('SET X newnotinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': text.eid, 'y': aff.eid}, 'x'))
+        self.failUnless(self.execute('SET X newnotinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': note.eid, 'y': aff.eid}, 'x'))
+        self.failUnless(self.execute('SET X newinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': text.eid, 'y': aff.eid}, 'x'))
+        self.failUnless(self.execute('SET X newinlined Y WHERE X eid %(x)s, Y eid %(y)s',
+                                     {'x': note.eid, 'y': aff.eid}, 'x'))
+        # XXX remove specializes by ourselves, else tearDown fails when removing
+        # Para because of Note inheritance. This could be fixed by putting the
+        # MemSchemaCWETypeDel(session, name) operation in the
+        # after_delete_entity(CWEType) hook, since in that case the MemSchemaSpecializesDel
+        # operation would be removed before, but I'm not sure this is a desired behaviour.
+        #
+        # also we need more tests about introducing/removing base classes or
+        # specialization relationship...
+        self.session.data['rebuild-infered'] = True
+        try:
+            self.execute('DELETE X specializes Y WHERE Y name "Para"')
+            self.commit()
+        finally:
+            self.session.data['rebuild-infered'] = False
+        self.assertEquals(sorted(et.type for et in self.schema['Para'].specialized_by()),
+                          [])
+        self.assertEquals(self.schema['Note'].specializes(), None)
+        self.assertEquals(self.schema['Text'].specializes(), None)
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_msplanner.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_msplanner.py	Tue Sep 22 13:08:42 2009 +0200
@@ -43,22 +43,23 @@
     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
-repo, cnx = init_test_database('sqlite')
+repo, cnx = init_test_database()
 
 class BaseMSPlannerTC(BasePlannerTC):
     """test planner related feature on a 3-sources repository:
@@ -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,6 +1961,22 @@
                      None, None, [self.system], {}, [])],
                    {'x': 999998, 'u': 999999})
 
+    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:
@@ -1974,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')
@@ -2127,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_multisources.py	Tue Sep 22 13:08:42 2009 +0200
@@ -11,7 +11,7 @@
 from logilab.common.decorators import cached
 
 from cubicweb.devtools import TestServerConfiguration, init_test_database
-from cubicweb.devtools.apptest import RepositoryBasedTC
+from cubicweb.devtools.testlib import CubicWebTC, refresh_repo
 from cubicweb.devtools.repotest import do_monkey_patch, undo_monkey_patch
 
 TestServerConfiguration.no_sqlite_wrap = True
@@ -26,16 +26,9 @@
 class ExternalSource2Configuration(TestServerConfiguration):
     sourcefile = 'sources_multi2'
 
-repo2, cnx2 = init_test_database('sqlite', config=ExternalSource1Configuration('data'))
-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]
-cnx2.commit()
-
 MTIME = datetime.now() - timedelta(0, 10)
-
-repo3, cnx3 = init_test_database('sqlite', config=ExternalSource2Configuration('data'))
+repo2, cnx2 = init_test_database(config=ExternalSource1Configuration('data'))
+repo3, cnx3 = init_test_database(config=ExternalSource2Configuration('data'))
 
 # XXX, access existing connection, no pyro connection
 from cubicweb.server.sources.pyrorql import PyroRQLSource
@@ -45,38 +38,47 @@
 from cubicweb.dbapi import Connection
 Connection.close = lambda x: None
 
-class TwoSourcesTC(RepositoryBasedTC):
-    repo_config = TwoSourcesConfiguration('data')
+class TwoSourcesTC(CubicWebTC):
+    config = TwoSourcesConfiguration('data')
+
+    @classmethod
+    def _refresh_repo(cls):
+        super(TwoSourcesTC, cls)._refresh_repo()
+        cnx2.rollback()
+        refresh_repo(repo2)
+        cnx3.rollback()
+        refresh_repo(repo3)
 
     def setUp(self):
-        RepositoryBasedTC.setUp(self)
-        self.repo.sources[-1]._query_cache.clear()
-        self.repo.sources[-2]._query_cache.clear()
-        # trigger discovery
-        self.execute('Card X')
-        self.execute('Affaire X')
-        self.execute('State X')
-        self.commit()
-        # don't delete external entities!
-        self.maxeid = self.session.system_sql('SELECT MAX(eid) FROM entities').fetchone()[0]
-        # add some entities
-        self.ic1 = self.execute('INSERT Card X: X title "C1: An internal card", X wikiid "aaai"')[0][0]
-        self.ic2 = self.execute('INSERT Card X: X title "C2: Ze internal card", X wikiid "zzzi"')[0][0]
-        self.commit()
+        CubicWebTC.setUp(self)
         do_monkey_patch()
 
     def tearDown(self):
-        RepositoryBasedTC.tearDown(self)
+        CubicWebTC.tearDown(self)
         undo_monkey_patch()
 
+    def setup_database(self):
+        cu = cnx2.cursor()
+        self.ec1 = cu.execute('INSERT Card X: X title "C3: An external card", X wikiid "aaa"')[0][0]
+        cu.execute('INSERT Card X: X title "C4: Ze external card", X wikiid "zzz"')
+        self.aff1 = cu.execute('INSERT Affaire X: X ref "AFFREF"')[0][0]
+        cnx2.commit()
+        # trigger discovery
+        self.sexecute('Card X')
+        self.sexecute('Affaire X')
+        self.sexecute('State X')
+        # add some entities
+        self.ic1 = self.sexecute('INSERT Card X: X title "C1: An internal card", X wikiid "aaai"')[0][0]
+        self.ic2 = self.sexecute('INSERT Card X: X title "C2: Ze internal card", X wikiid "zzzi"')[0][0]
+
     def test_eid_comp(self):
-        rset = self.execute('Card X WHERE X eid > 1')
+        rset = self.sexecute('Card X WHERE X eid > 1')
         self.assertEquals(len(rset), 4)
-        rset = self.execute('Any X,T WHERE X title T, X eid > 1')
+        rset = self.sexecute('Any X,T WHERE X title T, X eid > 1')
         self.assertEquals(len(rset), 4)
 
     def test_metainformation(self):
-        rset = self.execute('Card X ORDERBY T WHERE X title T')
+        rset = self.sexecute('Card X ORDERBY T WHERE X title T')
         # 2 added to the system source, 2 added to the external source
         self.assertEquals(len(rset), 4)
         # since they are orderd by eid, we know the 3 first one is coming from the system source
@@ -89,28 +91,28 @@
         self.assertEquals(metainf['source'], {'adapter': 'pyrorql', 'base-url': 'http://extern.org/', 'uri': 'extern'})
         self.assertEquals(metainf['type'], 'Card')
         self.assert_(metainf['extid'])
-        etype = self.execute('Any ETN WHERE X is ET, ET name ETN, X eid %(x)s',
+        etype = self.sexecute('Any ETN WHERE X is ET, ET name ETN, X eid %(x)s',
                              {'x': externent.eid}, 'x')[0][0]
         self.assertEquals(etype, 'Card')
 
     def test_order_limit_offset(self):
-        rsetbase = self.execute('Any W,X ORDERBY W,X WHERE X wikiid W')
+        rsetbase = self.sexecute('Any W,X ORDERBY W,X WHERE X wikiid W')
         self.assertEquals(len(rsetbase), 4)
         self.assertEquals(sorted(rsetbase.rows), rsetbase.rows)
-        rset = self.execute('Any W,X ORDERBY W,X LIMIT 2 OFFSET 2 WHERE X wikiid W')
+        rset = self.sexecute('Any W,X ORDERBY W,X LIMIT 2 OFFSET 2 WHERE X wikiid W')
         self.assertEquals(rset.rows, rsetbase.rows[2:4])
 
     def test_has_text(self):
         self.repo.sources_by_uri['extern'].synchronize(MTIME) # in case fti_update has been run before
-        self.failUnless(self.execute('Any X WHERE X has_text "affref"'))
-        self.failUnless(self.execute('Affaire X WHERE X has_text "affref"'))
+        self.failUnless(self.sexecute('Any X WHERE X has_text "affref"'))
+        self.failUnless(self.sexecute('Affaire X WHERE X has_text "affref"'))
 
     def test_anon_has_text(self):
         self.repo.sources_by_uri['extern'].synchronize(MTIME) # in case fti_update has been run before
-        self.execute('INSERT Affaire X: X ref "no readable card"')[0][0]
-        aff1 = self.execute('INSERT Affaire X: X ref "card"')[0][0]
+        self.sexecute('INSERT Affaire X: X ref "no readable card"')[0][0]
+        aff1 = self.sexecute('INSERT Affaire X: X ref "card"')[0][0]
         # grant read access
-        self.execute('SET X owned_by U WHERE X eid %(x)s, U login "anon"', {'x': aff1}, 'x')
+        self.sexecute('SET X owned_by U WHERE X eid %(x)s, U login "anon"', {'x': aff1}, 'x')
         self.commit()
         cnx = self.login('anon')
         cu = cnx.cursor()
@@ -120,79 +122,81 @@
 
     def test_synchronization(self):
         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]
+        assert cu.execute('Any X WHERE X eid %(x)s', {'x': self.aff1}, 'x')
+        cu.execute('SET X ref "BLAH" WHERE X eid %(x)s', {'x': self.aff1}, 'x')
+        aff2 = cu.execute('INSERT Affaire X: X ref "AFFREUX"')[0][0]
         cnx2.commit()
         try:
             # force sync
             self.repo.sources_by_uri['extern'].synchronize(MTIME)
-            self.failUnless(self.execute('Any X WHERE X has_text "blah"'))
-            self.failUnless(self.execute('Any X WHERE X has_text "affreux"'))
+            self.failUnless(self.sexecute('Any X WHERE X has_text "blah"'))
+            self.failUnless(self.sexecute('Any X WHERE X has_text "affreux"'))
             cu.execute('DELETE Affaire X WHERE X eid %(x)s', {'x': aff2})
             cnx2.commit()
             self.repo.sources_by_uri['extern'].synchronize(MTIME)
-            rset = self.execute('Any X WHERE X has_text "affreux"')
+            rset = self.sexecute('Any X WHERE X has_text "affreux"')
             self.failIf(rset)
         finally:
             # restore state
-            cu.execute('SET X ref "AFFREF" WHERE X eid %(x)s', {'x': aff1}, 'x')
+            cu.execute('SET X ref "AFFREF" WHERE X eid %(x)s', {'x': self.aff1}, 'x')
             cnx2.commit()
 
     def test_simplifiable_var(self):
-        affeid = self.execute('Affaire X WHERE X ref "AFFREF"')[0][0]
-        rset = self.execute('Any X,AA,AB WHERE E eid %(x)s, E in_state X, X name AA, X modification_date AB',
+        affeid = self.sexecute('Affaire X WHERE X ref "AFFREF"')[0][0]
+        rset = self.sexecute('Any X,AA,AB WHERE E eid %(x)s, E in_state X, X name AA, X modification_date AB',
                             {'x': affeid}, 'x')
         self.assertEquals(len(rset), 1)
         self.assertEquals(rset[0][1], "pitetre")
 
     def test_simplifiable_var_2(self):
-        affeid = self.execute('Affaire X WHERE X ref "AFFREF"')[0][0]
-        rset = self.execute('Any E WHERE E eid %(x)s, E in_state S, NOT S name "moved"',
+        affeid = self.sexecute('Affaire X WHERE X ref "AFFREF"')[0][0]
+        rset = self.sexecute('Any E WHERE E eid %(x)s, E in_state S, NOT S name "moved"',
                             {'x': affeid, 'u': self.session.user.eid}, 'x')
         self.assertEquals(len(rset), 1)
 
     def test_sort_func(self):
-        self.execute('Affaire X ORDERBY DUMB_SORT(RF) WHERE X ref RF')
+        self.sexecute('Affaire X ORDERBY DUMB_SORT(RF) WHERE X ref RF')
 
     def test_sort_func_ambigous(self):
-        self.execute('Any X ORDERBY DUMB_SORT(RF) WHERE X title RF')
+        self.sexecute('Any X ORDERBY DUMB_SORT(RF) WHERE X title RF')
 
     def test_in_eid(self):
-        iec1 = self.repo.extid2eid(self.repo.sources_by_uri['extern'], str(ec1),
+        iec1 = self.repo.extid2eid(self.repo.sources_by_uri['extern'], str(self.ec1),
                                    'Card', self.session)
-        rset = self.execute('Any X WHERE X eid IN (%s, %s)' % (iec1, self.ic1))
+        rset = self.sexecute('Any X WHERE X eid IN (%s, %s)' % (iec1, self.ic1))
         self.assertEquals(sorted(r[0] for r in rset.rows), sorted([iec1, self.ic1]))
 
     def test_greater_eid(self):
-        rset = self.execute('Any X WHERE X eid > %s' % self.maxeid)
+        rset = self.sexecute('Any X WHERE X eid > %s' % (self.ic1 - 1))
         self.assertEquals(len(rset.rows), 2) # self.ic1 and self.ic2
+        cu = cnx2.cursor()
         ec2 = cu.execute('INSERT Card X: X title "glup"')[0][0]
         cnx2.commit()
         # 'X eid > something' should not trigger discovery
-        rset = self.execute('Any X WHERE X eid > %s' % self.maxeid)
+        rset = self.sexecute('Any X WHERE X eid > %s' % (self.ic1 - 1))
         self.assertEquals(len(rset.rows), 2)
         # trigger discovery using another query
-        crset = self.execute('Card X WHERE X title "glup"')
+        crset = self.sexecute('Card X WHERE X title "glup"')
         self.assertEquals(len(crset.rows), 1)
-        rset = self.execute('Any X WHERE X eid > %s' % self.maxeid)
+        rset = self.sexecute('Any X WHERE X eid > %s' % (self.ic1 - 1))
         self.assertEquals(len(rset.rows), 3)
-        rset = self.execute('Any MAX(X)')
+        rset = self.sexecute('Any MAX(X)')
         self.assertEquals(len(rset.rows), 1)
         self.assertEquals(rset.rows[0][0], crset[0][0])
 
     def test_attr_unification_1(self):
-        n1 = self.execute('INSERT Note X: X type "AFFREF"')[0][0]
-        n2 = self.execute('INSERT Note X: X type "AFFREU"')[0][0]
-        rset = self.execute('Any X,Y WHERE X is Note, Y is Affaire, X type T, Y ref T')
+        n1 = self.sexecute('INSERT Note X: X type "AFFREF"')[0][0]
+        n2 = self.sexecute('INSERT Note X: X type "AFFREU"')[0][0]
+        rset = self.sexecute('Any X,Y WHERE X is Note, Y is Affaire, X type T, Y ref T')
         self.assertEquals(len(rset), 1, rset.rows)
 
     def test_attr_unification_2(self):
+        cu = cnx2.cursor()
         ec2 = cu.execute('INSERT Card X: X title "AFFREF"')[0][0]
         cnx2.commit()
         try:
-            c1 = self.execute('INSERT Card C: C title "AFFREF"')[0][0]
-            rset = self.execute('Any X,Y WHERE X is Card, Y is Affaire, X title T, Y ref T')
+            c1 = self.sexecute('INSERT Card C: C title "AFFREF"')[0][0]
+            rset = self.sexecute('Any X,Y WHERE X is Card, Y is Affaire, X title T, Y ref T')
             self.assertEquals(len(rset), 2, rset.rows)
         finally:
             cu.execute('DELETE Card X WHERE X eid %(x)s', {'x': ec2}, 'x')
@@ -200,81 +204,88 @@
 
     def test_attr_unification_neq_1(self):
         # XXX complete
-        self.execute('Any X,Y WHERE X is Note, Y is Affaire, X creation_date D, Y creation_date > D')
+        self.sexecute('Any X,Y WHERE X is Note, Y is Affaire, X creation_date D, Y creation_date > D')
 
     def test_attr_unification_neq_2(self):
         # XXX complete
-        self.execute('Any X,Y WHERE X is Card, Y is Affaire, X creation_date D, Y creation_date > D')
+        self.sexecute('Any X,Y WHERE X is Card, Y is Affaire, X creation_date D, Y creation_date > D')
 
     def test_union(self):
-        afeids = self.execute('Affaire X')
-        ueids = self.execute('CWUser X')
-        rset = self.execute('(Any X WHERE X is Affaire) UNION (Any X WHERE X is CWUser)')
+        afeids = self.sexecute('Affaire X')
+        ueids = self.sexecute('CWUser X')
+        rset = self.sexecute('(Any X WHERE X is Affaire) UNION (Any X WHERE X is CWUser)')
         self.assertEquals(sorted(r[0] for r in rset.rows),
                           sorted(r[0] for r in afeids + ueids))
 
     def test_subquery1(self):
-        rsetbase = self.execute('Any W,X WITH W,X BEING (Any W,X ORDERBY W,X WHERE X wikiid W)')
+        rsetbase = self.sexecute('Any W,X WITH W,X BEING (Any W,X ORDERBY W,X WHERE X wikiid W)')
         self.assertEquals(len(rsetbase), 4)
         self.assertEquals(sorted(rsetbase.rows), rsetbase.rows)
-        rset = self.execute('Any W,X LIMIT 2 OFFSET 2 WITH W,X BEING (Any W,X ORDERBY W,X WHERE X wikiid W)')
+        rset = self.sexecute('Any W,X LIMIT 2 OFFSET 2 WITH W,X BEING (Any W,X ORDERBY W,X WHERE X wikiid W)')
         self.assertEquals(rset.rows, rsetbase.rows[2:4])
-        rset = self.execute('Any W,X ORDERBY W,X LIMIT 2 OFFSET 2 WITH W,X BEING (Any W,X WHERE X wikiid W)')
+        rset = self.sexecute('Any W,X ORDERBY W,X LIMIT 2 OFFSET 2 WITH W,X BEING (Any W,X WHERE X wikiid W)')
         self.assertEquals(rset.rows, rsetbase.rows[2:4])
-        rset = self.execute('Any W,X WITH W,X BEING (Any W,X ORDERBY W,X LIMIT 2 OFFSET 2 WHERE X wikiid W)')
+        rset = self.sexecute('Any W,X WITH W,X BEING (Any W,X ORDERBY W,X LIMIT 2 OFFSET 2 WHERE X wikiid W)')
         self.assertEquals(rset.rows, rsetbase.rows[2:4])
 
     def test_subquery2(self):
-        affeid = self.execute('Affaire X WHERE X ref "AFFREF"')[0][0]
-        rset = self.execute('Any X,AA,AB WITH X,AA,AB BEING (Any X,AA,AB WHERE E eid %(x)s, E in_state X, X name AA, X modification_date AB)',
+        affeid = self.sexecute('Affaire X WHERE X ref "AFFREF"')[0][0]
+        rset = self.sexecute('Any X,AA,AB WITH X,AA,AB BEING (Any X,AA,AB WHERE E eid %(x)s, E in_state X, X name AA, X modification_date AB)',
                             {'x': affeid})
         self.assertEquals(len(rset), 1)
         self.assertEquals(rset[0][1], "pitetre")
 
     def test_not_relation(self):
-        states = set(tuple(x) for x in self.execute('Any S,SN WHERE S is State, S name SN'))
+        states = set(tuple(x) for x in self.sexecute('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',
+        notstates = set(tuple(x) for x in self.sexecute('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)
-        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.assertSetEquals(notstates, states)
+        aff1 = self.sexecute('Any X WHERE X is Affaire, X ref "AFFREF"')[0][0]
+        aff1stateeid, aff1statename = self.sexecute('Any S,SN WHERE X eid %(x)s, X in_state S, S name SN', {'x': aff1}, 'x')[0]
         self.assertEquals(aff1statename, 'pitetre')
         states.add((userstate.eid, userstate.name))
         states.remove((aff1stateeid, aff1statename))
-        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',
+        notstates = set(tuple(x) for x in self.sexecute('Any S,SN WHERE S is State, S name SN, NOT X in_state S, X eid %(x)s',
                                                        {'x': aff1}, 'x'))
         self.assertSetEquals(notstates, states)
 
     def test_absolute_url_base_url(self):
+        cu = cnx2.cursor()
         ceid = cu.execute('INSERT Card X: X title "without wikiid to get eid based url"')[0][0]
         cnx2.commit()
-        lc = self.execute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0)
+        lc = self.sexecute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0)
         self.assertEquals(lc.absolute_url(), 'http://extern.org/card/eid/%s' % ceid)
+        cu.execute('DELETE Card X WHERE X eid %(x)s', {'x':ceid})
+        cnx2.commit()
 
     def test_absolute_url_no_base_url(self):
         cu = cnx3.cursor()
         ceid = cu.execute('INSERT Card X: X title "without wikiid to get eid based url"')[0][0]
         cnx3.commit()
-        lc = self.execute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0)
+        lc = self.sexecute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0)
         self.assertEquals(lc.absolute_url(), 'http://testing.fr/cubicweb/card/eid/%s' % lc.eid)
+        cu.execute('DELETE Card X WHERE X eid %(x)s', {'x':ceid})
+        cnx3.commit()
 
     def test_nonregr1(self):
         ueid = self.session.user.eid
-        affaire = self.execute('Affaire X WHERE X ref "AFFREF"').get_entity(0, 0)
-        self.execute('Any U WHERE U in_group G, (G name IN ("managers", "logilab") OR (X require_permission P?, P name "bla", P require_group G)), X eid %(x)s, U eid %(u)s',
+        affaire = self.sexecute('Affaire X WHERE X ref "AFFREF"').get_entity(0, 0)
+        self.sexecute('Any U WHERE U in_group G, (G name IN ("managers", "logilab") OR (X require_permission P?, P name "bla", P require_group G)), X eid %(x)s, U eid %(u)s',
                      {'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',
+        rset = self.sexecute('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D',
                             {'x': treid})
         self.assertEquals(len(rset), 1)
         self.assertEquals(rset.rows[0], [self.session.user.eid])
 
     def test_nonregr3(self):
-        self.execute('DELETE Card X WHERE X eid %(x)s, NOT X multisource_inlined_rel Y', {'x': self.ic1})
+        self.sexecute('DELETE Card X WHERE X eid %(x)s, NOT X multisource_inlined_rel Y', {'x': self.ic1})
 
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
--- a/server/test/unittest_querier.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_querier.py	Tue Sep 22 13:08:42 2009 +0200
@@ -46,7 +46,7 @@
                           ('C0 text,C1 integer', {'A': 'table0.C0', 'B': 'table0.C1'}))
 
 
-repo, cnx = init_test_database('sqlite')
+repo, cnx = init_test_database()
 
 
 
@@ -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), '
@@ -210,6 +213,11 @@
         # should return an empty result set
         self.failIf(self.execute('Any X WHERE X eid 99999999'))
 
+    def test_typed_eid(self):
+        # should return an empty result set
+        rset = self.execute('Any X WHERE X eid %(x)s', {'x': '1'}, 'x')
+        self.assertIsInstance(rset[0][0], (int, long))
+
     def test_bytes_storage(self):
         feid = self.execute('INSERT File X: X name "foo.pdf", X data_format "text/plain", X data %(data)s',
                             {'data': Binary("xxx")})[0][0]
@@ -285,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"')
@@ -350,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]
@@ -466,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
@@ -721,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'])
@@ -995,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)
 
@@ -1028,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]
@@ -1130,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)
 
@@ -1260,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_repository.py	Tue Sep 22 13:08:42 2009 +0200
@@ -18,10 +18,11 @@
 
 from yams.constraints import UniqueConstraint
 
-from cubicweb import BadConnectionId, RepositoryError, ValidationError, UnknownEid, AuthenticationError
+from cubicweb import (BadConnectionId, RepositoryError, ValidationError,
+                      UnknownEid, AuthenticationError)
 from cubicweb.schema import CubicWebSchema, RQLConstraint
 from cubicweb.dbapi import connect, repo_connect, multiple_connections_unfix
-from cubicweb.devtools.apptest import RepositoryBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.repotest import tuplify
 from cubicweb.server import repository
 from cubicweb.server.sqlutils import SQL_PREFIX
@@ -31,48 +32,35 @@
 os.system('pyro-ns >/dev/null 2>/dev/null &')
 
 
-class RepositoryTC(RepositoryBasedTC):
+class RepositoryTC(CubicWebTC):
     """ singleton providing access to a persistent storage for entities
     and relation
     """
 
-#     def setUp(self):
-#         pass
-
-#     def tearDown(self):
-#         self.repo.config.db_perms = True
-#         cnxid = self.repo.connect(*self.default_user_password())
-#         for etype in ('Affaire', 'Note', 'Societe', 'Personne'):
-#             self.repo.execute(cnxid, 'DELETE %s X' % etype)
-#             self.repo.commit(cnxid)
-#         self.repo.close(cnxid)
-
     def test_fill_schema(self):
         self.repo.schema = CubicWebSchema(self.repo.config.appid)
         self.repo.config._cubes = None # avoid assertion error
+        self.repo.config.repairing = True # avoid versions checking
         self.repo.fill_schema()
-        pool = self.repo._get_pool()
         table = SQL_PREFIX + 'CWEType'
         namecol = SQL_PREFIX + 'name'
         finalcol = SQL_PREFIX + 'final'
-        try:
-            cu = self.session.system_sql('SELECT %s FROM %s WHERE %s is NULL' % (
-                namecol, table, finalcol))
-            self.assertEquals(cu.fetchall(), [])
-            cu = self.session.system_sql('SELECT %s FROM %s WHERE %s=%%(final)s ORDER BY %s'
-                              % (namecol, table, finalcol, namecol), {'final': 'TRUE'})
-            self.assertEquals(cu.fetchall(), [(u'Boolean',), (u'Bytes',),
-                                                     (u'Date',), (u'Datetime',),
-                                                     (u'Decimal',),(u'Float',),
-                                                     (u'Int',),
-                                                     (u'Interval',), (u'Password',),
-                                                     (u'String',), (u'Time',)])
-        finally:
-            self.repo._free_pool(pool)
+        self.session.set_pool()
+        cu = self.session.system_sql('SELECT %s FROM %s WHERE %s is NULL' % (
+            namecol, table, finalcol))
+        self.assertEquals(cu.fetchall(), [])
+        cu = self.session.system_sql('SELECT %s FROM %s WHERE %s=%%(final)s ORDER BY %s'
+                          % (namecol, table, finalcol, namecol), {'final': 'TRUE'})
+        self.assertEquals(cu.fetchall(), [(u'Boolean',), (u'Bytes',),
+                                          (u'Date',), (u'Datetime',),
+                                          (u'Decimal',),(u'Float',),
+                                          (u'Int',),
+                                          (u'Interval',), (u'Password',),
+                                          (u'String',), (u'Time',)])
 
     def test_schema_has_owner(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         self.failIf(repo.execute(cnxid, 'CWEType X WHERE NOT X owned_by U'))
         self.failIf(repo.execute(cnxid, 'CWRType X WHERE NOT X owned_by U'))
         self.failIf(repo.execute(cnxid, 'CWAttribute X WHERE NOT X owned_by U'))
@@ -81,18 +69,17 @@
         self.failIf(repo.execute(cnxid, 'CWConstraintType X WHERE NOT X owned_by U'))
 
     def test_connect(self):
-        login, passwd = self.default_user_password()
-        self.assert_(self.repo.connect(login, passwd))
+        self.assert_(self.repo.connect(self.admlogin, self.admpassword))
         self.assertRaises(AuthenticationError,
-                          self.repo.connect, login, 'nimportnawak')
+                          self.repo.connect, self.admlogin, 'nimportnawak')
         self.assertRaises(AuthenticationError,
-                          self.repo.connect, login, None)
+                          self.repo.connect, self.admlogin, None)
         self.assertRaises(AuthenticationError,
                           self.repo.connect, None, None)
 
     def test_execute(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         repo.execute(cnxid, 'Any X')
         repo.execute(cnxid, 'Any X where X is Personne')
         repo.execute(cnxid, 'Any X where X is Personne, X nom ~= "to"')
@@ -101,8 +88,8 @@
 
     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"',
+        cnxid = repo.connect(self.admlogin, self.admpassword)
+        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)
@@ -110,9 +97,9 @@
 
     def test_invalid_entity_rollback(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         # no group
-        repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s, X in_state S WHERE S name "activated"',
+        repo.execute(cnxid, 'INSERT CWUser X: X login %(login)s, X upassword %(passwd)s',
                      {'login': u"tutetute", 'passwd': 'tutetute'})
         self.assertRaises(ValidationError, repo.commit, cnxid)
         rset = repo.execute(cnxid, 'CWUser X WHERE X login "tutetute"')
@@ -120,7 +107,7 @@
 
     def test_close(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         self.assert_(cnxid)
         repo.close(cnxid)
         self.assertRaises(BadConnectionId, repo.execute, cnxid, 'Any X')
@@ -131,9 +118,9 @@
 
     def test_shared_data(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         repo.set_shared_data(cnxid, 'data', 4)
-        cnxid2 = repo.connect(*self.default_user_password())
+        cnxid2 = repo.connect(self.admlogin, self.admpassword)
         self.assertEquals(repo.get_shared_data(cnxid, 'data'), 4)
         self.assertEquals(repo.get_shared_data(cnxid2, 'data'), None)
         repo.set_shared_data(cnxid2, 'data', 5)
@@ -151,14 +138,14 @@
 
     def test_check_session(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         self.assertEquals(repo.check_session(cnxid), None)
         repo.close(cnxid)
         self.assertRaises(BadConnectionId, repo.check_session, cnxid)
 
     def test_transaction_base(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         # check db state
         result = repo.execute(cnxid, 'Personne X')
         self.assertEquals(result.rowcount, 0)
@@ -177,7 +164,7 @@
 
     def test_transaction_base2(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         # rollback relation insertion
         repo.execute(cnxid, "SET U in_group G WHERE U login 'admin', G name 'guests'")
         result = repo.execute(cnxid, "Any U WHERE U in_group G, U login 'admin', G name 'guests'")
@@ -188,25 +175,22 @@
 
     def test_transaction_base3(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         # rollback state change which trigger TrInfo insertion
-        ueid = repo._get_session(cnxid).user.eid
-        rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid})
+        user = repo._get_session(cnxid).user
+        user.fire_transition('deactivate')
+        rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid})
         self.assertEquals(len(rset), 1)
-        repo.execute(cnxid, 'SET X in_state S WHERE X eid %(x)s, S name "deactivated"',
-                     {'x': ueid}, 'x')
-        rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid})
-        self.assertEquals(len(rset), 2)
         repo.rollback(cnxid)
-        rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': ueid})
-        self.assertEquals(len(rset), 1)
+        rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid})
+        self.assertEquals(len(rset), 0)
 
     def test_transaction_interleaved(self):
         self.skip('implement me')
 
     def test_close_wait_processing_request(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         repo.execute(cnxid, 'INSERT CWUser X: X login "toto", X upassword "tutu", X in_group G WHERE G name "users"')
         repo.commit(cnxid)
         # close has to be in the thread due to sqlite limitations
@@ -290,7 +274,7 @@
 
     def test_internal_api(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         session = repo._get_session(cnxid, setpool=True)
         self.assertEquals(repo.type_and_source_from_eid(1, session),
                           ('CWGroup', 'system', None))
@@ -308,7 +292,7 @@
 
     def test_session_api(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         self.assertEquals(repo.user_info(cnxid), (5, 'admin', set([u'managers']), {}))
         self.assertEquals(repo.describe(cnxid, 1), (u'CWGroup', u'system', None))
         repo.close(cnxid)
@@ -317,7 +301,7 @@
 
     def test_shared_data_api(self):
         repo = self.repo
-        cnxid = repo.connect(*self.default_user_password())
+        cnxid = repo.connect(self.admlogin, self.admpassword)
         self.assertEquals(repo.get_shared_data(cnxid, 'data'), None)
         repo.set_shared_data(cnxid, 'data', 4)
         self.assertEquals(repo.get_shared_data(cnxid, 'data'), 4)
@@ -342,38 +326,51 @@
 #             self.set_debug(False)
 #         print 'test time: %.3f (time) %.3f (cpu)' % ((time() - t), clock() - c)
 
-
-class DataHelpersTC(RepositoryBasedTC):
+    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)
 
-    def setUp(self):
-        """ called before each test from this class """
-        cnxid = self.repo.connect(*self.default_user_password())
-        self.session = self.repo._sessions[cnxid]
-        self.session.set_pool()
 
-    def tearDown(self):
-        self.session.rollback()
+class DataHelpersTC(CubicWebTC):
 
     def test_create_eid(self):
+        self.session.set_pool()
         self.assert_(self.repo.system_source.create_eid(self.session))
 
     def test_source_from_eid(self):
+        self.session.set_pool()
         self.assertEquals(self.repo.source_from_eid(1, self.session),
                           self.repo.sources_by_uri['system'])
 
     def test_source_from_eid_raise(self):
+        self.session.set_pool()
         self.assertRaises(UnknownEid, self.repo.source_from_eid, -2, self.session)
 
     def test_type_from_eid(self):
+        self.session.set_pool()
         self.assertEquals(self.repo.type_from_eid(1, self.session), 'CWGroup')
 
     def test_type_from_eid_raise(self):
+        self.session.set_pool()
         self.assertRaises(UnknownEid, self.repo.type_from_eid, -2, self.session)
 
     def test_add_delete_info(self):
         entity = self.repo.vreg['etypes'].etype_class('Personne')(self.session)
         entity.eid = -1
         entity.complete = lambda x: None
+        self.session.set_pool()
         self.repo.add_info(self.session, entity, self.repo.sources_by_uri['system'])
         cu = self.session.system_sql('SELECT * FROM entities WHERE eid = -1')
         data = cu.fetchall()
@@ -388,13 +385,14 @@
         self.assertEquals(data, [])
 
 
-class FTITC(RepositoryBasedTC):
+class FTITC(CubicWebTC):
 
     def test_reindex_and_modified_since(self):
         eidp = self.execute('INSERT Personne X: X nom "toto", X prenom "tutu"')[0][0]
         self.commit()
         ts = datetime.now()
         self.assertEquals(len(self.execute('Personne X WHERE X has_text "tutu"')), 1)
+        self.session.set_pool()
         cu = self.session.system_sql('SELECT mtime, eid FROM entities WHERE eid = %s' % eidp)
         omtime = cu.fetchone()[0]
         # our sqlite datetime adapter is ignore seconds fraction, so we have to
@@ -403,6 +401,7 @@
         self.execute('SET X nom "tata" WHERE X eid %(x)s', {'x': eidp}, 'x')
         self.commit()
         self.assertEquals(len(self.execute('Personne X WHERE X has_text "tutu"')), 1)
+        self.session.set_pool()
         cu = self.session.system_sql('SELECT mtime FROM entities WHERE eid = %s' % eidp)
         mtime = cu.fetchone()[0]
         self.failUnless(omtime < mtime)
@@ -440,7 +439,7 @@
         self.assertEquals(rset.rows, [[self.session.user.eid]])
 
 
-class DBInitTC(RepositoryBasedTC):
+class DBInitTC(CubicWebTC):
 
     def test_versions_inserted(self):
         inserted = [r[0] for r in self.execute('Any K ORDERBY K WHERE P pkey K, P pkey ~= "system.version.%"')]
@@ -450,11 +449,11 @@
                            u'system.version.file', u'system.version.folder',
                            u'system.version.tag'])
 
-class InlineRelHooksTC(RepositoryBasedTC):
+class InlineRelHooksTC(CubicWebTC):
     """test relation hooks are called for inlined relations
     """
     def setUp(self):
-        RepositoryBasedTC.setUp(self)
+        CubicWebTC.setUp(self)
         self.hm = self.repo.hm
         self.called = []
 
@@ -485,11 +484,11 @@
 
     def test_after_add_inline(self):
         """make sure after_<event>_relation hooks are deferred"""
+        p1 = self.add_entity('Personne', nom=u'toto')
         self.hm.register_hook(self._after_relation_hook,
-                             'after_add_relation', 'in_state')
-        eidp = self.execute('INSERT CWUser X: X login "toto", X upassword "tutu", X in_state S WHERE S name "activated"')[0][0]
-        eids = self.execute('State X WHERE X name "activated"')[0][0]
-        self.assertEquals(self.called, [(eidp, 'in_state', eids,)])
+                             'after_add_relation', 'ecrit_par')
+        eidn = self.execute('INSERT Note N: N ecrit_par P WHERE P nom "toto"')[0][0]
+        self.assertEquals(self.called, [(eidn, 'ecrit_par', p1.eid,)])
 
     def test_before_delete_inline_relation(self):
         """make sure before_<event>_relation hooks are called directly"""
--- a/server/test/unittest_rql2sql.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_rql2sql.py	Tue Sep 22 13:08:42 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,14 +378,15 @@
 UNION ALL
 SELECT X.cw_name
 FROM cw_Transition AS X
+UNION ALL
+SELECT X.cw_name
+FROM cw_Workflow AS X
+UNION ALL
+SELECT X.cw_name
+FROM cw_WorkflowTransition AS X
 ORDER BY 1'''),
 
-#    ('Any XN WHERE X name XN GROUPBY XN',
-#     ''''''),
-#    ('Any XN, COUNT(X) WHERE X name XN GROUPBY XN',
-#     ''''''),
-
-    # DISTINCT, can use relatin under exists scope as principal
+    # DISTINCT, can use relation under exists scope as principal
     ('DISTINCT Any X,Y WHERE X name "CWGroup", Y eid IN(1, 2, 3), EXISTS(X read_permission Y)',
      '''SELECT DISTINCT X.cw_eid, rel_read_permission0.eid_to
 FROM cw_CWEType AS X, read_permission_relation AS rel_read_permission0
@@ -467,6 +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
@@ -503,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;',
@@ -629,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 = [
@@ -760,14 +779,10 @@
     ('Any X,S WHERE X travaille S?',
      '''SELECT X.cw_eid, rel_travaille0.eid_to
 FROM cw_Personne AS X LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_from=X.cw_eid)'''
-#SELECT X.cw_eid, S.cw_eid
-#FROM cw_Personne AS X LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_from=X.cw_eid) LEFT OUTER JOIN cw_Societe AS S ON (rel_travaille0.eid_to=S.cw_eid)'''
     ),
     ('Any S,X WHERE X? travaille S, S is Societe',
      '''SELECT S.cw_eid, rel_travaille0.eid_from
 FROM cw_Societe AS S LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_to=S.cw_eid)'''
-#SELECT S.cw_eid, X.cw_eid
-#FROM cw_Societe AS S LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_to=S.cw_eid) LEFT OUTER JOIN cw_Personne AS X ON (rel_travaille0.eid_from=X.cw_eid)'''
     ),
 
     ('Any N,A WHERE N inline1 A?',
@@ -803,8 +818,6 @@
     ('Any C,M WHERE C travaille G?, G evaluee M?, G is Societe',
      '''SELECT C.cw_eid, rel_evaluee1.eid_to
 FROM cw_Personne AS C LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_from=C.cw_eid) LEFT OUTER JOIN cw_Societe AS G ON (rel_travaille0.eid_to=G.cw_eid) LEFT OUTER JOIN evaluee_relation AS rel_evaluee1 ON (rel_evaluee1.eid_from=G.cw_eid)'''
-#SELECT C.cw_eid, M.cw_eid
-#FROM cw_Personne AS C LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_from=C.cw_eid) LEFT OUTER JOIN cw_Societe AS G ON (rel_travaille0.eid_to=G.cw_eid) LEFT OUTER JOIN evaluee_relation AS rel_evaluee1 ON (rel_evaluee1.eid_from=G.cw_eid) LEFT OUTER JOIN cw_Note AS M ON (rel_evaluee1.eid_to=M.cw_eid)'''
      ),
 
     ('Any A,C WHERE A documented_by C?, (C is NULL) OR (EXISTS(C require_permission F, '
@@ -817,9 +830,6 @@
      '''SELECT X.cw_eid
 FROM cw_Personne AS X LEFT OUTER JOIN connait_relation AS rel_connait0 ON (rel_connait0.eid_to=12)
 WHERE X.cw_eid=12'''
-#SELECT 12
-#FROM cw_Personne AS X LEFT OUTER JOIN connait_relation AS rel_connait0 ON (rel_connait0.eid_to=12) LEFT OUTER JOIN Personne AS P ON (rel_connait0.eid_from=P.cw_eid)
-#WHERE X.cw_eid=12'''
     ),
 
     ('Any GN, TN ORDERBY GN WHERE T tags G?, T name TN, G name GN',
@@ -880,14 +890,18 @@
      '''
 SELECT T.cw_eid, _T0.C0, _T0.C1
 FROM cw_Tag AS T LEFT OUTER JOIN tags_relation AS rel_tags0 ON (rel_tags0.eid_from=T.cw_eid) LEFT OUTER JOIN (SELECT G.cw_eid AS C0, S.cw_eid AS C1
-FROM cw_Affaire AS G LEFT OUTER JOIN cw_State AS S ON (G.cw_in_state=S.cw_eid AND S.cw_name=hop) 
+FROM cw_Affaire AS G LEFT OUTER JOIN cw_State AS S ON (G.cw_in_state=S.cw_eid AND S.cw_name=hop)
 UNION ALL
 SELECT G.cw_eid AS C0, S.cw_eid AS C1
-FROM cw_CWUser AS G LEFT OUTER JOIN cw_State AS S ON (G.cw_in_state=S.cw_eid AND S.cw_name=hop) 
+FROM cw_CWUser AS G LEFT OUTER JOIN cw_State AS S ON (G.cw_in_state=S.cw_eid AND S.cw_name=hop)
 UNION ALL
 SELECT G.cw_eid AS C0, S.cw_eid AS C1
 FROM cw_Note AS G LEFT OUTER JOIN cw_State AS S ON (G.cw_in_state=S.cw_eid AND S.cw_name=hop) ) AS _T0 ON (rel_tags0.eid_to=_T0.C0)'''),
 
+    ('Any O,AD  WHERE NOT S inline1 O, S eid 123, O todo_by AD?',
+     '''SELECT O.cw_eid, rel_todo_by0.eid_to
+FROM cw_Affaire AS O LEFT OUTER JOIN todo_by_relation AS rel_todo_by0 ON (rel_todo_by0.eid_from=O.cw_eid), cw_Note AS S
+WHERE NOT EXISTS(SELECT 1 WHERE S.cw_inline1=O.cw_eid) AND S.cw_eid=123''')
     ]
 
 VIRTUAL_VARS = [
@@ -949,10 +963,6 @@
     ("Any COUNT(P) WHERE P is Personne",
      '''SELECT COUNT(P.cw_eid)
 FROM cw_Personne AS P'''),
-##     ("Personne X where X nom upper('TOTO')",
-##      '''SELECT X.cw_eid\nFROM cw_Personne AS X\nWHERE UPPER(X.cw_nom) = TOTO'''),
-##     ("Personne X where X nom Y, UPPER(X) prenom upper(Y)",
-##      '''SELECT X.cw_eid\nFROM cw_Personne AS X\nWHERE UPPER(X.cw_prenom) = UPPER(X.cw_nom)'''),
     ]
 
 SYMETRIC = [
@@ -960,13 +970,6 @@
      '''SELECT DISTINCT P.cw_eid
 FROM connait_relation AS rel_connait0, cw_Personne AS P
 WHERE (rel_connait0.eid_from=0 AND rel_connait0.eid_to=P.cw_eid OR rel_connait0.eid_to=0 AND rel_connait0.eid_from=P.cw_eid)'''
-#      '''SELECT rel_connait0.eid_to
-# FROM connait_relation AS rel_connait0
-# WHERE rel_connait0.eid_from=0
-# UNION
-# SELECT rel_connait0.eid_from
-# FROM connait_relation AS rel_connait0
-# WHERE rel_connait0.eid_to=0'''
      ),
 
     ('Any P WHERE X connait P',
@@ -1050,8 +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
@@ -1120,6 +1124,32 @@
     ]
 from logilab.common.adbh import ADV_FUNC_HELPER_DIRECTORY
 
+class CWRQLTC(RQLGeneratorTC):
+    schema = schema
+
+    def test_nonregr_sol(self):
+        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 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,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):
+            s = set()
+            for sol in delete.solutions:
+                s.add(sol.get(var))
+            return s
+        self.assertEquals(var_sols('FROM_ENTITYOBJECT'), set(('CWAttribute', 'CWRelation')))
+        self.assertEquals(var_sols('FROM_ENTITYOBJECT'), delete.defined_vars['FROM_ENTITYOBJECT'].stinfo['possibletypes'])
+        self.assertEquals(var_sols('ISOBJECT'),
+                          set(x.type for x in self.schema.entities() if not x.is_final()))
+        self.assertEquals(var_sols('ISOBJECT'), delete.defined_vars['ISOBJECT'].stinfo['possibletypes'])
+
+
 class PostgresSQLGeneratorTC(RQLGeneratorTC):
     schema = schema
 
@@ -1145,7 +1175,7 @@
             r, nargs = self.o.generate(union, args,
                                       varmap=varmap)
             args.update(nargs)
-            self.assertLinesEquals((r % args).strip(), self._norm_sql(sql))
+            self.assertLinesEquals((r % args).strip(), self._norm_sql(sql), striplines=True)
         except Exception, ex:
             if 'r' in locals():
                 try:
@@ -1174,14 +1204,6 @@
                 print sql[0].strip()
             raise
         return
-#         rqlst, solutions = self._prepare(rql)
-#         for i, sol in enumerate(solutions):
-#             try:
-#                 r, args = self.o.generate([(rqlst, sol)])
-#                 self.assertEqual((r.strip(), args), sqls[i])
-#             except Exception, ex:
-#                 print rql
-#                 raise
 
     def test1(self):
         self._checkall('Any count(RDEF) WHERE RDEF relation_type X, X eid %(x)s',
@@ -1382,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
@@ -1394,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
@@ -1546,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
@@ -1558,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
 
@@ -1622,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
@@ -1634,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_rqlannotation.py	Tue Sep 22 13:08:42 2009 +0200
@@ -6,7 +6,7 @@
 from cubicweb.devtools import init_test_database
 from cubicweb.devtools.repotest import BaseQuerierTC
 
-repo, cnx = init_test_database('sqlite')
+repo, cnx = init_test_database()
 
 class SQLGenAnnotatorTC(BaseQuerierTC):
     repo = repo
@@ -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	Wed Aug 05 09:15:56 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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_schemaserial.py	Tue Sep 22 13:08:42 2009 +0200
@@ -13,7 +13,6 @@
 loader = CubicWebSchemaLoader()
 config = TestServerConfiguration('data')
 config.bootstrap_cubes()
-loader.lib_directory = config.schemas_lib_dir()
 schema = loader.load(config)
 
 from cubicweb.server.schemaserial import *
@@ -34,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_security.py	Tue Sep 22 13:08:42 2009 +0200
@@ -4,21 +4,21 @@
 import sys
 
 from logilab.common.testlib import unittest_main, TestCase
-from cubicweb.devtools.apptest import RepositoryBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 
 from cubicweb import Unauthorized, ValidationError
 from cubicweb.server.querier import check_read_access
 
-class BaseSecurityTC(RepositoryBasedTC):
+class BaseSecurityTC(CubicWebTC):
 
     def setUp(self):
-        RepositoryBasedTC.setUp(self)
+        CubicWebTC.setUp(self)
         self.create_user('iaminusersgrouponly')
         self.readoriggroups = self.schema['Personne'].get_groups('read')
         self.addoriggroups = self.schema['Personne'].get_groups('add')
 
     def tearDown(self):
-        RepositoryBasedTC.tearDown(self)
+        CubicWebTC.tearDown(self)
         self.schema['Personne'].set_groups('read', self.readoriggroups)
         self.schema['Personne'].set_groups('add', self.addoriggroups)
 
@@ -27,17 +27,17 @@
 
     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')
         cu = cnx.cursor()
         self.assertRaises(Unauthorized,
                           check_read_access,
-                          self.schema, cnx.user(self.current_session()), rqlst, solution)
+                          self.schema, cnx.user(self.session), rqlst, solution)
         self.assertRaises(Unauthorized, cu.execute, rql)
 
     def test_upassword_not_selectable(self):
@@ -165,7 +165,7 @@
 
     def test_insert_relation_rql_permission(self):
         cnx = self.login('iaminusersgrouponly')
-        session = self.current_session()
+        session = self.session
         cu = cnx.cursor(session)
         cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
         # should raise Unauthorized since user don't own S
@@ -210,7 +210,7 @@
 
 
     def test_user_can_change_its_upassword(self):
-        ueid = self.create_user('user')
+        ueid = self.create_user('user').eid
         cnx = self.login('user')
         cu = cnx.cursor()
         cu.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
@@ -220,7 +220,7 @@
         cnx = self.login('user', 'newpwd')
 
     def test_user_cant_change_other_upassword(self):
-        ueid = self.create_user('otheruser')
+        ueid = self.create_user('otheruser').eid
         cnx = self.login('iaminusersgrouponly')
         cu = cnx.cursor()
         cu.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
@@ -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"')
@@ -416,7 +415,7 @@
 
     def test_users_and_groups_non_readable_by_guests(self):
         cnx = self.login('anon')
-        anon = cnx.user(self.current_session())
+        anon = cnx.user(self.session)
         cu = cnx.cursor()
         # anonymous user can only read itself
         rset = cu.execute('Any L WHERE X owned_by U, U login L')
@@ -426,7 +425,7 @@
         # anonymous user can read groups (necessary to check allowed transitions for instance)
         self.assert_(cu.execute('CWGroup X'))
         # should only be able to read the anonymous user, not another one
-        origuser = self.session.user
+        origuser = self.adminsession.user
         self.assertRaises(Unauthorized,
                           cu.execute, 'CWUser X WHERE X eid %(x)s', {'x': origuser.eid}, 'x')
         # nothing selected, nothing updated, no exception raised
@@ -462,7 +461,7 @@
         self.commit()
         cnx = self.login('anon')
         cu = cnx.cursor()
-        anoneid = self.current_session().user.eid
+        anoneid = self.session.user.eid
         self.assertEquals(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
                                      'B bookmarked_by U, U eid %s' % anoneid).rows,
                           [['index', '?vid=index']])
@@ -491,7 +490,7 @@
         eid = self.execute('INSERT Affaire X: X ref "ARCT01"')[0][0]
         self.commit()
         cnx = self.login('iaminusersgrouponly')
-        session = self.current_session()
+        session = self.session
         # needed to avoid check_perm error
         session.set_pool()
         # needed to remove rql expr granting update perm to the user
@@ -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	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/test/unittest_ssplanner.py	Tue Sep 22 13:08:42 2009 +0200
@@ -9,8 +9,8 @@
 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
-repo, cnx = init_test_database('sqlite')
+# keep cnx so it's not garbage collected and the associated session closed
+repo, cnx = init_test_database()
 
 class SSPlannerTC(BasePlannerTC):
     repo = repo
@@ -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/server/utils.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/server/utils.py	Tue Sep 22 13:08:42 2009 +0200
@@ -96,11 +96,11 @@
 
 class LoopTask(object):
     """threaded task restarting itself once executed"""
-    def __init__(self, interval, func):
+    def __init__(self, interval, func, args):
         self.interval = interval
-        def auto_restart_func(self=self, func=func):
+        def auto_restart_func(self=self, func=func, args=args):
             try:
-                func()
+                func(*args)
             finally:
                 self.start()
         self.func = auto_restart_func
--- a/skeleton/__pkginfo__.py.tmpl	Wed Aug 05 09:15:56 2009 +0200
+++ b/skeleton/__pkginfo__.py.tmpl	Tue Sep 22 13:08:42 2009 +0200
@@ -19,8 +19,6 @@
 
 web = 'http://www.cubicweb.org/project/%%s' %% distname
 
-pyversions = ['2.4']
-
 
 from os import listdir as _listdir
 from os.path import join, isdir, exists, dirname
--- a/skeleton/migration/postcreate.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/skeleton/migration/postcreate.py	Tue Sep 22 13:08:42 2009 +0200
@@ -1,4 +1,4 @@
-# postcreate script. You could setup a workflow here for example
+# postcreate script. You could setup site properties or a workflow here for example
 """
 
 :organization: Logilab
@@ -7,3 +7,6 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 
+# Example of site property change
+#set_property('ui.site-title', "<sitename>")
+
--- a/sobjects/email.py	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-"""hooks to ensure use_email / primary_email relations consistency
-
-: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
-"""
-__docformat__ = "restructuredtext en"
-
-from cubicweb.server.hooksmanager import Hook
-from cubicweb.server.pool import PreCommitOperation
-
-class SetUseEmailRelationOp(PreCommitOperation):
-    """delay this operation to commit to avoid conflict with a late rql query
-    already setting the relation
-    """
-    rtype = 'use_email'
-    fromeid = toeid = None # make pylint happy
-
-    def condition(self):
-        """check entity has use_email set for the email address"""
-        return not self.session.unsafe_execute(
-            'Any X WHERE X eid %(x)s, X use_email Y, Y eid %(y)s',
-            {'x': self.fromeid, 'y': self.toeid}, 'x')
-
-    def precommit_event(self):
-        session = self.session
-        if self.condition():
-            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')
-
-class SetPrimaryEmailRelationOp(SetUseEmailRelationOp):
-    rtype = 'primary_email'
-
-    def condition(self):
-        """check entity has no primary_email set"""
-        return not self.session.unsafe_execute(
-            'Any X WHERE X eid %(x)s, X primary_email Y',
-            {'x': self.fromeid}, 'x')
-
-
-class SetPrimaryEmailHook(Hook):
-    """notify when a bug or story or version has its state modified"""
-    events = ('after_add_relation',)
-    accepts = ('use_email',)
-
-    def call(self, session, fromeid, rtype, toeid):
-        subjtype = session.describe(fromeid)[0]
-        eschema = self.vreg.schema[subjtype]
-        if 'primary_email' in eschema.subject_relations():
-            SetPrimaryEmailRelationOp(session, vreg=self.vreg,
-                                      fromeid=fromeid, toeid=toeid)
-
-class SetUseEmailHook(Hook):
-    """notify when a bug or story or version has its state modified"""
-    events = ('after_add_relation',)
-    accepts = ('primary_email',)
-
-    def call(self, session, fromeid, rtype, toeid):
-        subjtype = session.describe(fromeid)[0]
-        eschema = self.vreg.schema[subjtype]
-        if 'use_email' in eschema.subject_relations():
-            SetUseEmailRelationOp(session, vreg=self.vreg,
-                                  fromeid=fromeid, toeid=toeid)
--- a/sobjects/hooks.py	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-"""various library content hooks
-
-: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
-"""
-__docformat__ = "restructuredtext en"
-
-from datetime import datetime
-
-from cubicweb import RepositoryError
-from cubicweb.common.uilib import soup2xhtml
-from cubicweb.server.hooksmanager import Hook
-from cubicweb.server.pool import PreCommitOperation
-
-
-class SetModificationDateOnStateChange(Hook):
-    """update entity's modification date after changing its state"""
-    events = ('after_add_relation',)
-    accepts = ('in_state',)
-
-    def call(self, session, fromeid, rtype, toeid):
-        if fromeid in session.transaction_data.get('neweids', ()):
-            # new entity, not needed
-            return
-        entity = session.entity_from_eid(fromeid)
-        try:
-            entity.set_attributes(modification_date=datetime.now())
-        except RepositoryError, ex:
-            # usually occurs if entity is coming from a read-only source
-            # (eg ldap user)
-            self.warning('cant change modification date for %s: %s', entity, ex)
-
-
-class AddUpdateCWUserHook(Hook):
-    """ensure user logins are stripped"""
-    events = ('before_add_entity', 'before_update_entity',)
-    accepts = ('CWUser',)
-
-    def call(self, session, entity):
-        if 'login' in entity and entity['login']:
-            entity['login'] = entity['login'].strip()
-
-
-class AutoDeleteBookmark(PreCommitOperation):
-    beid = None # make pylint happy
-    def precommit_event(self):
-        session = self.session
-        if not self.beid in session.transaction_data.get('pendingeids', ()):
-            if not session.unsafe_execute('Any X WHERE X bookmarked_by U, X eid %(x)s',
-                                          {'x': self.beid}, 'x'):
-                session.unsafe_execute('DELETE Bookmark X WHERE X eid %(x)s',
-                                       {'x': self.beid}, 'x')
-
-class DelBookmarkedByHook(Hook):
-    """ensure user logins are stripped"""
-    events = ('after_delete_relation',)
-    accepts = ('bookmarked_by',)
-
-    def call(self, session, subj, rtype, obj):
-        AutoDeleteBookmark(session, beid=subj)
-
-
-class TidyHtmlFields(Hook):
-    """tidy HTML in rich text strings"""
-    events = ('before_add_entity', 'before_update_entity')
-    accepts = ('Any',)
-
-    def call(self, session, entity):
-        metaattrs = entity.e_schema.meta_attributes()
-        for metaattr, (metadata, attr) in metaattrs.iteritems():
-            if metadata == 'format':
-                try:
-                    value = entity[attr]
-                except KeyError:
-                    continue # no text to tidy
-                if isinstance(value, unicode): # filter out None and Binary
-                    if self.event == 'before_add_entity':
-                        fmt = entity.get(metaattr)
-                    else:
-                        fmt = entity.get_value(metaattr)
-                    if fmt == 'text/html':
-                        entity[attr] = soup2xhtml(value, session.encoding)
--- a/sobjects/notification.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/sobjects/notification.py	Tue Sep 22 13:08:42 2009 +0200
@@ -1,4 +1,4 @@
-"""some hooks and views to handle notification on entity's changes
+"""some views to handle notification on data changes
 
 :organization: Logilab
 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
@@ -8,26 +8,16 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from base64 import b64encode, b64decode
 from itertools import repeat
-from time import time
-try:
-    from socket import gethostname
-except ImportError:
-    def gethostname(): # gae
-        return 'XXX'
 
 from logilab.common.textutils import normalize_text
-from logilab.common.deprecation import class_renamed
+from logilab.common.deprecation import class_renamed, deprecated
 
-from cubicweb import RegistryException
-from cubicweb.selectors import implements, yes
-from cubicweb.view import EntityView, Component
+from cubicweb.selectors import yes
+from cubicweb.view import Component
 from cubicweb.common.mail import format_mail
-
-from cubicweb.server.pool import PreCommitOperation
+from cubicweb.common.mail import NotificationView
 from cubicweb.server.hookhelper import SendMailOp
-from cubicweb.server.hooksmanager import Hook
 
 
 class RecipientsFinder(Component):
@@ -42,7 +32,7 @@
                 'X primary_email E, E address A')
 
     def recipients(self):
-        mode = self.config['default-recipients-mode']
+        mode = self.req.vreg.config['default-recipients-mode']
         if mode == 'users':
             # use unsafe execute else we may don't have the right to see users
             # to notify...
@@ -51,201 +41,21 @@
                      for u in execute(self.user_rql, build_descr=True, propagate=True).entities()]
         elif mode == 'default-dest-addrs':
             lang = self.vreg.property_value('ui.language')
-            dests = zip(self.config['default-dest-addrs'], repeat(lang))
+            dests = zip(self.req.vreg.config['default-dest-addrs'], repeat(lang))
         else: # mode == 'none'
             dests = []
         return dests
 
 
-# hooks #######################################################################
-
-class RenderAndSendNotificationView(PreCommitOperation):
-    """delay rendering of notification view until precommit"""
-    def precommit_event(self):
-        if self.view.rset and self.view.rset[0][0] in self.session.transaction_data.get('pendingeids', ()):
-            return # entity added and deleted in the same transaction
-        self.view.render_and_send(**getattr(self, 'viewargs', {}))
-
-class StatusChangeHook(Hook):
-    """notify when a workflowable entity has its state modified"""
-    events = ('after_add_entity',)
-    accepts = ('TrInfo',)
-
-    def call(self, session, entity):
-        if not entity.from_state: # not a transition
-            return
-        rset = entity.related('wf_info_for')
-        try:
-            view = session.vreg['views'].select('notif_status_change', session,
-                                                rset=rset, row=0)
-        except RegistryException:
-            return
-        comment = entity.printable_value('comment', format='text/plain')
-        if comment:
-            comment = normalize_text(comment, 80,
-                                     rest=entity.comment_format=='text/rest')
-        RenderAndSendNotificationView(session, view=view, viewargs={
-            'comment': comment, 'previous_state': entity.previous_state.name,
-            'current_state': entity.new_state.name})
-
-
-class RelationChangeHook(Hook):
-    events = ('before_add_relation', 'after_add_relation',
-              'before_delete_relation', 'after_delete_relation')
-    accepts = ('Any',)
-    def call(self, session, fromeid, rtype, toeid):
-        """if a notification view is defined for the event, send notification
-        email defined by the view
-        """
-        rset = session.eid_rset(fromeid)
-        vid = 'notif_%s_%s' % (self.event,  rtype)
-        try:
-            view = session.vreg['views'].select(vid, session, rset=rset, row=0)
-        except RegistryException:
-            return
-        RenderAndSendNotificationView(session, view=view)
-
-
-class EntityChangeHook(Hook):
-    events = ('after_add_entity',
-              'after_update_entity')
-    accepts = ('Any',)
-    def call(self, session, entity):
-        """if a notification view is defined for the event, send notification
-        email defined by the view
-        """
-        rset = entity.as_rset()
-        vid = 'notif_%s' % self.event
-        try:
-            view = session.vreg['views'].select(vid, session, rset=rset, row=0)
-        except RegistryException:
-            return
-        RenderAndSendNotificationView(session, view=view)
-
-
 # abstract or deactivated notification views and mixin ########################
 
-class NotificationView(EntityView):
-    """abstract view implementing the email API
-
-    all you have to do by default is :
-    * set id and accepts attributes to match desired events and entity types
-    * set a content attribute to define the content of the email (unless you
-      override call)
+class NotificationView(NotificationView):
+    """overriden to delay actual sending of mails to a commit operation by
+    default
     """
-    # XXX refactor this class to work with len(rset) > 1
-
-    msgid_timestamp = True
-
-    def recipients(self):
-        finder = self.vreg['components'].select('recipients_finder', self.req,
-                                  rset=self.rset)
-        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_login()
-        return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals()
-
-    def user_login(self):
-        # req is actually a session (we are on the server side), and we have to
-        # prevent nested internal session
-        return self.req.actual_session().user.login
-
-    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_login(),
-                       'eid': entity.eid,
-                       'etype': entity.dc_type(),
-                       'url': entity.absolute_url(),
-                       'title': entity.dc_long_title(),})
-        return kwargs
-
-    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_and_send(self, **kwargs):
-        """generate and send an email message for this view"""
-        self._kwargs = kwargs
-        recipients = self.recipients()
-        if not recipients:
-            self.info('skipping %s notification, no recipients', self.id)
-            return
-        if not isinstance(recipients[0], tuple):
-            from warnings import warn
-            warn('recipients should now return a list of 2-uple (email, language)',
-                 DeprecationWarning, stacklevel=1)
-            lang = self.vreg.property_value('ui.language')
-            recipients = zip(recipients, repeat(lang))
-        if self.rset is not None:
-            entity = self.entity(self.row or 0, self.col or 0)
-            # if the view is using timestamp in message ids, no way to reference
-            # previous email
-            if not self.msgid_timestamp:
-                refs = [self.construct_message_id(eid)
-                        for eid in entity.notification_references(self)]
-            else:
-                refs = ()
-            msgid = self.construct_message_id(entity.eid)
-        else:
-            refs = ()
-            msgid = None
-        userdata = self.req.user_data()
-        origlang = self.req.lang
-        for emailaddr, lang in recipients:
-            self.req.set_language(lang)
-            # since the same view (eg self) may be called multiple time and we
-            # need a fresh stream at each iteration, reset it explicitly
-            self.w = None
-            # XXX call render before subject to set .row/.col attributes on the
-            #     view
-            content = self.render(row=0, col=0, **kwargs)
-            subject = self.subject()
-            msg = format_mail(userdata, [emailaddr], content, subject,
-                              config=self.config, msgid=msgid, references=refs)
-            self.send([emailaddr], msg)
-        # restore language
-        self.req.set_language(origlang)
-
-    def send(self, recipients, msg):
+    def send_on_commit(self, recipients, msg):
         SendMailOp(self.req, recipients=recipients, msg=msg)
-
-
-def construct_message_id(appid, eid, withtimestamp=True):
-    if withtimestamp:
-        addrpart = 'eid=%s&timestamp=%.10f' % (eid, time())
-    else:
-        addrpart = 'eid=%s' % eid
-    # we don't want any equal sign nor trailing newlines
-    leftpart = b64encode(addrpart, '.-').rstrip().rstrip('=')
-    return '<%s@%s.%s>' % (leftpart, appid, gethostname())
-
-
-def parse_message_id(msgid, appid):
-    if msgid[0] == '<':
-        msgid = msgid[1:]
-    if msgid[-1] == '>':
-        msgid = msgid[:-1]
-    try:
-        values, qualif = msgid.split('@')
-        padding = len(values) % 4
-        values = b64decode(str(values + '='*padding), '.-')
-        values = dict(v.split('=') for v in values.split('&'))
-        fromappid, host = qualif.split('.', 1)
-    except:
-        return None
-    if appid != fromappid or host != gethostname():
-        return None
-    return values
+    send = send_on_commit
 
 
 class StatusChangeMixIn(object):
@@ -271,6 +81,13 @@
 # XXX should be based on dc_title/dc_description, no?
 
 class ContentAddedView(NotificationView):
+    """abstract class for notification on entity/relation
+
+    all you have to do by default is :
+    * set id and __select__ attributes to match desired events and entity types
+    * set a content attribute to define the content of the email (unless you
+      override call)
+    """
     __abstract__ = True
     id = 'notif_after_add_entity'
     msgid_timestamp = False
@@ -284,16 +101,28 @@
 """
 
     def context(self, **kwargs):
-        entity = self.entity(self.row or 0, self.col or 0)
+        entity = self.rset.get_entity(self.row or 0, self.col or 0)
         content = entity.printable_value(self.content_attr, format='text/plain')
         if content:
-            contentformat = getattr(entity, self.content_attr + '_format', 'text/rest')
-            content = normalize_text(content, 80, rest=contentformat=='text/rest')
+            contentformat = getattr(entity, self.content_attr + '_format',
+                                    'text/rest')
+            # XXX don't try to wrap rest until we've a proper transformation (see
+            # #103822)
+            if contentformat != 'text/rest':
+                content = normalize_text(content, 80)
         return super(ContentAddedView, self).context(content=content, **kwargs)
 
     def subject(self):
-        entity = self.entity(self.row or 0, self.col or 0)
+        entity = self.rset.get_entity(self.row or 0, self.col or 0)
         return  u'%s #%s (%s)' % (self.req.__('New %s' % entity.e_schema),
-                                  entity.eid, self.user_login())
+                                  entity.eid, self.user_data['login'])
+
+
+from logilab.common.deprecation import class_renamed, class_moved, deprecated
+from cubicweb.hooks.notification import RenderAndSendNotificationView
+from cubicweb.common.mail import parse_message_id
 
 NormalizedTextView = class_renamed('NormalizedTextView', ContentAddedView)
+RenderAndSendNotificationView = class_moved(RenderAndSendNotificationView)
+parse_message_id = deprecated('parse_message_id is now defined in cubicweb.common.mail')(parse_message_id)
+
--- a/sobjects/supervising.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/sobjects/supervising.py	Tue Sep 22 13:08:42 2009 +0200
@@ -13,58 +13,9 @@
 from cubicweb.schema import display_name
 from cubicweb.view import Component
 from cubicweb.common.mail import format_mail
-from cubicweb.server.hooksmanager import Hook
 from cubicweb.server.hookhelper import SendMailOp
 
 
-class SomethingChangedHook(Hook):
-    events = ('before_add_relation', 'before_delete_relation',
-              'after_add_entity', 'before_update_entity')
-    accepts = ('Any',)
-
-    def call(self, session, *args):
-        dest = self.config['supervising-addrs']
-        if not dest: # no supervisors, don't do this for nothing...
-            return
-        self.session = session
-        if self._call(*args):
-            SupervisionMailOp(session)
-
-    def _call(self, *args):
-        if self._event() == 'update_entity':
-            if args[0].eid in self.session.transaction_data.get('neweids', ()):
-                return False
-            if args[0].e_schema == 'CWUser':
-                updated = set(args[0].iterkeys())
-                if not (updated - frozenset(('eid', 'modification_date',
-                                             'last_login_time'))):
-                    # don't record last_login_time update which are done
-                    # automatically at login time
-                    return False
-        self.session.transaction_data.setdefault('pendingchanges', []).append(
-            (self._event(), args))
-        return True
-
-    def _event(self):
-        return self.event.split('_', 1)[1]
-
-
-class EntityDeleteHook(SomethingChangedHook):
-    events = ('before_delete_entity',)
-
-    def _call(self, eid):
-        entity = self.session.entity_from_eid(eid)
-        try:
-            title = entity.dc_title()
-        except:
-            # may raise an error during deletion process, for instance due to
-            # missing required relation
-            title = '#%s' % eid
-        self.session.transaction_data.setdefault('pendingchanges', []).append(
-            ('delete_entity', (eid, str(entity.e_schema), title)))
-        return True
-
-
 def filter_changes(changes):
     """
     * when an entity has been deleted:
@@ -79,31 +30,22 @@
     for change in changes[:]:
         event, changedescr = change
         if event == 'add_entity':
-            entity = changedescr[0]
+            entity = changedescr.entity
             added.add(entity.eid)
             if entity.e_schema == 'TrInfo':
                 changes.remove(change)
-                if entity.from_state:
-                    try:
-                        changes.remove( ('delete_relation',
-                                         (entity.wf_info_for[0].eid, 'in_state',
-                                          entity.from_state[0].eid)) )
-                    except ValueError:
-                        pass
-                    try:
-                        changes.remove( ('add_relation',
-                                         (entity.wf_info_for[0].eid, 'in_state',
-                                          entity.to_state[0].eid)) )
-                    except ValueError:
-                        pass
-                    event = 'change_state'
-                    change = (event,
-                              (entity.wf_info_for[0],
-                               entity.from_state[0], entity.to_state[0]))
-                    changes.append(change)
+                event = 'change_state'
+                change = (event,
+                          (entity.wf_info_for[0],
+                           entity.from_state[0], entity.to_state[0]))
+                changes.append(change)
         elif event == 'delete_entity':
             deleted.add(changedescr[0])
         index.setdefault(event, set()).add(change)
+    for key in ('delete_relation', 'add_relation'):
+        for change in index.get(key, {}).copy():
+            if change[1].rtype == 'in_state':
+                index[key].remove(change)
     # filter changes
     for eid in added:
         try:
@@ -111,30 +53,24 @@
                 changedescr = change[1]
                 # 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',) \
-                       and changedescr[0] == eid:
+                if changedescr.rtype in ('created_by', 'owned_by', 'is', 'is_instance_of',
+                                      'from_state', 'to_state', 'by_transition',
+                                      'wf_info_for') \
+                       and changedescr.eidfrom == eid:
                     index['add_relation'].remove(change)
-                # skip in_state relation if the entity is being created
-                # XXX this may be automatized by skipping all mandatory relation
-                #     at entity creation time
-                elif changedescr[1] == 'in_state' and changedescr[0] in added:
-                    index['add_relation'].remove(change)
-
         except KeyError:
             break
     for eid in deleted:
         try:
             for change in index['delete_relation'].copy():
-                fromeid, rtype, toeid = change[1]
-                if fromeid == eid:
+                if change.eidfrom == eid:
                     index['delete_relation'].remove(change)
-                elif toeid == eid:
+                elif change.eidto == eid:
                     index['delete_relation'].remove(change)
-                    if rtype == 'wf_info_for':
-                        for change in index['delete_entity'].copy():
-                            if change[1][0] == fromeid:
-                                index['delete_entity'].remove(change)
+                    if change.rtype == 'wf_info_for':
+                        for change_ in index['delete_entity'].copy():
+                            if change_[1].eidfrom == change.eidfrom:
+                                index['delete_entity'].remove(change_)
         except KeyError:
             break
     for change in changes:
@@ -161,7 +97,7 @@
                % user.login)
         for event, changedescr in filter_changes(changes):
             self.w(u'* ')
-            getattr(self, event)(*changedescr)
+            getattr(self, event)(changedescr)
             self.w(u'\n\n')
 
     def _entity_context(self, entity):
@@ -169,30 +105,30 @@
                 'etype': entity.dc_type().lower(),
                 'title': entity.dc_title()}
 
-    def add_entity(self, entity):
+    def add_entity(self, changedescr):
         msg = self.req._('added %(etype)s #%(eid)s (%(title)s)')
-        self.w(u'%s\n' % (msg % self._entity_context(entity)))
-        self.w(u'  %s' % entity.absolute_url())
+        self.w(u'%s\n' % (msg % self._entity_context(changedescr.entity)))
+        self.w(u'  %s' % changedescr.entity.absolute_url())
 
-    def update_entity(self, entity):
+    def update_entity(self, changedescr):
         msg = self.req._('updated %(etype)s #%(eid)s (%(title)s)')
-        self.w(u'%s\n' % (msg % self._entity_context(entity)))
+        self.w(u'%s\n' % (msg % self._entity_context(changedescr.entity)))
         # XXX print changes
-        self.w(u'  %s' % entity.absolute_url())
+        self.w(u'  %s' % changedescr.entity.absolute_url())
 
-    def delete_entity(self, eid, etype, title):
+    def delete_entity(self, (eid, etype, title)):
         msg = self.req._('deleted %(etype)s #%(eid)s (%(title)s)')
         etype = display_name(self.req, etype).lower()
         self.w(msg % locals())
 
-    def change_state(self, entity, fromstate, tostate):
+    def change_state(self, (entity, fromstate, tostate)):
         msg = self.req._('changed state of %(etype)s #%(eid)s (%(title)s)')
         self.w(u'%s\n' % (msg % self._entity_context(entity)))
         self.w(_('  from state %(fromstate)s to state %(tostate)s\n' %
                  {'fromstate': _(fromstate.name), 'tostate': _(tostate.name)}))
         self.w(u'  %s' % entity.absolute_url())
 
-    def _relation_context(self, fromeid, rtype, toeid):
+    def _relation_context(self, changedescr):
         _ = self.req._
         session = self.req.actual_session()
         def describe(eid):
@@ -202,19 +138,20 @@
                 # may occurs when an entity has been deleted from an external
                 # source and we're cleaning its relation
                 return _('unknown external entity')
+        eidfrom, rtype, eidto = changedescr.eidfrom, changedescr.rtype, changedescr.eidto
         return {'rtype': _(rtype),
-                'fromeid': fromeid,
-                'frometype': describe(fromeid),
-                'toeid': toeid,
-                'toetype': describe(toeid)}
+                'eidfrom': eidfrom,
+                'frometype': describe(eidfrom),
+                'eidto': eidto,
+                'toetype': describe(eidto)}
 
-    def add_relation(self, fromeid, rtype, toeid):
-        msg = self.req._('added relation %(rtype)s from %(frometype)s #%(fromeid)s to %(toetype)s #%(toeid)s')
-        self.w(msg % self._relation_context(fromeid, rtype, toeid))
+    def add_relation(self, changedescr):
+        msg = self.req._('added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%(eidto)s')
+        self.w(msg % self._relation_context(changedescr))
 
-    def delete_relation(self, fromeid, rtype, toeid):
-        msg = self.req._('deleted relation %(rtype)s from %(frometype)s #%(fromeid)s to %(toetype)s #%(toeid)s')
-        self.w(msg % self._relation_context(fromeid, rtype, toeid))
+    def delete_relation(self, eidfrom, rtype, eidto):
+        msg = self.req._('deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%(eidto)s')
+        self.w(msg % self._relation_context(changedescr))
 
 
 class SupervisionMailOp(SendMailOp):
--- a/sobjects/test/unittest_email.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/sobjects/test/unittest_email.py	Tue Sep 22 13:08:42 2009 +0200
@@ -5,9 +5,11 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
-from cubicweb.devtools.apptest import EnvBasedTC
 
-class EmailAddressHooksTC(EnvBasedTC):
+from cubicweb import Unauthorized
+from cubicweb.devtools.testlib import CubicWebTC
+
+class EmailAddressHooksTC(CubicWebTC):
 
     def test_use_email_set_primary_email(self):
         self.execute('INSERT EmailAddress X: X address "admin@logilab.fr", U use_email X WHERE U login "admin"')
@@ -30,6 +32,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_hooks.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/sobjects/test/unittest_hooks.py	Tue Sep 22 13:08:42 2009 +0200
@@ -6,9 +6,9 @@
 :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
+from cubicweb.devtools.testlib import CubicWebTC
 
-class HooksTC(EnvBasedTC):
+class HooksTC(CubicWebTC):
 
     def test_euser_login_stripped(self):
         u = self.create_user('  joe  ')
--- a/sobjects/test/unittest_notification.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/sobjects/test/unittest_notification.py	Tue Sep 22 13:08:42 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.testlib import CubicWebTC, MAILBOX
 
-from cubicweb.sobjects.notification import construct_message_id, parse_message_id
+from cubicweb.common.mail import construct_message_id, parse_message_id
 
 class MessageIdTC(TestCase):
     def test_base(self):
@@ -48,7 +48,7 @@
             self.assertNotEquals(msgid1, '<@testapp.%s>' % gethostname())
 
 
-class RecipientsFinderTC(EnvBasedTC):
+class RecipientsFinderTC(CubicWebTC):
     def test(self):
         urset = self.execute('CWUser X WHERE X login "admin"')
         self.execute('INSERT EmailAddress X: X address "admin@logilab.fr", U primary_email X '
@@ -67,20 +67,17 @@
         self.assertEquals(finder.recipients(), [('abcd@logilab.fr', 'en'), ('efgh@logilab.fr', 'en')])
 
 
-class StatusChangeViewsTC(EnvBasedTC):
+class StatusChangeViewsTC(CubicWebTC):
 
     def test_status_change_view(self):
         req = self.session()
-        u = self.create_user('toto', req=req)
-        assert u.req
-        assert u.rset
-        self.execute('SET X in_state S WHERE X eid %s, S name "deactivated"' % u.eid)
-        v = self.vreg['views'].select('notif_status_change', req, rset=u.rset, row=0)
-        content = v.render(row=0, comment='yeah',
-                           previous_state='activated',
-                           current_state='deactivated')
-        # remove date
-        self.assertEquals(content,
+        u = self.create_user('toto', req=req, commit=False)
+        u.fire_transition('deactivate', comment=u'yeah')
+        self.failIf(MAILBOX)
+        self.commit()
+        self.assertEquals(len(MAILBOX), 1)
+        email = MAILBOX[0]
+        self.assertEquals(email.content,
                           '''
 admin changed status from <activated> to <deactivated> for entity
 'toto'
@@ -89,7 +86,7 @@
 
 url: http://testing.fr/cubicweb/cwuser/toto
 ''')
-        self.assertEquals(v.subject(), 'status changed cwuser #%s (admin)' % u.eid)
+        self.assertEquals(email.subject, 'status changed cwuser #%s (admin)' % u.eid)
 
 if __name__ == '__main__':
     unittest_main()
--- a/sobjects/test/unittest_supervising.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/sobjects/test/unittest_supervising.py	Tue Sep 22 13:08:42 2009 +0200
@@ -9,12 +9,12 @@
 import re
 
 from logilab.common.testlib import unittest_main
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 
 from cubicweb.sobjects.supervising import SendMailOp, SupervisionMailOp
 
 
-class SupervisingTC(EnvBasedTC):
+class SupervisingTC(CubicWebTC):
 
     def setup_database(self):
         self.add_entity('Card', title=u"une news !", content=u"cubicweb c'est beau")
@@ -26,14 +26,13 @@
 
 
     def test_supervision(self):
-        session = self.session()
+        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,20 +61,34 @@
 * 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()
+        session = self.session
         # do some unlogged modification
         self.execute('SET X last_login_time NOW WHERE X eid %(x)s', {'x': session.user.eid}, 'x')
         self.commit() # no crash
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/rewrite/bootstrap_cubes	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,1 @@
+card, person
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/rewrite/schema.py	Tue Sep 22 13:08:42 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_cwconfig.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/test/unittest_cwconfig.py	Tue Sep 22 13:08:42 2009 +0200
@@ -77,7 +77,7 @@
 
     def test_vregistry_path(self):
         self.assertEquals([unabsolutize(p) for p in self.config.vregistry_path()],
-                          ['entities', 'web/views', 'sobjects',
+                          ['entities', 'web/views', 'sobjects', 'hooks',
                            'file/entities.py', 'file/views', 'file/hooks.py',
                            'email/entities.py', 'email/views', 'email/hooks.py',
                            'test/data/entities.py'])
--- a/test/unittest_dbapi.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/test/unittest_dbapi.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,24 +7,21 @@
 """
 from cubicweb import ConnectionError
 from cubicweb.dbapi import ProgrammingError
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 
 
-class DBAPITC(EnvBasedTC):
-    @property
-    def cnx(self):
-        return self.login('anon')
+class DBAPITC(CubicWebTC):
 
     def test_public_repo_api(self):
-        cnx = self.cnx
-        self.assertEquals(cnx.get_schema(), self.env.repo.schema)
+        cnx = self.login('anon')
+        self.assertEquals(cnx.get_schema(), self.repo.schema)
         self.assertEquals(cnx.source_defs(), {'system': {'adapter': 'native', 'uri': 'system'}})
         self.restore_connection() # proper way to close cnx
         self.assertRaises(ProgrammingError, cnx.get_schema)
         self.assertRaises(ProgrammingError, cnx.source_defs)
 
     def test_db_api(self):
-        cnx = self.cnx
+        cnx = self.login('anon')
         self.assertEquals(cnx.rollback(), None)
         self.assertEquals(cnx.commit(), None)
         self.restore_connection() # proper way to close cnx
@@ -34,7 +31,7 @@
         self.assertRaises(ProgrammingError, cnx.close)
 
     def test_api(self):
-        cnx = self.cnx
+        cnx = self.login('anon')
         self.assertEquals(cnx.user(None).login, 'anon')
         self.assertEquals(cnx.describe(1), (u'CWGroup', u'system', None))
         self.restore_connection() # proper way to close cnx
@@ -42,7 +39,7 @@
         self.assertRaises(ConnectionError, cnx.describe, 1)
 
     def test_session_data_api(self):
-        cnx = self.cnx
+        cnx = self.login('anon')
         self.assertEquals(cnx.get_session_data('data'), None)
         self.assertEquals(cnx.session_data(), {})
         cnx.set_session_data('data', 4)
@@ -57,7 +54,7 @@
         self.assertEquals(cnx.session_data(), {'data': 4})
 
     def test_shared_data_api(self):
-        cnx = self.cnx
+        cnx = self.login('anon')
         self.assertEquals(cnx.get_shared_data('data'), None)
         cnx.set_shared_data('data', 4)
         self.assertEquals(cnx.get_shared_data('data'), 4)
@@ -71,19 +68,6 @@
         self.assertRaises(ConnectionError, cnx.set_shared_data, 'data', 0)
         self.assertRaises(ConnectionError, cnx.get_shared_data, 'data')
 
-
-# class DBAPICursorTC(EnvBasedTC):
-
-#     @property
-#     def cursor(self):
-#         return self.env.cnx.cursor()
-
-#     def test_api(self):
-#         cu = self.cursor
-#         self.assertEquals(cu.describe(1), (u'CWGroup', u'system', None))
-#         #cu.close()
-#         #self.assertRaises(ConnectionError, cu.describe, 1)
-
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- a/test/unittest_entity.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/test/unittest_entity.py	Tue Sep 22 13:08:42 2009 +0200
@@ -9,32 +9,26 @@
 
 from datetime import datetime
 
-from cubicweb import Binary
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb import Binary, Unauthorized
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.common.mttransforms import HAS_TAL
-
-class EntityTC(EnvBasedTC):
+from cubicweb.entities import fetch_config
 
-##     def setup_database(self):
-##         self.add_entity('Personne', nom=u'di mascio', prenom=u'adrien')
-##         self.add_entity('Task', title=u'fait ca !', description=u'et plus vite', start=now())
-##         self.add_entity('Tag', name=u'x')
-##         self.add_entity('Link', title=u'perdu', url=u'http://www.perdu.com',
-##                         embed=False)
+class EntityTC(CubicWebTC):
 
     def test_boolean_value(self):
-        e = self.etype_instance('CWUser')
+        e = self.vreg['etypes'].etype_class('CWUser')(self.request())
         self.failUnless(e)
 
     def test_yams_inheritance(self):
         from entities import Note
-        e = self.etype_instance('SubNote')
+        e = self.vreg['etypes'].etype_class('SubNote')(self.request())
         self.assertIsInstance(e, Note)
-        e2 = self.etype_instance('SubNote')
+        e2 = self.vreg['etypes'].etype_class('SubNote')(self.request())
         self.assertIs(e.__class__, e2.__class__)
 
     def test_has_eid(self):
-        e = self.etype_instance('CWUser')
+        e = self.vreg['etypes'].etype_class('CWUser')(self.request())
         self.assertEquals(e.eid, None)
         self.assertEquals(e.has_eid(), False)
         e.eid = 'X'
@@ -76,8 +70,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 +79,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')
@@ -101,7 +95,7 @@
         user = self.entity('Any X WHERE X eid %(x)s', {'x':self.user().eid}, 'x')
         adeleid = self.execute('INSERT EmailAddress X: X address "toto@logilab.org", U use_email X WHERE U login "admin"')[0][0]
         self.commit()
-        self.assertEquals(user._related_cache.keys(), [])
+        self.assertEquals(user._related_cache, {})
         email = user.primary_email[0]
         self.assertEquals(sorted(user._related_cache), ['primary_email_subject'])
         self.assertEquals(email._related_cache.keys(), ['primary_email_object'])
@@ -132,7 +126,8 @@
         seschema.subject_relation('evaluee').set_rproperty(seschema, Note.e_schema, 'cardinality', '1*')
         # testing basic fetch_attrs attribute
         self.assertEquals(Personne.fetch_rql(user),
-                          'Any X,AA,AB,AC ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC')
+                          'Any X,AA,AB,AC ORDERBY AA ASC '
+                          'WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC')
         pfetch_attrs = Personne.fetch_attrs
         sfetch_attrs = Societe.fetch_attrs
         try:
@@ -142,22 +137,26 @@
             # testing one non final relation
             Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
             self.assertEquals(Personne.fetch_rql(user),
-                              'Any X,AA,AB,AC,AD ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB, X travaille AC, AC nom AD')
+                              'Any X,AA,AB,AC,AD ORDERBY AA ASC '
+                              'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD')
             # testing two non final relations
             Personne.fetch_attrs = ('nom', 'prenom', 'travaille', 'evaluee')
             self.assertEquals(Personne.fetch_rql(user),
-                              'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC WHERE X is Personne, X nom AA, '
-                              'X prenom AB, X travaille AC, AC nom AD, X evaluee AE, AE modification_date AF')
+                              'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC '
+                              'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, '
+                              'X evaluee AE?, AE modification_date AF')
             # testing one non final relation with recursion
             Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
             Societe.fetch_attrs = ('nom', 'evaluee')
             self.assertEquals(Personne.fetch_rql(user),
-                              'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC WHERE X is Personne, X nom AA, X prenom AB, '
-                              'X travaille AC, AC nom AD, AC evaluee AE, AE modification_date AF'
+                              'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC '
+                              'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, '
+                              'AC evaluee AE?, AE modification_date AF'
                               )
             # testing symetric relation
             Personne.fetch_attrs = ('nom', 'connait')
-            self.assertEquals(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA ASC WHERE X is Personne, X nom AA, X connait AB')
+            self.assertEquals(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA ASC '
+                              'WHERE X is Personne, X nom AA, X connait AB?')
             # testing optional relation
             peschema.subject_relation('travaille').set_rproperty(peschema, seschema, 'cardinality', '?*')
             Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
@@ -174,21 +173,54 @@
             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))
         Personne.fetch_attrs, Personne.fetch_order = fetch_config(('nom', 'type'))
         Note.fetch_attrs, Note.fetch_order = fetch_config(('type',))
-        aff = self.add_entity('Personne', nom=u'pouet')
-        self.assertEquals(aff.related_rql('evaluee'),
+        p = self.add_entity('Personne', nom=u'pouet')
+        self.assertEquals(p.related_rql('evaluee'),
                           'Any X,AA,AB ORDERBY AA ASC WHERE E eid %(x)s, E evaluee X, '
                           'X type AA, X modification_date AB')
         Personne.fetch_attrs, Personne.fetch_order = fetch_config(('nom', ))
         # XXX
-        self.assertEquals(aff.related_rql('evaluee'),
+        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]
@@ -200,15 +232,41 @@
         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):
-        e = self.etype_instance('CWUser')
+    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.vreg['etypes'].etype_class('CWUser')(self.request())
         unrelated = [r[0] for r in e.unrelated('in_group', 'CWGroup', 'subject')]
         # should be default groups but owners, i.e. managers, users, guests
         self.assertEquals(len(unrelated), 3)
@@ -227,7 +285,6 @@
         self.assertEquals(e.printable_value('content'),
                           '<p>\ndu *texte*\n</p>')
         e['title'] = 'zou'
-        #e = self.etype_instance('Task')
         e['content'] = '''\
 a title
 =======
@@ -291,6 +348,18 @@
         e['content'] = u'C&apos;est un exemple s&eacute;rieux'
         self.assertEquals(tidy(e.printable_value('content')),
                           u"C'est un exemple sérieux")
+        e['content'] = u'<div x:foo="bar">ms orifice produces weird html</div>'
+        self.assertEquals(tidy(e.printable_value('content')),
+                          u'<div>ms orifice produces weird html</div>')
+        import tidy as tidymod # apt-get install python-tidy
+        tidy = lambda x: str(tidymod.parseString(x.encode('utf-8'),
+                                                 **{'drop_proprietary_attributes': True,
+                                                    'output_xhtml': True,
+                                                    'show_body_only' : True,
+                                                    'quote-nbsp' : False,
+                                                    'char_encoding' : 'utf8'})).decode('utf-8').strip()
+        self.assertEquals(tidy(e.printable_value('content')),
+                          u'<div>ms orifice produces weird html</div>')
         # make sure valid xhtml is left untouched
         e['content'] = u'<div>R&amp;D<br/></div>'
         self.assertEquals(e.printable_value('content'), e['content'])
@@ -303,7 +372,7 @@
 
 
     def test_fulltextindex(self):
-        e = self.etype_instance('File')
+        e = self.vreg['etypes'].etype_class('File')(self.request())
         e['name'] = 'an html file'
         e['description'] = 'du <em>html</em>'
         e['description_format'] = 'text/html'
@@ -322,33 +391,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()
+        session = self.session
+        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	Tue Sep 22 13:08:42 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_rset.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/test/unittest_rset.py	Tue Sep 22 13:08:42 2009 +0200
@@ -10,7 +10,7 @@
 
 from logilab.common.testlib import TestCase, unittest_main
 
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.selectors import traced_selection
 
 from urlparse import urlsplit
@@ -55,7 +55,7 @@
 
 
 
-class ResultSetTC(EnvBasedTC):
+class ResultSetTC(CubicWebTC):
 
     def setUp(self):
         super(ResultSetTC, self).setUp()
@@ -100,7 +100,7 @@
                        'Any U,L where U is CWUser, U login L',
                        description=[['CWUser', 'String']] * 3)
         rs.req = self.request()
-        rs.vreg = self.env.vreg
+        rs.vreg = self.vreg
 
         self.assertEquals(rs.limit(2).rows, [[12000, 'adim'], [13000, 'syt']])
         rs2 = rs.limit(2, offset=1)
@@ -115,7 +115,7 @@
                        'Any U,L where U is CWUser, U login L',
                        description=[['CWUser', 'String']] * 3)
         rs.req = self.request()
-        rs.vreg = self.env.vreg
+        rs.vreg = self.vreg
         def test_filter(entity):
             return entity.login != 'nico'
 
@@ -140,7 +140,7 @@
                        'Any U,L where U is CWUser, U login L',
                        description=[['CWUser', 'String']] * 3)
         rs.req = self.request()
-        rs.vreg = self.env.vreg
+        rs.vreg = self.vreg
 
         rs2 = rs.sorted_rset(lambda e:e['login'])
         self.assertEquals(len(rs2), 3)
@@ -170,7 +170,7 @@
                        'D created_by U, D title T',
                        description=[['CWUser', 'String', 'String']] * 5)
         rs.req = self.request()
-        rs.vreg = self.env.vreg
+        rs.vreg = self.vreg
 
         rsets = rs.split_rset(lambda e:e['login'])
         self.assertEquals(len(rsets), 3)
@@ -328,6 +328,7 @@
         entity, rtype = rset.related_entity(1, 1)
         self.assertEquals(entity.id, 'CWGroup')
         self.assertEquals(rtype, 'name')
+        #
         rset = self.execute('Any X,N ORDERBY N WHERE X is Bookmark WITH X,N BEING '
                             '((Any X,N WHERE X is CWGroup, X name N)'
                             ' UNION '
@@ -335,6 +336,14 @@
         entity, rtype = rset.related_entity(0, 1)
         self.assertEquals(entity.eid, e.eid)
         self.assertEquals(rtype, 'title')
+        #
+        rset = self.execute('Any X,N ORDERBY N WITH N,X BEING '
+                            '((Any N,X WHERE X is CWGroup, X name N)'
+                            ' UNION '
+                            ' (Any N,X WHERE X is Bookmark, X title N))')
+        entity, rtype = rset.related_entity(0, 1)
+        self.assertEquals(entity.eid, e.eid)
+        self.assertEquals(rtype, 'title')
 
     def test_entities(self):
         rset = self.execute('Any U,G WHERE U in_group G')
--- a/test/unittest_schema.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/test/unittest_schema.py	Tue Sep 22 13:08:42 2009 +0200
@@ -20,7 +20,7 @@
 
 from cubicweb.schema import CubicWebSchema, CubicWebEntitySchema, \
      RQLConstraint, CubicWebSchemaLoader, ERQLExpression, RRQLExpression, \
-     normalize_expression
+     normalize_expression, order_eschemas
 from cubicweb.devtools import TestServerConfiguration as TestConfiguration
 
 DATADIR = join(dirname(__file__), 'data')
@@ -126,13 +126,18 @@
         expr = RRQLExpression('U has_update_permission O')
         self.assertEquals(str(expr), 'Any O WHERE U has_update_permission O, O eid %(o)s, U eid %(u)s')
 
-
 loader = CubicWebSchemaLoader()
 config = TestConfiguration('data')
 config.bootstrap_cubes()
-loader.lib_directory = config.schemas_lib_dir()
+
+class SchemaReaderClassTest(TestCase):
 
-class SQLSchemaReaderClassTest(TestCase):
+    def test_order_eschemas(self):
+        schema = loader.load(config)
+        self.assertEquals(order_eschemas([schema['Note'], schema['SubNote']]),
+                                         [schema['Note'], schema['SubNote']])
+        self.assertEquals(order_eschemas([schema['SubNote'], schema['Note']]),
+                                         [schema['Note'], schema['SubNote']])
 
     def test_knownValues_load_schema(self):
         schema = loader.load(config)
@@ -140,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',
@@ -148,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',
@@ -169,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',
@@ -180,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',
 
@@ -192,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_selectors.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/test/unittest_selectors.py	Tue Sep 22 13:08:42 2009 +0200
@@ -8,7 +8,7 @@
 
 from logilab.common.testlib import TestCase, unittest_main
 
-from cubicweb.devtools.testlib import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.appobject import Selector, AndSelector, OrSelector
 from cubicweb.selectors import implements, match_user_groups
 from cubicweb.interfaces import IDownloadable
@@ -88,7 +88,7 @@
         self.assertIs(csel.search_selector(implements), sel)
 
 
-class ImplementsSelectorTC(EnvBasedTC):
+class ImplementsSelectorTC(CubicWebTC):
     def test_etype_priority(self):
         req = self.request()
         cls = self.vreg['etypes'].etype_class('File')
@@ -103,7 +103,7 @@
         self.failIf(implements('Societe').score_class(cls, self.request()))
 
 
-class MatchUserGroupsTC(EnvBasedTC):
+class MatchUserGroupsTC(CubicWebTC):
     def test_owners_group(self):
         """tests usage of 'owners' group with match_user_group"""
         class SomeAction(action.Action):
@@ -118,16 +118,16 @@
             self.create_user('john')
             self.login('john')
             # it should not be possible to use SomeAction not owned objects
-            rset, req = self.env.get_rset_and_req('Any G WHERE G is CWGroup, G name "managers"')
+            rset, req = self.rset_and_req('Any G WHERE G is CWGroup, G name "managers"')
             self.failIf('yo' in dict(self.pactions(req, rset)))
             # insert a new card, and check that we can use SomeAction on our object
             self.execute('INSERT Card C: C title "zoubidou"')
             self.commit()
-            rset, req = self.env.get_rset_and_req('Card C WHERE C title "zoubidou"')
+            rset, req = self.rset_and_req('Card C WHERE C title "zoubidou"')
             self.failUnless('yo' in dict(self.pactions(req, rset)), self.pactions(req, rset))
             # make sure even managers can't use the action
             self.restore_connection()
-            rset, req = self.env.get_rset_and_req('Card C WHERE C title "zoubidou"')
+            rset, req = self.rset_and_req('Card C WHERE C title "zoubidou"')
             self.failIf('yo' in dict(self.pactions(req, rset)))
         finally:
             del self.vreg[SomeAction.__registry__][SomeAction.id]
--- a/test/unittest_utils.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/test/unittest_utils.py	Tue Sep 22 13:08:42 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):
@@ -17,12 +21,12 @@
         self.assertNotEquals(make_uid('xyz'), make_uid('xyz'))
 
     def test_2(self):
-        d = {}
+        d = set()
         while len(d)<10000:
             uid = make_uid('xyz')
-            if d.has_key(uid):
+            if uid in d:
                 self.fail(len(d))
-            d[uid] = 1
+            d.add(uid)
 
 
 class UStringIOTC(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/test/unittest_vregistry.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/test/unittest_vregistry.py	Tue Sep 22 13:08:42 2009 +0200
@@ -40,14 +40,6 @@
         self.vreg.initialization_completed()
         self.assertEquals(len(self.vreg['views']['primary']), 1)
 
-    def test___selectors__compat(self):
-        myselector1 = lambda *args: 1
-        myselector2 = lambda *args: 1
-        class AnAppObject(AppObject):
-            __selectors__ = (myselector1, myselector2)
-        AnAppObject.build___select__()
-        self.assertEquals(AnAppObject.__select__(AnAppObject), 2)
-
     def test_properties(self):
         self.failIf('system.version.cubicweb' in self.vreg['propertydefs'])
         self.failUnless(self.vreg.property_info('system.version.cubicweb'))
--- a/toolsutils.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/toolsutils.py	Tue Sep 22 13:08:42 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
@@ -21,6 +27,9 @@
 from cubicweb import warning
 from cubicweb import ConfigurationError, ExecutionError
 
+def underline_title(title, car='-'):
+    return title+'\n'+(car*len(title))
+
 def iter_dir(directory, condition_file=None, ignore=()):
     """iterate on a directory"""
     for sub in listdir(directory):
--- a/utils.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/utils.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,12 +7,20 @@
 """
 __docformat__ = "restructuredtext en"
 
+from logilab.mtconverter import xml_escape
+
 import locale
+import sys
+import decimal
+import datetime as pydatetime
 from md5 import md5
 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()
@@ -101,10 +109,19 @@
     encoding = locale.getpreferredencoding(do_setlocale=False) or 'UTF-8'
     return unicode(date.strftime(str(fmt)), encoding)
 
-def make_uid(key):
-    """forge a unique identifier"""
-    msg = str(key) + "%.10f" % time() + str(randint(0, 1000000))
-    return md5(msg).hexdigest()
+
+if sys.version_info[:2] < (2, 5):
+    def make_uid(key):
+        """forge a unique identifier
+        not that unique on win32"""
+        msg = str(key) + "%.10f" % time() + str(randint(0, 1000000))
+        return md5(msg).hexdigest()
+else:
+    from uuid import uuid4
+    def make_uid(key):
+        # remove dash, generated uid are used as identifier sometimes (sql table
+        # names at least)
+        return str(key) + str(uuid4()).replace('-', '')
 
 
 def dump_class(cls, clsname):
@@ -254,7 +271,6 @@
         w = self.write
         # 1/ variable declaration if any
         if self.jsvars:
-            from simplejson import dumps
             w(u'<script type="text/javascript"><!--//--><![CDATA[//><!--\n')
             for var, value in self.jsvars:
                 w(u'%s = %s;\n' % (var, dumps(value)))
@@ -262,17 +278,18 @@
         # 2/ css files
         for cssfile, media in self.cssfiles:
             w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
-              (media, cssfile))
+              (media, xml_escape(cssfile)))
         # 3/ ie css if necessary
         if self.ie_cssfiles:
             w(u'<!--[if lt IE 8]>\n')
             for cssfile, media in self.ie_cssfiles:
                 w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
-                  (media, cssfile))
+                  (media, xml_escape(cssfile)))
             w(u'<![endif]--> \n')
         # 4/ js files
         for jsfile in self.jsfiles:
-            w(u'<script type="text/javascript" src="%s"></script>\n' % jsfile)
+            w(u'<script type="text/javascript" src="%s"></script>\n' %
+              xml_escape(jsfile))
         # 5/ post inlined scripts (i.e. scripts depending on other JS files)
         if self.post_inlined_scripts:
             w(u'<script type="text/javascript">\n')
@@ -305,7 +322,8 @@
         self.htmltag = u'<html xmlns="http://www.w3.org/1999/xhtml" ' \
                        'xmlns:cubicweb="http://www.logilab.org/2008/cubicweb" ' \
                        'xml:lang="%s" lang="%s">' % (req.lang, req.lang)
-
+        # keep main_stream's reference on req for easier text/html demoting
+        req.main_stream = self
 
     def write(self, data):
         """StringIO interface: this method will be assigned to self.w
@@ -320,9 +338,51 @@
                                                  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
 
+try:
+    # may not be there is cubicweb-web not there
+    from simplejson import JSONEncoder, dumps
+except ImportError:
+    pass
+else:
+    class CubicWebJsonEncoder(JSONEncoder):
+        """define a simplejson encoder to be able to encode yams std types"""
+        def default(self, obj):
+            if isinstance(obj, datetime):
+                return obj.strftime('%Y/%m/%d %H:%M:%S')
+            elif isinstance(obj, 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/view.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/view.py	Tue Sep 22 13:08:42 2009 +0200
@@ -17,7 +17,6 @@
 
 from cubicweb import NotAnEntity
 from cubicweb.selectors import yes, non_final_entity, nonempty_rset, none_rset
-from cubicweb.selectors import require_group_compat, accepts_compat
 from cubicweb.appobject import AppObject
 from cubicweb.utils import UStringIO, HTMLStream
 from cubicweb.schema import display_name
@@ -93,7 +92,6 @@
     time to a write function to use.
     """
     __registry__ = 'views'
-    registered = require_group_compat(AppObject.registered)
 
     templatable = True
     need_navigation = True
@@ -103,7 +101,7 @@
     category = 'view'
 
     def __init__(self, req=None, rset=None, **kwargs):
-        super(View, self).__init__(req, rset, **kwargs)
+        super(View, self).__init__(req, rset=rset, **kwargs)
         self.w = None
 
     @property
@@ -151,7 +149,20 @@
         if stream is not None:
             return self._stream.getvalue()
 
-    dispatch = deprecated('.dispatch is deprecated, use .render')(render)
+    def tal_render(self, template, variables):
+        """render a precompiled page template with variables in the given
+        dictionary as context
+        """
+        from cubicweb.ext.tal import CubicWebContext
+        context = CubicWebContext()
+        context.update({'self': self, 'rset': self.rset, '_' : self.req._,
+                        'req': self.req, 'user': self.req.user})
+        context.update(variables)
+        output = UStringIO()
+        template.expand(context, output)
+        return output.getvalue()
+
+    dispatch = deprecated('[3.4] .dispatch is deprecated, use .render')(render)
 
     # should default .call() method add a <div classs="section"> around each
     # rset item
@@ -230,7 +241,7 @@
         self.view(__vid, rset, __fallback_vid, w=self.w, **kwargs)
 
     # XXX Template bw compat
-    template = deprecated('.template is deprecated, use .view')(wview)
+    template = deprecated('[3.4] .template is deprecated, use .view')(wview)
 
     def whead(self, data):
         self.req.html_headers.write(data)
@@ -302,7 +313,7 @@
             w = self.w
         if row:
             w(u'<div class="row">')
-        if show_label:
+        if show_label and label:
             if tr:
                 label = display_name(self.req, label)
             w(u'<span class="label">%s</span>' % label)
@@ -317,8 +328,6 @@
 class EntityView(View):
     """base class for views applying on an entity (i.e. uniform result set)"""
     __select__ = non_final_entity()
-    registered = accepts_compat(View.registered)
-
     category = 'entityview'
 
 
@@ -327,7 +336,6 @@
     displayed (so they can always be displayed !)
     """
     __select__ = none_rset()
-    registered = require_group_compat(View.registered)
 
     category = 'startupview'
 
@@ -349,7 +357,7 @@
     default_rql = None
 
     def __init__(self, req, rset=None, **kwargs):
-        super(EntityStartupView, self).__init__(req, rset, **kwargs)
+        super(EntityStartupView, self).__init__(req, rset=rset, **kwargs)
         if rset is None:
             # this instance is not in the "entityview" category
             self.category = 'startupview'
@@ -400,7 +408,6 @@
     There is usually at least a regular main template and a simple fallback
     one to display error if the first one failed
     """
-    registered = require_group_compat(View.registered)
 
     @property
     def doctype(self):
@@ -466,10 +473,12 @@
     """base class for components"""
     __registry__ = 'components'
     __select__ = yes()
-    property_defs = {}
 
+    # XXX huummm, much probably useless
+    htmlclass = 'mainRelated'
     def div_class(self):
-        return '%s %s' % (self.propval('htmlclass'), self.id)
+        return '%s %s' % (self.htmlclass, self.id)
 
+    # XXX a generic '%s%s' % (self.id, self.__registry__.capitalize()) would probably be nicer
     def div_id(self):
         return '%sComponent' % self.id
--- a/vregistry.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/vregistry.py	Tue Sep 22 13:08:42 2009 +0200
@@ -54,6 +54,20 @@
     return _toload
 
 
+def classid(cls):
+    """returns a unique identifier for an appobject class"""
+    return '%s.%s' % (cls.__module__, cls.__name__)
+
+def class_regid(cls):
+    """returns a unique identifier for an appobject class"""
+    if 'id' in cls.__dict__:
+        warn('%s: id is deprecated, use __id__')
+        cls.__id__ = cls.id
+    if hasattr(cls, 'id'):
+        return cls.id
+    return cls.__id__
+
+
 class Registry(dict):
 
     def __init__(self, config):
@@ -72,15 +86,13 @@
     def register(self, obj, oid=None, clear=False):
         """base method to add an object in the registry"""
         assert not '__abstract__' in obj.__dict__
-        oid = oid or obj.id
+        oid = oid or class_regid(obj)
         assert oid
         if clear:
             appobjects = self[oid] =  []
         else:
             appobjects = self.setdefault(oid, [])
-        # registered() is technically a classmethod but is not declared
-        # as such because we need to compose registered in some cases
-        appobject = obj.registered.im_func(obj, self)
+        appobject = obj.__registered__(self)
         assert not appobject in appobjects, \
                'object %s is already registered' % appobject
         assert callable(appobject.__select__), appobject
@@ -90,11 +102,11 @@
         # XXXFIXME this is a duplication of unregister()
         # remove register_and_replace in favor of unregister + register
         # or simplify by calling unregister then register here
-        if hasattr(replaced, 'classid'):
-            replaced = replaced.classid()
-        registered_objs = self.get(obj.id, ())
+        if not isinstance(replaced, basestring):
+            replaced = classid(replaced)
+        registered_objs = self.get(class_regid(obj), ())
         for index, registered in enumerate(registered_objs):
-            if registered.classid() == replaced:
+            if classid(registered) == replaced:
                 del registered_objs[index]
                 break
         else:
@@ -103,17 +115,18 @@
         self.register(obj)
 
     def unregister(self, obj):
-        oid = obj.classid()
-        for registered in self.get(obj.id, ()):
+        clsid = classid(obj)
+        oid = class_regid(obj)
+        for registered in self.get(oid, ()):
             # use classid() to compare classes because vreg will probably
             # have its own version of the class, loaded through execfile
-            if registered.classid() == oid:
+            if classid(registered) == clsid:
                 # XXX automatic reloading management
-                self[obj.id].remove(registered)
+                self[oid].remove(registered)
                 break
         else:
             self.warning('can\'t remove %s, no id %s in the registry',
-                         oid, obj.id)
+                         clsid, oid)
 
     def all_objects(self):
         """return a list containing all objects in this registry.
@@ -125,6 +138,7 @@
 
     # dynamic selection methods ################################################
 
+    @deprecated('[3.6] use select instead of object_by_id')
     def object_by_id(self, oid, *args, **kwargs):
         """return object with the given oid. Only one object is expected to be
         found.
@@ -143,9 +157,9 @@
         raise `ObjectNotFound` if not object with id <oid> in <registry>
         raise `NoSelectableObject` if not object apply
         """
-        return self.select_best(self[oid], *args, **kwargs)
+        return self._select_best(self[oid], *args, **kwargs)
 
-    def select_object(self, oid, *args, **kwargs):
+    def select_or_none(self, oid, *args, **kwargs):
         """return the most specific object among those with the given oid
         according to the given context, or None if no object applies.
         """
@@ -153,6 +167,8 @@
             return self.select(oid, *args, **kwargs)
         except (NoSelectableObject, ObjectNotFound):
             return None
+    select_object = deprecated('[3.6] use select_or_none instead of select_object'
+                               )(select_or_none)
 
     def possible_objects(self, *args, **kwargs):
         """return an iterator on possible objects in this registry for the given
@@ -160,18 +176,18 @@
         """
         for appobjects in self.itervalues():
             try:
-                yield self.select_best(appobjects, *args, **kwargs)
+                yield self._select_best(appobjects, *args, **kwargs)
             except NoSelectableObject:
                 continue
 
-    def select_best(self, appobjects, *args, **kwargs):
+    def _select_best(self, appobjects, *args, **kwargs):
         """return an instance of the most specific object according
         to parameters
 
         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:
@@ -195,6 +211,8 @@
         # return the result of calling the appobject
         return winners[0](*args, **kwargs)
 
+    select_best = deprecated('[3.6] select_best is now private')(_select_best)
+
 
 class VRegistry(dict):
     """class responsible to register, propose and select the various
@@ -206,8 +224,10 @@
         super(VRegistry, self).__init__()
         self.config = config
 
-    def reset(self, force_reload=None):
-        self.clear()
+    def reset(self, path=None, force_reload=None):
+        # don't use self.clear, we want to keep existing subdictionaries
+        for subdict in self.itervalues():
+            subdict.clear()
         self._lastmodifs = {}
 
     def __getitem__(self, name):
@@ -221,7 +241,7 @@
 
     # dynamic selection methods ################################################
 
-    @deprecated('use vreg[registry].object_by_id(oid, *args, **kwargs)')
+    @deprecated('[3.4] use vreg[registry].object_by_id(oid, *args, **kwargs)')
     def object_by_id(self, registry, oid, *args, **kwargs):
         """return object in <registry>.<oid>
 
@@ -230,7 +250,7 @@
         """
         return self[registry].object_by_id(oid)
 
-    @deprecated('use vreg[registry].select(oid, *args, **kwargs)')
+    @deprecated('[3.4] use vreg[registry].select(oid, *args, **kwargs)')
     def select(self, registry, oid, *args, **kwargs):
         """return the most specific object in <registry>.<oid> according to
         the given context
@@ -240,14 +260,14 @@
         """
         return self[registry].select(oid, *args, **kwargs)
 
-    @deprecated('use vreg[registry].select_object(oid, *args, **kwargs)')
+    @deprecated('[3.4] use vreg[registry].select_or_none(oid, *args, **kwargs)')
     def select_object(self, registry, oid, *args, **kwargs):
         """return the most specific object in <registry>.<oid> according to
         the given context, or None if no object apply
         """
-        return self[registry].select_object(oid, *args, **kwargs)
+        return self[registry].select_or_none(oid, *args, **kwargs)
 
-    @deprecated('use vreg[registry].possible_objects(*args, **kwargs)')
+    @deprecated('[3.4] use vreg[registry].possible_objects(*args, **kwargs)')
     def possible_objects(self, registry, *args, **kwargs):
         """return an iterator on possible objects in <registry> for the given
         context
@@ -281,11 +301,12 @@
             try:
                 if obj.__module__ != modname or obj in butclasses:
                     continue
-                oid = obj.id
+                oid = class_regid(obj)
+                registryname = obj.__registry__
             except AttributeError:
                 continue
             if oid and not '__abstract__' in obj.__dict__:
-                self.register(obj)
+                self.register(obj, registryname)
 
     def register(self, obj, registryname=None, oid=None, clear=False):
         """base method to add an object in the registry"""
@@ -298,8 +319,8 @@
         except AttributeError:
             vname = obj.__class__.__name__
         self.debug('registered appobject %s in registry %s with id %s',
-                   vname, registryname, oid)
-        self._loadedmods[obj.__module__]['%s.%s' % (obj.__module__, oid)] = obj
+                   vname, registryname, oid or class_regid(obj))
+        self._loadedmods[obj.__module__][classid(obj)] = obj
 
     def unregister(self, obj, registryname=None):
         self[registryname or obj.__registry__].unregister(obj)
@@ -318,14 +339,7 @@
         self._loadedmods = {}
         return filemods
 
-    def register_objects(self, path, force_reload=None, extrapath=None):
-        if force_reload is None:
-            force_reload = self.config.mode == 'dev'
-        elif not force_reload:
-            # force_reload == False usually mean modules have been reloaded
-            # by another connection, so we want to update the registry
-            # content even if there has been no module content modification
-            self.reset()
+    def register_objects(self, path, force_reload, extrapath=None):
         # need to clean sys.path this to avoid import confusion pb (i.e.
         # having the same module loaded as 'cubicweb.web.views' subpackage and
         # as views'  or 'web.views' subpackage
@@ -395,10 +409,10 @@
                 return
         except TypeError:
             return
-        objname = '%s.%s' % (modname, obj.__name__)
-        if objname in self._loadedmods[modname]:
+        clsid = classid(obj)
+        if clsid in self._loadedmods[modname]:
             return
-        self._loadedmods[modname][objname] = obj
+        self._loadedmods[modname][clsid] = obj
         for parent in obj.__bases__:
             self._load_ancestors_then_object(modname, parent)
         self.load_object(obj)
@@ -422,10 +436,10 @@
         to a non empty string to be registered.
         """
         if (cls.__dict__.get('__abstract__') or cls.__name__[0] == '_'
-            or not cls.__registry__ or not cls.id):
+            or not cls.__registry__ or not class_regid(cls)):
             return
         regname = cls.__registry__
-        if '%s.%s' % (regname, cls.id) in self.config['disable-appobjects']:
+        if '%s.%s' % (regname, class_regid(cls)) in self.config['disable-appobjects']:
             return
         self.register(cls)
 
@@ -438,11 +452,11 @@
 
 from cubicweb.appobject import objectify_selector, AndSelector, OrSelector, Selector
 
-objectify_selector = deprecated('objectify_selector has been moved to appobject module')(objectify_selector)
+objectify_selector = deprecated('[3.4] objectify_selector has been moved to appobject module')(objectify_selector)
 
 Selector = class_moved(Selector)
 
-@deprecated('use & operator (binary and)')
+@deprecated('[3.4] use & operator (binary and)')
 def chainall(*selectors, **kwargs):
     """return a selector chaining given selectors. If one of
     the selectors fail, selection will fail, else the returned score
@@ -455,7 +469,7 @@
         selector.__name__ = kwargs['name']
     return selector
 
-@deprecated('use | operator (binary or)')
+@deprecated('[3.4] use | operator (binary or)')
 def chainfirst(*selectors, **kwargs):
     """return a selector chaining given selectors. If all
     the selectors fail, selection will fail, else the returned score
--- a/web/__init__.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/__init__.py	Tue Sep 22 13:08:42 2009 +0200
@@ -13,10 +13,10 @@
 from decimal import Decimal
 from datetime import datetime, date, timedelta
 from simplejson import dumps
+from urllib import quote as urlquote
 
 from logilab.common.deprecation import deprecated
 
-from cubicweb.common.uilib import urlquote
 from cubicweb.web._exceptions import *
 
 
@@ -65,7 +65,7 @@
         return json_dumps(function(*args, **kwargs))
     return newfunc
 
-@deprecated('use req.build_ajax_replace_url() instead')
+@deprecated('[3.4] use req.build_ajax_replace_url() instead')
 def ajax_replace_url(nodeid, rql, vid=None, swap=False, **extraparams):
     """builds a replacePageChunk-like url
     >>> ajax_replace_url('foo', 'Person P')
--- a/web/action.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/action.py	Tue Sep 22 13:08:42 2009 +0200
@@ -6,15 +6,13 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 __docformat__ = "restructuredtext en"
+_ = unicode
 
 from cubicweb import target
 from cubicweb.selectors import (partial_relation_possible, match_search_state,
-                                one_line_rset, partial_may_add_relation, yes,
-                                accepts_compat, condition_compat, deprecate)
+                                one_line_rset, partial_may_add_relation, yes)
 from cubicweb.appobject import AppObject
 
-_ = unicode
-
 
 class Action(AppObject):
     """abstract action. Handle the .search_states attribute to match
@@ -23,7 +21,7 @@
     __registry__ = 'actions'
     __select__ = match_search_state('normal')
 
-    property_defs = {
+    cw_property_defs = {
         'visible':  dict(type='Boolean', default=True,
                          help=_('display the action or not')),
         'order':    dict(type='Int', default=99,
@@ -33,8 +31,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"""
@@ -46,6 +54,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
@@ -55,7 +66,7 @@
     id = None
 
     def __init__(self, req, rset, title, path, **kwargs):
-        Action.__init__(self, req, rset)
+        Action.__init__(self, req, rset=rset)
         self.title = req._(title)
         self._path = path
         self.__dict__.update(kwargs)
@@ -74,22 +85,13 @@
     __select__ = (match_search_state('normal') & one_line_rset()
                   & partial_relation_possible(action='add')
                   & 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', ''))
 
-class EntityAction(Action):
-    """DEPRECATED / BACKWARD COMPAT
-    """
-    registered = deprecate(condition_compat(accepts_compat(Action.registered)),
-                           msg='EntityAction is deprecated, use Action with '
-                           'appropriate selectors')
-
--- a/web/application.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/application.py	Tue Sep 22 13:08:42 2009 +0200
@@ -31,21 +31,17 @@
     """manage session data associated to a session identifier"""
     id = 'sessionmanager'
 
-    def __init__(self):
-        self.session_time = self.vreg.config['http-session-time'] or None
+    def __init__(self, vreg):
+        self.session_time = vreg.config['http-session-time'] or None
         assert self.session_time is None or self.session_time > 0
-        self.cleanup_session_time = self.vreg.config['cleanup-session-time'] or 43200
+        self.cleanup_session_time = vreg.config['cleanup-session-time'] or 43200
         assert self.cleanup_session_time > 0
-        self.cleanup_anon_session_time = self.vreg.config['cleanup-anonymous-session-time'] or 120
+        self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 120
         assert self.cleanup_anon_session_time > 0
         if self.session_time:
             assert self.cleanup_session_time < self.session_time
             assert self.cleanup_anon_session_time < self.session_time
-        self.set_authmanager()
-        CW_EVENT_MANAGER.bind('after-source-reload', self.set_authmanager)
-
-    def set_authmanager(self):
-        self.authmanager = self.vreg['components'].select('authmanager')
+        self.authmanager = vreg['components'].select('authmanager', vreg=vreg)
 
     def clean_sessions(self):
         """cleanup sessions which has not been unused since a given amount of
@@ -96,6 +92,10 @@
 class AbstractAuthenticationManager(component.Component):
     """authenticate user associated to a request and check session validity"""
     id = 'authmanager'
+    vreg = None # XXX necessary until property for deprecation warning is on appobject
+
+    def __init__(self, vreg):
+        self.vreg = vreg
 
     def authenticate(self, req):
         """authenticate user and return corresponding user object
@@ -116,11 +116,22 @@
     SESSION_VAR = '__session'
 
     def __init__(self, appli):
-        self.session_manager = appli.vreg['components'].select('sessionmanager')
+        self.vreg = appli.vreg
+        self.session_manager = self.vreg['components'].select('sessionmanager',
+                                                              vreg=self.vreg)
         global SESSION_MANAGER
         SESSION_MANAGER = self.session_manager
-        if not 'last_login_time' in appli.vreg.schema:
+        if not 'last_login_time' in self.vreg.schema:
             self._update_last_login_time = lambda x: None
+        CW_EVENT_MANAGER.bind('after-registry-reload', self.reset_session_manager)
+
+    def reset_session_manager(self):
+        data = self.session_manager.dump_data()
+        self.session_manager = self.vreg['components'].select('sessionmanager',
+                                                              vreg=self.vreg)
+        self.session_manager.restore_data(data)
+        global SESSION_MANAGER
+        SESSION_MANAGER = self.session_manager
 
     def clean_sessions(self):
         """cleanup sessions which has not been unused since a given amount of
@@ -244,10 +255,11 @@
         # instantiate session and url resolving helpers
         self.session_handler = session_handler_fact(self)
         self.set_urlresolver()
-        CW_EVENT_MANAGER.bind('after-source-reload', self.set_urlresolver)
+        CW_EVENT_MANAGER.bind('after-registry-reload', self.set_urlresolver)
 
     def set_urlresolver(self):
-        self.url_resolver = self.vreg['components'].select('urlpublisher')
+        self.url_resolver = self.vreg['components'].select('urlpublisher',
+                                                           vreg=self.vreg)
 
     def connect(self, req):
         """return a connection for a logged user object according to existing
@@ -280,7 +292,7 @@
             finally:
                 self._logfile_lock.release()
 
-    @deprecated("use vreg.select('controllers', ...)")
+    @deprecated("[3.4] use vreg['controllers'].select(...)")
     def select_controller(self, oid, req):
         try:
             return self.vreg['controllers'].select(oid, req=req, appli=self)
--- a/web/box.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/box.py	Tue Sep 22 13:08:42 2009 +0200
@@ -11,10 +11,9 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb import Unauthorized, role as get_role, target as get_target
+from cubicweb.schema import display_name
 from cubicweb.selectors import (one_line_rset,  primary_view,
-                                match_context_prop, partial_has_related_entities,
-                                accepts_compat, has_relation_compat,
-                                condition_compat, require_group_compat)
+                                match_context_prop, partial_has_related_entities)
 from cubicweb.view import View, ReloadableMixIn
 
 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
@@ -38,10 +37,9 @@
     """
     __registry__ = 'boxes'
     __select__ = match_context_prop()
-    registered = classmethod(require_group_compat(View.registered))
 
     categories_in_order = ()
-    property_defs = {
+    cw_property_defs = {
         _('visible'): dict(type='Boolean', default=True,
                            help=_('display the box or not')),
         _('order'):   dict(type='Int', default=99,
@@ -59,7 +57,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:
@@ -137,7 +136,6 @@
 class EntityBoxTemplate(BoxTemplate):
     """base class for boxes related to a single entity"""
     __select__ = BoxTemplate.__select__ & one_line_rset() & primary_view()
-    registered = accepts_compat(has_relation_compat(condition_compat(BoxTemplate.registered)))
     context = 'incontext'
 
     def call(self, row=0, col=0, **kwargs):
@@ -149,7 +147,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">')
@@ -168,13 +166,14 @@
 
     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)
-        count = self.w_related(box, entity)
-        if count:
+        related = self.related_boxitems(entity)
+        unrelated = self.unrelated_boxitems(entity)
+        box.extend(related)
+        if related and unrelated:
             box.append(BoxSeparator())
-        if not self.w_unrelated(box, entity):
-            del box.items[-1] # remove useless separator
+        box.extend(unrelated)
         box.render(self.w)
 
     def div_id(self):
@@ -191,22 +190,22 @@
                                                etarget.view('incontext'))
         return RawBoxItem(label, liclass=u'invisible')
 
-    def w_related(self, box, entity):
-        """appends existing relations to the `box`"""
+    def related_boxitems(self, entity):
         rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
-        related = self.related_entities(entity)
-        for etarget in related:
-            box.append(self.box_item(entity, etarget, rql, u'-'))
-        return len(related)
+        related = []
+        for etarget in self.related_entities(entity):
+            related.append(self.box_item(entity, etarget, rql, u'-'))
+        return related
 
-    def w_unrelated(self, box, entity):
-        """appends unrelated entities to the `box`"""
+    def unrelated_boxitems(self, entity):
         rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
-        i = 0
+        unrelated = []
         for etarget in self.unrelated_entities(entity):
-            box.append(self.box_item(entity, etarget, rql, u'+'))
-            i += 1
-        return i
+            unrelated.append(self.box_item(entity, etarget, rql, u'+'))
+        return unrelated
+
+    def related_entities(self, entity):
+        return entity.related(self.rtype, get_role(self), entities=True)
 
     def unrelated_entities(self, entity):
         """returns the list of unrelated entities
@@ -219,8 +218,8 @@
             return entity.unrelated(self.rtype, self.etype, get_role(self)).entities()
         # in other cases, use vocabulary functions
         entities = []
-        form = self.vreg.select('forms', 'edition', self.req, rset=self.rset,
-                                row=self.row or 0)
+        form = self.vreg['forms'].select('edition', self.req, rset=self.rset,
+                                         row=self.row or 0)
         field = form.field_by_name(self.rtype, get_role(self), entity.e_schema)
         for _, eid in form.form_field_vocabulary(field):
             if eid is not None:
@@ -228,6 +227,3 @@
                 entities.append(rset.get_entity(0, 0))
         return entities
 
-    def related_entities(self, entity):
-        return entity.related(self.rtype, get_role(self), entities=True)
-
--- a/web/component.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/component.py	Tue Sep 22 13:08:42 2009 +0200
@@ -8,16 +8,17 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from simplejson import dumps
+
 from logilab.common.deprecation import class_renamed
 from logilab.mtconverter import xml_escape
 
 from cubicweb import role
 from cubicweb.utils import merge_dicts
-from cubicweb.view import View, Component
+from cubicweb.view import Component
 from cubicweb.selectors import (
     paginated_rset, one_line_rset, primary_view, match_context_prop,
-    partial_has_related_entities, condition_compat, accepts_compat,
-    has_relation_compat)
+    partial_has_related_entities)
 
 
 class EntityVComponent(Component):
@@ -33,9 +34,8 @@
 
     __registry__ = 'contentnavigation'
     __select__ = one_line_rset() & primary_view() & match_context_prop()
-    registered = accepts_compat(has_relation_compat(condition_compat(View.registered)))
 
-    property_defs = {
+    cw_property_defs = {
         _('visible'):  dict(type='Boolean', default=True,
                             help=_('display the component or not')),
         _('order'):    dict(type='Int', default=99,
@@ -45,8 +45,6 @@
                                         _('navcontenttop'), _('navcontentbottom')),
                             #vocabulary=(_('header'), _('incontext'), _('footer')),
                             help=_('context where this component should be displayed')),
-        _('htmlclass'):dict(type='String', default='mainRelated',
-                            help=_('html class of the component')),
     }
 
     context = 'navcontentbottom' # 'footer' | 'header' | 'incontext'
@@ -63,7 +61,7 @@
     id = 'navigation'
     __select__ = paginated_rset()
 
-    property_defs = {
+    cw_property_defs = {
         _('visible'):  dict(type='Boolean', default=True,
                             help=_('display the component or not')),
         }
@@ -74,10 +72,11 @@
     page_link_templ = u'<span class="slice"><a href="%s" title="%s">%s</a></span>'
     selected_page_link_templ = u'<span class="selectedSlice"><a href="%s" title="%s">%s</a></span>'
     previous_page_link_templ = next_page_link_templ = page_link_templ
-    no_previous_page_link = no_next_page_link = u''
+    no_previous_page_link = u'&lt;&lt;'
+    no_next_page_link = u'&gt;&gt;'
 
     def __init__(self, req, rset, **kwargs):
-        super(NavigationComponent, self).__init__(req, rset, **kwargs)
+        super(NavigationComponent, self).__init__(req, rset=rset, **kwargs)
         self.starting_from = 0
         self.total = rset.rowcount
 
@@ -114,33 +113,40 @@
         if self.stop_param in params:
             del params[self.stop_param]
 
+    def page_url(self, path, params, start, stop):
+        params = merge_dicts(params, {self.start_param : start,
+                                      self.stop_param : stop,})
+        if path == 'json':
+            rql = params.pop('rql', self.rset.printable_rql())
+            # latest 'true' used for 'swap' mode
+            url = 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % (
+                dumps(params.get('divid', 'paginated-content')),
+                dumps(rql), dumps(params.pop('vid', None)), dumps(params))
+        else:
+            url = self.build_url(path, **params)
+        return url
+
     def page_link(self, path, params, start, stop, content):
-        url = self.build_url(path, **merge_dicts(params, {self.start_param : start,
-                                                          self.stop_param : stop,}))
-        url = xml_escape(url)
+        url = xml_escape(self.page_url(path, params, start, stop))
         if start == self.starting_from:
             return self.selected_page_link_templ % (url, content, content)
         return self.page_link_templ % (url, content, content)
 
-    def previous_link(self, params, content='&lt;&lt;', title=_('previous_results')):
+    def previous_link(self, path, params, content='&lt;&lt;', title=_('previous_results')):
         start = self.starting_from
         if not start :
             return self.no_previous_page_link
         start = max(0, start - self.page_size)
         stop = start + self.page_size - 1
-        url = self.build_url(**merge_dicts(params, {self.start_param : start,
-                                                    self.stop_param : stop,}))
-        url = xml_escape(url)
+        url = xml_escape(self.page_url(path, params, start, stop))
         return self.previous_page_link_templ % (url, title, content)
 
-    def next_link(self, params, content='&gt;&gt;', title=_('next_results')):
+    def next_link(self, path, params, content='&gt;&gt;', title=_('next_results')):
         start = self.starting_from + self.page_size
         if start >= self.total:
             return self.no_next_page_link
         stop = start + self.page_size - 1
-        url = self.build_url(**merge_dicts(params, {self.start_param : start,
-                                                    self.stop_param : stop,}))
-        url = xml_escape(url)
+        url = xml_escape(self.page_url(path, params, start, stop))
         return self.next_page_link_templ % (url, title, content)
 
 
--- a/web/controller.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/controller.py	Tue Sep 22 13:08:42 2009 +0200
@@ -11,7 +11,7 @@
 import datetime
 
 from cubicweb import typed_eid
-from cubicweb.selectors import yes, require_group_compat
+from cubicweb.selectors import yes
 from cubicweb.appobject import AppObject
 from cubicweb.web import LOGGER, Redirect, RequestError
 
@@ -68,7 +68,6 @@
     """
     __registry__ = 'controllers'
     __select__ = yes()
-    registered = require_group_compat(AppObject.registered)
 
     def __init__(self, *args, **kwargs):
         self.appli = kwargs.pop('appli', None)
@@ -88,15 +87,15 @@
     def process_rql(self, rql):
         """execute rql if specified"""
         # XXX assigning to self really necessary?
-        self.rset = None
+        self.cw_rset = None
         if rql:
-            self.ensure_ro_rql(rql)
+            self.req.ensure_ro_rql(rql)
             if not isinstance(rql, unicode):
                 rql = unicode(rql, self.req.encoding)
-            pp = self.vreg['components'].select_object('magicsearch', self.req)
+            pp = self.vreg['components'].select_or_none('magicsearch', self.req)
             if pp is not None:
-                self.rset = pp.process_query(rql, self.req)
-        return self.rset
+                self.cw_rset = pp.process_query(rql, self.req)
+        return self.cw_rset
 
     def check_expected_params(self, params):
         """check that the given list of parameters are specified in the form
--- a/web/data/cubicweb.ajax.js	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/data/cubicweb.ajax.js	Tue Sep 22 13:08:42 2009 +0200
@@ -51,7 +51,7 @@
     }
     // find textareas and wrap them if there are some
     if (typeof(FCKeditor) != 'undefined') {
-	buildWysiwygEditors(node);
+	buildWysiwygEditors();
     }
     if (typeof initFacetBoxEvents != 'undefined') {
 	initFacetBoxEvents(node);
@@ -63,10 +63,24 @@
 	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
+/* cubicweb loadxhtml plugin to make jquery handle xhtml response
+ *
+ * fetches `url` and replaces this's content with the result
+ *
+ * @param mode how the replacement should be done (default is 'replace')
+ *  Possible values are :
+ *    - 'replace' to replace the node's content with the generated HTML
+ *    - 'swap' to replace the node itself with the generated HTML
+ *    - 'append' to append the generated HTML to the node's content
+ */
 jQuery.fn.loadxhtml = function(url, data, reqtype, mode) {
     var ajax = null;
     if (reqtype == 'post') {
@@ -323,7 +337,7 @@
     }
 }
 
-/*
+/* XXX deprecates?
  * fetches `url` and replaces `nodeid`'s content with the result
  * @param replacemode how the replacement should be done (default is 'replace')
  *  Possible values are :
@@ -342,7 +356,10 @@
  */
 function buildWysiwygEditors(parent) {
     jQuery('textarea').each(function () {
-	if (this.getAttribute('cubicweb:type', 'wysiwyg')) {
+	if (this.getAttribute('cubicweb:type') == 'wysiwyg') {
+            // mark editor as instanciated, we may be called a number of times
+            // (see postAjaxLoad)
+            this.setAttribute('cubicweb:type', 'fckeditor');
 	    if (typeof FCKeditor != "undefined") {
 		var fck = new FCKeditor(this.id);
 		fck.Config['CustomConfigurationsPath'] = fckconfigpath;
@@ -375,24 +392,30 @@
     return stripped;
 }
 
-/* convenience function that returns a DOM node based on req's result. */
+/* convenience function that returns a DOM node based on req's result.
+ * XXX clarify the need to clone
+ * */
 function getDomFromResponse(response) {
     if (typeof(response) == 'string') {
-	return html2dom(response);
+	var doc = html2dom(response);
+    } else {
+        var doc = response.documentElement;
     }
-    var doc = response.documentElement;
     var children = doc.childNodes;
     if (!children.length) {
 	// no child (error cases) => return the whole document
-	return doc.cloneNode(true);
+	return jQuery(doc).clone().context;
     }
     children = stripEmptyTextNodes(children);
     if (children.length == 1) {
 	// only one child => return it
-	return children[0].cloneNode(true);
+	return jQuery(children[0]).clone().context;
     }
     // several children => wrap them in a single node and return the wrap
-    return DIV(null, map(methodcaller('cloneNode', true), children));
+    return DIV(null, map(function(node) {
+                           return jQuery(node).clone().context;
+                         },
+                         children));
 }
 
 function postJSON(url, data, callback) {
--- a/web/data/cubicweb.calendar.js	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/data/cubicweb.calendar.js	Tue Sep 22 13:08:42 2009 +0200
@@ -112,7 +112,8 @@
 	this.domtable = TABLE({'class': this.cssclass},
 			      THEAD(null, TR(null,
 					     TH(null, A({'href' : prevlink}, "<<")),
-					     TH({'colspan' : 5, 'style' : "text-align: center;"}, monthname),
+					     // IE 6/7 requires colSpan instead of colspan
+					     TH({'colSpan': 5, 'colspan':5, 'style' : "text-align: center;"}, monthname),
 					     TH(null, A({'href' : nextlink}, ">>")))),
 			      TBODY(null,
 				    this._headdisplay(),
--- a/web/data/cubicweb.calendar_popup.css	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/data/cubicweb.calendar_popup.css	Tue Sep 22 13:08:42 2009 +0200
@@ -24,17 +24,16 @@
 table.popupCalendar {
   text-align: center;
   border: 1px solid #ccc;
-  z-index: 100;
+  z-index: 400;
 }
 
-
 table.popupCalendar th {
+  border:1px solid #ccc;
   background : #d9d9c1;
   color: black;
   padding: 2px 3px;
 }
 
-
 table.popupCalendar th.calTitle,
 table.popupCalendar th.prev,
 table.popupCalendar th.next {
@@ -51,12 +50,12 @@
   padding: 2px 0px;
 }
 
-
 table.popupCalendar td {
   width: 2em;
   height: 2em;
   background : #f6f5e1;
   font-size: 85%;
+  border:1px solid #ccc;
 }
 
 table.popupCalendar td.today {
--- a/web/data/cubicweb.compat.js	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/data/cubicweb.compat.js	Tue Sep 22 13:08:42 2009 +0200
@@ -172,7 +172,7 @@
 		}
 		node[key] = value;
 	    } else { // normal node attribute
-		node.setAttribute(key, params[key]);
+		jQuery(node).attr(key, params[key]);
 	    }
 	}
 	if (children) {
--- a/web/data/cubicweb.css	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/data/cubicweb.css	Tue Sep 22 13:08:42 2009 +0200
@@ -256,13 +256,17 @@
 }
 
 /* Popup on login box and userActionBox */
+div.popupWrapper{
+ position:relative;
+ z-index:100;
+}
+
 div.popup {
   position: absolute;
-  z-index: 400;
   background: #fff;
   border: 1px solid black;
   text-align: left;
-  float:left;
+  z-index:400;
 }
 
 div.popup ul li a {
@@ -388,7 +392,7 @@
   padding: 1px 9px 1px 3px;
 }
 
-ul.boxListing a.selected {
+ul.boxListing .selected {
   color: #FF4500;
   font-weight: bold;
 }
@@ -522,6 +526,20 @@
   text-decoration: underline;
 }
 
+/* download box XXX move to its own file? */
+div.downloadBoxTitle{
+ background : #8FBC8F;
+ font-weight: bold;
+}
+
+div.downloadBox{
+ font-weight: bold;
+}
+
+div.downloadBox div.sideBoxBody{
+ background : #EEFED9;
+}
+
 /**************/
 /* navigation */
 /**************/
@@ -838,4 +856,12 @@
   border: 1px solid #edecd2;
   border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
   background: #fffff8 url("button.png") bottom left repeat-x;
-}
\ No newline at end of file
+}
+
+/********************************/
+/* placement of alt. view icons */
+/********************************/
+
+.otherView {
+  float: right;
+}
--- a/web/data/cubicweb.edition.js	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/data/cubicweb.edition.js	Tue Sep 22 13:08:42 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);
@@ -243,21 +243,27 @@
  * @param ttype : the target (inlined) entity type
  * @param rtype : the relation type between both entities
  */
-function addInlineCreationForm(peid, ttype, rtype, role) {
-    var d = asyncRemoteExec('inline_creation_form', peid, ttype, rtype, role);
+function addInlineCreationForm(peid, ttype, rtype, role, i18nctx, insertBefore) {
+    insertBefore = insertBefore || getNode('add' + rtype + ':' + peid + 'link').parentNode;
+    var d = asyncRemoteExec('inline_creation_form', peid, ttype, rtype, role, i18nctx);
     d.addCallback(function (response) {
-	var linknode = getNode('add' + rtype + ':' + peid + 'link');
         var dom = getDomFromResponse(response);
-	var form = jQuery(dom);
-	form.css('display', 'none');
-	form.insertBefore(linknode.parentNode).slideDown('fast');
-	updateInlinedEntitiesCounters(rtype);
-	reorderTabindex();
-	form.trigger('inlinedform-added');
+        preprocessAjaxLoad(null, dom);
+        var form = jQuery(dom);
+        form.css('display', 'none');
+        form.insertBefore(insertBefore).slideDown('fast');
+        updateInlinedEntitiesCounters(rtype);
+        reorderTabindex();
+        jQuery(CubicWeb).trigger('inlinedform-added', form);
+        // if the inlined form contains a file input, we must force
+        // the form enctype to multipart/form-data
+        if (form.find('input:file').length) {
+            form.closest('form').attr('enctype', 'multipart/form-data');
+        }
         postAjaxLoad(dom);
     });
     d.addErrback(function (xxx) {
-	log('xxx =', xxx);
+        log('xxx =', xxx);
     });
 }
 
@@ -344,15 +350,15 @@
 }
 
 
-function handleFormValidationResponse(formid, onsuccess, onfailure, result) {
+function handleFormValidationResponse(formid, onsuccess, onfailure, result, cbargs) {
     // Success
     if (result[0]) {
 	if (onsuccess) {
-             onsuccess(result[1], formid);
+             onsuccess(result, formid, cbargs);
 	} else {
 	    document.location.href = result[1];
 	}
-      return;
+      return true;
     }
     unfreezeFormButtons(formid);
     // Failures
@@ -362,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){
-	onfailure(formid);
+    if (onfailure) {
+	onfailure(formid, cbargs);
     }
-    return;
+    return false;
 }
 
 
@@ -475,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
@@ -486,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;
@@ -505,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
@@ -534,6 +537,8 @@
 }
 
 function hideInlineEdit(eid, rtype, divid) {
+    jQuery('#appMsg').hide();
+    jQuery('div.errorMessage').remove();
     jQuery('#' + divid).show();
     jQuery('#' + divid+'-form').hide();
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.facets.js	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,229 @@
+/*
+ *  :organization: Logilab
+ *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ */
+
+CubicWeb.require('htmlhelpers.js');
+CubicWeb.require('ajax.js');
+
+//============= filter form functions ========================================//
+function copyParam(origparams, newparams, param) {
+    var index = findValue(origparams[0], param);
+    if (index > -1) {
+	newparams[param] = origparams[1][index];
+    }
+}
+
+function facetFormContent(form) {
+    var names = [];
+    var values = [];
+    jQuery(form).find('.facet').each(function () {
+        var facetName = jQuery(this).find('.facetTitle').attr('cubicweb:facetName');
+        var facetValues = jQuery(this).find('.facetValueSelected').each(function(x) {
+  	    names.push(facetName);
+  	    values.push(this.getAttribute('cubicweb:value'));
+        });
+    });
+    jQuery(form).find('input').each(function () {
+        names.push(this.name);
+        values.push(this.value);
+    });
+    jQuery(form).find('select option[selected]').each(function () {
+	names.push(this.parentNode.name);
+	values.push(this.value);
+    });
+    return [names, values];
+}
+
+function buildRQL(divid, vid, paginate, vidargs) {
+    jQuery(CubicWeb).trigger('facets-content-loading', [divid, vid, paginate, vidargs]);
+    var form = getNode(divid+'Form');
+    var zipped = facetFormContent(form);
+    zipped[0].push('facetargs');
+    zipped[1].push(vidargs);
+    var d = asyncRemoteExec('filter_build_rql', zipped[0], zipped[1]);
+    d.addCallback(function(result) {
+	var rql = result[0];
+	var $bkLink = jQuery('#facetBkLink');
+	if ($bkLink.length) {
+	    var bkUrl = $bkLink.attr('cubicweb:target') + '&path=view?rql=' + rql;
+	    if (vid) {
+		bkUrl += '&vid=' + vid;
+	    }
+	    $bkLink.attr('href', bkUrl);
+	}
+	var toupdate = result[1];
+	var extraparams = vidargs;
+	var displayactions = jQuery('#' + divid).attr('cubicweb:displayactions');
+	if (displayactions) { extraparams['displayactions'] = displayactions; }
+	if (paginate) { extraparams['paginate'] = '1'; }
+	// copy some parameters
+	// XXX cleanup vid/divid mess
+	// if vid argument is specified , the one specified in form params will
+	// be overriden by replacePageChunk
+	copyParam(zipped, extraparams, 'vid');
+	extraparams['divid'] = divid;
+	copyParam(zipped, extraparams, 'divid');
+	copyParam(zipped, extraparams, 'subvid');
+	copyParam(zipped, extraparams, 'fromformfilter');
+	// paginate used to know if the filter box is acting, in which case we
+	// want to reload action box to match current selection (we don't want
+	// this from a table filter)
+	replacePageChunk(divid, rql, vid, extraparams, true, function() {
+	  jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
+	});
+	if (paginate) {
+	    // FIXME the edit box might not be displayed in which case we don't
+	    // know where to put the potential new one, just skip this case
+	    // for now
+	    if (jQuery('#edit_box').length) {
+		reloadComponent('edit_box', rql, 'boxes', 'edit_box');
+	    }
+	    if (jQuery('#breadcrumbs').length) {
+		reloadComponent('breadcrumbs', rql, 'components', 'breadcrumbs');
+	    }
+	}
+	var d = asyncRemoteExec('filter_select_content', toupdate, rql);
+	d.addCallback(function(updateMap) {
+	    for (facetId in updateMap) {
+		var values = updateMap[facetId];
+		jqNode(facetId).find('.facetCheckBox').each(function () {
+		    var value = this.getAttribute('cubicweb:value');
+		    if (!values.contains(value)) {
+			if (!jQuery(this).hasClass('facetValueDisabled')) {
+			    jQuery(this).addClass('facetValueDisabled');
+			}
+		    } else {
+			if (jQuery(this).hasClass('facetValueDisabled')) {
+			    jQuery(this).removeClass('facetValueDisabled');
+			}
+		    }
+		});
+	    }
+	});
+    });
+}
+
+
+var SELECTED_IMG = baseuri()+"data/black-check.png";
+var UNSELECTED_IMG = baseuri()+"data/no-check-no-border.png";
+var UNSELECTED_BORDER_IMG = baseuri()+"data/black-uncheck.png";
+
+function initFacetBoxEvents(root) {
+    // facetargs : (divid, vid, paginate, extraargs)
+    root = root || document;
+    jQuery(root).find('form').each(function () {
+	var form = jQuery(this);
+	// NOTE: don't evaluate facetargs here but in callbacks since its value
+	//       may changes and we must send its value when the callback is
+	//       called, not when the page is initialized
+	var facetargs = form.attr('cubicweb:facetargs');
+	if (facetargs !== undefined) {
+	    form.submit(function() {
+	        buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
+	        return false;
+	    });
+	    form.find('div.facet').each(function() {
+		var facet = jQuery(this);
+		facet.find('div.facetCheckBox').each(function (i) {
+		    this.setAttribute('cubicweb:idx', i);
+		});
+		facet.find('div.facetCheckBox').click(function () {
+		    var $this = jQuery(this);
+		    // NOTE : add test on the facet operator (i.e. OR, AND)
+		    // if ($this.hasClass('facetValueDisabled')){
+		    //  	    return
+		    // }
+		    if ($this.hasClass('facetValueSelected')) {
+			$this.removeClass('facetValueSelected');
+			$this.find('img').each(function (i){
+			if (this.getAttribute('cubicweb:unselimg')){
+			       this.setAttribute('src', UNSELECTED_BORDER_IMG);
+			       this.setAttribute('alt', (_('not selected')));
+			    }
+			    else{
+			       this.setAttribute('src', UNSELECTED_IMG);
+			       this.setAttribute('alt', (_('not selected')));
+			    }
+			});
+			var index = parseInt($this.attr('cubicweb:idx'));
+			// we dont need to move the element when cubicweb:idx == 0
+			if (index > 0){
+			    var shift = jQuery.grep(facet.find('.facetValueSelected'), function (n) {
+				    var nindex = parseInt(n.getAttribute('cubicweb:idx'));
+				    return nindex > index;
+				}).length;
+			    index += shift;
+			    var parent = this.parentNode;
+			    var $insertAfter = jQuery(parent).find('.facetCheckBox:nth('+index+')');
+			    if ( ! ($insertAfter.length == 1 && shift == 0) ) {
+				// only rearrange element if necessary
+				$insertAfter.after(this);
+			    }
+			}
+		    } else {
+			var lastSelected = facet.find('.facetValueSelected:last');
+			if (lastSelected.length) {
+			    lastSelected.after(this);
+			} else {
+			    var parent = this.parentNode;
+			    jQuery(parent).prepend(this);
+			}
+			jQuery(this).addClass('facetValueSelected');
+			var $img = jQuery(this).find('img');
+			$img.attr('src', SELECTED_IMG).attr('alt', (_('selected')));
+		    }
+		    buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
+		    facet.find('.facetBody').animate({scrollTop: 0}, '');
+		});
+		facet.find('select.facetOperator').change(function() {
+		    var nbselected = facet.find('div.facetValueSelected').length;
+		    if (nbselected >= 2) {
+			buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
+		    }
+		});
+		facet.find('div.facetTitle').click(function() {
+		  facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened');
+		  jQuery(this).toggleClass('opened');
+		   });
+
+	    });
+	}
+    });
+}
+
+// trigger this function on document ready event if you provide some kind of
+// persistent search (eg crih)
+function reorderFacetsItems(root){
+    root = root || document;
+    jQuery(root).find('form').each(function () {
+	var form = jQuery(this);
+	if (form.attr('cubicweb:facetargs')) {
+	    form.find('div.facet').each(function() {
+		var facet = jQuery(this);
+		var lastSelected = null;
+		facet.find('div.facetCheckBox').each(function (i) {
+		    var $this = jQuery(this);
+		    if ($this.hasClass('facetValueSelected')) {
+			if (lastSelected) {
+			    lastSelected.after(this);
+			} else {
+			    var parent = this.parentNode;
+			    jQuery(parent).prepend(this);
+			}
+			lastSelected = $this;
+		    }
+		});
+	    });
+	}
+    });
+}
+
+// we need to differenciate cases where initFacetBoxEvents is called
+// with one argument or without any argument. If we use `initFacetBoxEvents`
+// as the direct callback on the jQuery.ready event, jQuery will pass some argument
+// of his, so we use this small anonymous function instead.
+jQuery(document).ready(function() {initFacetBoxEvents();});
+
+CubicWeb.provide('facets.js');
--- a/web/data/cubicweb.form.css	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/data/cubicweb.form.css	Tue Sep 22 13:08:42 2009 +0200
@@ -78,7 +78,7 @@
 
 table.attributeForm th,
 table.attributeForm td {
-  padding : .7em 2px;
+  padding : .2em 2px;
 }
 
 table.attributeForm th {
--- a/web/data/cubicweb.formfilter.js	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,223 +0,0 @@
-/*
- *  :organization: Logilab
- *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
- *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
- */
-
-CubicWeb.require('htmlhelpers.js');
-CubicWeb.require('ajax.js');
-
-//============= filter form functions ========================================//
-function copyParam(origparams, newparams, param) {
-    var index = findValue(origparams[0], param);
-    if (index > -1) {
-	newparams[param] = origparams[1][index];
-    }
-}
-
-function facetFormContent(form) {
-    var names = [];
-    var values = [];
-    jQuery(form).find('.facet').each(function () {
-        var facetName = jQuery(this).find('.facetTitle').attr('cubicweb:facetName');
-        var facetValues = jQuery(this).find('.facetValueSelected').each(function(x) {
-  	    names.push(facetName);
-  	    values.push(this.getAttribute('cubicweb:value'));
-        });
-    });
-    jQuery(form).find('input').each(function () {
-        names.push(this.name);
-        values.push(this.value);
-    });
-    jQuery(form).find('select option[selected]').each(function () {
-	names.push(this.parentNode.name);
-	values.push(this.value);
-    });
-    return [names, values];
-}
-
-function buildRQL(divid, vid, paginate, vidargs) {
-    jQuery(CubicWeb).trigger('facets-content-loading', [divid, vid, paginate, vidargs]);
-    var form = getNode(divid+'Form');
-    var zipped = facetFormContent(form);
-    zipped[0].push('facetargs');
-    zipped[1].push(vidargs);
-    var d = asyncRemoteExec('filter_build_rql', zipped[0], zipped[1]);
-    d.addCallback(function(result) {
-	var rql = result[0];
-	var $bkLink = jQuery('#facetBkLink');
-	if ($bkLink.length) {
-	    var bkUrl = $bkLink.attr('cubicweb:target') + '&path=view?rql=' + rql;
-	    if (vid) {
-		bkUrl += '&vid=' + vid;
-	    }
-	    $bkLink.attr('href', bkUrl);
-	}
-	var toupdate = result[1];
-	var extraparams = vidargs;
-	var displayactions = jQuery('#' + divid).attr('cubicweb:displayactions');
-	if (displayactions) { extraparams['displayactions'] = displayactions; }
-	if (paginate) { extraparams['paginate'] = '1'; }
-	// copy some parameters
-	// XXX cleanup vid/divid mess
-	// if vid argument is specified , the one specified in form params will
-	// be overriden by replacePageChunk
-	copyParam(zipped, extraparams, 'vid');
-	extraparams['divid'] = divid;
-	copyParam(zipped, extraparams, 'divid');
-	copyParam(zipped, extraparams, 'subvid');
-	// paginate used to know if the filter box is acting, in which case we
-	// want to reload action box to match current selection
-	replacePageChunk(divid, rql, vid, extraparams, true, function() {
-	  jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
-	});
-	if (paginate) {
-	    // FIXME the edit box might not be displayed in which case we don't
-	    // know where to put the potential new one, just skip this case
-	    // for now
-	    if (jQuery('#edit_box').length) {
-		reloadComponent('edit_box', rql, 'boxes', 'edit_box');
-	    }
-	}
-	var d = asyncRemoteExec('filter_select_content', toupdate, rql);
-	d.addCallback(function(updateMap) {
-	    for (facetId in updateMap) {
-		var values = updateMap[facetId];
-		jqNode(facetId).find('.facetCheckBox').each(function () {
-		    var value = this.getAttribute('cubicweb:value');
-		    if (!values.contains(value)) {
-			if (!jQuery(this).hasClass('facetValueDisabled')) {
-			    jQuery(this).addClass('facetValueDisabled');
-			}
-		    } else {
-			if (jQuery(this).hasClass('facetValueDisabled')) {
-			    jQuery(this).removeClass('facetValueDisabled');
-			}
-		    }
-		});
-	    }
-	});
-    });
-}
-
-
-var SELECTED_IMG = baseuri()+"data/black-check.png";
-var UNSELECTED_IMG = baseuri()+"data/no-check-no-border.png";
-var UNSELECTED_BORDER_IMG = baseuri()+"data/black-uncheck.png";
-
-function initFacetBoxEvents(root) {
-    // facetargs : (divid, vid, paginate, extraargs)
-    root = root || document;
-    jQuery(root).find('form').each(function () {
-	var form = jQuery(this);
-	// NOTE: don't evaluate facetargs here but in callbacks since its value
-	//       may changes and we must send its value when the callback is
-	//       called, not when the page is initialized
-	var facetargs = form.attr('cubicweb:facetargs');
-	if (facetargs !== undefined) {
-	    form.submit(function() {
-	        buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-	        return false;
-	    });
-	    form.find('div.facet').each(function() {
-		var facet = jQuery(this);
-		facet.find('div.facetCheckBox').each(function (i) {
-		    this.setAttribute('cubicweb:idx', i);
-		});
-		facet.find('div.facetCheckBox').click(function () {
-		    var $this = jQuery(this);
-		    if ($this.hasClass('facetValueDisabled')){
-		     	    return
-		    }
-		    if ($this.hasClass('facetValueSelected')) {
-			$this.removeClass('facetValueSelected');
-			$this.find('img').each(function (i){
-			if (this.getAttribute('cubicweb:unselimg')){
-			       this.setAttribute('src', UNSELECTED_BORDER_IMG);
-			       this.setAttribute('alt', (_('not selected')));
-			    }
-			    else{
-			       this.setAttribute('src', UNSELECTED_IMG);
-			       this.setAttribute('alt', (_('not selected')));
-			    }
-			});
-			var index = parseInt($this.attr('cubicweb:idx'));
-			// we dont need to move the element when cubicweb:idx == 0
-			if (index > 0){
-			    var shift = jQuery.grep(facet.find('.facetValueSelected'), function (n) {
-				    var nindex = parseInt(n.getAttribute('cubicweb:idx'));
-				    return nindex > index;
-				}).length;
-			    index += shift;
-			    var parent = this.parentNode;
-			    var $insertAfter = jQuery(parent).find('.facetCheckBox:nth('+index+')');
-			    if ( ! ($insertAfter.length == 1 && shift == 0) ) {
-				// only rearrange element if necessary
-				$insertAfter.after(this);
-			    }
-			}
-		    } else {
-			var lastSelected = facet.find('.facetValueSelected:last');
-			if (lastSelected.length) {
-			    lastSelected.after(this);
-			} else {
-			    var parent = this.parentNode;
-			    jQuery(parent).prepend(this);
-			}
-			jQuery(this).addClass('facetValueSelected');
-			var $img = jQuery(this).find('img');
-			$img.attr('src', SELECTED_IMG).attr('alt', (_('selected')));
-		    }
-		    buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-		    facet.find('.facetBody').animate({scrollTop: 0}, '');
-		});
-		facet.find('select.facetOperator').change(function() {
-		    var nbselected = facet.find('div.facetValueSelected').length;
-		    if (nbselected >= 2) {
-			buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-		    }
-		});
-		facet.find('div.facetTitle').click(function() {
-		  facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened');
-		  jQuery(this).toggleClass('opened');
-		   });
-
-	    });
-	}
-    });
-}
-
-// trigger this function on document ready event if you provide some kind of
-// persistent search (eg crih)
-function reorderFacetsItems(root){
-    root = root || document;
-    jQuery(root).find('form').each(function () {
-	var form = jQuery(this);
-	if (form.attr('cubicweb:facetargs')) {
-	    form.find('div.facet').each(function() {
-		var facet = jQuery(this);
-		var lastSelected = null;
-		facet.find('div.facetCheckBox').each(function (i) {
-		    var $this = jQuery(this);
-		    if ($this.hasClass('facetValueSelected')) {
-			if (lastSelected) {
-			    lastSelected.after(this);
-			} else {
-			    var parent = this.parentNode;
-			    jQuery(parent).prepend(this);
-			}
-			lastSelected = $this;
-		    }
-		});
-	    });
-	}
-    });
-}
-
-// we need to differenciate cases where initFacetBoxEvents is called
-// with one argument or without any argument. If we use `initFacetBoxEvents`
-// as the direct callback on the jQuery.ready event, jQuery will pass some argument
-// of his, so we use this small anonymous function instead.
-jQuery(document).ready(function() {initFacetBoxEvents();});
-
-CubicWeb.provide('formfilter.js');
--- a/web/data/cubicweb.python.js	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/data/cubicweb.python.js	Tue Sep 22 13:08:42 2009 +0200
@@ -83,10 +83,10 @@
     var skip0 = new RegExp('^0*[0-9]+');
     var parsed = {};
     for (var i1=0,i2=0;i1<format.length;i1++,i2++) {
-	var c1 = format[i1];
-	var c2 = datestring[i2];
+	var c1 = format.charAt(i1);
+	var c2 = datestring.charAt(i2);
 	if (c1 == '%') {
-	    c1 = format[++i1];
+	    c1 = format.charAt(++i1);
 	    var data = _DATE_FORMAT_REGXES[c1].exec(datestring.substring(i2));
 	    if (!data.length) {
 		return null;
--- a/web/data/cubicweb.widgets.js	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/data/cubicweb.widgets.js	Tue Sep 22 13:08:42 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/
--- a/web/data/jquery.treeview.js	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/data/jquery.treeview.js	Tue Sep 22 13:08:42 2009 +0200
@@ -1,6 +1,6 @@
 /*
  * Treeview 1.4 - jQuery plugin to hide and show branches of a tree
- * 
+ *
  * http://bassistance.de/jquery-plugins/jquery-plugin-treeview/
  * http://docs.jquery.com/Plugins/Treeview
  *
@@ -11,5 +11,6 @@
  *   http://www.gnu.org/licenses/gpl.html
  *
  * Revision: $Id: jquery.treeview.js 4684 2008-02-07 19:08:06Z joern.zaefferer $
+ * updated by Aurelien Campeas, 2009-09-01, to handle top-level ajax loads
  *
- */;(function($){$.extend($.fn,{swapClass:function(c1,c2){var c1Elements=this.filter('.'+c1);this.filter('.'+c2).removeClass(c2).addClass(c1);c1Elements.removeClass(c1).addClass(c2);return this;},replaceClass:function(c1,c2){return this.filter('.'+c1).removeClass(c1).addClass(c2).end();},hoverClass:function(className){className=className||"hover";return this.hover(function(){$(this).addClass(className);},function(){$(this).removeClass(className);});},heightToggle:function(animated,callback){animated?this.animate({height:"toggle"},animated,callback):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();if(callback)callback.apply(this,arguments);});},heightHide:function(animated,callback){if(animated){this.animate({height:"hide"},animated,callback);}else{this.hide();if(callback)this.each(callback);}},prepareBranches:function(settings){if(!settings.prerendered){this.filter(":last-child:not(ul)").addClass(CLASSES.last);this.filter((settings.collapsed?"":"."+CLASSES.closed)+":not(."+CLASSES.open+")").find(">ul").hide();}return this.filter(":has(>ul)");},applyClasses:function(settings,toggler){this.filter(":has(>ul):not(:has(>a))").find(">span").click(function(event){toggler.apply($(this).next());}).add($("a",this)).hoverClass();if(!settings.prerendered){this.filter(":has(>ul:hidden)").addClass(CLASSES.expandable).replaceClass(CLASSES.last,CLASSES.lastExpandable);this.not(":has(>ul:hidden)").addClass(CLASSES.collapsable).replaceClass(CLASSES.last,CLASSES.lastCollapsable);this.prepend("<div class=\""+CLASSES.hitarea+"\"/>").find("div."+CLASSES.hitarea).each(function(){var classes="";$.each($(this).parent().attr("class").split(" "),function(){classes+=this+"-hitarea ";});$(this).addClass(classes);});}this.find("div."+CLASSES.hitarea).click(toggler);},treeview:function(settings){settings=$.extend({cookieId:"treeview"},settings);if(settings.add){return this.trigger("add",[settings.add]);}if(settings.toggle){var callback=settings.toggle;settings.toggle=function(){return callback.apply($(this).parent()[0],arguments);};}function treeController(tree,control){function handler(filter){return function(){toggler.apply($("div."+CLASSES.hitarea,tree).filter(function(){return filter?$(this).parent("."+filter).length:true;}));return false;};}$("a:eq(0)",control).click(handler(CLASSES.collapsable));$("a:eq(1)",control).click(handler(CLASSES.expandable));$("a:eq(2)",control).click(handler());}function toggler(){$(this).parent().find(">.hitarea").swapClass(CLASSES.collapsableHitarea,CLASSES.expandableHitarea).swapClass(CLASSES.lastCollapsableHitarea,CLASSES.lastExpandableHitarea).end().swapClass(CLASSES.collapsable,CLASSES.expandable).swapClass(CLASSES.lastCollapsable,CLASSES.lastExpandable).find(">ul").heightToggle(settings.animated,settings.toggle);if(settings.unique){$(this).parent().siblings().find(">.hitarea").replaceClass(CLASSES.collapsableHitarea,CLASSES.expandableHitarea).replaceClass(CLASSES.lastCollapsableHitarea,CLASSES.lastExpandableHitarea).end().replaceClass(CLASSES.collapsable,CLASSES.expandable).replaceClass(CLASSES.lastCollapsable,CLASSES.lastExpandable).find(">ul").heightHide(settings.animated,settings.toggle);}}function serialize(){function binary(arg){return arg?1:0;}var data=[];branches.each(function(i,e){data[i]=$(e).is(":has(>ul:visible)")?1:0;});$.cookie(settings.cookieId,data.join(""));}function deserialize(){var stored=$.cookie(settings.cookieId);if(stored){var data=stored.split("");branches.each(function(i,e){$(e).find(">ul")[parseInt(data[i])?"show":"hide"]();});}}this.addClass("treeview");var branches=this.find("li").prepareBranches(settings);switch(settings.persist){case"cookie":var toggleCallback=settings.toggle;settings.toggle=function(){serialize();if(toggleCallback){toggleCallback.apply(this,arguments);}};deserialize();break;case"location":var current=this.find("a").filter(function(){return this.href.toLowerCase()==location.href.toLowerCase();});if(current.length){current.addClass("selected").parents("ul, li").add(current.next()).show();}break;}branches.applyClasses(settings,toggler);if(settings.control){treeController(this,settings.control);$(settings.control).show();}return this.bind("add",function(event,branches){$(branches).prev().removeClass(CLASSES.last).removeClass(CLASSES.lastCollapsable).removeClass(CLASSES.lastExpandable).find(">.hitarea").removeClass(CLASSES.lastCollapsableHitarea).removeClass(CLASSES.lastExpandableHitarea);$(branches).find("li").andSelf().prepareBranches(settings).applyClasses(settings,toggler);});}});var CLASSES=$.fn.treeview.classes={open:"open",closed:"closed",expandable:"expandable",expandableHitarea:"expandable-hitarea",lastExpandableHitarea:"lastExpandable-hitarea",collapsable:"collapsable",collapsableHitarea:"collapsable-hitarea",lastCollapsableHitarea:"lastCollapsable-hitarea",lastCollapsable:"lastCollapsable",lastExpandable:"lastExpandable",last:"last",hitarea:"hitarea"};$.fn.Treeview=$.fn.treeview;})(jQuery);
\ No newline at end of file
+ */;(function($){$.extend($.fn,{swapClass:function(c1,c2){var c1Elements=this.filter('.'+c1);this.filter('.'+c2).removeClass(c2).addClass(c1);c1Elements.removeClass(c1).addClass(c2);return this;},replaceClass:function(c1,c2){return this.filter('.'+c1).removeClass(c1).addClass(c2).end();},hoverClass:function(className){className=className||"hover";return this.hover(function(){$(this).addClass(className);},function(){$(this).removeClass(className);});},heightToggle:function(animated,callback){animated?this.animate({height:"toggle"},animated,callback):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();if(callback)callback.apply(this,arguments);});},heightHide:function(animated,callback){if(animated){this.animate({height:"hide"},animated,callback);}else{this.hide();if(callback)this.each(callback);}},prepareBranches:function(settings){if(!settings.prerendered){this.filter(":last-child:not(ul)").addClass(CLASSES.last);this.filter((settings.collapsed?"":"."+CLASSES.closed)+":not(."+CLASSES.open+")").find(">ul").hide();}return this.filter(":has(>ul)");},applyClasses:function(settings,toggler){this.filter(":has(>ul):not(:has(>a))").find(">span").click(function(event){toggler.apply($(this).next());}).add($("a",this)).hoverClass();if(!settings.prerendered){this.filter(":has(>ul:hidden)").addClass(CLASSES.expandable).replaceClass(CLASSES.last,CLASSES.lastExpandable);this.not(":has(>ul:hidden)").addClass(CLASSES.collapsable).replaceClass(CLASSES.last,CLASSES.lastCollapsable);this.prepend("<div class=\""+CLASSES.hitarea+"\"/>").find("div."+CLASSES.hitarea).each(function(){var classes="";$.each($(this).parent().attr("class").split(" "),function(){classes+=this+"-hitarea ";});$(this).addClass(classes);});}this.find("div."+CLASSES.hitarea).click(toggler);},treeview:function(settings){if(this.attr('cubicweb:type')=='prepared-treeview'){return this;}this.attr('cubicweb:type','prepared-treeview');settings=$.extend({cookieId:"treeview"},settings);if(settings.add){return this.trigger("add",[settings.add]);}if(settings.toggle){var callback=settings.toggle;settings.toggle=function(){return callback.apply($(this).parent()[0],arguments);};}function treeController(tree,control){function handler(filter){return function(){toggler.apply($("div."+CLASSES.hitarea,tree).filter(function(){return filter?$(this).parent("."+filter).length:true;}));return false;};}$("a:eq(0)",control).click(handler(CLASSES.collapsable));$("a:eq(1)",control).click(handler(CLASSES.expandable));$("a:eq(2)",control).click(handler());}function toggler(){$(this).parent().find(">.hitarea").swapClass(CLASSES.collapsableHitarea,CLASSES.expandableHitarea).swapClass(CLASSES.lastCollapsableHitarea,CLASSES.lastExpandableHitarea).end().swapClass(CLASSES.collapsable,CLASSES.expandable).swapClass(CLASSES.lastCollapsable,CLASSES.lastExpandable).find(">ul").heightToggle(settings.animated,settings.toggle);if(settings.unique){$(this).parent().siblings().find(">.hitarea").replaceClass(CLASSES.collapsableHitarea,CLASSES.expandableHitarea).replaceClass(CLASSES.lastCollapsableHitarea,CLASSES.lastExpandableHitarea).end().replaceClass(CLASSES.collapsable,CLASSES.expandable).replaceClass(CLASSES.lastCollapsable,CLASSES.lastExpandable).find(">ul").heightHide(settings.animated,settings.toggle);}}function serialize(){function binary(arg){return arg?1:0;}var data=[];branches.each(function(i,e){data[i]=$(e).is(":has(>ul:visible)")?1:0;});$.cookie(settings.cookieId,data.join(""));}function deserialize(){var stored=$.cookie(settings.cookieId);if(stored){var data=stored.split("");branches.each(function(i,e){$(e).find(">ul")[parseInt(data[i])?"show":"hide"]();});}}this.addClass("treeview");var branches=this.find("li").prepareBranches(settings);switch(settings.persist){case"cookie":var toggleCallback=settings.toggle;settings.toggle=function(){serialize();if(toggleCallback){toggleCallback.apply(this,arguments);}};deserialize();break;case"location":var current=this.find("a").filter(function(){return this.href.toLowerCase()==location.href.toLowerCase();});if(current.length){current.addClass("selected").parents("ul, li").add(current.next()).show();}break;}branches.applyClasses(settings,toggler);if(settings.control){treeController(this,settings.control);$(settings.control).show();}return this.bind("add",function(event,branches){$(branches).prev().removeClass(CLASSES.last).removeClass(CLASSES.lastCollapsable).removeClass(CLASSES.lastExpandable).find(">.hitarea").removeClass(CLASSES.lastCollapsableHitarea).removeClass(CLASSES.lastExpandableHitarea);$(branches).find("li").andSelf().prepareBranches(settings).applyClasses(settings,toggler);});}});var CLASSES=$.fn.treeview.classes={open:"open",closed:"closed",expandable:"expandable",expandableHitarea:"expandable-hitarea",lastExpandableHitarea:"lastExpandable-hitarea",collapsable:"collapsable",collapsableHitarea:"collapsable-hitarea",lastCollapsableHitarea:"lastCollapsable-hitarea",lastCollapsable:"lastCollapsable",lastExpandable:"lastExpandable",last:"last",hitarea:"hitarea"};$.fn.Treeview=$.fn.treeview;})(jQuery);
\ No newline at end of file
Binary file web/data/pdf_icon.gif has changed
--- a/web/facet.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/facet.py	Tue Sep 22 13:08:42 2009 +0200
@@ -64,8 +64,8 @@
 
 
 def get_facet(req, facetid, rqlst, mainvar):
-    return req.vreg['facets'].object_by_id(facetid, req, rqlst=rqlst,
-                                           filtered_variable=mainvar)
+    return req.vreg['facets'].select(facetid, req, rqlst=rqlst,
+                                     filtered_variable=mainvar)
 
 
 def filter_hiddens(w, **kwargs):
@@ -176,7 +176,15 @@
 
 def insert_attr_select_relation(rqlst, mainvar, rtype, role, attrname,
                                 sortfuncname=None, sortasc=True):
-    """modify a syntax tree to retrieve only relevant attribute `attr` of `var`"""
+    """modify a syntax tree to :
+    * link a new variable to `mainvar` through `rtype` (where mainvar has `role`)
+    * retrieve only the newly inserted variable and its `attrname`
+
+    Sorting:
+    * on `attrname` ascendant (`sortasc`=True) or descendant (`sortasc`=False)
+    * on `sortfuncname`(`attrname`) if `sortfuncname` is specified
+    * no sort if `sortasc` is None
+    """
     _cleanup_rqlst(rqlst, mainvar)
     var = _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role)
     # not found, create one
@@ -186,7 +194,8 @@
     if rqlst.groupby:
         if not attrvar in rqlst.groupby:
             rqlst.add_group_var(attrvar)
-    _set_orderby(rqlst, attrvar, sortasc, sortfuncname)
+    if sortasc is not None:
+        _set_orderby(rqlst, attrvar, sortasc, sortfuncname)
     # add attribute variable to selection
     rqlst.add_selected(attrvar)
     # add is restriction if necessary
@@ -244,7 +253,7 @@
 class AbstractFacet(AppObject):
     __abstract__ = True
     __registry__ = 'facets'
-    property_defs = {
+    cw_property_defs = {
         _('visible'): dict(type='Boolean', default=True,
                            help=_('display the box or not')),
         _('order'):   dict(type='Int', default=99,
@@ -261,7 +270,7 @@
 
     def __init__(self, req, rset=None, rqlst=None, filtered_variable=None,
                  **kwargs):
-        super(AbstractFacet, self).__init__(req, rset, **kwargs)
+        super(AbstractFacet, self).__init__(req, rset=rset, **kwargs)
         assert rset is not None or rqlst is not None
         assert filtered_variable
         # facet retreived using `object_by_id` from an ajax call
@@ -346,6 +355,8 @@
     sortfunc = None
     # ascendant/descendant sorting
     sortasc = True
+    # if you want to call a view on the entity instead of using `target_attr`
+    label_vid = None
 
     @property
     def title(self):
@@ -356,10 +367,14 @@
         """
         rqlst = self.rqlst
         rqlst.save_state()
+        if self.label_vid is not None and self.sortfunc is None:
+            sort = None # will be sorted on label
+        else:
+            sort = self.sortasc
         try:
             mainvar = self.filtered_variable
             insert_attr_select_relation(rqlst, mainvar, self.rtype, self.role,
-                                        self.target_attr, self.sortfunc, self.sortasc)
+                                        self.target_attr, self.sortfunc, sort)
             try:
                 rset = self.rqlexec(rqlst.as_string(), self.rset.args, self.rset.cachekey)
             except:
@@ -384,8 +399,14 @@
             rqlst.recover()
 
     def rset_vocabulary(self, rset):
-        _ = self.req._
-        return [(_(label), eid) for eid, label in rset]
+        if self.label_vid is None:
+            _ = self.req._
+            return [(_(label), eid) for eid, label in rset]
+        if self.sortfunc is None:
+            return sorted((entity.view(self.label_vid), entity.eid)
+                          for entity in rset.entities())
+        return [(entity.view(self.label_vid), entity.eid)
+                for entity in rset.entities()]
 
     @cached
     def support_and(self):
@@ -531,6 +552,7 @@
                                             self.formatvalue(supvalue),
                                             self.attrtype, '<=')
 
+
 class DateRangeFacet(RangeFacet):
     attrtype = 'Date' # only date types are supported
 
@@ -571,6 +593,7 @@
         else:
             self.rqlst.add_relation(var, self.rtype, self.filtered_variable)
 
+
 ## html widets ################################################################
 
 class FacetVocabularyWidget(HTMLWidget):
@@ -656,7 +679,7 @@
         facet = self.facet
         facet.req.add_js('ui.slider.js')
         facet.req.add_css('ui.all.css')
-        sliderid = make_uid('the slider')
+        sliderid = make_uid('theslider')
         facetid = xml_escape(self.facet.id)
         facet.req.html_headers.add_onload(self.onload % {
             'sliderid': sliderid,
@@ -718,10 +741,11 @@
             imgalt = self.req._('not selected')
         self.w(u'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n'
                % (cssclass, xml_escape(unicode(self.value))))
-        self.w(u'<img src="%s" alt="%s"/>&nbsp;' % (imgsrc, imgalt))
+        self.w(u'<img src="%s" alt="%s"/>&#160;' % (imgsrc, imgalt))
         self.w(u'<a href="javascript: {}">%s</a>' % xml_escape(self.label))
         self.w(u'</div>')
 
+
 class CheckBoxFacetWidget(HTMLWidget):
     selected_img = "black-check.png"
     unselected_img = "black-uncheck.png"
@@ -747,15 +771,16 @@
         self.w(u'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n'
                % (cssclass, xml_escape(unicode(self.value))))
         self.w(u'<div class="facetCheckBoxWidget">')
-        self.w(u'<img src="%s" alt="%s" cubicweb:unselimg="true" />&nbsp;' % (imgsrc, imgalt))
+        self.w(u'<img src="%s" alt="%s" cubicweb:unselimg="true" />&#160;' % (imgsrc, imgalt))
         self.w(u'<label class="facetTitle" cubicweb:facetName="%s"><a href="javascript: {}">%s</a></label>' % (facetid, title))
         self.w(u'</div>\n')
         self.w(u'</div>\n')
         self.w(u'</div>\n')
 
+
 class FacetSeparator(HTMLWidget):
     def __init__(self, label=None):
-        self.label = label or u'&nbsp;'
+        self.label = label or u'&#160;'
 
     def _render(self):
         pass
--- a/web/form.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/form.py	Tue Sep 22 13:08:42 2009 +0200
@@ -16,7 +16,6 @@
 class FormViewMixIn(object):
     """abstract form view mix-in"""
     category = 'form'
-    controller = 'edit'
     http_cache_manager = httpcache.NoHTTPCacheManager
     add_to_breadcrumbs = False
 
@@ -48,7 +47,7 @@
         return '%s#%s' % (self.req.url(), self.domid)
 
     def __init__(self, req, rset, **kwargs):
-        super(FormMixIn, self).__init__(req, rset, **kwargs)
+        super(FormMixIn, self).__init__(req, rset=rset, **kwargs)
         self.restore_previous_post(self.session_key())
 
     def restore_previous_post(self, sessionkey):
@@ -79,7 +78,6 @@
 
     domid = 'entityForm'
     category = 'form'
-    controller = 'edit'
     http_cache_manager = httpcache.NoHTTPCacheManager
     add_to_breadcrumbs = False
 
@@ -164,7 +162,7 @@
                 if len(errors) > 1:
                     templstr = '<li>%s</li>\n'
                 else:
-                    templstr = '&nbsp;%s\n'
+                    templstr = '&#160;%s\n'
                 for field, err in errors:
                     if field is None:
                         errormsg += templstr % err
--- a/web/formfields.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/formfields.py	Tue Sep 22 13:08:42 2009 +0200
@@ -22,6 +22,22 @@
     Radio, Select, DateTimePicker)
 
 
+def vocab_sort(vocab):
+    """sort vocabulary, considering option groups"""
+    result = []
+    partresult = []
+    for label, value in vocab:
+        if value is None: # opt group start
+            if partresult:
+                result += sorted(partresult)
+                partresult = []
+            result.append( (label, value) )
+        else:
+            partresult.append( (label, value) )
+    result += sorted(partresult)
+    return result
+
+
 class Field(object):
     """field class is introduced to control what's displayed in forms. It makes
     the link between something to edit and its display in the form. Actual
@@ -189,7 +205,7 @@
         if self.internationalizable:
             vocab = [(form.req._(label), value) for label, value in vocab]
         if self.sort:
-            vocab = sorted(vocab)
+            vocab = vocab_sort(vocab)
         return vocab
 
     def form_init(self, form):
@@ -272,7 +288,7 @@
         try:
             return req.data[self]
         except KeyError:
-            fkwargs = {}
+            fkwargs = {'eidparam': self.eidparam}
             if self.use_fckeditor(form):
                 # if fckeditor is used and format field isn't explicitly
                 # deactivated, we want an hidden field for the format
@@ -282,7 +298,7 @@
                 # else we want a format selector
                 fkwargs['widget'] = Select()
                 fcstr = FormatConstraint()
-                fkwargs['choices'] = fcstr.vocabulary(req=req)
+                fkwargs['choices'] = fcstr.vocabulary(form=form)
                 fkwargs['internationalizable'] = True
                 fkwargs['initial'] = lambda f: f.form_field_format(self)
             fkwargs['eidparam'] = self.eidparam
@@ -308,7 +324,8 @@
         format_field = self.get_format_field(form)
         if format_field:
             # XXX we want both fields to remain vertically aligned
-            format_field.widget.attrs['style'] = 'display: block'
+            if format_field.is_visible():
+                format_field.widget.attrs['style'] = 'display: block'
             result = format_field.render(form, renderer)
         else:
             result = u''
@@ -499,9 +516,16 @@
         return time
 
 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):
@@ -528,7 +552,7 @@
             relatedvocab = []
         vocab = res + form.form_field_vocabulary(self) + relatedvocab
         if self.sort:
-            vocab = sorted(vocab)
+            vocab = vocab_sort(vocab)
         return vocab
 
     def format_single_value(self, req, value):
@@ -567,6 +591,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['eidparam'] = True
     kwargs.setdefault('help', help)
     if rschema.is_final():
--- a/web/formwidgets.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/formwidgets.py	Tue Sep 22 13:08:42 2009 +0200
@@ -114,7 +114,7 @@
                   '<br/>',
                   tags.input(name=confirmname, value=values[0], type=self.type,
                              **attrs),
-                  '&nbsp;', tags.span(form.req._('confirm password'),
+                  '&#160;', tags.span(form.req._('confirm password'),
                                       **{'class': 'emphasis'})]
         return u'\n'.join(inputs)
 
@@ -448,14 +448,15 @@
         # XXX entity form specific
         entity = form.edited_entity
         attrs['cubicweb:etype_to'] = entity.e_schema
-        etype_from = entity.e_schema.subject_relation(self.name).objects(entity.e_schema)[0]
+        etype_from = entity.e_schema.subject_relation(field.name).objects(entity.e_schema)[0]
         attrs['cubicweb:etype_from'] = etype_from
+        return name, values, attrs
 
     def render(self, form, field, renderer):
         return super(AddComboBoxWidget, self).render(form, field, renderer) + u'''
 <div id="newvalue">
   <input type="text" id="newopt" />
-  <a href="javascript:noop()" id="add_newopt">&nbsp;</a></div>
+  <a href="javascript:noop()" id="add_newopt">&#160;</a></div>
 '''
 
 # buttons ######################################################################
--- a/web/htmlwidgets.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/htmlwidgets.py	Tue Sep 22 13:08:42 2009 +0200
@@ -58,6 +58,9 @@
     def append(self, item):
         self.items.append(item)
 
+    def extend(self, items):
+        self.items.extend(items)
+
     title_class = 'boxTitle'
     main_div_class = 'boxContent'
     listing_class = 'boxListing'
@@ -72,7 +75,7 @@
             self.w(u'</ul>\n')
         self.w(u'</div>\n')
         if self.shadow:
-            self.w(u'<div class="shadow">&nbsp;</div>')
+            self.w(u'<div class="shadow">&#160;</div>')
 
     def _render(self):
         if self.id:
@@ -188,7 +191,7 @@
         self.value = value
 
     def _render(self):
-        self.w(u'<li><div><span class="label">%s</span>&nbsp;'
+        self.w(u'<li><div><span class="label">%s</span>&#160;'
                u'<span class="value">%s</span></div></li>'
                % (self.label, self.value))
 
--- a/web/request.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/request.py	Tue Sep 22 13:08:42 2009 +0200
@@ -15,18 +15,19 @@
 from urlparse import urlsplit
 from itertools import count
 
+from simplejson import dumps
+
 from rql.utils import rqlvar_maker
 
 from logilab.common.decorators import cached
 from logilab.common.deprecation import deprecated
-
 from logilab.mtconverter import xml_escape
 
 from cubicweb.dbapi import DBAPIRequest
 from cubicweb.common.mail import header
 from cubicweb.common.uilib import remove_html_tags
 from cubicweb.utils import SizeConstrainedList, HTMLHead
-from cubicweb.view import STRICT_DOCTYPE
+from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
                           RequestError, StatusResponse)
 
@@ -85,9 +86,16 @@
         self.next_tabindex = self.tabindexgen.next
         # page id, set by htmlheader template
         self.pageid = None
-        self.varmaker = rqlvar_maker()
         self.datadir_url = self._datadir_url()
 
+    @property
+    def varmaker(self):
+        varmaker = self.get_page_data('rql_varmaker')
+        if varmaker is None:
+            varmaker = rqlvar_maker()
+            self.set_page_data('rql_varmaker', varmaker)
+        return varmaker
+
     def set_connection(self, cnx, user=None):
         """method called by the session handler when the user is authenticated
         or an anonymous connection is open
@@ -114,7 +122,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)
@@ -257,6 +266,24 @@
             return breadcrumbs.pop()
         return self.base_url()
 
+    def user_rql_callback(self, args, msg=None):
+        """register a user callback to execute some rql query and return an url
+        to call it ready to be inserted in html
+        """
+        def rqlexec(req, rql, args=None, key=None):
+            req.execute(rql, args, key)
+        return self.user_callback(rqlexec, args, msg)
+
+    def user_callback(self, cb, args, msg=None, nonify=False):
+        """register the given user callback and return an url to call it ready to be
+        inserted in html
+        """
+        self.add_js('cubicweb.ajax.js')
+        cbname = self.register_onetime_callback(cb, *args)
+        msg = dumps(msg or '')
+        return "javascript:userCallbackThenReloadPage('%s', %s)" % (
+            cbname, msg)
+
     def register_onetime_callback(self, func, *args):
         cbname = 'cb_%s' % (
             sha.sha('%s%s%s%s' % (time.time(), func.__name__,
@@ -629,7 +656,7 @@
                            auth, ex.__class__.__name__, ex)
         return None, None
 
-    @deprecated("use parse_accept_header('Accept-Language')")
+    @deprecated("[3.4] use parse_accept_header('Accept-Language')")
     def header_accept_language(self):
         """returns an ordered list of preferred languages"""
         return [value.split('-')[0] for value in
@@ -663,6 +690,16 @@
         """
         raise NotImplementedError()
 
+    def demote_to_html(self):
+        """helper method to dynamically set request content type to text/html
+
+        The global doctype and xmldec must also be changed otherwise the browser
+        will display '<[' at the beginning of the page
+        """
+        self.set_content_type('text/html')
+        self.main_stream.doctype = TRANSITIONAL_DOCTYPE_NOEXT
+        self.main_stream.xmldecl = u''
+
     # page data management ####################################################
 
     def get_page_data(self, key, default=None):
--- a/web/test/data/bootstrap_cubes	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/data/bootstrap_cubes	Tue Sep 22 13:08:42 2009 +0200
@@ -1,1 +1,1 @@
-file, blog, tag
+file, blog, tag, folder
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	Tue Sep 22 13:08:42 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/test_views.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/test_views.py	Tue Sep 22 13:08:42 2009 +0200
@@ -6,7 +6,7 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 
-from cubicweb.devtools.testlib import WebTest, AutomaticWebTest
+from cubicweb.devtools.testlib import CubicWebTC, AutoPopulateTest, AutomaticWebTest
 from cubicweb.view import AnyRsetView
 
 AutomaticWebTest.application_rql = [
@@ -15,7 +15,7 @@
     'Any COUNT(X) WHERE X is CWUser',
     ]
 
-class ComposityCopy(WebTest):
+class ComposityCopy(CubicWebTC):
 
     def test_regr_copy_view(self):
         """regression test: make sure we can ask a copy of a
@@ -34,7 +34,7 @@
         self.req.add_js('spam.js')
 
 
-class ManualWebTests(WebTest):
+class ManualCubicWebTCs(AutoPopulateTest):
     def setup_database(self):
         self.auto_populate(10)
 
@@ -59,7 +59,7 @@
 
 
 
-class ExplicitViewsTest(WebTest):
+class ExplicitViewsTest(CubicWebTC):
 
     def test_unrelateddivs(self):
         rset = self.execute('Any X WHERE X is CWUser, X login "admin"')
--- a/web/test/unittest_application.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_application.py	Tue Sep 22 13:08:42 2009 +0200
@@ -14,7 +14,7 @@
 from logilab.common.testlib import TestCase, unittest_main
 from logilab.common.decorators import clear_cache
 
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.fake import FakeRequest
 from cubicweb.web import Redirect, AuthenticationError, ExplicitLogin, INTERNAL_FIELD_VALUE
 from cubicweb.web.views.basecontrollers import ViewController
@@ -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')
 
@@ -134,7 +137,7 @@
                            for i in (12, 13, 14)])
 
 
-class ApplicationTC(EnvBasedTC):
+class ApplicationTC(CubicWebTC):
 
     def publish(self, req, path='view'):
         return self.app.publish(path, req)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_breadcrumbs.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,17 @@
+from cubicweb.devtools.testlib import WebTest
+
+class BreadCrumbsTC(WebTest):
+
+    def test_base(self):
+        f1 = self.add_entity('Folder', name=u'par&ent')
+        f2 = self.add_entity('Folder', name=u'chi&ld')
+        self.execute('SET F2 filed_under F1 WHERE F1 eid %(f1)s, F2 eid %(f2)s',
+                     {'f1' : f1.eid, 'f2' : f2.eid})
+        self.commit()
+        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/%s" title="">par&amp;ent</a>&#160;&gt;&#160;
+chi&amp;ld</span>""" % f1.eid)
--- a/web/test/unittest_controller.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_controller.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,37 +7,35 @@
 
 from logilab.common.testlib import unittest_main
 
-from cubicweb.devtools import apptest
+from cubicweb.devtools import testlib
 
-class BaseControllerTC(apptest.ControllerTC):
+class BaseControllerTC(testlib.CubicWebTC):
 
     def test_parse_datetime_ok(self):
-        self.assertIsInstance(self.ctrl.parse_datetime('2006/06/24 12:18'),
-                              datetime)
-        self.assertIsInstance(self.ctrl.parse_datetime('2006/06/24'),
-                              date)
-        self.assertIsInstance(self.ctrl.parse_datetime('2006/06/24 12:18', 'Datetime'),
-                              datetime)
-        self.assertIsInstance(self.ctrl.parse_datetime('2006/06/24', 'Datetime'),
-                              datetime)
-        self.assertIsInstance(self.ctrl.parse_datetime('2006/06/24', 'Date'),
-                              date)
-        self.assertIsInstance(self.ctrl.parse_datetime('12:18', 'Time'),
-                              time)
+        ctrl = self.vreg['controllers'].select('view', self.request())
+        pd = ctrl.parse_datetime
+        self.assertIsInstance(pd('2006/06/24 12:18'), datetime)
+        self.assertIsInstance(pd('2006/06/24'), date)
+        self.assertIsInstance(pd('2006/06/24 12:18', 'Datetime'), datetime)
+        self.assertIsInstance(pd('2006/06/24', 'Datetime'), datetime)
+        self.assertIsInstance(pd('2006/06/24', 'Date'), date)
+        self.assertIsInstance(pd('12:18', 'Time'), time)
 
     def test_parse_datetime_ko(self):
+        ctrl = self.vreg['controllers'].select('view', self.request())
+        pd = ctrl.parse_datetime
         self.assertRaises(ValueError,
-                          self.ctrl.parse_datetime, '2006/06/24 12:188', 'Datetime')
+                          pd, '2006/06/24 12:188', 'Datetime')
         self.assertRaises(ValueError,
-                          self.ctrl.parse_datetime, '2006/06/240', 'Datetime')
+                          pd, '2006/06/240', 'Datetime')
         self.assertRaises(ValueError,
-                          self.ctrl.parse_datetime, '2006/06/24 12:18', 'Date')
+                          pd, '2006/06/24 12:18', 'Date')
         self.assertRaises(ValueError,
-                          self.ctrl.parse_datetime, '2006/24/06', 'Date')
+                          pd, '2006/24/06', 'Date')
         self.assertRaises(ValueError,
-                          self.ctrl.parse_datetime, '2006/06/240', 'Date')
+                          pd, '2006/06/240', 'Date')
         self.assertRaises(ValueError,
-                          self.ctrl.parse_datetime, '12:188', 'Time')
+                          pd, '12:188', 'Time')
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_form.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_form.py	Tue Sep 22 13:08:42 2009 +0200
@@ -12,7 +12,7 @@
 from logilab.common.testlib import unittest_main, mock_object
 
 from cubicweb import Binary
-from cubicweb.devtools.testlib import WebTest
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web.formfields import (IntField, StringField, RichTextField,
                                      DateTimeField, DateTimePicker,
                                      FileField, EditableFileField)
@@ -22,7 +22,7 @@
 from cubicweb.web.views.formrenderers import FormRenderer
 
 
-class FieldsFormTC(WebTest):
+class FieldsFormTC(CubicWebTC):
 
     def test_form_field_format(self):
         form = FieldsForm(self.request(), None)
@@ -32,7 +32,7 @@
         self.assertEquals(form.form_field_format(None), 'text/rest')
 
 
-class EntityFieldsFormTC(WebTest):
+class EntityFieldsFormTC(CubicWebTC):
 
     def setUp(self):
         super(EntityFieldsFormTC, self).setUp()
@@ -54,29 +54,30 @@
         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)
+        e = self.vreg['etypes'].etype_class('CWUser')(self.request())
+        form = EntityFieldsForm(e.req, None, entity=e)
         unrelated = [rview for rview, reid in form.subject_relation_vocabulary('in_group')]
         # 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')
+        e = self.vreg['etypes'].etype_class('CWUser')(self.request())
         e.eid = 'A'
         form = EntityFieldsForm(self.request(login=u'toto'), None, entity=e)
         field = StringField(name='login', eidparam=True)
@@ -86,7 +87,7 @@
 
 
     def test_linkto_field_duplication(self):
-        e = self.etype_instance('CWUser')
+        e = self.vreg['etypes'].etype_class('CWUser')(self.request())
         e.eid = 'A'
         e.req = self.req
         geid = self.execute('CWGroup X WHERE X name "users"')[0][0]
@@ -124,12 +125,14 @@
             creation_date = DateTimeField(widget=DateTimePicker)
         form = CustomChangeStateForm(self.req, redirect_path='perdu.com',
                                      entity=self.entity)
-        form.form_render(state=123, trcomment=u'')
+        form.form_render(state=123, trcomment=u'',
+                         trcomment_format=u'text/plain')
 
     def test_change_state_form(self):
         form = ChangeStateForm(self.req, redirect_path='perdu.com',
                                entity=self.entity)
-        form.form_render(state=123, trcomment=u'')
+        form.form_render(state=123, trcomment=u'',
+                         trcomment_format=u'text/plain')
 
     # fields tests ############################################################
 
@@ -141,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'
@@ -161,7 +164,7 @@
 
     def test_richtextfield_2(self):
         self.req.use_fckeditor = lambda: True
-        self._test_richtextfield('<input name="description_format:%(eid)s" style="display: block" type="hidden" value="text/rest" /><textarea cols="80" cubicweb:type="wysiwyg" id="description:%(eid)s" name="description:%(eid)s" onkeyup="autogrow(this)" rows="2" tabindex="1"></textarea>')
+        self._test_richtextfield('<input name="description_format:%(eid)s" type="hidden" value="text/rest" /><textarea cols="80" cubicweb:type="wysiwyg" id="description:%(eid)s" name="description:%(eid)s" onkeyup="autogrow(this)" rows="2" tabindex="1"></textarea>')
 
 
     def test_filefield(self):
@@ -217,7 +220,7 @@
                               '''<input id="upassword:%(eid)s" name="upassword:%(eid)s" tabindex="1" type="password" value="__cubicweb_internal_field__" />
 <br/>
 <input name="upassword-confirm:%(eid)s" tabindex="1" type="password" value="__cubicweb_internal_field__" />
-&nbsp;
+&#160;
 <span class="emphasis">confirm password</span>''' % {'eid': self.entity.eid})
 
 
--- a/web/test/unittest_formfields.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_formfields.py	Tue Sep 22 13:08:42 2009 +0200
@@ -11,7 +11,7 @@
 from yams.constraints import StaticVocabularyConstraint, SizeConstraint
 
 from cubicweb.devtools import TestServerConfiguration
-from cubicweb.devtools.testlib import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web.formwidgets import PasswordInput, TextArea, Select, Radio
 from cubicweb.web.formfields import *
 from cubicweb.web.views.forms import EntityFieldsForm
@@ -112,11 +112,11 @@
                           [(u'maybe', '1'), (u'no', '')])
 
 
-class MoreFieldsTC(EnvBasedTC):
+class MoreFieldsTC(CubicWebTC):
     def test_rtf_format_field(self):
         req = self.request()
         req.use_fckeditor = lambda: False
-        e = self.etype_instance('State')
+        e = self.vreg['etypes'].etype_class('State')(req)
         form = EntityFieldsForm(req, entity=e)
         description_field = guess_field(schema['State'], schema['description'])
         description_format_field = description_field.get_format_field(form)
@@ -128,5 +128,16 @@
         self.commit()
         self.assertEquals(description_format_field.initial(form), 'text/rest')
 
+
+class UtilsTC(TestCase):
+    def test_vocab_sort(self):
+        self.assertEquals(vocab_sort([('Z', 1), ('A', 2),
+                                      ('Group 1', None), ('Y', 3), ('B', 4),
+                                      ('Group 2', None), ('X', 5), ('C', 6)]),
+                          [('A', 2), ('Z', 1),
+                           ('Group 1', None), ('B', 4), ('Y', 3),
+                           ('Group 2', None), ('C', 6), ('X', 5)]
+                          )
+
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_magicsearch.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_magicsearch.py	Tue Sep 22 13:08:42 2009 +0200
@@ -13,37 +13,36 @@
 
 from rql import BadRQLQuery, RQLSyntaxError
 
-from cubicweb.devtools.apptest import EnvBasedTC, TestEnvironment
+from cubicweb.devtools.testlib import CubicWebTC
 
 
 translations = {
     u'CWUser' : u"Utilisateur",
-#    u'Workcase' : u"Affaire",
     u'EmailAddress' : u"Adresse",
-#    u'Division' : u"Division",
-#    u'Comment' : u"Commentaire",
     u'name' : u"nom",
     u'alias' : u"nom",
     u'surname' : u"nom",
     u'firstname' : u"prénom",
     u'state' : u"état",
-#    u'subject' : u"sujet",
     u'address' : u"adresse",
     u'use_email' : u"adel",
     }
+
 def _translate(msgid):
     return translations.get(msgid, msgid)
 
+def _ctxtranslate(ctx, msgid):
+    return _translate(msgid)
 
 from cubicweb.web.views.magicsearch import translate_rql_tree, QSPreProcessor, QueryTranslator
 
-class QueryTranslatorTC(EnvBasedTC):
+class QueryTranslatorTC(CubicWebTC):
     """test suite for QueryTranslatorTC"""
 
     def setUp(self):
         super(QueryTranslatorTC, self).setUp()
-        self.req = self.env.create_request()
-        self.vreg.config.translations = {'en': _translate}
+        self.req = self.request()
+        self.vreg.config.translations = {'en': (_translate, _ctxtranslate)}
         proc = self.vreg['components'].select('magicsearch', self.req)
         self.proc = [p for p in proc.processors if isinstance(p, QueryTranslator)][0]
 
@@ -63,11 +62,11 @@
         self.assertEquals(rql, "Any P WHERE P is CWUser, P use_email C, P surname 'Smith'")
 
 
-class QSPreProcessorTC(EnvBasedTC):
+class QSPreProcessorTC(CubicWebTC):
     """test suite for QSPreProcessor"""
     def setUp(self):
         super(QSPreProcessorTC, self).setUp()
-        self.vreg.config.translations = {'en': _translate}
+        self.vreg.config.translations = {'en': (_translate, _ctxtranslate)}
         self.req = self.request()
         proc = self.vreg['components'].select('magicsearch', self.req)
         self.proc = [p for p in proc.processors if isinstance(p, QSPreProcessor)][0]
@@ -88,7 +87,6 @@
         eschema = self.schema.eschema('CWUser')
         self.assertEquals(translate(u'prénom', eschema), "firstname")
         self.assertEquals(translate(u'nom', eschema), 'surname')
-        #self.assert_(translate(u'nom') in ('name', 'surname'))
         eschema = self.schema.eschema('EmailAddress')
         self.assertEquals(translate(u'adresse', eschema), "address")
         self.assertEquals(translate(u'nom', eschema), 'alias')
@@ -125,7 +123,6 @@
         self.assertEquals(transform(u'adresse', 'Logi%'),
                           ('EmailAddress E WHERE E alias LIKE %(text)s', {'text': 'Logi%'}))
         self.assertRaises(BadRQLQuery, transform, "pers", "taratata")
-        #self.assertEquals(transform('CWUser', '%mi'), 'CWUser E WHERE P surname LIKE "%mi"')
 
     def test_three_words_query(self):
         """tests the 'three words shortcut queries'"""
@@ -180,12 +177,12 @@
 ## Processor Chains tests ############################################
 
 
-class ProcessorChainTC(EnvBasedTC):
+class ProcessorChainTC(CubicWebTC):
     """test suite for magic_search's processor chains"""
 
     def setUp(self):
         super(ProcessorChainTC, self).setUp()
-        self.vreg.config.translations = {'en': _translate}
+        self.vreg.config.translations = {'en': (_translate, _ctxtranslate)}
         self.req = self.request()
         self.proc = self.vreg['components'].select('magicsearch', self.req)
 
@@ -195,7 +192,7 @@
             (u'foo',
              ("Any X WHERE X has_text %(text)s", {'text': u'foo'})),
             # XXX this sounds like a language translator test...
-            # and it fail
+            # and it fails
             (u'Utilisateur Smith',
              ('CWUser C WHERE C has_text %(text)s', {'text': u'Smith'})),
             (u'utilisateur nom Smith',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_pdf.py	Tue Sep 22 13:08:42 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])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_uicfg.py	Tue Sep 22 13:08:42 2009 +0200
@@ -0,0 +1,14 @@
+from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.web import uicfg
+
+class UICFGTC(EnvBasedTC):
+
+    def test_autoform_section_inlined(self):
+        self.assertEquals(uicfg.autoform_is_inlined.etype_get('CWUser', 'use_email', 'subject', 'EmailAddress'),
+                          True)
+        self.assertEquals(uicfg.autoform_section.etype_get('CWUser', 'use_email', 'subject', 'EmailAddress'),
+                          'generated')
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/web/test/unittest_urlpublisher.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_urlpublisher.py	Tue Sep 22 13:08:42 2009 +0200
@@ -11,15 +11,14 @@
 
 from logilab.common.testlib import unittest_main
 
-from cubicweb.devtools.apptest import EnvBasedTC
-from cubicweb.devtools._apptest import FakeRequest
-
 from cubicweb.rset import ResultSet
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.devtools.fake import FakeRequest
 from cubicweb.web import NotFound, Redirect
 from cubicweb.web.views.urlrewrite import SimpleReqRewriter
 
 
-class URLPublisherTC(EnvBasedTC):
+class URLPublisherTC(CubicWebTC):
     """test suite for QSPreProcessor"""
 
     def setup_database(self):
@@ -30,7 +29,7 @@
 
     def process(self, url):
         req = self.req = self.request()
-        return self.env.app.url_resolver.process(req, url)
+        return self.app.url_resolver.process(req, url)
 
     def test_raw_path(self):
         """tests raw path resolution'"""
--- a/web/test/unittest_urlrewrite.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_urlrewrite.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,8 +7,8 @@
 """
 from logilab.common.testlib import TestCase, unittest_main
 
-from cubicweb.devtools._apptest import FakeRequest
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.devtools.fake import FakeRequest
 
 from cubicweb.web.views.urlrewrite import SimpleReqRewriter, SchemaBasedRewriter, rgx, rgx_action
 
@@ -29,6 +29,8 @@
         self.assertListEquals(rules, [
             ('foo' , dict(rql='Foo F')),
             ('/index' , dict(vid='index2')),
+            ('/_', dict(vid='manage')),
+            ('/_registry', dict(vid='registry')),
             ('/schema', dict(vid='schema')),
             ('/myprefs', dict(vid='propertiesform')),
             ('/siteconfig', dict(vid='systempropertiesform')),
@@ -82,7 +84,7 @@
 
 
 
-class RgxActionRewriteTC(EnvBasedTC):
+class RgxActionRewriteTC(CubicWebTC):
 
     def setup_database(self):
         self.p1 = self.create_user(u'user1')
@@ -104,6 +106,73 @@
         self.assertEquals(len(rset), 1)
         self.assertEquals(rset[0][0], self.p1.eid)
 
+    def test_inheritance_precedence(self):
+        RQL1 = 'Any C WHERE C is CWEType'
+        RQL2 = 'Any C WHERE C is CWUser'
+
+        class BaseRewriter(SchemaBasedRewriter):
+            rules = [
+               (rgx('/collector(.*)'),
+                rgx_action(rql=RQL1,
+                    form=dict(vid='baseindex')),
+                ),
+                ]
+        class Rewriter(BaseRewriter):
+            rules = [
+               (rgx('/collector/something(/?)'),
+                rgx_action(rql=RQL2,
+                    form=dict(vid='index')),
+                ),
+                ]
+
+        rewriter = Rewriter()
+        req = self.request()
+        pmid, rset = rewriter.rewrite(req, '/collector')
+        self.assertEquals(rset.rql, RQL1)
+        self.assertEquals(req.form, {'vid' : "baseindex"})
+        pmid, rset = rewriter.rewrite(req, '/collector/something')
+        self.assertEquals(rset.rql, RQL2)
+        self.assertEquals(req.form, {'vid' : "index"})
+        pmid, rset = rewriter.rewrite(req, '/collector/something/')
+        self.assertEquals(req.form, {'vid' : "index"})
+        self.assertEquals(rset.rql, RQL2)
+        pmid, rset = rewriter.rewrite(req, '/collector/somethingelse/')
+        self.assertEquals(rset.rql, RQL1)
+        self.assertEquals(req.form, {'vid' : "baseindex"})
+
+    def test_inheritance_precedence_same_rgx(self):
+        RQL1 = 'Any C WHERE C is CWEType'
+        RQL2 = 'Any C WHERE C is CWUser'
+
+        class BaseRewriter(SchemaBasedRewriter):
+            rules = [
+               (rgx('/collector(.*)'),
+                rgx_action(rql=RQL1,
+                    form=dict(vid='baseindex')),
+                ),
+                ]
+        class Rewriter(BaseRewriter):
+            rules = [
+               (rgx('/collector(.*)'),
+                rgx_action(rql=RQL2,
+                    form=dict(vid='index')),
+                ),
+                ]
+
+        rewriter = Rewriter()
+        req = self.request()
+        pmid, rset = rewriter.rewrite(req, '/collector')
+        self.assertEquals(rset.rql, RQL2)
+        self.assertEquals(req.form, {'vid' : "index"})
+        pmid, rset = rewriter.rewrite(req, '/collector/something')
+        self.assertEquals(rset.rql, RQL2)
+        self.assertEquals(req.form, {'vid' : "index"})
+        pmid, rset = rewriter.rewrite(req, '/collector/something/')
+        self.assertEquals(req.form, {'vid' : "index"})
+        self.assertEquals(rset.rql, RQL2)
+        pmid, rset = rewriter.rewrite(req, '/collector/somethingelse/')
+        self.assertEquals(rset.rql, RQL2)
+        self.assertEquals(req.form, {'vid' : "index"})
 
 
 if __name__ == '__main__':
--- a/web/test/unittest_views_actions.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_views_actions.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,26 +7,26 @@
 """
 from logilab.common.testlib import unittest_main
 
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 
-class ActionsTC(EnvBasedTC):
+class ActionsTC(CubicWebTC):
     def test_view_action(self):
         req = self.request(__message='bla bla bla', vid='rss', rql='CWUser X')
         rset = self.execute('CWUser X')
-        vaction = [action for action in self.vreg['actions'].possible_vobjects(req, rset=rset)
-                   if action.id == 'view'][0]
+        actions = self.vreg['actions'].poss_visible_objects(req, rset=rset)
+        vaction = [action for action in actions if action.id == 'view'][0]
         self.assertEquals(vaction.url(), 'http://testing.fr/cubicweb/view?rql=CWUser%20X')
 
     def test_sendmail_action(self):
         req = self.request()
         rset = self.execute('Any X WHERE X login "admin"', req=req)
-        self.failUnless([action for action in self.vreg['actions'].possible_vobjects(req, rset=rset)
-                         if action.id == 'sendemail'])
+        actions = self.vreg['actions'].poss_visible_objects(req, rset=rset)
+        self.failUnless([action for action in actions if action.id == 'sendemail'])
         self.login('anon')
         req = self.request()
         rset = self.execute('Any X WHERE X login "anon"', req=req)
-        self.failIf([action for action in self.vreg['actions'].possible_vobjects(req, rset=rset)
-                     if action.id == 'sendemail'])
+        actions = self.vreg['actions'].poss_visible_objects(req, rset=rset)
+        self.failIf([action for action in actions if action.id == 'sendemail'])
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_views_basecontrollers.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Tue Sep 22 13:08:42 2009 +0200
@@ -8,42 +8,39 @@
 import simplejson
 
 from logilab.common.testlib import unittest_main, mock_object
-from cubicweb.devtools.apptest import EnvBasedTC, ControllerTC
 
 from cubicweb import Binary, NoSelectableObject, ValidationError
 from cubicweb.view import STRICT_DOCTYPE
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.common.uilib import rql_for_eid
-
 from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect, RequestError
-
 from cubicweb.entities.authobjs import CWUser
 
 
-class EditControllerTC(ControllerTC):
+class EditControllerTC(CubicWebTC):
     def setUp(self):
-        ControllerTC.setUp(self)
+        CubicWebTC.setUp(self)
         self.failUnless('users' in self.schema.eschema('CWGroup').get_groups('read'))
 
     def tearDown(self):
-        ControllerTC.tearDown(self)
+        CubicWebTC.tearDown(self)
         self.failUnless('users' in self.schema.eschema('CWGroup').get_groups('read'))
 
     def test_noparam_edit(self):
         """check behaviour of this controller without any form parameter
         """
-
-        self.req.form = {}
-        self.assertRaises(ValidationError, self.publish, self.req)
+        self.assertRaises(ValidationError, self.publish, self.request())
 
     def test_validation_unique(self):
         """test creation of two linked entities
         """
         user = self.user()
-        self.req.form = {'eid': 'X', '__type:X': 'CWUser',
-                         'login:X': u'admin', 'edits-login:X': u'',
-                         'upassword:X': u'toto', 'upassword-confirm:X': u'toto', 'edits-upassword:X': u'',
-                         }
-        self.assertRaises(ValidationError, self.publish, self.req)
+        req = self.request()
+        req.form = {'eid': 'X', '__type:X': 'CWUser',
+                    'login:X': u'admin', 'edits-login:X': u'',
+                    'upassword:X': u'toto', 'upassword-confirm:X': u'toto', 'edits-upassword:X': u'',
+                    }
+        self.assertRaises(ValidationError, self.publish, req)
 
 
     def test_user_editing_itself(self):
@@ -54,7 +51,8 @@
         groupeids = [eid for eid, in self.execute('CWGroup G WHERE G name in ("managers", "users")')]
         groups = [str(eid) for eid in groupeids]
         stateeid = [eid for eid, in self.execute('State S WHERE S name "activated"')][0]
-        self.req.form = {
+        req = self.request()
+        req.form = {
             'eid':       `user.eid`,
             '__type:'+`user.eid`:    'CWUser',
             'login:'+`user.eid`:     unicode(user.login),
@@ -69,7 +67,7 @@
             'edits-in_group:'+`user.eid`:  basegroups,
             'edits-in_state:'+`user.eid`:  `stateeid`,
             }
-        path, params = self.expect_redirect_publish()
+        path, params = self.expect_redirect_publish(req)
         e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}, 'x').get_entity(0, 0)
         self.assertEquals(e.firstname, u'Th\xe9nault')
         self.assertEquals(e.surname, u'Sylvain')
@@ -81,8 +79,6 @@
         user = self.create_user('user')
         cnx = self.login('user')
         req = self.request()
-        #self.assertEquals(self.ctrl.schema['CWUser']._groups['read'],
-        #                  ('managers', 'users'))
         req.form = {
             'eid': `user.eid`, '__type:'+`user.eid`: 'CWUser',
             '__maineid' : str(user.eid),
@@ -101,7 +97,8 @@
         """
         user = self.user()
         groupeids = [eid for eid, in self.execute('CWGroup G WHERE X in_group G, X eid %(x)s', {'x': user.eid})]
-        self.req.form = {
+        req = self.request()
+        req.form = {
             'eid':       `user.eid`,
             '__type:'+`user.eid`:    'CWUser',
             'login:'+`user.eid`:     unicode(user.login),
@@ -112,7 +109,7 @@
             'edits-firstname:'+`user.eid`: u'',
             'edits-surname:'+`user.eid`:   u'',
             }
-        path, params = self.expect_redirect_publish()
+        path, params = self.expect_redirect_publish(req)
         e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}, 'x').get_entity(0, 0)
         self.assertEquals(e.login, user.login)
         self.assertEquals(e.firstname, u'Th\xe9nault')
@@ -124,21 +121,22 @@
 
     def test_create_multiple_linked(self):
         gueid = self.execute('CWGroup G WHERE G name "users"')[0][0]
-        self.req.form = {'eid': ['X', 'Y'],
-
-                         '__type:X': 'CWUser',
-                         '__maineid' : 'X',
-                         'login:X': u'adim', 'edits-login:X': u'',
-                         'upassword:X': u'toto', 'upassword-confirm:X': u'toto', 'edits-upassword:X': u'',
-                         'surname:X': u'Di Mascio', 'edits-surname:X': '',
+        req = self.request()
+        req.form = {'eid': ['X', 'Y'],
 
-                         'in_group:X': `gueid`, 'edits-in_group:X': INTERNAL_FIELD_VALUE,
+                    '__type:X': 'CWUser',
+                    '__maineid' : 'X',
+                    'login:X': u'adim', 'edits-login:X': u'',
+                    'upassword:X': u'toto', 'upassword-confirm:X': u'toto', 'edits-upassword:X': u'',
+                    'surname:X': u'Di Mascio', 'edits-surname:X': '',
 
-                         '__type:Y': 'EmailAddress',
-                         'address:Y': u'dima@logilab.fr', 'edits-address:Y': '',
-                         'use_email:X': 'Y', 'edits-use_email:X': INTERNAL_FIELD_VALUE,
-                         }
-        path, params = self.expect_redirect_publish()
+                    'in_group:X': `gueid`, 'edits-in_group:X': INTERNAL_FIELD_VALUE,
+
+                    '__type:Y': 'EmailAddress',
+                    'address:Y': u'dima@logilab.fr', 'edits-address:Y': '',
+                    'use_email:X': 'Y', 'edits-use_email:X': INTERNAL_FIELD_VALUE,
+                    }
+        path, params = self.expect_redirect_publish(req)
         # should be redirected on the created person
         self.assertEquals(path, 'cwuser/adim')
         e = self.execute('Any P WHERE P surname "Di Mascio"').get_entity(0, 0)
@@ -148,17 +146,18 @@
 
     def test_edit_multiple_linked(self):
         peid = self.create_user('adim').eid
-        self.req.form = {'eid': [`peid`, 'Y'],
-                         '__type:%s'%peid: 'CWUser',
-                         'surname:%s'%peid: u'Di Masci', 'edits-surname:%s'%peid: '',
+        req = self.request()
+        req.form = {'eid': [`peid`, 'Y'],
+                    '__type:%s'%peid: 'CWUser',
+                    'surname:%s'%peid: u'Di Masci', 'edits-surname:%s'%peid: '',
 
-                         '__type:Y': 'EmailAddress',
-                         'address:Y': u'dima@logilab.fr', 'edits-address:Y': '',
-                         'use_email:%s'%peid: 'Y', 'edits-use_email:%s'%peid: INTERNAL_FIELD_VALUE,
+                    '__type:Y': 'EmailAddress',
+                    'address:Y': u'dima@logilab.fr', 'edits-address:Y': '',
+                    'use_email:%s'%peid: 'Y', 'edits-use_email:%s'%peid: INTERNAL_FIELD_VALUE,
 
-                         '__redirectrql': 'Any X WHERE X eid %s'%peid,
-                         }
-        path, params = self.expect_redirect_publish()
+                    '__redirectrql': 'Any X WHERE X eid %s'%peid,
+                    }
+        path, params = self.expect_redirect_publish(req)
         # should be redirected on the created person
         eid = params['rql'].split()[-1]
         e = self.execute('Any X WHERE X eid %(x)s', {'x': eid}, 'x').get_entity(0, 0)
@@ -167,7 +166,8 @@
         self.assertEquals(email.address, 'dima@logilab.fr')
 
         emaileid = email.eid
-        self.req.form = {'eid': [`peid`, `emaileid`],
+        req = self.request()
+        req.form = {'eid': [`peid`, `emaileid`],
                          '__type:%s'%peid: 'CWUser',
                          'surname:%s'%peid: u'Di Masci', 'edits-surname:%s'%peid: 'Di Masci',
                          '__type:%s'%emaileid: 'EmailAddress',
@@ -175,7 +175,7 @@
                          'use_email:%s'%peid: `emaileid`, 'edits-use_email:%s'%peid: `emaileid`,
                          '__redirectrql': 'Any X WHERE X eid %s'%peid,
                          }
-        path, params = self.expect_redirect_publish()
+        path, params = self.expect_redirect_publish(req)
         # should be redirected on the created person
         eid = params['rql'].split()[-1]
         e = self.execute('Any X WHERE X eid %(x)s', {'x': eid}, 'x').get_entity(0, 0)
@@ -188,41 +188,47 @@
         """test creation of two linked entities
         """
         user = self.user()
-        self.req.form = {'__cloned_eid:X': user.eid,
-                         'eid': 'X', '__type:X': 'CWUser',
-                         'login:X': u'toto', 'edits-login:X': u'',
-                         'upassword:X': u'toto', 'edits-upassword:X': u'',
-                         }
-        self.assertRaises(ValidationError, self.publish, self.req)
-        self.req.form = {'__cloned_eid:X': user.eid,
-                         'eid': 'X', '__type:X': 'CWUser',
-                         'login:X': u'toto', 'edits-login:X': u'',
-                         'upassword:X': u'toto', 'upassword-confirm:X': u'tutu', 'edits-upassword:X': u'',
-                         }
-        self.assertRaises(ValidationError, self.publish, self.req)
+        req = self.request()
+        req.form = {'__cloned_eid:X': user.eid,
+                    'eid': 'X', '__type:X': 'CWUser',
+                    'login:X': u'toto', 'edits-login:X': u'',
+                    'upassword:X': u'toto', 'edits-upassword:X': u'',
+                    }
+        self.assertRaises(ValidationError, self.publish, req)
+        req = self.request()
+        req.form = {'__cloned_eid:X': user.eid,
+                    'eid': 'X', '__type:X': 'CWUser',
+                    'login:X': u'toto', 'edits-login:X': u'',
+                    'upassword:X': u'toto',
+                    'upassword-confirm:X': u'tutu', 'edits-upassword:X': u'',
+                    }
+        self.assertRaises(ValidationError, self.publish, req)
 
 
     def test_interval_bound_constraint_success(self):
         feid = self.execute('INSERT File X: X name "toto.txt", X data %(data)s',
                             {'data': Binary('yo')})[0][0]
-        self.req.form = {'eid': ['X'],
-                         '__type:X': 'Salesterm',
-                         'amount:X': u'-10', 'edits-amount:X': '',
-                         'described_by_test:X': str(feid), 'edits-described_by_test:X': INTERNAL_FIELD_VALUE,
-                         }
-        self.assertRaises(ValidationError, self.publish, self.req)
-        self.req.form = {'eid': ['X'],
-                         '__type:X': 'Salesterm',
-                         'amount:X': u'110', 'edits-amount:X': '',
-                         'described_by_test:X': str(feid), 'edits-described_by_test:X': INTERNAL_FIELD_VALUE,
-                         }
-        self.assertRaises(ValidationError, self.publish, self.req)
-        self.req.form = {'eid': ['X'],
-                         '__type:X': 'Salesterm',
-                         'amount:X': u'10', 'edits-amount:X': '',
-                         'described_by_test:X': str(feid), 'edits-described_by_test:X': INTERNAL_FIELD_VALUE,
-                         }
-        self.expect_redirect_publish()
+        req = self.request()
+        req.form = {'eid': ['X'],
+                    '__type:X': 'Salesterm',
+                    'amount:X': u'-10', 'edits-amount:X': '',
+                    'described_by_test:X': str(feid), 'edits-described_by_test:X': INTERNAL_FIELD_VALUE,
+                }
+        self.assertRaises(ValidationError, self.publish, req)
+        req = self.request()
+        req.form = {'eid': ['X'],
+                    '__type:X': 'Salesterm',
+                    'amount:X': u'110', 'edits-amount:X': '',
+                    'described_by_test:X': str(feid), 'edits-described_by_test:X': INTERNAL_FIELD_VALUE,
+                    }
+        self.assertRaises(ValidationError, self.publish, req)
+        req = self.request()
+        req.form = {'eid': ['X'],
+                    '__type:X': 'Salesterm',
+                    'amount:X': u'10', 'edits-amount:X': '',
+                    'described_by_test:X': str(feid), 'edits-described_by_test:X': INTERNAL_FIELD_VALUE,
+                    }
+        self.expect_redirect_publish(req)
         # should be redirected on the created
         #eid = params['rql'].split()[-1]
         e = self.execute('Salesterm X').get_entity(0, 0)
@@ -232,12 +238,13 @@
         """make sure req's pending insertions are taken into account"""
         tmpgroup = self.add_entity('CWGroup', name=u"test")
         user = self.user()
-        self.req.set_session_data('pending_insert', set([(user.eid, 'in_group', tmpgroup.eid)]))
-        path, params = self.expect_redirect_publish()
+        req = self.request()
+        req.set_session_data('pending_insert', set([(user.eid, 'in_group', tmpgroup.eid)]))
+        path, params = self.expect_redirect_publish(req)
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
         self.assertUnorderedIterableEquals(usergroups, ['managers', 'test'])
-        self.assertEquals(self.req.get_pending_inserts(), [])
+        self.assertEquals(req.get_pending_inserts(), [])
 
 
     def test_req_pending_delete(self):
@@ -250,12 +257,13 @@
         # just make sure everything was set correctly
         self.assertUnorderedIterableEquals(usergroups, ['managers', 'test'])
         # now try to delete the relation
-        self.req.set_session_data('pending_delete', set([(user.eid, 'in_group', groupeid)]))
-        path, params = self.expect_redirect_publish()
+        req = self.request()
+        req.set_session_data('pending_delete', set([(user.eid, 'in_group', groupeid)]))
+        path, params = self.expect_redirect_publish(req)
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
         self.assertUnorderedIterableEquals(usergroups, ['managers'])
-        self.assertEquals(self.req.get_pending_deletes(), [])
+        self.assertEquals(req.get_pending_deletes(), [])
 
     def test_custom_attribute_handler(self):
         def custom_login_edit(self, formparams, value, relations):
@@ -265,13 +273,14 @@
         try:
             user = self.user()
             eid = repr(user.eid)
-            self.req.form = {
+            req = self.request()
+            req.form = {
                 'eid': eid,
                 '__type:'+eid:  'CWUser',
                 'login:'+eid: u'foo',
                 'edits-login:'+eid:  unicode(user.login),
                 }
-            path, params = self.expect_redirect_publish()
+            path, params = self.expect_redirect_publish(req)
             rset = self.execute('Any L WHERE X eid %(x)s, X login L', {'x': user.eid}, 'x')
             self.assertEquals(rset[0][0], 'FOO')
         finally:
@@ -279,18 +288,19 @@
 
     def test_redirect_apply_button(self):
         redirectrql = rql_for_eid(4012) # whatever
-        self.req.form = {
-                         'eid': 'A', '__type:A': 'BlogEntry',
-                         '__maineid' : 'A',
-                         'content:A': u'"13:03:43"', 'edits-content:A': '',
-                         'title:A': u'huuu', 'edits-title:A': '',
-                         '__redirectrql': redirectrql,
-                         '__redirectvid': 'primary',
-                         '__redirectparams': 'toto=tutu&tata=titi',
-                         '__form_id': 'edition',
-                         '__action_apply': '',
-                         }
-        path, params = self.expect_redirect_publish()
+        req = self.request()
+        req.form = {
+            'eid': 'A', '__type:A': 'BlogEntry',
+            '__maineid' : 'A',
+            'content:A': u'"13:03:43"', 'edits-content:A': '',
+            'title:A': u'huuu', 'edits-title:A': '',
+            '__redirectrql': redirectrql,
+            '__redirectvid': 'primary',
+            '__redirectparams': 'toto=tutu&tata=titi',
+            '__form_id': 'edition',
+            '__action_apply': '',
+            }
+        path, params = self.expect_redirect_publish(req)
         self.failUnless(path.startswith('blogentry/'))
         eid = path.split('/')[1]
         self.assertEquals(params['vid'], 'edition')
@@ -301,17 +311,18 @@
 
     def test_redirect_ok_button(self):
         redirectrql = rql_for_eid(4012) # whatever
-        self.req.form = {
-                         'eid': 'A', '__type:A': 'BlogEntry',
-                         '__maineid' : 'A',
-                         'content:A': u'"13:03:43"', 'edits-content:A': '',
-                         'title:A': u'huuu', 'edits-title:A': '',
-                         '__redirectrql': redirectrql,
-                         '__redirectvid': 'primary',
-                         '__redirectparams': 'toto=tutu&tata=titi',
-                         '__form_id': 'edition',
-                         }
-        path, params = self.expect_redirect_publish()
+        req = self.request()
+        req.form = {
+            'eid': 'A', '__type:A': 'BlogEntry',
+            '__maineid' : 'A',
+            'content:A': u'"13:03:43"', 'edits-content:A': '',
+            'title:A': u'huuu', 'edits-title:A': '',
+            '__redirectrql': redirectrql,
+            '__redirectvid': 'primary',
+            '__redirectparams': 'toto=tutu&tata=titi',
+            '__form_id': 'edition',
+            }
+        path, params = self.expect_redirect_publish(req)
         self.assertEquals(path, 'view')
         self.assertEquals(params['rql'], redirectrql)
         self.assertEquals(params['vid'], 'primary')
@@ -320,27 +331,30 @@
 
     def test_redirect_delete_button(self):
         eid = self.add_entity('BlogEntry', title=u'hop', content=u'hop').eid
-        self.req.form = {'eid': str(eid), '__type:%s'%eid: 'BlogEntry',
-                         '__action_delete': ''}
-        path, params = self.expect_redirect_publish()
+        req = self.request()
+        req.form = {'eid': str(eid), '__type:%s'%eid: 'BlogEntry',
+                    '__action_delete': ''}
+        path, params = self.expect_redirect_publish(req)
         self.assertEquals(path, 'blogentry')
         self.assertEquals(params, {u'__message': u'entity deleted'})
         eid = self.add_entity('EmailAddress', address=u'hop@logilab.fr').eid
         self.execute('SET X use_email E WHERE E eid %(e)s, X eid %(x)s',
-                     {'x': self.session().user.eid, 'e': eid}, 'x')
+                     {'x': self.session.user.eid, 'e': eid}, 'x')
         self.commit()
-        self.req.form = {'eid': str(eid), '__type:%s'%eid: 'EmailAddress',
-                         '__action_delete': ''}
-        path, params = self.expect_redirect_publish()
+        req = self.request()
+        req.form = {'eid': str(eid), '__type:%s'%eid: 'EmailAddress',
+                    '__action_delete': ''}
+        path, params = self.expect_redirect_publish(req)
         self.assertEquals(path, 'cwuser/admin')
         self.assertEquals(params, {u'__message': u'entity deleted'})
         eid1 = self.add_entity('BlogEntry', title=u'hop', content=u'hop').eid
         eid2 = self.add_entity('EmailAddress', address=u'hop@logilab.fr').eid
-        self.req.form = {'eid': [str(eid1), str(eid2)],
-                         '__type:%s'%eid1: 'BlogEntry',
-                         '__type:%s'%eid2: 'EmailAddress',
-                         '__action_delete': ''}
-        path, params = self.expect_redirect_publish()
+        req = self.request()
+        req.form = {'eid': [str(eid1), str(eid2)],
+                    '__type:%s'%eid1: 'BlogEntry',
+                    '__type:%s'%eid2: 'EmailAddress',
+                    '__action_delete': ''}
+        path, params = self.expect_redirect_publish(req)
         self.assertEquals(path, 'view')
         self.assertEquals(params, {u'__message': u'entities deleted'})
 
@@ -351,23 +365,24 @@
         groups = [str(eid) for eid in groupeids]
         eeetypeeid = self.execute('CWEType X WHERE X name "CWGroup"')[0][0]
         basegroups = [str(eid) for eid, in self.execute('CWGroup G WHERE X read_permission G, X eid %(x)s', {'x': eeetypeeid})]
-        self.req.form = {
-                'eid':      `eeetypeeid`,
-                '__type:'+`eeetypeeid`:   'CWEType',
-                'name:'+`eeetypeeid`:     u'CWGroup',
-                'final:'+`eeetypeeid`:    False,
-                'meta:'+`eeetypeeid`:     True,
-                'description:'+`eeetypeeid`:     u'users group',
-                'read_permission:'+`eeetypeeid`:  groups,
-                #
-                'edits-name:'+`eeetypeeid`:     u'CWGroup',
-                'edits-final:'+`eeetypeeid`:    False,
-                'edits-meta:'+`eeetypeeid`:     True,
-                'edits-description:'+`eeetypeeid`:     u'users group',
-                'edits-read_permission:'+`eeetypeeid`:  basegroups,
-                }
+        req = self.request()
+        req.form = {
+            'eid':      `eeetypeeid`,
+            '__type:'+`eeetypeeid`:   'CWEType',
+            'name:'+`eeetypeeid`:     u'CWGroup',
+            'final:'+`eeetypeeid`:    False,
+            'meta:'+`eeetypeeid`:     True,
+            'description:'+`eeetypeeid`:     u'users group',
+            'read_permission:'+`eeetypeeid`:  groups,
+            #
+            'edits-name:'+`eeetypeeid`:     u'CWGroup',
+            'edits-final:'+`eeetypeeid`:    False,
+            'edits-meta:'+`eeetypeeid`:     True,
+            'edits-description:'+`eeetypeeid`:     u'users group',
+            'edits-read_permission:'+`eeetypeeid`:  basegroups,
+            }
         try:
-            path, params = self.expect_redirect_publish()
+            path, params = self.expect_redirect_publish(req)
             e = self.execute('Any X WHERE X eid %(x)s', {'x': eeetypeeid}, 'x').get_entity(0, 0)
             self.assertEquals(e.name, 'CWGroup')
             self.assertEquals([g.eid for g in e.read_permission], groupeids)
@@ -383,23 +398,24 @@
         groups = [str(eid) for eid in groupeids]
         eeetypeeid = self.execute('CWEType X WHERE X name "CWEType"')[0][0]
         basegroups = [str(eid) for eid, in self.execute('CWGroup G WHERE X read_permission G, X eid %(x)s', {'x': eeetypeeid})]
-        self.req.form = {
-                'eid':      `eeetypeeid`,
-                '__type:'+`eeetypeeid`:  'CWEType',
-                'name:'+`eeetypeeid`:     u'CWEType',
-                'final:'+`eeetypeeid`:    False,
-                'meta:'+`eeetypeeid`:     True,
-                'description:'+`eeetypeeid`:     u'users group',
-                'read_permission:'+`eeetypeeid`:  groups,
+        req = self.request()
+        req.form = {
+            'eid':      `eeetypeeid`,
+            '__type:'+`eeetypeeid`:  'CWEType',
+            'name:'+`eeetypeeid`:     u'CWEType',
+            'final:'+`eeetypeeid`:    False,
+            'meta:'+`eeetypeeid`:     True,
+            'description:'+`eeetypeeid`:     u'users group',
+            'read_permission:'+`eeetypeeid`:  groups,
 
-                'edits-name:'+`eeetypeeid`:     u'CWEType',
-                'edits-final:'+`eeetypeeid`:    False,
-                'edits-meta:'+`eeetypeeid`:     True,
-                'edits-description:'+`eeetypeeid`:     u'users group',
-                'edits-read_permission:'+`eeetypeeid`:  basegroups,
-                }
+            'edits-name:'+`eeetypeeid`:     u'CWEType',
+            'edits-final:'+`eeetypeeid`:    False,
+            'edits-meta:'+`eeetypeeid`:     True,
+            'edits-description:'+`eeetypeeid`:     u'users group',
+            'edits-read_permission:'+`eeetypeeid`:  basegroups,
+            }
         try:
-            path, params = self.expect_redirect_publish()
+            path, params = self.expect_redirect_publish(req)
             e = self.execute('Any X WHERE X eid %(x)s', {'x': eeetypeeid}, 'x').get_entity(0, 0)
             self.assertEquals(e.name, 'CWEType')
             self.assertEquals(sorted(g.eid for g in e.read_permission), groupeids)
@@ -413,12 +429,13 @@
 
         this seems to be postgres (tsearch?) specific
         """
-        self.req.form = {
-                         'eid': 'A', '__type:A': 'BlogEntry',
-                         '__maineid' : 'A',
-                         'title:A': u'"13:03:40"', 'edits-title:A': '',
-                         'content:A': u'"13:03:43"', 'edits-content:A': ''}
-        path, params = self.expect_redirect_publish()
+        req = self.request()
+        req.form = {
+            'eid': 'A', '__type:A': 'BlogEntry',
+            '__maineid' : 'A',
+            'title:A': u'"13:03:40"', 'edits-title:A': '',
+            'content:A': u'"13:03:43"', 'edits-content:A': ''}
+        path, params = self.expect_redirect_publish(req)
         self.failUnless(path.startswith('blogentry/'))
         eid = path.split('/')[1]
         e = self.execute('Any C, T WHERE C eid %(x)s, C content T', {'x': eid}, 'x').get_entity(0, 0)
@@ -428,29 +445,31 @@
 
     def test_nonregr_multiple_empty_email_addr(self):
         gueid = self.execute('CWGroup G WHERE G name "users"')[0][0]
-        self.req.form = {'eid': ['X', 'Y'],
-
-                         '__type:X': 'CWUser',
-                         'login:X': u'adim', 'edits-login:X': u'',
-                         'upassword:X': u'toto', 'upassword-confirm:X': u'toto', 'edits-upassword:X': u'',
-                         'in_group:X': `gueid`, 'edits-in_group:X': INTERNAL_FIELD_VALUE,
+        req = self.request()
+        req.form = {'eid': ['X', 'Y'],
 
-                         '__type:Y': 'EmailAddress',
-                         'address:Y': u'', 'edits-address:Y': '',
-                         'alias:Y': u'', 'edits-alias:Y': '',
-                         'use_email:X': 'Y', 'edits-use_email:X': INTERNAL_FIELD_VALUE,
-                         }
-        self.assertRaises(ValidationError, self.publish, self.req)
+                    '__type:X': 'CWUser',
+                    'login:X': u'adim', 'edits-login:X': u'',
+                    'upassword:X': u'toto', 'upassword-confirm:X': u'toto', 'edits-upassword:X': u'',
+                    'in_group:X': `gueid`, 'edits-in_group:X': INTERNAL_FIELD_VALUE,
+
+                    '__type:Y': 'EmailAddress',
+                    'address:Y': u'', 'edits-address:Y': '',
+                    'alias:Y': u'', 'edits-alias:Y': '',
+                    'use_email:X': 'Y', 'edits-use_email:X': INTERNAL_FIELD_VALUE,
+                    }
+        self.assertRaises(ValidationError, self.publish, req)
 
     def test_nonregr_copy(self):
         user = self.user()
-        self.req.form = {'__cloned_eid:X': user.eid,
-                         'eid': 'X', '__type:X': 'CWUser',
-                         '__maineid' : 'X',
-                         'login:X': u'toto', 'edits-login:X': u'',
-                         'upassword:X': u'toto', 'upassword-confirm:X': u'toto', 'edits-upassword:X': u'',
-                         }
-        path, params = self.expect_redirect_publish()
+        req = self.request()
+        req.form = {'__cloned_eid:X': user.eid,
+                    'eid': 'X', '__type:X': 'CWUser',
+                    '__maineid' : 'X',
+                    'login:X': u'toto', 'edits-login:X': u'',
+                    'upassword:X': u'toto', 'upassword-confirm:X': u'toto', 'edits-upassword:X': u'',
+                    }
+        path, params = self.expect_redirect_publish(req)
         self.assertEquals(path, 'cwuser/toto')
         e = self.execute('Any X WHERE X is CWUser, X login "toto"').get_entity(0, 0)
         self.assertEquals(e.login, 'toto')
@@ -466,29 +485,30 @@
             e = self.add_entity('EmailAddress', address=u'doe@doe.com')
             self.execute('SET P use_email E, P primary_email E WHERE P eid %(p)s, E eid %(e)s',
                          {'p' : p.eid, 'e' : e.eid})
-            self.req.form = {'__cloned_eid:X': p.eid,
-                             'eid': 'X', '__type:X': 'CWUser',
-                             'login': u'dodo', 'edits-login': u'dodo',
-                             'surname:X': u'Boom', 'edits-surname:X': u'',
-                             '__errorurl' : "whatever but required",
+            req = self.request()
+            req.form = {'__cloned_eid:X': p.eid,
+                        'eid': 'X', '__type:X': 'CWUser',
+                        'login': u'dodo', 'edits-login': u'dodo',
+                        'surname:X': u'Boom', 'edits-surname:X': u'',
+                        '__errorurl' : "whatever but required",
                              }
             # try to emulate what really happens in the web application
             # 1/ validate form => EditController.publish raises a ValidationError
             #    which fires a Redirect
             # 2/ When re-publishing the copy form, the publisher implicitly commits
             try:
-                self.env.app.publish('edit', self.req)
+                self.app.publish('edit', req)
             except Redirect:
-                self.req.form['rql'] = 'Any X WHERE X eid %s' % p.eid
-                self.req.form['vid'] = 'copy'
-                self.env.app.publish('view', self.req)
+                req.form['rql'] = 'Any X WHERE X eid %s' % p.eid
+                req.form['vid'] = 'copy'
+                self.app.publish('view', req)
             rset = self.execute('CWUser P WHERE P surname "Boom"')
             self.assertEquals(len(rset), 0)
         finally:
             p.__class__.skip_copy_for = old_skips
 
 
-class EmbedControllerTC(EnvBasedTC):
+class EmbedControllerTC(CubicWebTC):
 
     def test_nonregr_embed_publish(self):
         # This test looks a bit stupid but at least it will probably
@@ -500,23 +520,23 @@
         result = controller.publish(rset=None)
 
 
-class ReportBugControllerTC(EnvBasedTC):
+class ReportBugControllerTC(CubicWebTC):
 
     def test_usable_by_guets(self):
-        req = self.request()
-        self.vreg['controllers'].select('reportbug', req)
+        self.login('anon')
+        self.vreg['controllers'].select('reportbug', self.request())
 
 
-class SendMailControllerTC(EnvBasedTC):
+class SendMailControllerTC(CubicWebTC):
 
     def test_not_usable_by_guets(self):
         self.login('anon')
-        req = self.request()
-        self.assertRaises(NoSelectableObject, self.env.vreg['controllers'].select, 'sendmail', req)
+        self.assertRaises(NoSelectableObject,
+                          self.vreg['controllers'].select, 'sendmail', self.request())
 
 
 
-class JSONControllerTC(EnvBasedTC):
+class JSONControllerTC(CubicWebTC):
 
     def ctrl(self, req=None):
         req = req or self.request(url='http://whatever.fr/')
--- a/web/test/unittest_views_basetemplates.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_views_basetemplates.py	Tue Sep 22 13:08:42 2009 +0200
@@ -5,11 +5,11 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
-from cubicweb.devtools.testlib import WebTest
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.htmlparser import DTDValidator
 
 
-class LogFormTemplateTC(WebTest):
+class LogFormTemplateTC(CubicWebTC):
 
     def _login_labels(self):
         valid = self.content_type_validators.get('text/html', DTDValidator)()
--- a/web/test/unittest_views_baseviews.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_views_baseviews.py	Tue Sep 22 13:08:42 2009 +0200
@@ -10,7 +10,7 @@
 from logilab.common.testlib import unittest_main
 from logilab.mtconverter import html_unescape
 
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 
 from cubicweb.web.htmlwidgets import TableWidget
 from cubicweb.web.views import vid_from_rset
@@ -18,7 +18,7 @@
 def loadjson(value):
     return loads(html_unescape(value))
 
-class VidFromRsetTC(EnvBasedTC):
+class VidFromRsetTC(CubicWebTC):
 
     def test_no_rset(self):
         req = self.request()
@@ -84,7 +84,7 @@
         self.assertEquals(vid_from_rset(req, rset, self.schema), 'table')
 
 
-class TableViewTC(EnvBasedTC):
+class TableViewTC(CubicWebTC):
 
     def _prepare_entity(self):
         e = self.add_entity("State", name=u'<toto>', description=u'loo"ong blabla')
--- a/web/test/unittest_views_editforms.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_views_editforms.py	Tue Sep 22 13:08:42 2009 +0200
@@ -6,14 +6,13 @@
 :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
-from cubicweb.devtools.testlib import WebTest
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web.views.autoform import AutomaticEntityForm as AEF
 from cubicweb.web.formwidgets import AutoCompletionWidget
 def rbc(entity, category):
     return [(rschema.type, x) for rschema, tschemas, x in AEF.erelations_by_category(entity, category)]
 
-class AutomaticEntityFormTC(EnvBasedTC):
+class AutomaticEntityFormTC(CubicWebTC):
 
     def test_custom_widget(self):
         AEF.rfields_kwargs.tag_subject_of(('CWUser', 'login', '*'),
@@ -29,7 +28,7 @@
         #for (rtype, role, stype, otype), tag in AEF.rcategories._tagdefs.items():
         #    if rtype == 'tags':
         #        print rtype, role, stype, otype, ':', tag
-        e = self.etype_instance('CWUser')
+        e = self.vreg['etypes'].etype_class('CWUser')(self.request())
         # see custom configuration in views.cwuser
         self.assertEquals(rbc(e, 'primary'),
                           [('login', 'subject'),
@@ -53,6 +52,7 @@
                                ])
         self.assertListEquals(rbc(e, 'generic'),
                               [('primary_email', 'subject'),
+                               ('custom_workflow', 'subject'),
                                ('connait', 'subject'),
                                ('checked_by', 'object'),
                                ])
@@ -76,7 +76,7 @@
         self.failIf(AEF.rinlined.etype_get('CWUser', 'primary_email', 'subject'))
 
     def test_personne_relations_by_category(self):
-        e = self.etype_instance('Personne')
+        e = self.vreg['etypes'].etype_class('Personne')(self.request())
         self.assertListEquals(rbc(e, 'primary'),
                               [('nom', 'subject'),
                                ('eid', 'subject')
@@ -124,7 +124,7 @@
         self.failIf(any(f for f in form.fields if f is None))
 
 
-class FormViewsTC(WebTest):
+class FormViewsTC(CubicWebTC):
     def test_delete_conf_formview(self):
         rset = self.execute('CWGroup X')
         self.view('deleteconf', rset, template=None).source
@@ -151,11 +151,13 @@
     def test_automatic_inline_edit_formview(self):
         geid = self.execute('CWGroup X LIMIT 1')[0][0]
         rset = self.execute('CWUser X LIMIT 1')
-        self.view('inline-edition', rset, row=0, rtype='in_group', peid=geid, template=None).source
+        self.view('inline-edition', rset, row=0, col=0, rtype='in_group',
+                  peid=geid, role='subject', template=None, i18nctx='').source
 
     def test_automatic_inline_creation_formview(self):
         geid = self.execute('CWGroup X LIMIT 1')[0][0]
-        self.view('inline-creation', None, etype='CWUser', rtype='in_group', peid=geid, template=None).source
+        self.view('inline-creation', None, etype='CWUser', rtype='in_group',
+                  peid=geid, template=None, i18nctx='', role='subject').source
 
 
 if __name__ == '__main__':
--- a/web/test/unittest_views_navigation.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_views_navigation.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,14 +7,14 @@
 """
 
 from logilab.common.testlib import unittest_main, mock_object
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 
 from cubicweb.web.views.navigation import PageNavigation, SortedNavigation
 from cubicweb.web.views.ibreadcrumbs import BreadCrumbEntityVComponent
 
 BreadCrumbEntityVComponent.visible = True
 
-class NavigationTC(EnvBasedTC):
+class NavigationTC(CubicWebTC):
 
     def test_navigation_selection_whatever(self):
         req = self.request()
@@ -40,10 +40,10 @@
     def test_navigation_selection_not_enough(self):
         req = self.request()
         rset = self.execute('Any X,N LIMIT 10 WHERE X name N')
-        navcomp = self.vreg['components'].select_object('navigation', req, rset=rset)
+        navcomp = self.vreg['components'].select_or_none('navigation', req, rset=rset)
         self.assertEquals(navcomp, None)
         req.set_search_state('W:X:Y:Z')
-        navcomp = self.vreg['components'].select_object('navigation', req, rset=rset)
+        navcomp = self.vreg['components'].select_or_none('navigation', req, rset=rset)
         self.assertEquals(navcomp, None)
         req.set_search_state('normal')
 
@@ -61,10 +61,8 @@
         rset = self.execute('Any X,N WHERE X name N')
         req = self.request()
         req.form['__start'] = 1000000
-        print 'len', len(rset)
         navcomp = self.vreg['components'].select('navigation', req, rset=rset)
         html = navcomp.render()
-        print html
 
     def test_sorted_navigation_1(self):
         req = self.request()
@@ -97,37 +95,36 @@
         html = navcomp.render()
 
 
-
-class ContentNavigationTC(EnvBasedTC):
+# XXX deactivate, contextual component has been removed
+# class ContentNavigationTC(CubicWebTC):
+    # 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'].poss_visible_objects(
+    #         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'].poss_visible_objects(
+    #         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'].poss_visible_objects(
+    #         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'].poss_visible_objects(
+    #         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_views_pyviews.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_views_pyviews.py	Tue Sep 22 13:08:42 2009 +0200
@@ -1,7 +1,7 @@
 from logilab.common.testlib import unittest_main
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 
-class PyViewsTC(EnvBasedTC):
+class PyViewsTC(CubicWebTC):
 
     def test_pyvaltable(self):
         content = self.vreg['views'].render('pyvaltable', self.request(),
--- a/web/test/unittest_views_searchrestriction.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_views_searchrestriction.py	Tue Sep 22 13:08:42 2009 +0200
@@ -5,11 +5,11 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web.facet import insert_attr_select_relation, prepare_facets_rqlst
 
 
-class InsertAttrRelationTC(EnvBasedTC):
+class InsertAttrRelationTC(CubicWebTC):
 
     def parse(self, query):
         rqlst = self.vreg.parse(self.session, query)
--- a/web/test/unittest_viewselector.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_viewselector.py	Tue Sep 22 13:08:42 2009 +0200
@@ -4,7 +4,7 @@
 """
 from logilab.common.testlib import unittest_main
 
-from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import CW_SOFTWARE_ROOT as BASE, Binary
 from cubicweb.selectors import (match_user_groups, implements,
                                 specified_etype_implements, rql_condition,
@@ -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)]
@@ -26,7 +28,7 @@
                ('siteinfo', actions.SiteInfoAction),
                ]
 
-class ViewSelectorTC(EnvBasedTC):
+class ViewSelectorTC(CubicWebTC):
 
     def setup_database(self):
         self.add_entity('BlogEntry', title=u"une news !", content=u"cubicweb c'est beau")
@@ -74,16 +76,19 @@
                               ('manage', startup.ManageView),
                               ('owl', owl.OWLView),
                               ('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')
+        rset, req = self.rset_and_req('Any X WHERE X eid 999999')
         self.assertListEqual(self.pviews(req, rset),
                              [])
 
     def test_possible_views_one_egroup(self):
-        rset, req = self.env.get_rset_and_req('CWGroup X WHERE X name "managers"')
+        rset, req = self.rset_and_req('CWGroup X WHERE X name "managers"')
         self.assertListEqual(self.pviews(req, rset),
                              [('adaptedlist', baseviews.AdaptedListView),
                               ('csvexport', csvexport.CSVRsetView),
@@ -106,7 +111,7 @@
                               ])
 
     def test_possible_views_multiple_egroups(self):
-        rset, req = self.env.get_rset_and_req('CWGroup X')
+        rset, req = self.rset_and_req('CWGroup X')
         self.assertListEqual(self.pviews(req, rset),
                              [('adaptedlist', baseviews.AdaptedListView),
                               ('csvexport', csvexport.CSVRsetView),
@@ -130,16 +135,16 @@
 
     def test_propertiesform_admin(self):
         assert self.vreg['views']['propertiesform']
-        rset1, req1 = self.env.get_rset_and_req('CWUser X WHERE X login "admin"')
-        rset2, req2 = self.env.get_rset_and_req('CWUser X WHERE X login "anon"')
+        rset1, req1 = self.rset_and_req('CWUser X WHERE X login "admin"')
+        rset2, req2 = self.rset_and_req('CWUser X WHERE X login "anon"')
         self.failUnless(self.vreg['views'].select('propertiesform', req1, rset=None))
         self.failUnless(self.vreg['views'].select('propertiesform', req1, rset=rset1))
         self.failUnless(self.vreg['views'].select('propertiesform', req2, rset=rset2))
 
     def test_propertiesform_anon(self):
         self.login('anon')
-        rset1, req1 = self.env.get_rset_and_req('CWUser X WHERE X login "admin"')
-        rset2, req2 = self.env.get_rset_and_req('CWUser X WHERE X login "anon"')
+        rset1, req1 = self.rset_and_req('CWUser X WHERE X login "admin"')
+        rset2, req2 = self.rset_and_req('CWUser X WHERE X login "anon"')
         self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'propertiesform', req1, rset=None)
         self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'propertiesform', req1, rset=rset1)
         self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'propertiesform', req1, rset=rset2)
@@ -147,14 +152,14 @@
     def test_propertiesform_jdoe(self):
         self.create_user('jdoe')
         self.login('jdoe')
-        rset1, req1 = self.env.get_rset_and_req('CWUser X WHERE X login "admin"')
-        rset2, req2 = self.env.get_rset_and_req('CWUser X WHERE X login "jdoe"')
+        rset1, req1 = self.rset_and_req('CWUser X WHERE X login "admin"')
+        rset2, req2 = self.rset_and_req('CWUser X WHERE X login "jdoe"')
         self.failUnless(self.vreg['views'].select('propertiesform', req1, rset=None))
         self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'propertiesform', req1, rset=rset1)
         self.failUnless(self.vreg['views'].select('propertiesform', req2, rset=rset2))
 
     def test_possible_views_multiple_different_types(self):
-        rset, req = self.env.get_rset_and_req('Any X')
+        rset, req = self.rset_and_req('Any X')
         self.assertListEqual(self.pviews(req, rset),
                              [('csvexport', csvexport.CSVRsetView),
                               ('ecsvexport', csvexport.CSVEntityView),
@@ -176,7 +181,7 @@
                               ])
 
     def test_possible_views_any_rset(self):
-        rset, req = self.env.get_rset_and_req('Any N, X WHERE X in_group Y, Y name N')
+        rset, req = self.rset_and_req('Any N, X WHERE X in_group Y, Y name N')
         self.assertListEqual(self.pviews(req, rset),
                              [('csvexport', csvexport.CSVRsetView),
                               ('editable-table', tableview.EditableTableView),
@@ -185,7 +190,7 @@
                               ])
 
     def test_possible_views_multiple_eusers(self):
-        rset, req = self.env.get_rset_and_req('CWUser X')
+        rset, req = self.rset_and_req('CWUser X')
         self.assertListEqual(self.pviews(req, rset),
                              [('adaptedlist', baseviews.AdaptedListView),
                               ('csvexport', csvexport.CSVRsetView),
@@ -218,14 +223,14 @@
 
                               })
     def test_possible_actions_no_entity(self):
-        rset, req = self.env.get_rset_and_req('Any X WHERE X eid 999999')
+        rset, req = self.rset_and_req('Any X WHERE X eid 999999')
         self.assertDictEqual(self.pactions(req, rset),
                              {'useractions': USERACTIONS,
                               'siteactions': SITEACTIONS,
                               })
 
     def test_possible_actions_same_type_entities(self):
-        rset, req = self.env.get_rset_and_req('CWGroup X')
+        rset, req = self.rset_and_req('CWGroup X')
         self.assertDictEqual(self.pactions(req, rset),
                              {'useractions': USERACTIONS,
                               'siteactions': SITEACTIONS,
@@ -235,7 +240,7 @@
                               })
 
     def test_possible_actions_different_types_entities(self):
-        rset, req = self.env.get_rset_and_req('Any X')
+        rset, req = self.rset_and_req('Any X')
         self.assertDictEqual(self.pactions(req, rset),
                              {'useractions': USERACTIONS,
                               'siteactions': SITEACTIONS,
@@ -243,19 +248,19 @@
                               })
 
     def test_possible_actions_final_entities(self):
-        rset, req = self.env.get_rset_and_req('Any N, X WHERE X in_group Y, Y name N')
+        rset, req = self.rset_and_req('Any N, X WHERE X in_group Y, Y name N')
         self.assertDictEqual(self.pactions(req, rset),
                              {'useractions': USERACTIONS,
                               'siteactions': SITEACTIONS})
 
     def test_possible_actions_eetype_cwuser_entity(self):
-        rset, req = self.env.get_rset_and_req('CWEType X WHERE X name "CWUser"')
+        rset, req = self.rset_and_req('CWEType X WHERE X name "CWUser"')
         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),
                                               ],
@@ -291,7 +296,7 @@
                              self.vreg['views'].select, 'table', req, rset=rset)
 
         # no entity
-        rset, req = self.env.get_rset_and_req('Any X WHERE X eid 999999')
+        rset, req = self.rset_and_req('Any X WHERE X eid 999999')
         self.failUnlessRaises(NoSelectableObject,
                               self.vreg['views'].select, 'index', req, rset=rset)
         self.failUnlessRaises(NoSelectableObject,
@@ -301,7 +306,7 @@
         self.failUnlessRaises(NoSelectableObject,
                              self.vreg['views'].select, 'table', req, rset=rset)
         # one entity
-        rset, req = self.env.get_rset_and_req('CWGroup X WHERE X name "managers"')
+        rset, req = self.rset_and_req('CWGroup X WHERE X name "managers"')
         self.assertIsInstance(self.vreg['views'].select('primary', req, rset=rset),
                              primary.PrimaryView)
         self.assertIsInstance(self.vreg['views'].select('list', req, rset=rset),
@@ -315,7 +320,7 @@
         self.failUnlessRaises(NoSelectableObject,
                               self.vreg['views'].select, 'index', req, rset=rset)
         # list of entities of the same type
-        rset, req = self.env.get_rset_and_req('CWGroup X')
+        rset, req = self.rset_and_req('CWGroup X')
         self.assertIsInstance(self.vreg['views'].select('primary', req, rset=rset),
                              primary.PrimaryView)
         self.assertIsInstance(self.vreg['views'].select('list', req, rset=rset),
@@ -325,7 +330,7 @@
         self.failUnlessRaises(NoSelectableObject,
                               self.vreg['views'].select, 'creation', req, rset=rset)
         # list of entities of different types
-        rset, req = self.env.get_rset_and_req('Any X')
+        rset, req = self.rset_and_req('Any X')
         self.assertIsInstance(self.vreg['views'].select('primary', req, rset=rset),
                                   primary.PrimaryView)
         self.assertIsInstance(self.vreg['views'].select('list', req, rset=rset),
@@ -337,7 +342,7 @@
         self.failUnlessRaises(NoSelectableObject,
                               self.vreg['views'].select, 'index', req, rset=rset)
         # whatever
-        rset, req = self.env.get_rset_and_req('Any N, X WHERE X in_group Y, Y name N')
+        rset, req = self.rset_and_req('Any N, X WHERE X in_group Y, Y name N')
         self.assertIsInstance(self.vreg['views'].select('table', req, rset=rset),
                                   tableview.TableView)
         self.failUnlessRaises(NoSelectableObject,
@@ -351,7 +356,7 @@
         self.failUnlessRaises(NoSelectableObject,
                              self.vreg['views'].select, 'edition', req, rset=rset)
         # mixed query
-        rset, req = self.env.get_rset_and_req('Any U,G WHERE U is CWUser, G is CWGroup')
+        rset, req = self.rset_and_req('Any U,G WHERE U is CWUser, G is CWGroup')
         self.failUnlessRaises(NoSelectableObject,
                               self.vreg['views'].select, 'edition', req, rset=rset)
         self.failUnlessRaises(NoSelectableObject,
@@ -362,7 +367,7 @@
     def test_interface_selector(self):
         image = self.add_entity('Image', name=u'bim.png', data=Binary('bim'))
         # image primary view priority
-        rset, req = self.env.get_rset_and_req('Image X WHERE X name "bim.png"')
+        rset, req = self.rset_and_req('Image X WHERE X name "bim.png"')
         self.assertIsInstance(self.vreg['views'].select('primary', req, rset=rset),
                               idownloadable.IDownloadablePrimaryView)
 
@@ -370,12 +375,12 @@
     def test_score_entity_selector(self):
         image = self.add_entity('Image', name=u'bim.png', data=Binary('bim'))
         # image primary view priority
-        rset, req = self.env.get_rset_and_req('Image X WHERE X name "bim.png"')
+        rset, req = self.rset_and_req('Image X WHERE X name "bim.png"')
         self.assertIsInstance(self.vreg['views'].select('image', req, rset=rset),
                               idownloadable.ImageView)
         fileobj = self.add_entity('File', name=u'bim.txt', data=Binary('bim'))
         # image primary view priority
-        rset, req = self.env.get_rset_and_req('File X WHERE X name "bim.txt"')
+        rset, req = self.rset_and_req('File X WHERE X name "bim.txt"')
         self.assertRaises(NoSelectableObject, self.vreg['views'].select, 'image', req, rset=rset)
 
 
@@ -385,7 +390,7 @@
             rset = None
             req = self.request()
         else:
-            rset, req = self.env.get_rset_and_req(rql)
+            rset, req = self.rset_and_req(rql)
         try:
             self.vreg['views'].render(vid, req, rset=rset, **args)
         except:
@@ -438,23 +443,25 @@
         del self.vreg['actions']['testaction']
 
     def test(self):
-        rset, req = self.env.get_rset_and_req('CWEType X WHERE X name "CWEType"')
+        rset, req = self.rset_and_req('CWEType X WHERE X name "CWEType"')
         self.assertDictEqual(self.pactions(req, rset),
                              {'useractions': USERACTIONS,
                               'siteactions': SITEACTIONS,
                               'mainactions': [('edit', actions.ModifyAction)],
                               'moreactions': [('managepermission', actions.ManagePermissionsAction),
+                                              ('addrelated', actions.AddRelatedActions),
                                               ('delete', actions.DeleteAction),
                                               ('copy', actions.CopyAction),
                                               ('testaction', CWETypeRQLAction),
                                               ],
                               })
-        rset, req = self.env.get_rset_and_req('CWEType X WHERE X name "CWRType"')
+        rset, req = self.rset_and_req('CWEType X WHERE X name "CWRType"')
         self.assertDictEqual(self.pactions(req, rset),
                              {'useractions': USERACTIONS,
                               'siteactions': SITEACTIONS,
                               'mainactions': [('edit', actions.ModifyAction)],
                               'moreactions': [('managepermission', actions.ManagePermissionsAction),
+                                              ('addrelated', actions.AddRelatedActions),
                                               ('delete', actions.DeleteAction),
                                               ('copy', actions.CopyAction),
                                               ],
--- a/web/test/unittest_webconfig.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/test/unittest_webconfig.py	Tue Sep 22 13:08:42 2009 +0200
@@ -9,8 +9,7 @@
 
 from logilab.common.testlib import TestCase, unittest_main
 
-from cubicweb.devtools._apptest import FakeRequest
-from cubicweb.devtools import ApptestConfiguration
+from cubicweb.devtools import ApptestConfiguration, fake
 
 class WebconfigTC(TestCase):
     def setUp(self):
@@ -21,7 +20,7 @@
     def test_nonregr_print_css_as_list(self):
         """make sure PRINT_CSS *must* is a list"""
         config = self.config
-        req = FakeRequest()
+        req = fake.FakeRequest()
         print_css = req.external_resource('STYLESHEETS_PRINT')
         self.failUnless(isinstance(print_css, list))
         ie_css = req.external_resource('IE_STYLESHEETS')
--- a/web/uicfg.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/uicfg.py	Tue Sep 22 13:08:42 2009 +0200
@@ -67,9 +67,9 @@
 """
 __docformat__ = "restructuredtext en"
 
-from cubicweb import neg_role, onevent
+from cubicweb import neg_role
 from cubicweb.rtags import (RelationTags, RelationTagsBool,
-                            RelationTagsSet, RelationTagsDict)
+                            RelationTagsSet, RelationTagsDict, register_rtag)
 from cubicweb.web import formwidgets
 
 
@@ -149,14 +149,30 @@
 # * 'application'
 # * 'system'
 # * 'schema'
+# * 'hidden'
 # * 'subobject' (not displayed by default)
 
-indexview_etype_section = {'EmailAddress': 'subobject',
-                           'CWUser': 'system',
-                           'CWGroup': 'system',
-                           'CWPermission': 'system',
-                           }
+class InitializableDict(dict):
+    def __init__(self, *args, **kwargs):
+        super(InitializableDict, self).__init__(*args, **kwargs)
+        register_rtag(self)
 
+    def init(self, schema, check=True):
+        for eschema in schema.entities():
+            if eschema.schema_entity():
+                self.setdefault(eschema, 'schema')
+            elif eschema.is_subobject(strict=True):
+                self.setdefault(eschema, 'subobject')
+            else:
+                self.setdefault(eschema, 'application')
+
+indexview_etype_section = InitializableDict(EmailAddress='subobject',
+                                            CWUser='system',
+                                            CWGroup='system',
+                                            CWPermission='system',
+                                            CWCache='system',
+                                            BaseTransition='hidden',
+                                            )
 
 # autoform.AutomaticEntityForm configuration ##################################
 
@@ -164,23 +180,29 @@
 
 def init_autoform_section(rtag, sschema, rschema, oschema, role):
     if rtag.get(sschema, rschema, oschema, role) is None:
-        if role == 'subject':
-            card = rschema.rproperty(sschema, oschema, 'cardinality')[0]
-            composed = rschema.rproperty(sschema, oschema, 'composite') == 'object'
-        else:
-            card = rschema.rproperty(sschema, oschema, 'cardinality')[1]
-            composed = rschema.rproperty(sschema, oschema, 'composite') == 'subject'
-        if sschema.is_metadata(rschema):
+        if autoform_is_inlined.get(sschema, rschema, oschema, role) or \
+               autoform_is_inlined.get(sschema, rschema, oschema, neg_role(role)):
+            section = 'generated'
+        elif sschema.is_metadata(rschema):
             section = 'metadata'
-        elif card in '1+':
-            if not rschema.is_final() and composed:
-                section = 'generated'
+        else:
+            if role == 'subject':
+                card = rschema.rproperty(sschema, oschema, 'cardinality')[0]
+                composed = rschema.rproperty(sschema, oschema, 'composite') == 'object'
             else:
-                section = 'primary'
-        elif rschema.is_final():
-            section = 'secondary'
-        else:
-            section = 'generic'
+                card = rschema.rproperty(sschema, oschema, 'cardinality')[1]
+                composed = rschema.rproperty(sschema, oschema, 'composite') == 'subject'
+            if card in '1+':
+                if not rschema.is_final() and composed:
+                    # XXX why? probably because we want it unlined, though this
+                    # is not the case by default
+                    section = 'generated'
+                else:
+                    section = 'primary'
+            elif rschema.is_final():
+                section = 'secondary'
+            else:
+                section = 'generic'
         rtag.tag_relation((sschema, rschema, oschema, role), section)
 
 autoform_section = RelationTags('autoform_section', init_autoform_section,
@@ -216,15 +238,3 @@
 
 actionbox_appearsin_addmenu = RelationTagsBool('actionbox_appearsin_addmenu',
                                                init_actionbox_appearsin_addmenu)
-
-@onevent('before-source-reload')
-def clear_rtag_objects():
-    print 'YAHOO ' * 80
-    primaryview_section.clear()
-    primaryview_display_ctrl.clear()
-    autoform_section.clear()
-    autoform_field.clear()
-    autoform_field_kwargs.clear()
-    autoform_is_inlined.clear()
-    autoform_permissions_overrides.clear()
-    actionbox_appearsin_addmenu.clear()
--- a/web/views/__init__.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/__init__.py	Tue Sep 22 13:08:42 2009 +0200
@@ -8,8 +8,8 @@
 __docformat__ = "restructuredtext en"
 
 import os
+import sys
 import tempfile
-
 from rql import nodes
 
 
@@ -110,13 +110,9 @@
         self.cell_call()
 
     def cell_call(self, row=0, col=0):
-        self.row, self.col = row, col # in case one need it
-        _, tmpfile = tempfile.mkstemp('.png')
-        try:
-            self._generate(tmpfile)
-            self.w(open(tmpfile).read())
-        finally:
-            try:
-                os.unlink(tmpfile)
-            except Exception, ex:
-                self.warning('cant delete %s: %s', tmpfile, ex)
+        self.row, self.col = row, col # in case one needs it
+        fd, tmpfile = tempfile.mkstemp('.png')
+        os.close(fd)
+        self._generate(tmpfile)
+        self.w(open(tmpfile, 'rb').read())
+        os.unlink(tmpfile)
--- a/web/views/actions.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/actions.py	Tue Sep 22 13:08:42 2009 +0200
@@ -15,7 +15,7 @@
     authenticated_user, match_user_groups, match_search_state,
     has_permission, has_add_permission,
     )
-from cubicweb.web import uicfg
+from cubicweb.web import uicfg, controller
 from cubicweb.web.action import Action
 from cubicweb.web.views import linksearch_select_url, vid_from_rset
 from cubicweb.web.views.autoform import AutomaticEntityForm
@@ -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
@@ -112,8 +112,8 @@
 
     def url(self):
         params = self.req.form.copy()
-        params.pop('vid', None)
-        params.pop('__message', None)
+        for param in ('vid', '__message') + controller.NAV_FORM_PARAMETERS:
+            params.pop(param, None)
         return self.build_url(self.req.relative_path(includeparams=False),
                               **params)
 
@@ -158,14 +158,13 @@
     order = 15
 
     @classmethod
-    def registered(cls, vreg):
-        super(ManagePermissionsAction, cls).registered(vreg)
+    def __registered__(cls, vreg):
         if 'require_permission' in vreg.schema:
             cls.__select__ = (one_line_rset() & non_final_entity() &
                               (match_user_groups('managers')
                                | relation_possible('require_permission', 'subject', 'CWPermission',
                                                    action='add')))
-        return super(ManagePermissionsAction, cls).registered(vreg)
+        return super(ManagePermissionsAction, cls).__registered__(vreg)
 
     def url(self):
         return self.rset.get_entity(self.row or 0, self.col or 0).absolute_url(vid='security')
@@ -226,6 +225,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/ajaxedit.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/ajaxedit.py	Tue Sep 22 13:08:42 2009 +0200
@@ -21,7 +21,7 @@
     __registry__ = 'views'
     __select__ = (match_form_params('rtype', 'target')
                   | match_kwargs('rtype', 'target'))
-    property_defs = {} # don't want to inherit this from Box
+    cw_property_defs = {} # don't want to inherit this from Box
     id = 'xaddrelation'
     expected_kwargs = form_params = ('rtype', 'target')
 
@@ -31,7 +31,7 @@
         self.rtype = rtype or self.req.form['rtype']
         self.target = target or self.req.form['target']
         self.etype = etype or self.req.form.get('etype')
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         rschema = self.schema.rschema(self.rtype)
         if not self.etype:
             if self.target == 'object':
@@ -40,14 +40,12 @@
                 etypes = rschema.subjects(entity.e_schema)
             if len(etypes) == 1:
                 self.etype = etypes[0]
-        fakebox = []
         self.w(u'<div id="%s">' % self.id)
         self.w(u'<h1>%s</h1>' % self.req._('relation %(relname)s of %(ent)s')
                % {'relname': rschema.display_name(self.req, role(self)),
                   'ent': entity.view('incontext')})
         self.w(u'<ul>')
-        self.w_unrelated(fakebox, entity)
-        for boxitem in fakebox:
+        for boxitem in self.unrelated_boxitems(entity):
             boxitem.render(self.w)
         self.w(u'</ul></div>')
 
--- a/web/views/authentication.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/authentication.py	Tue Sep 22 13:08:42 2009 +0200
@@ -18,9 +18,10 @@
 class RepositoryAuthenticationManager(AbstractAuthenticationManager):
     """authenticate user associated to a request and check session validity"""
 
-    def __init__(self):
-        self.repo = self.config.repository(self.vreg)
-        self.log_queries = self.config['query-log-file']
+    def __init__(self, vreg):
+        super(RepositoryAuthenticationManager, self).__init__(vreg)
+        self.repo = vreg.config.repository(vreg)
+        self.log_queries = vreg.config['query-log-file']
 
     def validate_session(self, req, session):
         """check session validity, and return eventually hijacked session
--- a/web/views/autoform.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/autoform.py	Tue Sep 22 13:08:42 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
 
 
@@ -25,6 +24,8 @@
     * rtags (rcategories, rfields, rwidgets, inlined, rpermissions)
     * various standard form parameters
 
+    XXX s/rtags/uicfg/ ?
+
     You can also easily customise it by adding/removing fields in
     AutomaticEntityForm instances.
     """
@@ -33,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
@@ -45,11 +46,16 @@
     rinlined = uicfg.autoform_is_inlined
     rpermissions_overrides = uicfg.autoform_permissions_overrides
 
+    # class methods mapping schema relations to fields in the form ############
+
     @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)):
@@ -64,6 +70,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
@@ -83,7 +90,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
@@ -95,7 +102,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
@@ -109,7 +116,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
 
@@ -117,7 +125,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))
@@ -131,7 +139,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)
@@ -152,6 +160,8 @@
                 raise
             return field
 
+    # base automatic entity form methods #######################################
+
     def __init__(self, *args, **kwargs):
         super(AutomaticEntityForm, self).__init__(*args, **kwargs)
         entity = self.edited_entity
@@ -161,13 +171,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')
@@ -179,6 +189,51 @@
             return None
         return self.maxrelitems + 1
 
+    @property
+    def form_needs_multipart(self):
+        """true if the form needs enctype=multipart/form-data"""
+        if super(AutomaticEntityForm, self).form_needs_multipart:
+            return True
+        # take a look at inlined forms to check (recursively) if they
+        # need multipart handling.
+        # XXX: this is very suboptimal because inlined forms will be
+        #      selected / instantiated twice : here and during form rendering.
+        #      Potential solutions:
+        #       -> use subforms for inlined forms to get easiser access
+        #       -> use a simple onload js function to check if there is
+        #          a input type=file in the form
+        #       -> generate the <form> node when the content is rendered
+        #          and we know the correct enctype (formrenderer's w attribute
+        #          is not a StringIO)
+        for rschema, targettypes, role in self.inlined_relations():
+            # inlined forms don't handle multiple target types
+            if len(targettypes) != 1:
+                continue
+            targettype = targettypes[0]
+            if self.should_inline_relation_form(rschema, targettype, role):
+                entity = self.vreg['etypes'].etype_class(targettype)(self.req)
+                subform = self.vreg['forms'].select('edition', self.req, entity=entity)
+                if subform.form_needs_multipart:
+                    return True
+        return False
+
+    def action(self):
+        """return the form's action attribute. Default to validateform if not
+        explicitly overriden.
+        """
+        try:
+            return self._action
+        except AttributeError:
+            return self.build_url('validateform')
+
+    def set_action(self, value):
+        """override default action"""
+        self._action = value
+
+    action = property(action, set_action)
+
+    # methods mapping edited entity relations to fields in the form ############
+
     def relations_by_category(self, categories=None, permission=None):
         """return a list of (relation schema, target schemas, role) matching
         given category(ies) and permission
@@ -195,35 +250,23 @@
         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)
-
-    def action(self):
-        """return the form's action attribute. Default to validateform if not
-        explicitly overriden.
-        """
-        try:
-            return self._action
-        except AttributeError:
-            return self.build_url('validateform')
-
-    def set_action(self, value):
-        """override default action"""
-        self._action = value
-
-    action = property(action, set_action)
+                                           permission, strict=strict)
 
     def editable_attributes(self):
         """return a list of (relation schema, role) to edit for the entity"""
         return [(rschema, role) for rschema, _, role in self.relations_by_category(
                 self.attrcategories, 'add') if rschema != 'eid']
 
+    # generic relations modifier ###############################################
+
     def relations_table(self):
         """yiels 3-tuples (rtype, target, related_list)
         where <related_list> itself a list of :
@@ -234,7 +277,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
@@ -279,13 +323,33 @@
                 eview = '%s (%s)' % (eview, display_name(self.req, 'Basket'))
             yield rtype, pendingid, jscall, label, reid, eview
 
-    # should_* method extracted to allow overriding
+    # inlined forms support ####################################################
 
     def should_inline_relation_form(self, rschema, targettype, role):
         """return true if the given relation with entity has role and a
         targettype target should be inlined
         """
-        return self.rinlined.etype_get(self.edited_entity.id, rschema, role, targettype)
+        return self.rinlined.etype_get(self.edited_entity.id, rschema, role,
+                                       targettype)
+
+    def display_inline_edition_form(self, w, rschema, targettype, role,
+                                     i18nctx):
+        """display inline forms for already related entities.
+
+        Return True if some inlined form are actually displayed
+        """
+        existant = False
+        entity = self.edited_entity
+        related = entity.has_eid() and entity.related(rschema, role)
+        if related:
+            # display inline-edition view for all existing related entities
+            for i, relentity in enumerate(related.entities()):
+                if relentity.has_perm('update'):
+                    w(self.view('inline-edition', related, row=i, col=0,
+                                rtype=rschema, role=role, ptype=entity.e_schema,
+                                peid=entity.eid, i18nctx=i18nctx))
+                    existant = True
+        return existant
 
     def should_display_inline_creation_form(self, rschema, existant, card):
         """return true if a creation form should be inlined
@@ -294,6 +358,17 @@
         """
         return not existant and card in '1+' or self.req.form.has_key('force_%s_display' % rschema)
 
+    def display_inline_creation_form(self, w, rschema, targettype, role,
+                                     i18nctx):
+        """display inline forms to a newly related (hence created) entity.
+
+        Return True if some inlined form are actually displayed
+        """
+        entity = self.edited_entity
+        w(self.view('inline-creation', None, etype=targettype,
+                    peid=entity.eid, ptype=entity.e_schema,
+                    rtype=rschema, role=role, i18nctx=i18nctx))
+
     def should_display_add_new_relation_link(self, rschema, existant, card):
         """return true if we should add a link to add a new creation form
         (through ajax call)
@@ -303,6 +378,14 @@
         """
         return not existant or card in '+*'
 
+    def should_hide_add_new_relation_link(self, rschema, card):
+        """return true if once an inlined creation form is added, the 'add new'
+        link should be hidden
+
+        by default true if the relation has single cardinality
+        """
+        return card in '1?'
+
 
 def etype_relation_field(etype, rtype, role='subject'):
     eschema = AutomaticEntityForm.schema.eschema(etype)
@@ -328,7 +411,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')
@@ -343,13 +430,14 @@
 uicfg.autoform_section.tag_object_of(('*', 'created_by', 'CWUser'), 'generated')
 uicfg.autoform_section.tag_object_of(('*', 'bookmarked_by', 'CWUser'), 'metadata')
 uicfg.autoform_section.tag_attribute(('Bookmark', 'path'), 'primary')
-uicfg.autoform_section.tag_subject_of(('*', 'use_email', '*'), 'generated') # inlined actually
 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	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/basecomponents.py	Tue Sep 22 13:08:42 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.
@@ -29,7 +30,7 @@
 class RQLInputForm(component.Component):
     """build the rql input form, usually displayed in the header"""
     id = 'rqlinput'
-    property_defs = VISIBLE_PROP_DEF
+    cw_property_defs = VISIBLE_PROP_DEF
     visible = False
 
     def call(self, view=None):
@@ -47,7 +48,7 @@
 <input type="text" id="rql" name="rql" value="%s"  title="%s" tabindex="%s" accesskey="q" class="searchField" />
 <input type="submit" value="" class="rqlsubmit" tabindex="%s" />
 </fieldset>
-''' % (not self.propval('visible') and 'hidden' or '',
+''' % (not self.cw_propval('visible') and 'hidden' or '',
        self.build_url('view'), xml_escape(rql), req._('full text or RQL query'), req.next_tabindex(),
         req.next_tabindex()))
         if self.req.search_state[0] != 'normal':
@@ -59,7 +60,7 @@
 class ApplLogo(component.Component):
     """build the instance logo, usually displayed in the header"""
     id = 'logo'
-    property_defs = VISIBLE_PROP_DEF
+    cw_property_defs = VISIBLE_PROP_DEF
     # don't want user to hide this component using an cwproperty
     site_wide = True
 
@@ -71,9 +72,9 @@
 class ApplHelp(component.Component):
     """build the help button, usually displayed in the header"""
     id = 'help'
-    property_defs = VISIBLE_PROP_DEF
+    cw_property_defs = VISIBLE_PROP_DEF
     def call(self):
-        self.w(u'<a href="%s" class="help" title="%s">&nbsp;</a>'
+        self.w(u'<a href="%s" class="help" title="%s">&#160;</a>'
                % (self.build_url(_restpath='doc/main'),
                   self.req._(u'help'),))
 
@@ -82,7 +83,7 @@
     """if the user is the anonymous user, build a link to login
     else a link to the connected user object with a loggout link
     """
-    property_defs = VISIBLE_PROP_DEF
+    cw_property_defs = VISIBLE_PROP_DEF
     # don't want user to hide this component using an cwproperty
     site_wide = True
     id = 'loggeduserlink'
@@ -109,11 +110,11 @@
     def anon_user_link(self):
         if self.config['auth-mode'] == 'cookie':
             self.w(self.req._('anonymous'))
-            self.w(u'''&nbsp;[<a class="logout" href="javascript: popupLoginBox();">%s</a>]'''
+            self.w(u'''&#160;[<a class="logout" href="javascript: popupLoginBox();">%s</a>]'''
                    % (self.req._('i18n_login_popup')))
         else:
             self.w(self.req._('anonymous'))
-            self.w(u'&nbsp;[<a class="logout" href="%s">%s</a>]'
+            self.w(u'&#160;[<a class="logout" href="%s">%s</a>]'
                    % (self.build_url('login'), self.req._('login')))
 
 
@@ -124,7 +125,7 @@
     __select__ = yes()
     id = 'applmessages'
     # don't want user to hide this component using an cwproperty
-    property_defs = {}
+    cw_property_defs = {}
 
     def call(self):
         msgs = [msg for msg in (self.req.get_shared_data('sources_error', pop=True),
@@ -140,7 +141,7 @@
 class ApplicationName(component.Component):
     """display the instance name"""
     id = 'appliname'
-    property_defs = VISIBLE_PROP_DEF
+    cw_property_defs = VISIBLE_PROP_DEF
     # don't want user to hide this component using an cwproperty
     site_wide = True
 
@@ -148,7 +149,7 @@
         title = self.req.property_value('ui.site-title')
         if title:
             self.w(u'<span id="appliName"><a href="%s">%s</a></span>' % (
-                self.req.base_url(), title))
+                self.req.base_url(), xml_escape(title)))
 
 
 class SeeAlsoVComponent(component.RelatedObjectsVComponent):
@@ -170,7 +171,7 @@
     id = 'etypenavigation'
     __select__ = two_etypes_rset() | match_form_params('__restrtype', '__restrtypes',
                                                        '__restrrql')
-    property_defs = VISIBLE_PROP_DEF
+    cw_property_defs = VISIBLE_PROP_DEF
     # don't want user to hide this component using an cwproperty
     site_wide = True
     visible = False # disabled by default
@@ -211,9 +212,26 @@
                     url, _('Any')))
         else:
             html.insert(0, u'<span class="selected">%s</span>' % _('Any'))
-        self.w(u'&nbsp;|&nbsp;'.join(html))
+        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	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/basecontrollers.py	Tue Sep 22 13:08:42 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
@@ -129,7 +128,8 @@
             if rset:
                 req.set_message(req._("The view %s can not be applied to this query") % vid)
             else:
-                req.set_message(req._("You have no access to this view or it's not applyable to current data"))
+                req.set_message(req._("You have no access to this view or it can not "
+                                      "be used to display the current data."))
             self.warning("the view %s can not be applied to this query", vid)
             vid = vid_from_rset(req, rset, self.schema)
             view = self.vreg['views'].select(vid, req, rset=rset)
@@ -180,48 +180,55 @@
             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'))
+        cbargs = str(self.req.form.get('__cbargs', '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, %s);
+</script>""" %  (domid, callback, errback, jsargs, cbargs)
 
     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):
@@ -290,7 +297,7 @@
     def _exec(self, rql, args=None, eidkey=None, rocheck=True):
         """json mode: execute RQL and return resultset as json"""
         if rocheck:
-            self.ensure_ro_rql(rql)
+            self.req.ensure_ro_rql(rql)
         try:
             return self.req.execute(rql, args, eidkey)
         except Exception, ex:
@@ -298,6 +305,31 @@
             return None
         return None
 
+    def _call_view(self, view, **kwargs):
+        req = self.req
+        divid = req.form.get('divid', 'pageContent')
+        # we need to call pagination before with the stream set
+        stream = view.set_stream()
+        if req.form.get('paginate'):
+            if divid == 'pageContent':
+                # mimick main template behaviour
+                stream.write(u'<div id="pageContent">')
+                vtitle = self.req.form.get('vtitle')
+                if vtitle:
+                    stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
+            view.paginate()
+            if divid == 'pageContent':
+                stream.write(u'<div id="contentmain">')
+        view.render(**kwargs)
+        extresources = req.html_headers.getvalue(skiphead=True)
+        if extresources:
+            stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ?
+            stream.write(extresources)
+            stream.write(u'</div>\n')
+        if req.form.get('paginate') and divid == 'pageContent':
+            stream.write(u'</div></div>')
+        return stream.getvalue()
+
     @xhtmlize
     def js_view(self):
         # XXX try to use the page-content template
@@ -313,28 +345,7 @@
         except NoSelectableObject:
             vid = req.form.get('fallbackvid', 'noresult')
             view = self.vreg['views'].select(vid, req, rset=rset)
-        divid = req.form.get('divid', 'pageContent')
-        # we need to call pagination before with the stream set
-        stream = view.set_stream()
-        if req.form.get('paginate'):
-            if divid == 'pageContent':
-                # mimick main template behaviour
-                stream.write(u'<div id="pageContent">')
-                vtitle = self.req.form.get('vtitle')
-                if vtitle:
-                    stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
-            view.pagination(req, rset, view.w, not view.need_navigation)
-            if divid == 'pageContent':
-                stream.write(u'<div id="contentmain">')
-        view.render()
-        extresources = req.html_headers.getvalue(skiphead=True)
-        if extresources:
-            stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ?
-            stream.write(extresources)
-            stream.write(u'</div>\n')
-        if req.form.get('paginate') and divid == 'pageContent':
-            stream.write(u'</div></div>')
-        return stream.getvalue()
+        return self._call_view(view)
 
     @xhtmlize
     def js_prop_widget(self, propkey, varname, tabindex=None):
@@ -366,11 +377,12 @@
 
     @check_pageid
     @xhtmlize
-    def js_inline_creation_form(self, peid, ttype, rtype, role):
+    def js_inline_creation_form(self, peid, ttype, rtype, role, i18nctx):
         view = self.vreg['views'].select('inline-creation', self.req,
                                          etype=ttype, peid=peid, rtype=rtype,
                                          role=role)
-        return view.render(etype=ttype, peid=peid, rtype=rtype, role=role)
+        return self._call_view(view, etype=ttype, peid=peid,
+                               rtype=rtype, role=role, i18nctx=i18nctx)
 
     @jsonize
     def js_validate_form(self, action, names, values):
@@ -382,7 +394,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	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,607 +0,0 @@
-"""Set of HTML automatic forms to create, delete, copy or edit a single entity
-or a list of entities of the same type
-
-: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
-"""
-__docformat__ = "restructuredtext en"
-
-from copy import copy
-
-from simplejson import dumps
-
-from logilab.mtconverter import xml_escape
-from logilab.common.decorators import cached
-
-from cubicweb.selectors import (specified_etype_implements, accepts_etype_compat,
-                                non_final_entity, match_kwargs, one_line_rset)
-from cubicweb.view import View, EntityView
-from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param
-from cubicweb.web.controller import NAV_FORM_PARAMETERS
-from cubicweb.web.widgets import checkbox, InputWidget, ComboBoxWidget
-from cubicweb.web.form import FormMixIn
-from cubicweb.web.views.autoform import AutomaticEntityForm
-
-_ = unicode
-
-
-class EditionForm(FormMixIn, EntityView):
-    """primary entity edition form
-
-    When generating a new attribute_input, the editor will look for a method
-    named 'default_ATTRNAME' on the entity instance, where ATTRNAME is the
-    name of the attribute being edited. You may use this feature to compute
-    dynamic default values such as the 'tomorrow' date or the user's login
-    being connected
-    """
-    id = 'edition'
-    __select__ = one_line_rset() & non_final_entity()
-
-    title = _('edition')
-    controller = 'edit'
-    skip_relations = set()
-
-    EDITION_BODY = u'''\
- %(errormsg)s
-<form id="%(formid)s" class="entityForm" cubicweb:target="eformframe"
-      method="post" onsubmit="%(onsubmit)s" enctype="%(enctype)s" action="%(action)s">
- %(title)s
- <div id="progress">%(inprogress)s</div>
- <div class="iformTitle"><span>%(mainattrs_label)s</span></div>
- <div class="formBody"><fieldset>
- %(base)s
- %(attrform)s
- %(relattrform)s
-</fieldset>
- %(relform)s
- </div>
- <table width="100%%">
-  <tbody>
-   <tr><td align="center">
-     %(validate)s
-   </td><td style="align: right; width: 50%%;">
-     %(apply)s
-     %(cancel)s
-   </td></tr>
-  </tbody>
- </table>
-</form>
-'''
-
-    def cell_call(self, row, col, **kwargs):
-        self.req.add_js( ('cubicweb.ajax.js', ) )
-        entity = self.complete_entity(row, col)
-        self.edit_form(entity, kwargs)
-
-    def edit_form(self, entity, kwargs):
-        varmaker = self.req.get_page_data('rql_varmaker')
-        if varmaker is None:
-            varmaker = self.req.varmaker
-            self.req.set_page_data('rql_varmaker', varmaker)
-        self.varmaker = varmaker
-        self.w(self.EDITION_BODY % self.form_context(entity, kwargs))
-
-    def form_context(self, entity, kwargs):
-        """returns the dictionnary used to fill the EDITION_BODY template
-
-        If you create your own edition form, you can probably just override
-        `EDITION_BODY` and `form_context`
-        """
-        if self.need_multipart(entity):
-            enctype = 'multipart/form-data'
-        else:
-            enctype = 'application/x-www-form-urlencoded'
-        self._hiddens = []
-        if entity.eid is None:
-            entity.eid = self.varmaker.next()
-        # XXX (hack) action_title might need __linkto req's original value
-        #            and widgets such as DynamicComboWidget might change it
-        #            so we need to compute title before calling atttributes_form
-        formtitle = self.action_title(entity)
-        # be sure to call .*_form first so tabindexes are correct and inlined
-        # fields errors are consumed
-        if not entity.has_eid() or entity.has_perm('update'):
-            attrform = self.attributes_form(entity, kwargs)
-        else:
-            attrform = ''
-        inlineform = self.inline_entities_form(entity, kwargs)
-        relform = self.relations_form(entity, kwargs)
-        vindex = self.req.next_tabindex()
-        aindex = self.req.next_tabindex()
-        cindex = self.req.next_tabindex()
-        self.add_hidden_web_behaviour_params(entity)
-        _ = self.req._
-        return {
-            'formid'   : self.domid,
-            'onsubmit' : self.on_submit(entity),
-            'enctype'  : enctype,
-            'errormsg' : self.error_message(),
-            'action'   : self.build_url('validateform'),
-            'eids'     : entity.has_eid() and [entity.eid] or [],
-            'inprogress': _('validating...'),
-            'title'    : formtitle,
-            'mainattrs_label' : _('main informations'),
-            'reseturl' : self.redirect_url(entity),
-            'attrform' : attrform,
-            'relform'  : relform,
-            'relattrform': inlineform,
-            'base'     : self.base_form(entity, kwargs),
-            'validate' : self.button_ok(tabindex=vindex),
-            'apply'    : self.button_apply(tabindex=aindex),
-            'cancel'   : self.button_cancel(tabindex=cindex),
-            }
-
-    @property
-    def formid(self):
-        return self.id
-
-    def action_title(self, entity):
-        """form's title"""
-        ptitle = self.req._(self.title)
-        return u'<div class="formTitle"><span>%s %s</span></div>' % (
-            entity.dc_type(), ptitle and '(%s)' % ptitle)
-
-
-    def base_form(self, entity, kwargs):
-        output = []
-        for name, value, iid in self._hiddens:
-            if isinstance(value, basestring):
-                value = xml_escape(value)
-            if iid:
-                output.append(u'<input id="%s" type="hidden" name="%s" value="%s" />'
-                              % (iid, name, value))
-            else:
-                output.append(u'<input type="hidden" name="%s" value="%s" />'
-                              % (name, value))
-        return u'\n'.join(output)
-
-    def add_hidden_web_behaviour_params(self, entity):
-        """inserts hidden params controlling how errors and redirection
-        should be handled
-        """
-        req = self.req
-        self._hiddens.append( (u'__maineid', entity.eid, u'') )
-        self._hiddens.append( (u'__errorurl', req.url(), u'errorurl') )
-        self._hiddens.append( (u'__form_id', self.formid, u'') )
-        for param in NAV_FORM_PARAMETERS:
-            value = req.form.get(param)
-            if value:
-                self._hiddens.append( (param, value, u'') )
-        msg = self.submited_message()
-        # If we need to directly attach the new object to another one
-        for linkto in req.list_form_param('__linkto'):
-            self._hiddens.append( ('__linkto', linkto, '') )
-            msg = '%s %s' % (msg, self.req._('and linked'))
-        self._hiddens.append( ('__message', msg, '') )
-
-
-    def attributes_form(self, entity, kwargs, include_eid=True):
-        """create a form to edit entity's attributes"""
-        html = []
-        w = html.append
-        eid = entity.eid
-        wdg = entity.get_widget
-        lines = (wdg(rschema, x) for rschema, x in self.editable_attributes(entity))
-        if include_eid:
-            self._hiddens.append( ('eid', entity.eid, '') )
-        self._hiddens.append( (eid_param('__type', eid), entity.e_schema, '') )
-        w(u'<table id="%s" class="%s" style="width:100%%;">' %
-          (kwargs.get('tab_id', 'entityForm%s' % eid),
-           kwargs.get('tab_class', 'attributeForm')))
-        for widget in lines:
-            w(u'<tr>\n<th class="labelCol">%s</th>' % widget.render_label(entity))
-            error = widget.render_error(entity)
-            if error:
-                w(u'<td class="error" style="width:100%;">')
-            else:
-                w(u'<td style="width:100%;">')
-            if error:
-                w(error)
-            w(widget.edit_render(entity))
-            w(widget.render_help(entity))
-            w(u'</td>\n</tr>')
-        w(u'</table>')
-        return u'\n'.join(html)
-
-    def editable_attributes(self, entity):
-        # XXX both (add, delete)
-        return [(rschema, x) for rschema, _, x in entity.relations_by_category(('primary', 'secondary'), 'add')
-                if rschema != 'eid']
-
-    def relations_form(self, entity, kwargs):
-        srels_by_cat = entity.srelations_by_category(('generic', 'metadata'), 'add')
-        if not srels_by_cat:
-            return u''
-        req = self.req
-        _ = self.req._
-        label = u'%s :' % _('This %s' % entity.e_schema).capitalize()
-        eid = entity.eid
-        html = []
-        w = html.append
-        w(u'<fieldset class="subentity">')
-        w(u'<legend class="iformTitle">%s</legend>' % label)
-        w(u'<table id="relatedEntities">')
-        for row in self.relations_table(entity):
-            # already linked entities
-            if row[2]:
-                w(u'<tr><th class="labelCol">%s</th>' % row[0].display_name(req, row[1]))
-                w(u'<td>')
-                w(u'<ul>')
-                for viewparams in row[2]:
-                    w(u'<li class="invisible">%s<div id="span%s" class="%s">%s</div></li>'
-                      % (viewparams[1], viewparams[0], viewparams[2], viewparams[3]))
-                if not self.force_display and self.maxrelitems < len(row[2]):
-                    w(u'<li class="invisible">%s</li>' % self.force_display_link())
-                w(u'</ul>')
-                w(u'</td>')
-                w(u'</tr>')
-        pendings = list(self.restore_pending_inserts(entity))
-        if not pendings:
-            w(u'<tr><th>&nbsp;</th><td>&nbsp;</td></tr>')
-        else:
-            for row in pendings:
-                # soon to be linked to entities
-                w(u'<tr id="tr%s">' % row[1])
-                w(u'<th>%s</th>' % row[3])
-                w(u'<td>')
-                w(u'<a class="handle" title="%s" href="%s">[x]</a>' %
-                  (_('cancel this insert'), row[2]))
-                w(u'<a id="a%s" class="editionPending" href="%s">%s</a>'
-                  % (row[1], row[4], xml_escape(row[5])))
-                w(u'</td>')
-                w(u'</tr>')
-        w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid)
-        w(u'<th class="labelCol">')
-        w(u'<span>%s</span>' % _('add relation'))
-        w(u'<select id="relationSelector_%s" tabindex="%s" onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">'
-          % (eid, req.next_tabindex(), xml_escape(dumps(eid))))
-        w(u'<option value="">%s</option>' % _('select a relation'))
-        for i18nrtype, rschema, target in srels_by_cat:
-            # more entities to link to
-            w(u'<option value="%s_%s">%s</option>' % (rschema, target, i18nrtype))
-        w(u'</select>')
-        w(u'</th>')
-        w(u'<td id="unrelatedDivs_%s"></td>' % eid)
-        w(u'</tr>')
-        w(u'</table>')
-        w(u'</fieldset>')
-        return '\n'.join(html)
-
-    def inline_entities_form(self, entity, kwargs):
-        """create a form to edit entity's inlined relations"""
-        result = []
-        _ = self.req._
-        for rschema, targettypes, x in entity.relations_by_category('inlineview', 'add'):
-            # show inline forms only if there's one possible target type
-            # for rschema
-            if len(targettypes) != 1:
-                self.warning('entity related by the %s relation should have '
-                             'inlined form but there is multiple target types, '
-                             'dunno what to do', rschema)
-                continue
-            targettype = targettypes[0].type
-            if self.should_inline_relation_form(entity, rschema, targettype, x):
-                result.append(u'<div id="inline%sslot">' % rschema)
-                existant = entity.has_eid() and entity.related(rschema)
-                if existant:
-                    # display inline-edition view for all existing related entities
-                    result.append(self.view('inline-edition', existant,
-                                            ptype=entity.e_schema, peid=entity.eid,
-                                            rtype=rschema, role=x, **kwargs))
-                if x == 'subject':
-                    card = rschema.rproperty(entity.e_schema, targettype, 'cardinality')[0]
-                else:
-                    card = rschema.rproperty(targettype, entity.e_schema, 'cardinality')[1]
-                # there is no related entity and we need at least one : we need to
-                # display one explicit inline-creation view
-                if self.should_display_inline_relation_form(rschema, existant, card):
-                    result.append(self.view('inline-creation', None, etype=targettype,
-                                            peid=entity.eid, ptype=entity.e_schema,
-                                            rtype=rschema, role=x, **kwargs))
-                # we can create more than one related entity, we thus display a link
-                # to add new related entities
-                if self.should_display_add_inline_relation_link(rschema, existant, card):
-                    divid = "addNew%s%s%s:%s" % (targettype, rschema, x, entity.eid)
-                    result.append(u'<div class="inlinedform" id="%s" cubicweb:limit="true">'
-                                  % divid)
-                    js = "addInlineCreationForm('%s', '%s', '%s', '%s', '%s')" % (
-                        entity.eid, entity.e_schema, targettype, rschema, x)
-                    if card in '1?':
-                        js = "toggleVisibility('%s'); %s" % (divid, js)
-                    result.append(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>'
-                                  % (rschema, entity.eid, js,
-                                     self.req.__('add a %s' % targettype)))
-                    result.append(u'</div>')
-                    result.append(u'<div class="trame_grise">&nbsp;</div>')
-                result.append(u'</div>')
-        return '\n'.join(result)
-
-    # should_* method extracted to allow overriding
-
-    def should_inline_relation_form(self, entity, rschema, targettype, role):
-        return AutomaticEntityForm.rinlined.etype_get(entity.id, rschema, role,
-                                                      targettype)
-
-    def should_display_inline_relation_form(self, rschema, existant, card):
-        return not existant and card in '1+'
-
-    def should_display_add_inline_relation_link(self, rschema, existant, card):
-        return not existant or card in '+*'
-
-    def reset_url(self, entity):
-        return entity.absolute_url()
-
-    def on_submit(self, entity):
-        return u'return freezeFormButtons(\'%s\')' % (self.domid)
-
-    def submited_message(self):
-        return self.req._('element edited')
-
-
-
-class CreationForm(EditionForm):
-    __select__ = specified_etype_implements('Any')
-    # XXX bw compat, use View.registered since we don't want accept_compat
-    #    wrapper set in EntityView
-    registered = accepts_etype_compat(View.registered)
-    id = 'creation'
-    title = _('creation')
-
-    def call(self, **kwargs):
-        """creation view for an entity"""
-        self.req.add_js( ('cubicweb.ajax.js',) )
-        self.initialize_varmaker()
-        etype = kwargs.pop('etype', self.req.form.get('etype'))
-        try:
-            entity = self.vreg.etype_class(etype)(self.req, None, None)
-        except:
-            self.w(self.req._('no such entity type %s') % etype)
-        else:
-            entity.eid = self.varmaker.next()
-            self.edit_form(entity, kwargs)
-
-    def action_title(self, entity):
-        """custom form title if creating a entity with __linkto"""
-        if '__linkto' in self.req.form:
-            if isinstance(self.req.form['__linkto'], list):
-                # XXX which one should be considered (case: add a ticket to a version in jpl)
-                rtype, linkto_eid, role = self.req.form['__linkto'][0].split(':')
-            else:
-                rtype, linkto_eid, role = self.req.form['__linkto'].split(':')
-            linkto_rset = self.req.eid_rset(linkto_eid)
-            linkto_type = linkto_rset.description[0][0]
-            if role == 'subject':
-                title = self.req.__('creating %s (%s %s %s %%(linkto)s)' % (
-                    entity.e_schema, entity.e_schema, rtype, linkto_type))
-            else:
-                title = self.req.__('creating %s (%s %%(linkto)s %s %s)' % (
-                    entity.e_schema, linkto_type, rtype, entity.e_schema))
-            msg = title % {'linkto' : self.view('incontext', linkto_rset)}
-            return u'<div class="formTitle notransform"><span>%s</span></div>' % msg
-        else:
-            return super(CreationForm, self).action_title(entity)
-
-    @property
-    def formid(self):
-        return 'edition'
-
-    def relations_form(self, entity, kwargs):
-        return u''
-
-    def reset_url(self, entity=None):
-        return self.build_url(self.req.form.get('etype', '').lower())
-
-    def submited_message(self):
-        return self.req._('element created')
-
-    def url(self):
-        """return the url associated with this view"""
-        return self.create_url(self.req.form.get('etype'))
-
-
-class InlineFormMixIn(object):
-
-    @cached
-    def card(self, etype):
-        return self.rschema.rproperty(self.parent_schema, etype, 'cardinality')[0]
-
-    def action_title(self, entity):
-        return self.rschema.display_name(self.req, self.role)
-
-    def add_hidden_web_behaviour_params(self, entity):
-        pass
-
-    def edit_form(self, entity, ptype, peid, rtype,
-                  role='subject', **kwargs):
-        self.rschema = self.schema.rschema(rtype)
-        self.role = role
-        self.parent_schema = self.schema.eschema(ptype)
-        self.parent_eid = peid
-        super(InlineFormMixIn, self).edit_form(entity, kwargs)
-
-    def should_inline_relation_form(self, entity, rschema, targettype, role):
-        if rschema == self.rschema:
-            return False
-        return AutomaticEntityForm.rinlined.etype_get(entity.id, rschema, role,
-                                                      targettype)
-
-    @cached
-    def keep_entity(self, entity):
-        req = self.req
-        # are we regenerating form because of a validation error ?
-        erroneous_post = req.data.get('formvalues')
-        if erroneous_post:
-            cdvalues = req.list_form_param('%s:%s' % (self.rschema,
-                                                      self.parent_eid),
-                                           erroneous_post)
-            if unicode(entity.eid) not in cdvalues:
-                return False
-        return True
-
-    def form_context(self, entity, kwargs):
-        ctx = super(InlineFormMixIn, self).form_context(entity, kwargs)
-        _ = self.req._
-        local_ctx = {'createmsg' : self.req.__('add a %s' % entity.e_schema),
-                     'so': self.role[0], # 's' for subject, 'o' for object
-                     'eid' : entity.eid,
-                     'rtype' : self.rschema,
-                     'parenteid' : self.parent_eid,
-                     'parenttype' : self.parent_schema,
-                     'etype' : entity.e_schema,
-                     'novalue' : INTERNAL_FIELD_VALUE,
-                     'removemsg' : self.req.__('remove this %s' % entity.e_schema),
-                     'notice' : self.req._('click on the box to cancel the deletion'),
-                     }
-        ctx.update(local_ctx)
-        return ctx
-
-
-class CopyEditionForm(EditionForm):
-    id = 'copy'
-    title = _('copy edition')
-
-    def cell_call(self, row, col, **kwargs):
-        self.req.add_js(('cubicweb.ajax.js',))
-        entity = self.complete_entity(row, col, skip_bytes=True)
-        # make a copy of entity to avoid altering the entity in the
-        # request's cache.
-        self.newentity = copy(entity)
-        self.copying = self.newentity.eid
-        self.newentity.eid = None
-        self.edit_form(self.newentity, kwargs)
-        del self.newentity
-
-    def action_title(self, entity):
-        """form's title"""
-        msg = super(CopyEditionForm, self).action_title(entity)
-        return msg + (u'<script type="text/javascript">updateMessage("%s");</script>\n'
-                      % self.req._('Please note that this is only a shallow copy'))
-        # XXX above message should have style of a warning
-
-    @property
-    def formid(self):
-        return 'edition'
-
-    def relations_form(self, entity, kwargs):
-        return u''
-
-    def reset_url(self, entity):
-        return self.build_url('view', rql='Any X WHERE X eid %s' % self.copying)
-
-    def attributes_form(self, entity, kwargs, include_eid=True):
-        # we don't want __clone_eid on inlined edited entities
-        if entity.eid == self.newentity.eid:
-            self._hiddens.append((eid_param('__cloned_eid', entity.eid), self.copying, ''))
-        return EditionForm.attributes_form(self, entity, kwargs, include_eid)
-
-    def submited_message(self):
-        return self.req._('element copied')
-
-
-class TableEditForm(FormMixIn, EntityView):
-    id = 'muledit'
-    title = _('multiple edit')
-
-    EDITION_BODY = u'''<form method="post" id="entityForm" onsubmit="return validateForm('entityForm', null);" action="%(action)s">
-  %(error)s
-  <div id="progress">%(progress)s</div>
-  <fieldset>
-  <input type="hidden" name="__errorurl" value="%(url)s" />
-  <input type="hidden" name="__form_id" value="%(formid)s" />
-  <input type="hidden" name="__redirectvid" value="%(redirectvid)s" />
-  <input type="hidden" name="__redirectrql" value="%(redirectrql)s" />
-  <table class="listing">
-    <tr class="header">
-      <th align="left"><input type="checkbox" onclick="setCheckboxesState('eid', this.checked)" value="" title="toggle check boxes" /></th>
-      %(attrheaders)s
-    </tr>
-    %(lines)s
-  </table>
-  <table width="100%%">
-    <tr>
-      <td align="left">
-        <input class="validateButton" type="submit"  value="%(okvalue)s" title="%(oktitle)s" />
-        <input class="validateButton" type="reset" name="__action_cancel" value="%(cancelvalue)s" title="%(canceltitle)s" />
-      </td>
-    </tr>
-  </table>
-  </fieldset>
-</form>
-'''
-
-    WIDGET_CELL = u'''\
-<td%(csscls)s>
-  %(error)s
-  <div>%(widget)s</div>
-</td>'''
-
-    def call(self, **kwargs):
-        """a view to edit multiple entities of the same type
-        the first column should be the eid
-        """
-        req = self.req
-        form = req.form
-        _ = req._
-        sampleentity = self.complete_entity(0)
-        attrheaders = [u'<th>%s</th>' % rdef[0].display_name(req, rdef[-1])
-                       for rdef in sampleentity.relations_by_category('primary', 'add')
-                       if rdef[0].type != 'eid']
-        ctx = {'action' : self.build_url('edit'),
-               'error': self.error_message(),
-               'progress': _('validating...'),
-               'url': xml_escape(req.url()),
-               'formid': self.id,
-               'redirectvid': xml_escape(form.get('__redirectvid', 'list')),
-               'redirectrql': xml_escape(form.get('__redirectrql', self.rset.printable_rql())),
-               'attrheaders': u'\n'.join(attrheaders),
-               'lines': u'\n'.join(self.edit_form(ent) for ent in self.rset.entities()),
-               'okvalue': _('button_ok').capitalize(),
-               'oktitle': _('validate modifications on selected items').capitalize(),
-               'cancelvalue': _('button_reset').capitalize(),
-               'canceltitle': _('revert changes').capitalize(),
-               }
-        self.w(self.EDITION_BODY % ctx)
-
-
-    def reset_url(self, entity=None):
-        self.build_url('view', rql=self.rset.printable_rql())
-
-    def edit_form(self, entity):
-        html = []
-        w = html.append
-        entity.complete()
-        eid = entity.eid
-        values = self.req.data.get('formvalues', ())
-        qeid = eid_param('eid', eid)
-        checked = qeid in values
-        w(u'<tr class="%s">' % (entity.row % 2 and u'even' or u'odd'))
-        w(u'<td>%s<input type="hidden" name="__type:%s" value="%s" /></td>'
-          % (checkbox('eid', eid, checked=checked), eid, entity.e_schema))
-        # attribute relations (skip eid which is handled by the checkbox
-        wdg = entity.get_widget
-        wdgfactories = [wdg(rschema, x) for rschema, _, x in entity.relations_by_category('primary', 'add')
-                        if rschema.type != 'eid'] # XXX both (add, delete)
-        seid = xml_escape(dumps(eid))
-        for wobj in wdgfactories:
-            if isinstance(wobj, ComboBoxWidget):
-                wobj.attrs['onchange'] = "setCheckboxesState2('eid', %s, 'checked')" % seid
-            elif isinstance(wobj, InputWidget):
-                wobj.attrs['onkeypress'] = "setCheckboxesState2('eid', %s, 'checked')" % seid
-            error = wobj.render_error(entity)
-            if error:
-                csscls = u' class="error"'
-            else:
-                csscls = u''
-            w(self.WIDGET_CELL % {'csscls': csscls, 'error': error,
-                                  'widget': wobj.edit_render(entity)})
-        w(u'</tr>')
-        return '\n'.join(html)
-
-
-# XXX bw compat
-
-from logilab.common.deprecation import class_moved
-from cubicweb.web.views import editviews
-ComboboxView = class_moved(editviews.ComboboxView)
--- a/web/views/basetemplates.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/basetemplates.py	Tue Sep 22 13:08:42 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 ##############################################################
 
@@ -113,9 +113,9 @@
         if vtitle:
             w(u'<h1 class="vtitle">%s</h1>\n' % xml_escape(vtitle))
         # display entity type restriction component
-        etypefilter = self.vreg['components'].select_vobject(
+        etypefilter = self.vreg['components'].select_or_none(
             'etypenavigation', self.req, rset=self.rset)
-        if etypefilter:
+        if etypefilter and etypefilter.cw_propval('visible'):
             etypefilter.render(w=w)
         self.nav_html = UStringIO()
         if view and view.need_navigation:
@@ -154,12 +154,11 @@
         w(u'<div id="page"><table width="100%" border="0" id="mainLayout"><tr>\n')
         self.nav_column(view, 'left')
         w(u'<td id="contentcol">\n')
-        rqlcomp = self.vreg['components'].select_object('rqlinput', self.req,
-                                                        rset=self.rset)
+        components = self.vreg['components']
+        rqlcomp = components.select_or_none('rqlinput', self.req, rset=self.rset)
         if rqlcomp:
             rqlcomp.render(w=self.w, view=view)
-        msgcomp = self.vreg['components'].select_object('applmessages',
-                                                        self.req, rset=self.rset)
+        msgcomp = components.select_or_none('applmessages', self.req, rset=self.rset)
         if msgcomp:
             msgcomp.render(w=self.w)
         self.content_header(view)
@@ -173,7 +172,7 @@
         self.w(u'</body>')
 
     def nav_column(self, view, context):
-        boxes = list(self.vreg['boxes'].possible_vobjects(
+        boxes = list(self.vreg['boxes'].poss_visible_objects(
             self.req, rset=self.rset, view=view, context=context))
         if boxes:
             self.w(u'<td class="navcol"><div class="navboxes">\n')
@@ -242,7 +241,7 @@
         w(u'<table width="100%" height="100%" border="0"><tr>\n')
         w(u'<td class="navcol">\n')
         self.topleft_header()
-        boxes = list(self.vreg['boxes'].possible_vobjects(
+        boxes = list(self.vreg['boxes'].poss_visible_objects(
             self.req, rset=self.rset, view=view, context='left'))
         if boxes:
             w(u'<div class="navboxes">\n')
@@ -257,15 +256,54 @@
             w(u'<h1 class="vtitle">%s</h1>' % xml_escape(vtitle))
 
     def topleft_header(self):
-        logo = self.vreg['components'].select_vobject('logo', self.req,
+        logo = self.vreg['components'].select_or_none('logo', self.req,
                                                       rset=self.rset)
-        if logo:
+        if logo and logo.cw_propval('visible'):
             self.w(u'<table id="header"><tr>\n')
             self.w(u'<td>')
             logo.render(w=self.w)
             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):
@@ -299,8 +337,8 @@
             self.req.add_js(jscript, localfile=False)
 
     def alternates(self):
-        urlgetter = self.vreg['components'].select_object('rss_feed_url',
-                                            self.req, rset=self.rset)
+        urlgetter = self.vreg['components'].select_or_none('rss_feed_url',
+                                                           self.req, rset=self.rset)
         if urlgetter is not None:
             self.whead(u'<link rel="alternate" type="application/rss+xml" title="RSS feed" href="%s"/>\n'
                        %  xml_escape(urlgetter.feed_url()))
@@ -315,6 +353,7 @@
 class HTMLPageHeader(View):
     """default html page header"""
     id = 'header'
+    main_cell_components = ('appliname', 'breadcrumbs')
 
     def call(self, view, **kwargs):
         self.main_header(view)
@@ -329,29 +368,29 @@
         """build the top menu with authentification info and the rql box"""
         self.w(u'<table id="header"><tr>\n')
         self.w(u'<td id="firstcolumn">')
-        logo = self.vreg['components'].select_vobject(
+        logo = self.vreg['components'].select_or_none(
             'logo', self.req, rset=self.rset)
-        if logo:
+        if logo and logo.cw_propval('visible'):
             logo.render(w=self.w)
         self.w(u'</td>\n')
         # appliname and breadcrumbs
         self.w(u'<td id="headtext">')
-        for cid in ('appliname', 'breadcrumbs'):
-            comp = self.vreg['components'].select_vobject(
+        for cid in self.main_cell_components:
+            comp = self.vreg['components'].select_or_none(
                 cid, self.req, rset=self.rset)
-            if comp:
+            if comp and comp.cw_propval('visible'):
                 comp.render(w=self.w)
         self.w(u'</td>')
         # logged user and help
         self.w(u'<td>\n')
-        comp = self.vreg['components'].select_vobject(
+        comp = self.vreg['components'].select_or_none(
             'loggeduserlink', self.req, rset=self.rset)
-        if comp:
+        if comp and comp.cw_propval('visible'):
             comp.render(w=self.w)
         self.w(u'</td><td>')
-        helpcomp = self.vreg['components'].select_vobject(
+        helpcomp = self.vreg['components'].select_or_none(
             'help', self.req, rset=self.rset)
-        if helpcomp:
+        if helpcomp and helpcomp.cw_propval('visible'):
             helpcomp.render(w=self.w)
         self.w(u'</td>')
         # lastcolumn
@@ -392,7 +431,7 @@
                                             req._(ChangeLogView.title).lower()))
         self.w(u'<a href="%s">%s</a> | ' % (req.build_url('doc/about'),
                                             req._('about this site')))
-        self.w(u'© 2001-2009 <a href="http://www.logilab.fr">Logilab S.A.</a>')
+        self.w(u'<a href="http://www.cubicweb.org">%s</a>' % req._('powered by CubicWeb'))
         self.w(u'</div>')
 
 
@@ -405,7 +444,7 @@
 
     def call(self, view, **kwargs):
         """by default, display informal messages in content header"""
-        components = self.vreg['contentnavigation'].possible_vobjects(
+        components = self.vreg['contentnavigation'].poss_visible_objects(
             self.req, rset=self.rset, view=view, context='navtop')
         if components:
             self.w(u'<div id="contentheader">')
@@ -421,7 +460,7 @@
     id = 'contentfooter'
 
     def call(self, view, **kwargs):
-        components = self.vreg['contentnavigation'].possible_vobjects(
+        components = self.vreg['contentnavigation'].poss_visible_objects(
             self.req, rset=self.rset, view=view, context='navbottom')
         if components:
             self.w(u'<div id="contentfooter">')
@@ -440,8 +479,12 @@
         self.req.add_css('cubicweb.login.css')
         self.w(u'<div id="%s" class="%s">' % (id, klass))
         if title:
-            self.w(u'<div id="loginTitle">%s</div>'
-                   % (self.req.property_value('ui.site-title') or u'&nbsp;'))
+            stitle = self.req.property_value('ui.site-title')
+            if stitle:
+                stitle = xml_escape(stitle)
+            else:
+                stitle = u'&#160;'
+            self.w(u'<div id="loginTitle">%s</div>' % stitle)
         self.w(u'<div id="loginContent">\n')
 
         if message:
@@ -472,7 +515,7 @@
         self.w(u'<td><label for="__password" >%s</label></td>' % _('password'))
         self.w(u'<td><input name="__password" id="__password" class="data" type="password" /></td>\n')
         self.w(u'</tr><tr>\n')
-        self.w(u'<td>&nbsp;</td><td><input type="submit" class="loginButton right" value="%s" />\n</td>' % _('log in'))
+        self.w(u'<td>&#160;</td><td><input type="submit" class="loginButton right" value="%s" />\n</td>' % _('log in'))
         self.w(u'</tr>\n')
         self.w(u'</table>\n')
         self.w(u'</form>\n')
@@ -489,4 +532,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/baseviews.py	Tue Sep 22 13:08:42 2009 +0200
@@ -15,6 +15,8 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from datetime import timedelta
+
 from rql import nodes
 
 from logilab.mtconverter import TransformError, xml_escape, xml_escape
@@ -51,13 +53,13 @@
     """
     id = 'final'
     # record generated i18n catalog messages
-    _('%d&nbsp;years')
-    _('%d&nbsp;months')
-    _('%d&nbsp;weeks')
-    _('%d&nbsp;days')
-    _('%d&nbsp;hours')
-    _('%d&nbsp;minutes')
-    _('%d&nbsp;seconds')
+    _('%d&#160;years')
+    _('%d&#160;months')
+    _('%d&#160;weeks')
+    _('%d&#160;days')
+    _('%d&#160;hours')
+    _('%d&#160;minutes')
+    _('%d&#160;seconds')
     _('%d years')
     _('%d months')
     _('%d weeks')
@@ -66,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]
 
@@ -77,10 +79,17 @@
                 self.w(entity.printable_value(rtype, value, format=format))
                 return
         if etype in ('Time', 'Interval'):
+            if etype == 'Interval' and isinstance(value, (int, long)):
+                # `date - date`, unlike `datetime - datetime` gives an int
+                # (number of days), not a timedelta
+                # XXX should rql be fixed to return Int instead of Interval in
+                #     that case? that would be probably the proper fix but we
+                #     loose information on the way...
+                value = timedelta(days=value)
             # value is DateTimeDelta but we have no idea about what is the
             # reference date here, so we can only approximate years and months
             if format == 'text/html':
-                space = '&nbsp;'
+                space = '&#160;'
             else:
                 space = ' '
             if value.days > 730: # 2 years
@@ -98,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
@@ -110,8 +119,8 @@
         """the secondary view for an entity
         secondary = icon + view(oneline)
         """
-        entity = self.entity(row, col)
-        self.w(u'&nbsp;')
+        entity = self.rset.get_entity(row, col)
+        self.w(u'&#160;')
         self.wview('oneline', self.rset, row=row, col=col)
 
 
@@ -122,7 +131,7 @@
     def cell_call(self, row, col):
         """the one line view for an entity: linked text view
         """
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         self.w(u'<a href="%s">' % xml_escape(entity.absolute_url()))
         self.w(xml_escape(self.view('text', self.rset, row=row, col=col)))
         self.w(u'</a>')
@@ -150,7 +159,7 @@
                 self.w(u"\n")
 
     def cell_call(self, row, col=0, **kwargs):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         self.w(cut(entity.dc_title(),
                    self.req.property_value('navigation.short-line-size')))
 
@@ -162,7 +171,7 @@
 
     def cell_call(self, row, col):
         _ = self.req._
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         self.w(u'<div class="metadata">')
         if self.show_eid:
             self.w(u'#%s - ' % entity.eid)
@@ -185,7 +194,7 @@
     id = 'textincontext'
     title = None # not listed as a possible view
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         self.w(entity.dc_title())
 
 
@@ -193,7 +202,7 @@
     id = 'textoutofcontext'
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         self.w(entity.dc_long_title())
 
 
@@ -201,7 +210,7 @@
     id = 'incontext'
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         desc = cut(entity.dc_description(), 50)
         self.w(u'<a href="%s" title="%s">' % (
             xml_escape(entity.absolute_url()), xml_escape(desc)))
@@ -214,7 +223,7 @@
     id = 'outofcontext'
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         desc = cut(entity.dc_description(), 50)
         self.w(u'<a href="%s" title="%s">' % (
             xml_escape(entity.absolute_url()), xml_escape(desc)))
--- a/web/views/bookmark.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/bookmark.py	Tue Sep 22 13:08:42 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'
@@ -33,7 +36,7 @@
     def cell_call(self, row, col):
         """the primary view for bookmark entity"""
         entity = self.complete_entity(row, col)
-        self.w(u'&nbsp;')
+        self.w(u'&#160;')
         self.w(u"<span class='title'><b>")
         self.w(u"%s : %s" % (self.req._('Bookmark'), xml_escape(entity.title)))
         self.w(u"</b></span>")
--- a/web/views/boxes.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/boxes.py	Tue Sep 22 13:08:42 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/calendar.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/calendar.py	Tue Sep 22 13:08:42 2009 +0200
@@ -293,7 +293,7 @@
                                  __redirectvid=self.id
                                  )
             self.w(u'<div class="cmd"><a href="%s">%s</a></div>' % (xml_escape(url), self.req._(u'add')))
-            self.w(u'&nbsp;')
+            self.w(u'&#160;')
         self.w(u'</div>')
         self.w(u'<div class="cellContent">')
         for task_descr in rows:
@@ -312,7 +312,7 @@
                 self.w(u'</div>')
             else:
                 self.w(u'<div class="task">')
-                self.w(u"&nbsp;")
+                self.w(u"&#160;")
             self.w(u'</div>')
         self.w(u'</div>')
         self.w(u'</td>')
@@ -443,7 +443,7 @@
         self.w(u'</tr>')
         self.w(u'</table></div>')
         self.w(u'<div id="coord"></div>')
-        self.w(u'<div id="debug">&nbsp;</div>')
+        self.w(u'<div id="debug">&#160;</div>')
 
     def _build_calendar_cell(self, date, task_descrs):
         inday_tasks = [t for t in task_descrs if t.is_one_day_task() and  t.in_working_hours()]
--- a/web/views/csvexport.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/csvexport.py	Tue Sep 22 13:08:42 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/cwuser.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/cwuser.py	Tue Sep 22 13:08:42 2009 +0200
@@ -11,9 +11,10 @@
 
 from cubicweb.selectors import one_line_rset, implements, match_user_groups
 from cubicweb.view import EntityView
-from cubicweb.web import action
+from cubicweb.web import action, uicfg
 from cubicweb.web.views import primary
 
+uicfg.primaryview_section.tag_attribute(('CWUser', 'login'), 'hidden')
 
 class UserPreferencesEntityAction(action.Action):
     id = 'prefs'
--- a/web/views/editcontroller.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/editcontroller.py	Tue Sep 22 13:08:42 2009 +0200
@@ -78,7 +78,8 @@
         try:
             methodname = req.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)
@@ -193,6 +194,7 @@
         errorurl = self.req.form.get('__errorurl')
         if errorurl:
             self.req.cancel_edition(errorurl)
+        self.req.message = self.req._('edit canceled')
         return self.reset()
 
     def _action_delete(self):
--- a/web/views/editforms.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/editforms.py	Tue Sep 22 13:08:42 2009 +0200
@@ -20,7 +20,8 @@
 from cubicweb.utils import make_uid
 from cubicweb.view import EntityView
 from cubicweb.common import tags
-from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs, eid_param, uicfg
+from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, stdmsgs, eid_param
+from cubicweb.web import uicfg
 from cubicweb.web.form import FormViewMixIn, FieldNotFound
 from cubicweb.web.formfields import guess_field
 from cubicweb.web.formwidgets import Button, SubmitButton, ResetButton
@@ -162,7 +163,7 @@
         assert role in ('subject', 'object')
         if default is None:
             default = xml_escape(self.req._('<no value>'))
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         rschema = entity.schema.rschema(rtype)
         lzone = self._build_landing_zone(landing_zone)
         # compute value, checking perms, build form
@@ -325,15 +326,13 @@
 
     def call(self, **kwargs):
         """creation view for an entity"""
+        # at this point we know etype is a valid entity type, thanks to our
+        # selector
         etype = kwargs.pop('etype', self.req.form.get('etype'))
-        try:
-            entity = self.vreg['etypes'].etype_class(etype)(self.req)
-        except:
-            self.w(self.req._('no such entity type %s') % etype)
-        else:
-            self.initialize_varmaker()
-            entity.eid = self.varmaker.next()
-            self.render_form(entity)
+        entity = self.vreg['etypes'].etype_class(etype)(self.req)
+        self.initialize_varmaker()
+        entity.eid = self.varmaker.next()
+        self.render_form(entity)
 
     def form_title(self, entity):
         """the form view title"""
@@ -371,6 +370,8 @@
     entity
     """
     id = 'copy'
+    warning_message = _('Please note that this is only a shallow copy')
+
     def render_form(self, entity):
         """fetch and render the form"""
         # make a copy of entity to avoid altering the entity in the
@@ -381,7 +382,7 @@
         self.initialize_varmaker()
         self.newentity.eid = self.varmaker.next()
         self.w(u'<script type="text/javascript">updateMessage("%s");</script>\n'
-               % self.req._('Please note that this is only a shallow copy'))
+               % self.req._(self.warning_message))
         super(CopyFormView, self).render_form(self.newentity)
         del self.newentity
 
@@ -412,7 +413,7 @@
 
     def __init__(self, req, rset, **kwargs):
         kwargs.setdefault('__redirectrql', rset.printable_rql())
-        super(TableEditForm, self).__init__(req, rset, **kwargs)
+        super(TableEditForm, self).__init__(req, rset=rset, **kwargs)
         for row in xrange(len(self.rset)):
             form = self.vreg['forms'].select('edition', self.req,
                                              rset=self.rset, row=row,
@@ -451,32 +452,34 @@
         for i in xrange(len(rset)):
             self.wview(self.id, rset, row=i, **kwargs)
 
-    def cell_call(self, row, col, peid, rtype, role='subject', **kwargs):
+    def cell_call(self, row, col, peid, rtype, role, i18nctx, **kwargs):
         """
         :param peid: the parent entity's eid hosting the inline form
         :param rtype: the relation bridging `etype` and `peid`
         :param role: the role played by the `peid` in the relation
         """
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         divonclick = "restoreInlinedEntity('%s', '%s', '%s')" % (peid, rtype,
                                                                  entity.eid)
-        self.render_form(entity, peid, rtype, role, divonclick=divonclick)
+        self.render_form(entity, peid, rtype, role, i18nctx,
+                         divonclick=divonclick)
 
-    def render_form(self, entity, peid, rtype, role, **kwargs):
+    def render_form(self, entity, peid, rtype, role, i18nctx, **kwargs):
         """fetch and render the form"""
         form = self.vreg['forms'].select('edition', self.req, entity=entity,
                                          form_renderer_id='inline',
                                          mainform=False, copy_nav_params=False)
         self.add_hiddens(form, entity, peid, rtype, role)
         divid = '%s-%s-%s' % (peid, rtype, entity.eid)
-        title = self.schema.rschema(rtype).display_name(self.req, role)
-        removejs = self.removejs % (peid, rtype,entity.eid)
+        title = self.req.pgettext(i18nctx, 'This %s' % entity.e_schema)
+        removejs = self.removejs % (peid, rtype, entity.eid)
         countkey = '%s_count' % rtype
         try:
             self.req.data[countkey] += 1
         except:
             self.req.data[countkey] = 1
         self.w(form.form_render(divid=divid, title=title, removejs=removejs,
+                                i18nctx=i18nctx,
                                 counter=self.req.data[countkey], **kwargs))
 
     def add_hiddens(self, form, entity, peid, rtype, role):
@@ -502,7 +505,7 @@
                   & specified_etype_implements('Any'))
     removejs = "removeInlineForm('%s', '%s', '%s')"
 
-    def call(self, etype, peid, rtype, role='subject', **kwargs):
+    def call(self, etype, peid, rtype, role, i18nctx, **kwargs):
         """
         :param etype: the entity type being created in the inline form
         :param peid: the parent entity's eid hosting the inline form
@@ -510,10 +513,11 @@
         :param role: the role played by the `peid` in the relation
         """
         try:
-            entity = self.vreg['etypes'].etype_class(etype)(self.req, None, None)
+            cls = self.vreg['etypes'].etype_class(etype)
         except:
             self.w(self.req._('no such entity type %s') % etype)
             return
         self.initialize_varmaker()
+        entity = cls(self.req)
         entity.eid = self.varmaker.next()
-        self.render_form(entity, peid, rtype, role)
+        self.render_form(entity, peid, rtype, role, i18nctx, **kwargs)
--- a/web/views/editviews.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/editviews.py	Tue Sep 22 13:08:42 2009 +0200
@@ -43,27 +43,27 @@
 
     @cached
     def filter_box_context_info(self):
-        entity = self.entity(0, 0)
+        entity = self.rset.get_entity(0, 0)
         role, eid, rtype, etype = self.req.search_state[1]
         assert entity.eid == typed_eid(eid)
         # the default behaviour is to fetch all unrelated entities and display
         # 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
 
 
 class OutOfContextSearch(EntityView):
     id = 'outofcontext-search'
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         erset = entity.as_rset()
         if self.req.match_search_state(erset):
-            self.w(u'<a href="%s" title="%s">%s</a>&nbsp;<a href="%s" title="%s">[...]</a>' % (
+            self.w(u'<a href="%s" title="%s">%s</a>&#160;<a href="%s" title="%s">[...]</a>' % (
                 xml_escape(linksearch_select_url(self.req, erset)),
                 self.req._('select this entity'),
                 xml_escape(entity.view('textoutofcontext')),
@@ -78,7 +78,7 @@
     __select__ = match_form_params('relation')
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         relname, target = self.req.form.get('relation').rsplit('_', 1)
         rschema = self.schema.rschema(relname)
         hidden = 'hidden' in self.req.form
@@ -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	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/emailaddress.py	Tue Sep 22 13:08:42 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'&nbsp;(<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/embedding.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/embedding.py	Tue Sep 22 13:08:42 2009 +0200
@@ -12,10 +12,10 @@
 import re
 from urlparse import urljoin
 from urllib2 import urlopen, Request, HTTPError
+from urllib import quote as urlquote # XXX should use view.url_quote method
 
 from logilab.mtconverter import guess_encoding
 
-from cubicweb import urlquote # XXX should use view.url_quote method
 from cubicweb.selectors import (one_line_rset, score_entity,
                                 match_search_state, implements)
 from cubicweb.interfaces import IEmbedable
@@ -98,14 +98,13 @@
                   & score_entity(entity_has_embedable_url))
 
     title = _('embed')
-    controller = 'embed'
 
     def url(self, row=0):
         entity = self.rset.get_entity(row, 0)
         url = urljoin(self.req.base_url(), entity.embeded_url())
         if self.req.form.has_key('rql'):
-            return self.build_url(url=url, rql=self.req.form['rql'])
-        return self.build_url(url=url)
+            return self.build_url('embed', url=url, rql=self.req.form['rql'])
+        return self.build_url('embed', url=url)
 
 
 
--- a/web/views/facets.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/facets.py	Tue Sep 22 13:08:42 2009 +0200
@@ -40,7 +40,7 @@
     roundcorners = True
 
     needs_css = 'cubicweb.facets.css'
-    needs_js = ('cubicweb.ajax.js', 'cubicweb.formfilter.js')
+    needs_js = ('cubicweb.ajax.js', 'cubicweb.facets.js')
 
     bk_linkbox_template = u'<div class="facetTitle">%s</div>'
 
@@ -77,7 +77,7 @@
             mainvar, baserql = prepare_facets_rqlst(rqlst, rset.args)
             widgets = []
             for facet in self.get_facets(rset, mainvar):
-                if facet.propval('visible'):
+                if facet.cw_propval('visible'):
                     wdg = facet.get_widget()
                     if wdg is not None:
                         widgets.append(wdg)
@@ -118,9 +118,9 @@
             self.w(self.bk_linkbox_template % bk_link)
 
     def get_facets(self, rset, mainvar):
-        return self.vreg['facets'].possible_vobjects(self.req, rset=rset,
-                                                     context='facetbox',
-                                                     filtered_variable=mainvar)
+        return self.vreg['facets'].poss_visible_objects(self.req, rset=rset,
+                                                        context='facetbox',
+                                                        filtered_variable=mainvar)
 
 # facets ######################################################################
 
--- a/web/views/formrenderers.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/formrenderers.py	Tue Sep 22 13:08:42 2009 +0200
@@ -15,10 +15,16 @@
 from cubicweb.common import tags
 from cubicweb.appobject import AppObject
 from cubicweb.selectors import entity_implements, yes
-from cubicweb.web import eid_param
-from cubicweb.web import formwidgets as fwdgs
+from cubicweb.web import eid_param, formwidgets as fwdgs
 from cubicweb.web.widgets import checkbox
 
+def checkbox(name, value, attrs='', checked=None):
+    if checked is None:
+        checked = value
+    checked = checked and 'checked="checked"' or ''
+    return u'<input type="checkbox" name="%s" value="%s" %s %s />' % (
+        name, value, checked, attrs)
+
 
 class FormRenderer(AppObject):
     """basic renderer displaying fields in a two columns table label | value
@@ -47,7 +53,7 @@
     button_bar_class = u'formButtonBar'
 
     def __init__(self, req=None, rset=None, row=None, col=None, **kwargs):
-        super(FormRenderer, self).__init__(req, rset, row, col)
+        super(FormRenderer, self).__init__(req, rset=rset, row=row, col=col)
         if self._set_options(kwargs):
             raise ValueError('unconsumed arguments %s' % kwargs)
 
@@ -86,7 +92,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'
@@ -103,7 +112,7 @@
         if example:
             help.append('<div class="helper">(%s: %s)</div>'
                         % (self.req._('sample format'), example))
-        return u'&nbsp;'.join(help)
+        return u'&#160;'.join(help)
 
     # specific methods (mostly to ease overriding) #############################
 
@@ -124,7 +133,7 @@
                 if len(errors) > 1:
                     templstr = '<li>%s</li>\n'
                 else:
-                    templstr = '&nbsp;%s\n'
+                    templstr = '&#160;%s\n'
                 for field, err in errors:
                     if field is None:
                         errormsg += templstr % err
@@ -274,7 +283,7 @@
             if self.display_help:
                 w(self.render_help(form, field))
         # empty slot for buttons
-        w(u'<th class="labelCol">&nbsp;</th>')
+        w(u'<th class="labelCol">&#160;</th>')
         w(u'</tr>')
         w(u'<tr>')
         for field in fields:
@@ -402,12 +411,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)
@@ -431,7 +441,7 @@
                 w(u'</tr>')
         pendings = list(form.restore_pending_inserts())
         if not pendings:
-            w(u'<tr><th>&nbsp;</th><td>&nbsp;</td></tr>')
+            w(u'<tr><th>&#160;</th><td>&#160;</td></tr>')
         else:
             for row in pendings:
                 # soon to be linked to entities
@@ -461,6 +471,9 @@
         w(u'</table>')
         w(u'</fieldset>')
 
+    # NOTE: should_* and display_* method extracted and moved to the form to
+    # ease overriding
+
     def inline_entities_form(self, w, form):
         """create a form to edit entity's inlined relations"""
         if not hasattr(form, 'inlined_relations'):
@@ -479,13 +492,11 @@
 
     def inline_relation_form(self, w, form, rschema, targettype, role):
         entity = form.edited_entity
-        __ = self.req.__
+        __ = self.req.pgettext
+        i18nctx = 'inlined:%s.%s.%s' % (entity.e_schema, rschema, role)
         w(u'<div id="inline%sslot">' % rschema)
-        existant = entity.has_eid() and entity.related(rschema)
-        if existant:
-            # display inline-edition view for all existing related entities
-            w(form.view('inline-edition', existant, rtype=rschema, role=role,
-                        ptype=entity.e_schema, peid=entity.eid))
+        existant = form.display_inline_edition_form(w, rschema, targettype,
+                                                    role, i18nctx)
         if role == 'subject':
             card = rschema.rproperty(entity.e_schema, targettype, 'cardinality')[0]
         else:
@@ -493,23 +504,23 @@
         # there is no related entity and we need at least one: we need to
         # display one explicit inline-creation view
         if form.should_display_inline_creation_form(rschema, existant, card):
-            w(form.view('inline-creation', None, etype=targettype,
-                        peid=entity.eid, ptype=entity.e_schema,
-                        rtype=rschema, role=role))
+            form.display_inline_creation_form(w, rschema, targettype,
+                                              role, i18nctx)
+            existant = True
         # we can create more than one related entity, we thus display a link
         # to add new related entities
         if form.should_display_add_new_relation_link(rschema, existant, card):
             divid = "addNew%s%s%s:%s" % (targettype, rschema, role, entity.eid)
             w(u'<div class="inlinedform" id="%s" cubicweb:limit="true">'
               % divid)
-            js = "addInlineCreationForm('%s', '%s', '%s', '%s')" % (
-                entity.eid, targettype, rschema, role)
-            if card in '1?':
+            js = "addInlineCreationForm('%s', '%s', '%s', '%s', '%s')" % (
+                entity.eid, targettype, rschema, role, i18nctx)
+            if form.should_hide_add_new_relation_link(rschema, card):
                 js = "toggleVisibility('%s'); %s" % (divid, js)
             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, __(i18nctx, 'add a %s' % targettype)))
             w(u'</div>')
-            w(u'<div class="trame_grise">&nbsp;</div>')
+            w(u'<div class="trame_grise">&#160;</div>')
         w(u'</div>')
 
 
@@ -531,7 +542,9 @@
             w(u'<div id="notice-%s" class="notice">%s</div>' % (
                 values['divid'], self.req._('click on the box to cancel the deletion')))
         w(u'<div class="iformBody">')
-        values['removemsg'] = self.req.__('remove this %s' % form.edited_entity.e_schema)
+        eschema = form.edited_entity.e_schema
+        ctx = values.pop('i18nctx')
+        values['removemsg'] = self.req.pgettext(ctx, 'remove this %s' % eschema)
         w(u'<div class="iformTitle"><span>%(title)s</span> '
           '#<span class="icounter">%(counter)s</span> '
           '[<a href="javascript: %(removejs)s;noop();">%(removemsg)s</a>]</div>'
--- a/web/views/forms.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/forms.py	Tue Sep 22 13:08:42 2009 +0200
@@ -77,14 +77,15 @@
     def __init__(self, req, rset=None, row=None, col=None,
                  submitmsg=None, mainform=True,
                  **kwargs):
-        super(FieldsForm, self).__init__(req, rset, row=row, col=col)
+        super(FieldsForm, self).__init__(req, rset=rset, row=row, col=col)
         self.fields = list(self.__class__._fields_)
         for key, val in kwargs.items():
             if key in NAV_FORM_PARAMETERS:
                 self.form_add_hidden(key, val)
-            else:
-                assert hasattr(self.__class__, key) and not key[0] == '_', key
+            elif hasattr(self.__class__, key) and not key[0] == '_':
                 setattr(self, key, val)
+            # skip other parameters, usually given for selection
+            # (else write a custom class to handle them)
         if mainform:
             self.form_add_hidden('__errorurl', self.session_key())
             self.form_add_hidden('__domid', self.domid)
@@ -546,26 +547,8 @@
                 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):
+    def srelations_by_category(self, categories=None, permission=None,
+                               strict=False):
         return ()
 
     def should_display_add_new_relation_link(self, rschema, existant, card):
@@ -573,7 +556,7 @@
 
 
 class CompositeForm(FieldsForm):
-    """form composed for sub-forms"""
+    """form composed of sub-forms"""
     id = 'composite'
     form_renderer_id = id
 
@@ -585,3 +568,18 @@
         """mark given form as a subform and append it"""
         subform.is_subform = True
         self.forms.append(subform)
+
+
+class CompositeEntityForm(EntityFieldsForm):
+    """form composed of sub-forms"""
+    id = 'composite'
+    form_renderer_id = id
+
+    def __init__(self, *args, **kwargs):
+        super(CompositeEntityForm, self).__init__(*args, **kwargs)
+        self.forms = []
+
+    def form_add_subform(self, subform):
+        """mark given form as a subform and append it"""
+        subform.is_subform = True
+        self.forms.append(subform)
--- a/web/views/ibreadcrumbs.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/ibreadcrumbs.py	Tue Sep 22 13:08:42 2009 +0200
@@ -10,77 +10,104 @@
 
 from logilab.mtconverter import xml_escape
 
+from cubicweb.interfaces import IBreadCrumbs
+from cubicweb.selectors import (one_line_rset, implements, one_etype_rset,
+                                two_lines_rset, any_rset)
+from cubicweb.view import EntityView, Component
 # don't use AnyEntity since this may cause bug with isinstance() due to reloading
-from cubicweb.interfaces import IBreadCrumbs
-from cubicweb.selectors import match_context_prop, one_line_rset, implements
 from cubicweb.entity import Entity
-from cubicweb.view import EntityView
-from cubicweb.common.uilib import cut
-from cubicweb.web.component import EntityVComponent
+from cubicweb.common import tags, uilib
 
 
-def bc_title(entity):
-    textsize = entity.req.property_value('navigation.short-line-size')
-    return xml_escape(cut(entity.dc_title(), textsize))
+class BreadCrumbEntityVComponent(Component):
+    id = 'breadcrumbs'
+    __select__ = one_line_rset() & implements(IBreadCrumbs)
 
-
-class BreadCrumbEntityVComponent(EntityVComponent):
-    id = 'breadcrumbs'
-    # register msg not generated since no entity implements IPrevNext in cubicweb itself
+    property_defs = {
+        _('visible'):  dict(type='Boolean', default=True,
+                            help=_('display the component or not')),
+        }
     title = _('contentnavigation_breadcrumbs')
     help = _('contentnavigation_breadcrumbs_description')
-    __select__ = (one_line_rset() & match_context_prop() & implements(IBreadCrumbs))
-    context = 'navtop'
-    order = 5
-    visible = False
-    separator = u'&nbsp;&gt;&nbsp;'
+    separator = u'&#160;&gt;&#160;'
 
     def call(self, view=None, first_separator=True):
-        entity = self.entity(0)
+        entity = self.rset.get_entity(0, 0)
         path = entity.breadcrumbs(view)
         if path:
-            self.w(u'<span class="pathbar">')
+            self.w(u'<span id="breadcrumbs" class="pathbar">')
             if first_separator:
                 self.w(self.separator)
-            root = path.pop(0)
-            if isinstance(root, Entity):
-                self.w(u'<a href="%s">%s</a>' % (self.req.build_url(root.id),
-                                                 root.dc_type('plural')))
-                self.w(self.separator)
-            self.wpath_part(root, entity, not path)
-            for i, parent in enumerate(path):
-                self.w(self.separator)
-                self.w(u"\n")
-                self.wpath_part(parent, entity, i == len(path) - 1)
+            self.render_breadcrumbs(entity, path)
             self.w(u'</span>')
 
+    def render_breadcrumbs(self, contextentity, path):
+        root = path.pop(0)
+        if isinstance(root, Entity):
+            self.w(u'<a href="%s">%s</a>' % (self.req.build_url(root.id),
+                                             root.dc_type('plural')))
+            self.w(self.separator)
+        self.wpath_part(root, contextentity, not path)
+        for i, parent in enumerate(path):
+            self.w(self.separator)
+            self.w(u"\n")
+            self.wpath_part(parent, contextentity, i == len(path) - 1)
+
     def wpath_part(self, part, contextentity, last=False):
         if isinstance(part, Entity):
             if last and part.eid == contextentity.eid:
-                self.w(bc_title(part))
+                self.w(xml_escape(part.view('breadcrumbtext')))
             else:
-                part.view('breadcrumbs', w=self.w)
+                self.w(part.view('breadcrumbs'))
         elif isinstance(part, tuple):
             url, title = part
             textsize = self.req.property_value('navigation.short-line-size')
             self.w(u'<a href="%s">%s</a>' % (
-                xml_escape(url), xml_escape(cut(title, textsize))))
+                xml_escape(url), xml_escape(uilib.cut(title, textsize))))
         else:
             textsize = self.req.property_value('navigation.short-line-size')
-            self.w(cut(unicode(part), textsize))
+            self.w(uilib.cut(unicode(part), textsize))
 
 
-class BreadCrumbComponent(BreadCrumbEntityVComponent):
-    __registry__ = 'components'
-    __select__ = (one_line_rset() & implements(IBreadCrumbs))
-    visible = True
+class BreadCrumbETypeVComponent(BreadCrumbEntityVComponent):
+    __select__ = two_lines_rset() & one_etype_rset() & implements(IBreadCrumbs)
+
+    def render_breadcrumbs(self, contextentity, path):
+        # XXX hack: only display etype name or first non entity path part
+        root = path.pop(0)
+        if isinstance(root, Entity):
+            self.w(u'<a href="%s">%s</a>' % (self.req.build_url(root.id),
+                                             root.dc_type('plural')))
+        else:
+            self.wpath_part(root, contextentity, not path)
+
+
+class BreadCrumbAnyRSetVComponent(BreadCrumbEntityVComponent):
+    __select__ = any_rset()
+
+    def call(self, view=None, first_separator=True):
+        self.w(u'<span id="breadcrumbs" class="pathbar">')
+        if first_separator:
+            self.w(self.separator)
+        self.w(self.req._('search'))
+        self.w(u'</span>')
 
 
 class BreadCrumbView(EntityView):
     id = 'breadcrumbs'
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
-        desc = xml_escape(cut(entity.dc_description(), 50))
-        self.w(u'<a href="%s" title="%s">%s</a>' % (
-            xml_escape(entity.absolute_url()), desc, bc_title(entity)))
+        entity = self.rset.get_entity(row, col)
+        desc = xml_escape(uilib.cut(entity.dc_description(), 50))
+        # XXX remember camember : tags.a autoescapes !
+        self.w(tags.a(entity.view('breadcrumbtext'),
+                      href=entity.absolute_url(), title=desc))
+
+
+class BreadCrumbTextView(EntityView):
+    id = 'breadcrumbtext'
+
+    def cell_call(self, row, col):
+        entity = self.rset.get_entity(row, col)
+        textsize = self.req.property_value('navigation.short-line-size')
+        self.w(uilib.cut(entity.dc_title(), textsize))
--- a/web/views/idownloadable.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/idownloadable.py	Tue Sep 22 13:08:42 2009 +0200
@@ -50,7 +50,7 @@
     order = 10
 
     def cell_call(self, row, col, title=None, label=None, **kwargs):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         download_box(self.w, entity, title, label)
 
 
@@ -91,7 +91,7 @@
 
 
     def cell_call(self, row, col, title=None, **kwargs):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         url = xml_escape(entity.download_url())
         self.w(u'<a href="%s">%s</a>' % (url, xml_escape(title or entity.dc_title())))
 
@@ -123,7 +123,7 @@
 
     def cell_call(self, row, col, title=None, **kwargs):
         """the oneline view is a link to download the file"""
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         url = xml_escape(entity.absolute_url())
         name = xml_escape(title or entity.download_file_name())
         durl = xml_escape(entity.download_url())
@@ -145,19 +145,19 @@
             self.w(u'</div>')
 
     def cell_call(self, row, col, width=None, height=None, link=False):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         #if entity.data_format.startswith('image/'):
-        imgtag = u'<img src="%s" alt="%s" ' % (xml_escape(entity.download_url()),
-                                               xml_escape(entity.download_file_name()))
+        imgtag = u'<img src="%s" alt="%s" ' % (
+            xml_escape(entity.download_url()),
+            (self.req._('download %s')  % xml_escape(entity.download_file_name())))
         if width:
             imgtag += u'width="%i" ' % width
         if height:
             imgtag += u'height="%i" ' % height
         imgtag += u'/>'
         if link:
-            self.w(u'<a href="%s" alt="%s">%s</a>' % (entity.absolute_url(vid='download'),
-                                                      self.req._('download image'),
-                                                      imgtag))
+            self.w(u'<a href="%s">%s</a>' % (entity.absolute_url(vid='download'),
+                                             imgtag))
         else:
             self.w(imgtag)
 
--- a/web/views/igeocodable.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/igeocodable.py	Tue Sep 22 13:08:42 2009 +0200
@@ -22,6 +22,8 @@
     __select__ = implements(IGeocodable)
 
     def call(self):
+        # remove entities that don't define latitude and longitude
+        self.rset = self.rset.filtered_rset(lambda e: e.latitude and e.longitude)
         zoomlevel = self.req.form.pop('zoomlevel', 8)
         extraparams = self.req.form.copy()
         extraparams.pop('vid', None)
@@ -40,11 +42,16 @@
         self.w(simplejson.dumps(geodata))
 
     def build_marker_data(self, row, extraparams):
-        entity = self.entity(row, 0)
+        entity = self.rset.get_entity(row, 0)
+        icon = None
+        if hasattr(entity, 'marker_icon'):
+            icon = entity.marker_icon()
+        else:
+            icon = (self.req.external_resource('GMARKER_ICON'), (20, 34), (4, 34), None)
         return {'latitude': entity.latitude, 'longitude': entity.longitude,
                 'title': entity.dc_long_title(),
                 #icon defines : (icon._url, icon.size,  icon.iconAncho', icon.shadow)
-                'icon': entity.marker_icon() or (self.req.external_resource('GMARKER_ICON'), (20, 34), (4, 34), None),
+                'icon': icon,
                 'bubbleUrl': entity.absolute_url(vid='gmap-bubble', __notemplate=1, **extraparams),
                 }
 
@@ -55,7 +62,7 @@
     __select__ = implements(IGeocodable)
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         self.w(u'<div>%s</div>' % entity.view('oneline'))
         # FIXME: we should call something like address-view if available
 
@@ -67,7 +74,10 @@
     need_navigation = False
 
     def call(self, gmap_key, width=400, height=400, uselabel=True, urlparams=None):
-        self.req.add_js('http://maps.google.com/maps?file=api&amp;v=2&amp;key=%s' % gmap_key,
+        self.req.demote_to_html()
+        # remove entities that don't define latitude and longitude
+        self.rset = self.rset.filtered_rset(lambda e: e.latitude and e.longitude)
+        self.req.add_js('http://maps.google.com/maps?sensor=false&file=api&amp;v=2&amp;key=%s' % gmap_key,
                         localfile=False)
         self.req.add_js( ('cubicweb.widgets.js', 'cubicweb.gmap.js', 'gmap.utility.labeledmarker.js') )
         rql = self.rset.printable_rql()
--- a/web/views/iprogress.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/iprogress.py	Tue Sep 22 13:08:42 2009 +0200
@@ -59,7 +59,7 @@
 
     def cell_call(self, row, col):
         _ = self.req._
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         infos = {}
         for col in self.columns:
             meth = getattr(self, 'build_%s_cell' % col, None)
@@ -167,10 +167,10 @@
     """
     id = 'ic_progress_table_view'
 
-    def call(self):
+    def call(self, columns=None):
         view = self.vreg['views'].select('progress_table_view', self.req,
                                          rset=self.rset)
-        columns = list(view.columns)
+        columns = list(columns or view.columns)
         try:
             columns.remove('project')
         except ValueError:
@@ -186,7 +186,7 @@
 
     def cell_call(self, row, col):
         self.req.add_css('cubicweb.iprogress.css')
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         widget = ProgressBarWidget(entity.done, entity.todo,
                                    entity.revised_cost)
         self.w(widget.render())
--- a/web/views/magicsearch.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/magicsearch.py	Tue Sep 22 13:08:42 2009 +0200
@@ -119,7 +119,7 @@
         return TRANSLATION_MAPS[lang]
     except KeyError:
         assert lang in config.translations, '%s %s' % (lang, config.translations)
-        tr = config.translations[lang]
+        tr, ctxtr = config.translations[lang]
         langmap = {}
         for etype in schema.entities():
             etype = str(etype)
@@ -345,7 +345,7 @@
 class MagicSearchComponent(Component):
     id  = 'magicsearch'
     def __init__(self, req, rset=None):
-        super(MagicSearchComponent, self).__init__(req, rset)
+        super(MagicSearchComponent, self).__init__(req, rset=rset)
         processors = []
         self.by_name = {}
         for processorcls in self.vreg['components']['magicsearch_processor']:
--- a/web/views/management.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/management.py	Tue Sep 22 13:08:42 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>'
@@ -139,7 +139,7 @@
                 # don't give __delete value to build_url else it will be urlquoted
                 # and this will replace %s by %25s
                 delurl += '&__delete=%s:require_permission:%%s' % entity.eid
-                dellinktempl = u'[<a href="%s" title="%s">-</a>]&nbsp;' % (
+                dellinktempl = u'[<a href="%s" title="%s">-</a>]&#160;' % (
                     xml_escape(delurl), _('delete this permission'))
             else:
                 dellinktempl = None
@@ -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/navigation.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/navigation.py	Tue Sep 22 13:08:42 2009 +0200
@@ -40,9 +40,9 @@
                                             self.index_display(start, stop)))
             start = stop + 1
         w(u'<div class="pagination">')
-        w(u'%s&nbsp;' % self.previous_link(params))
-        w(u'[&nbsp;%s&nbsp;]' % u'&nbsp;| '.join(blocklist))
-        w(u'&nbsp;%s' % self.next_link(params))
+        w(u'%s&#160;' % self.previous_link(basepath, params))
+        w(u'[&#160;%s&#160;]' % u'&#160;| '.join(blocklist))
+        w(u'&#160;%s' % self.next_link(basepath, params))
         w(u'</div>')
 
     def index_display(self, start, stop):
@@ -131,26 +131,26 @@
             cell = self.format_link_content(index_display(start), index_display(stop))
             blocklist.append(self.page_link(basepath, params, start, stop, cell))
             start = stop + 1
-        self.write_links(params, blocklist)
+        self.write_links(basepath, params, blocklist)
 
     def format_link_content(self, startstr, stopstr):
         text = u'%s - %s' % (startstr.lower()[:self.nb_chars],
                              stopstr.lower()[:self.nb_chars])
         return xml_escape(text)
 
-    def write_links(self, params, blocklist):
+    def write_links(self, basepath, params, blocklist):
         self.w(u'<div class="pagination">')
-        self.w(u'%s&nbsp;' % self.previous_link(params))
-        self.w(u'[&nbsp;%s&nbsp;]' % u'&nbsp;| '.join(blocklist))
-        self.w(u'&nbsp;%s' % self.next_link(params))
+        self.w(u'%s&#160;' % self.previous_link(basepath, params))
+        self.w(u'[&#160;%s&#160;]' % u'&#160;| '.join(blocklist))
+        self.w(u'&#160;%s' % self.next_link(basepath, params))
         self.w(u'</div>')
 
 
 def limit_rset_using_paged_nav(self, req, rset, w, forcedisplay=False,
                                show_all_option=True, page_size=None):
     if not (forcedisplay or req.form.get('__force_display') is not None):
-        nav = self.vreg['components'].select_object('navigation', req,
-                                      rset=rset, page_size=page_size)
+        nav = self.vreg['components'].select_or_none('navigation', req,
+                                                     rset=rset, page_size=page_size)
         if nav:
             # get boundaries before component rendering
             start, stop = nav.page_boundaries()
@@ -160,7 +160,7 @@
             # make a link to see them all
             if show_all_option:
                 url = xml_escape(self.build_url(__force_display=1, **params))
-                w(u'<p><a href="%s">%s</a></p>\n'
+                w(u'<span><a href="%s">%s</a></span>\n'
                   % (url, req._('show %s results') % len(rset)))
             rset.limit(offset=start, limit=stop-start, inplace=True)
 
@@ -187,7 +187,7 @@
     context = 'navbottom'
     order = 10
     def call(self, view=None):
-        entity = self.entity(0)
+        entity = self.rset.get_entity(0,0)
         previous = entity.previous_entity()
         next = entity.next_entity()
         if previous or next:
--- a/web/views/old_calendar.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/old_calendar.py	Tue Sep 22 13:08:42 2009 +0200
@@ -30,8 +30,8 @@
 
     # Navigation building methods / views ####################################
 
-    PREV = u'<a href="%s">&lt;&lt;</a>&nbsp;&nbsp;<a href="%s">&lt;</a>'
-    NEXT = u'<a href="%s">&gt;</a>&nbsp;&nbsp;<a href="%s">&gt;&gt;</a>'
+    PREV = u'<a href="%s">&lt;&lt;</a>&#160;&#160;<a href="%s">&lt;</a>'
+    NEXT = u'<a href="%s">&gt;</a>&#160;&#160;<a href="%s">&gt;&gt;</a>'
     NAV_HEADER = u"""<table class="calendarPageHeader">
 <tr><td class="prev">%s</td><td class="next">%s</td></tr>
 </table>
@@ -109,7 +109,7 @@
         self.req.add_css('cubicweb.calendar.css')
         schedule = {}
         for row in xrange(len(self.rset.rows)):
-            entity = self.entity(row)
+            entity = self.rset.get_entity(row,0)
             infos = u'<div class="event">'
             infos += self.view(itemvid, self.rset, row=row)
             infos += u'</div>'
@@ -200,7 +200,7 @@
         self.w(u'<tr>')
         rql = self.rset.printable_rql()
         for cur_month in date_range(begin, end, incmonth=1):
-            umonth = u'%s&nbsp;%s' % (self.format_date(cur_month, '%B'), cur_month.year)
+            umonth = u'%s&#160;%s' % (self.format_date(cur_month, '%B'), cur_month.year)
             url = self.build_url(rql=rql, vid=self.id,
                                  year=cur_month.year, month=cur_month.month)
             self.w(u'<th colspan="2"><a href="%s">%s</a></th>' % (xml_escape(url),
@@ -215,7 +215,7 @@
                 else:
                     day = date(cur_month.year, cur_month.month, day_num+1)
                     events = schedule.get(day)
-                    self.w(u'<td>%s&nbsp;%s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), day_num+1))
+                    self.w(u'<td>%s&#160;%s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), day_num+1))
                     self.format_day_events(day, events)
             self.w(u'</tr>')
 
@@ -345,8 +345,8 @@
             am_row = [am for day, am, pm in row]
             pm_row = [pm for day, am, pm in row]
             formatted_rows.append('<tr>%s%s</tr>'% (week_title, '\n'.join(day_row)))
-            formatted_rows.append('<tr class="amRow"><td>&nbsp;</td>%s</tr>'% '\n'.join(am_row))
-            formatted_rows.append('<tr class="pmRow"><td>&nbsp;</td>%s</tr>'% '\n'.join(pm_row))
+            formatted_rows.append('<tr class="amRow"><td>&#160;</td>%s</tr>'% '\n'.join(am_row))
+            formatted_rows.append('<tr class="pmRow"><td>&#160;</td>%s</tr>'% '\n'.join(pm_row))
         # tigh everything together
         url = self.build_url(rql=rql, vid='ampmcalendarmonth',
                              year=first_day.year, month=first_day.month)
@@ -364,7 +364,7 @@
         self.w(u'<tr>')
         rql = self.rset.printable_rql()
         for cur_month in date_range(begin, end, incmonth=1):
-            umonth = u'%s&nbsp;%s' % (self.format_date(cur_month, '%B'), cur_month.year)
+            umonth = u'%s&#160;%s' % (self.format_date(cur_month, '%B'), cur_month.year)
             url = self.build_url(rql=rql, vid=self.id,
                                  year=cur_month.year, month=cur_month.month)
             self.w(u'<th colspan="3"><a href="%s">%s</a></th>' % (xml_escape(url),
@@ -379,7 +379,7 @@
                 else:
                     day = date(cur_month.year, cur_month.month, day_num+1)
                     events = schedule.get(day)
-                    self.w(u'<td>%s&nbsp;%s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(),
+                    self.w(u'<td>%s&#160;%s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(),
                                                        day_num+1))
                     self.format_day_events(day, events)
             self.w(u'</tr>')
@@ -437,8 +437,8 @@
             am_row = [am for day, am, pm in row]
             pm_row = [pm for day, am, pm in row]
             formatted_rows.append('<tr>%s%s</tr>'% (week_title, '\n'.join(day_row)))
-            formatted_rows.append('<tr class="amRow"><td>&nbsp;</td>%s</tr>'% '\n'.join(am_row))
-            formatted_rows.append('<tr class="pmRow"><td>&nbsp;</td>%s</tr>'% '\n'.join(pm_row))
+            formatted_rows.append('<tr class="amRow"><td>&#160;</td>%s</tr>'% '\n'.join(am_row))
+            formatted_rows.append('<tr class="pmRow"><td>&#160;</td>%s</tr>'% '\n'.join(pm_row))
         # tigh everything together
         url = self.build_url(rql=rql, vid='ampmcalendarmonth',
                              year=first_day.year, month=first_day.month)
@@ -464,7 +464,7 @@
             monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
             w(u'<tr>%s</tr>' % (
                 WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink)))
-            w(u'<tr><th>%s</th><th>&nbsp;</th></tr>'% _(u'Date'))
+            w(u'<tr><th>%s</th><th>&#160;</th></tr>'% _(u'Date'))
             for day in date_range(monday, sunday):
                 events = schedule.get(day)
                 style = day.weekday() % 2 and "even" or "odd"
@@ -534,9 +534,9 @@
 AMPM_CONTENT = u'<td class="%s"><span class="cellTitle">%s</span><div class="cellContent">%s</div></td>'
 
 WEEK_TITLE = u'<th class="weekTitle" colspan="2">%s %s (%s)</th>'
-WEEK_EMPTY_CELL = u'<td class="weekEmptyCell">&nbsp;</td>'
+WEEK_EMPTY_CELL = u'<td class="weekEmptyCell">&#160;</td>'
 WEEK_CELL = u'<td class="weekCell"><div class="cellContent">%s</div></td>'
 
-AMPM_DAYWEEK_EMPTY = u'<td>%s&nbsp;%s</td>'
-AMPM_DAYWEEK = u'<td rowspan="%d">%s&nbsp;%s</td>'
+AMPM_DAYWEEK_EMPTY = u'<td>%s&#160;%s</td>'
+AMPM_DAYWEEK = u'<td rowspan="%d">%s&#160;%s</td>'
 AMPM_WEEK_CELL = u'<td class="ampmWeekCell"><div class="cellContent">%02d:%02d - %s</div></td>'
--- a/web/views/plots.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/plots.py	Tue Sep 22 13:08:42 2009 +0200
@@ -103,8 +103,8 @@
 
     def _render(self, req, width=500, height=400):
         # XXX IE requires excanvas.js
-        req.add_js( ('jquery.flot.js', 'cubicweb.flot.js') )
-        figid = u'figure%s' % make_uid('foo')
+        req.add_js( ('jquery.flot.js', 'cubicweb.flot.js', 'excanvas.js') )
+        figid = u'figure%s' % req.varmaker.next()
         plotdefs = []
         plotdata = []
         self.w(u'<div id="%s" style="width: %spx; height: %spx;"></div>' %
--- a/web/views/primary.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/primary.py	Tue Sep 22 13:08:42 2009 +0200
@@ -38,7 +38,8 @@
         return []
 
     def cell_call(self, row, col):
-        self.row = row
+        self.cw_row = row
+        self.cw_col = col
         self.maxrelated = self.req.property_value('navigation.related-limit')
         entity = self.complete_entity(row, col)
         self.render_entity(entity)
@@ -51,6 +52,7 @@
         boxes = self._prepare_side_boxes(entity)
         if boxes or hasattr(self, 'render_side_related'):
             self.w(u'<table width="100%"><tr><td style="width: 75%">')
+        self.render_entity_summary(entity)
         self.w(u'<div class="mainInfo">')
         self.content_navigation_components('navcontenttop')
         self.render_entity_attributes(entity)
@@ -71,7 +73,7 @@
 
     def content_navigation_components(self, context):
         self.w(u'<div class="%s">' % context)
-        for comp in self.vreg['contentnavigation'].possible_vobjects(
+        for comp in self.vreg['contentnavigation'].poss_visible_objects(
             self.req, rset=self.rset, row=self.row, view=self, context=context):
             try:
                 comp.render(w=self.w, row=self.row, view=self)
@@ -90,6 +92,8 @@
 
     def render_entity_metadata(self, entity):
         entity.view('metadata', w=self.w)
+
+    def render_entity_summary(self, entity):
         summary = self.summary(entity) # deprecate summary?
         if summary:
             self.w(u'<div class="summary">%s</div>' % summary)
@@ -147,7 +151,7 @@
             label = display_name(self.req, rschema.type, role)
             vid = dispctrl.get('vid', 'sidebox')
             sideboxes.append( (label, rset, vid) )
-        sideboxes += self.vreg['boxes'].possible_vobjects(
+        sideboxes += self.vreg['boxes'].poss_visible_objects(
             self.req, rset=self.rset, row=self.row, view=self,
             context='incontext')
         return sideboxes
@@ -229,9 +233,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/schema.py	Tue Sep 22 13:08:42 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,29 @@
                                 {'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 +390,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 +402,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/sessions.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/sessions.py	Tue Sep 22 13:08:42 2009 +0200
@@ -15,13 +15,18 @@
 class InMemoryRepositorySessionManager(AbstractSessionManager):
     """manage session data associated to a session identifier"""
 
-    def __init__(self):
-        AbstractSessionManager.__init__(self)
+    def __init__(self, *args, **kwargs):
+        AbstractSessionManager.__init__(self, *args, **kwargs)
         # XXX require a RepositoryAuthenticationManager which violates
         #     authenticate interface by returning a session instead of a user
         #assert isinstance(self.authmanager, RepositoryAuthenticationManager)
         self._sessions = {}
 
+    def dump_data(self):
+        return self._sessions
+    def restore_data(self, data):
+        self._sessions = data
+
     def current_sessions(self):
         return self._sessions.values()
 
--- a/web/views/sparql.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/sparql.py	Tue Sep 22 13:08:42 2009 +0200
@@ -108,7 +108,7 @@
                                        datatype=xmlschema(celltype)),
                              name=varname)
         else:
-            entity = self.entity(row, col)
+            entity = self.rset.get_entity(row, col)
             return E.binding(E.uri(entity.absolute_url()), name=varname)
 
     def set_request_content_type(self):
--- a/web/views/startup.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/startup.py	Tue Sep 22 13:08:42 2009 +0200
@@ -22,16 +22,6 @@
     title = _('manage')
     http_cache_manager = httpcache.EtagHTTPCacheManager
 
-    @classmethod
-    def vreg_initialization_completed(cls):
-        for eschema in cls.schema.entities():
-            if eschema.schema_entity():
-                uicfg.indexview_etype_section.setdefault(eschema, 'schema')
-            elif eschema.is_subobject(strict=True):
-                uicfg.indexview_etype_section.setdefault(eschema, 'subobject')
-            else:
-                uicfg.indexview_etype_section.setdefault(eschema, 'application')
-
     def display_folders(self):
         return False
 
@@ -62,10 +52,10 @@
             self.wview('inlined', rset, row=0)
         else:
             self.entities()
-            self.w(u'<div class="hr">&nbsp;</div>')
+            self.w(u'<div class="hr">&#160;</div>')
             self.startup_views()
         if manager and 'Card' in self.schema:
-            self.w(u'<div class="hr">&nbsp;</div>')
+            self.w(u'<div class="hr">&#160;</div>')
             if rset:
                 href = rset.get_entity(0, 0).absolute_url(vid='edition')
                 label = self.req._('edit the index page')
@@ -114,7 +104,7 @@
                        key=lambda (l,a,e):unormalize(l))
         q, r = divmod(len(infos), 2)
         if r:
-            infos.append( (None, '&nbsp;', '&nbsp;') )
+            infos.append( (None, '&#160;', '&#160;') )
         infos = zip(infos[:q+r], infos[q+r:])
         for (_, etypelink, addlink), (_, etypelink2, addlink2) in infos:
             self.w(u'<tr>\n')
@@ -136,7 +126,7 @@
             label = display_name(req, etype, 'plural')
             nb = req.execute('Any COUNT(X) WHERE X is %s' % etype)[0][0]
             url = self.build_url(etype)
-            etypelink = u'&nbsp;<a href="%s">%s</a> (%d)' % (
+            etypelink = u'&#160;<a href="%s">%s</a> (%d)' % (
                 xml_escape(url), label, nb)
             yield (label, etypelink, self.add_entity_link(eschema, req))
 
@@ -156,3 +146,25 @@
     def display_folders(self):
         return 'Folder' in self.schema and self.req.execute('Any COUNT(X) WHERE X is Folder')[0][0]
 
+
+class RegistryView(StartupView):
+    id = 'registry'
+    title = _('registry')
+    __select__ = StartupView.__select__ & match_user_groups('managers')
+
+    def call(self, **kwargs):
+        """The default view representing the instance's management"""
+        self.w(u'<h1>%s</h1>' % _("Registry's content"))
+        keys = sorted(self.vreg)
+        self.w(u'<p>%s</p>\n' % ' - '.join('<a href="/_registry#%s">%s</a>' % (key, key) for key in keys))
+        for key in keys:
+            self.w(u'<h2><a name="%s">%s</a></h2>' % (key,key))
+            items = self.vreg[key].items()
+            if items:
+                self.w(u'<table><tbody>')
+                for key, value in sorted(items):
+                    self.w(u'<tr><td>%s</td><td>%s</td></tr>' % (key, xml_escape(repr(value))))
+                self.w(u'</tbody></table>\n')
+            else:
+                self.w(u'<p>Empty</p>\n')
+
--- a/web/views/tableview.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/tableview.py	Tue Sep 22 13:08:42 2009 +0200
@@ -34,7 +34,7 @@
             return ()
         rqlst.save_state()
         mainvar, baserql = prepare_facets_rqlst(rqlst, self.rset.args)
-        wdgs = [facet.get_widget() for facet in self.vreg['facets'].possible_vobjects(
+        wdgs = [facet.get_widget() for facet in self.vreg['facets'].poss_visible_objects(
             self.req, rset=self.rset, context='tablefilter',
             filtered_variable=mainvar)]
         wdgs = [wdg for wdg in wdgs if wdg is not None]
@@ -51,14 +51,17 @@
         """display a form to filter table's content. This should only
         occurs when a context eid is given
         """
-        self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.formfilter.js'))
+        self.req.add_css('cubicweb.facets.css')
+        self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.facets.js'))
         # drop False / None values from vidargs
         vidargs = dict((k, v) for k, v in vidargs.iteritems() if v)
         self.w(u'<form method="post" cubicweb:facetargs="%s" action="">' %
                xml_escape(dumps([divid, 'table', False, vidargs])))
         self.w(u'<fieldset id="%sForm" class="%s">' % (divid, hidden and 'hidden' or ''))
         self.w(u'<input type="hidden" name="divid" value="%s" />' % divid)
-        filter_hiddens(self.w, facets=','.join(wdg.facet.id for wdg in fwidgets), baserql=baserql)
+        self.w(u'<input type="hidden" name="fromformfilter" value="1" />')
+        filter_hiddens(self.w, facets=','.join(wdg.facet.id for wdg in fwidgets),
+                       baserql=baserql)
         self.w(u'<table class="filter">\n')
         self.w(u'<tr>\n')
         for wdg in fwidgets:
@@ -129,8 +132,7 @@
         # replace the inner div, so don't regenerate everything under the if
         # below
         if not fromformfilter:
-            div_class = 'section'
-            self.w(u'<div class="%s">' % div_class)
+            self.w(u'<div class="section">')
             if not title and 'title' in req.form:
                 title = req.form['title']
             if title:
@@ -139,12 +141,14 @@
                 actions += self.form_filter(divid, displaycols, displayfilter,
                                             displayactions)
         elif displayfilter:
-            req.add_css('cubicweb.facets.css')
             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
@@ -293,7 +297,7 @@
              displaycols=None, displayactions=None, mainindex=None):
         """Dumps a table displaying a composite query"""
         actrql = self.req.form['actualrql']
-        self.ensure_ro_rql(actrql)
+        self.req.ensure_ro_rql(actrql)
         displaycols = self.displaycols(displaycols)
         if displayactions is None and 'displayactions' in self.req.form:
             displayactions = True
--- a/web/views/tabs.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/tabs.py	Tue Sep 22 13:08:42 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:
@@ -130,14 +133,15 @@
         # call the set_tab() JS function *after* each tab is generated
         # because the callback binding needs to be done before
         # XXX make work history: true
-        self.req.add_onload(u'''
+        self.req.add_onload(u"""
   jQuery('#entity-tabs-%(eeid)s > ul').tabs( { selected: %(tabindex)s });
   set_tab('%(vid)s', '%(cookiename)s');
-''' % {'tabindex'   : tabs.index(active_tab),
+""" % {'tabindex'   : tabs.index(active_tab),
        'vid'        : active_tab,
        'eeid'       : (entity and entity.eid or uid),
        'cookiename' : self.cookie_name})
 
+
 class EntityRelationView(EntityView):
     """view displaying entity related stuff.
     Such a view _must_ provide the rtype, target and vid attributes :
@@ -152,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.
     """
@@ -160,7 +164,7 @@
     vid = 'list'
 
     def cell_call(self, row, col):
-        rset = self.entity(row, col).related(self.rtype, role(self))
+        rset = self.rset.get_entity(row, col).related(self.rtype, role(self))
         self.w(u'<div class="mainInfo">')
         if self.title:
             self.w(tags.h1(self.req._(self.title)))
--- a/web/views/timetable.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/timetable.py	Tue Sep 22 13:08:42 2009 +0200
@@ -133,7 +133,7 @@
         """ render column headers """
         self.w(u'<tr class="header">\n')
 
-        self.w(u'<th class="ttdate">&nbsp;</th>\n')
+        self.w(u'<th class="ttdate">&#160;</th>\n')
         columns = []
         for user, width in zip(users, widths):
             self.w(u'<th colspan="%s">' % max(MIN_COLS, width))
@@ -191,13 +191,13 @@
                         task_descr, first_row = value
                         if first_row:
                             url = xml_escape(task_descr.task.absolute_url(vid="edition"))
-                            self.w(u'<td rowspan="%d" class="%s %s" onclick="document.location=\'%s\'">&nbsp;<div>' % (
+                            self.w(u'<td rowspan="%d" class="%s %s" onclick="document.location=\'%s\'">&#160;<div>' % (
                                 task_descr.lines, task_descr.color, filled_klasses[kj], url))
                             task_descr.task.view('tooltip', w=self.w)
                             self.w(u'</div></td>')
                     else:
                         if empty_line:
-                            self.w(u'<td class="ttempty">&nbsp;</td>')
+                            self.w(u'<td class="ttempty">&#160;</td>')
                         else:
-                            self.w(u'<td class="%s">&nbsp;</td>' % empty_klasses[kj] )
+                            self.w(u'<td class="%s">&#160;</td>' % empty_klasses[kj] )
             self.w(u'</tr>\n')
--- a/web/views/treeview.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/treeview.py	Tue Sep 22 13:08:42 2009 +0200
@@ -7,9 +7,10 @@
 """
 __docformat__ = "restructuredtext en"
 
+import simplejson as json
+
 from logilab.common.decorators import monkeypatch
 from logilab.mtconverter import xml_escape
-
 from cubicweb.utils import make_uid
 from cubicweb.interfaces import ITree
 from cubicweb.selectors import implements
@@ -21,28 +22,54 @@
 class TreeView(EntityView):
     id = 'treeview'
     itemvid = 'treeitemview'
+    subvid = 'oneline'
     css_classes = 'treeview widget'
     title = _('tree view')
 
-    def call(self, subvid=None, treeid=None, initial_load=True):
+    def _init_params(self, subvid, treeid, initial_load, initial_thru_ajax, morekwargs):
+        form = self.req.form
         if subvid is None:
-            subvid = self.req.form.pop('treesubvid', 'oneline') # consume it
+            subvid = form.pop('treesubvid', self.subvid) # consume it
         if treeid is None:
-            treeid = self.req.form.pop('treeid', None)
+            treeid = form.pop('treeid', None)
             if treeid is None:
-                self.warning('Tree state won\'t be properly restored after next reload')
-                treeid = make_uid('throw away uid')
-        self.w(u'<ul id="tree-%s" class="%s">' % (treeid, self.css_classes))
+                treeid = 'throw_away' + make_uid('uid')
+        if 'morekwargs' in self.req.form:
+            ajaxargs = json.loads(form.pop('morekwargs'))
+            # got unicode & python keywords must be strings
+            morekwargs.update(dict((str(k), v)
+                                   for k, v in ajaxargs.iteritems()))
+        toplevel_thru_ajax = form.pop('treeview_top', False) or initial_thru_ajax
+        toplevel = toplevel_thru_ajax or (initial_load and not form.get('fname'))
+        return subvid, treeid, toplevel_thru_ajax, toplevel
+
+    def _init_headers(self, treeid, toplevel_thru_ajax):
+        self.req.add_css('jquery.treeview.css')
+        self.req.add_js(('cubicweb.ajax.js', 'cubicweb.widgets.js', 'jquery.treeview.js'))
+        self.req.html_headers.add_onload(u"""
+jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid,
+                                         jsoncall=toplevel_thru_ajax)
+
+    def call(self, subvid=None, treeid=None,
+             initial_load=True, initial_thru_ajax=False, **morekwargs):
+        subvid, treeid, toplevel_thru_ajax, toplevel = self._init_params(
+            subvid, treeid, initial_load, initial_thru_ajax, morekwargs)
+        ulid = ' '
+        if toplevel:
+            self._init_headers(treeid, toplevel_thru_ajax)
+            ulid = ' id="tree-%s"' % treeid
+        self.w(u'<ul%s class="%s">' % (ulid, self.css_classes))
         for rowidx in xrange(len(self.rset)):
             self.wview(self.itemvid, self.rset, row=rowidx, col=0,
-                       vid=subvid, parentvid=self.id, treeid=treeid)
+                       vid=subvid, parentvid=self.id, treeid=treeid, **morekwargs)
         self.w(u'</ul>')
-        if initial_load and not self.req.form.get('fname'):
-            self.req.add_css('jquery.treeview.css')
-            self.req.add_js(('cubicweb.ajax.js', 'cubicweb.widgets.js', 'jquery.treeview.js'))
-            self.req.html_headers.add_onload(u"""
-jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid)
 
+    def cell_call(self, *args, **allargs):
+        """ does not makes much sense until you have to invoke
+        somentity.view('treeview') """
+        allargs.pop('row')
+        allargs.pop('col')
+        self.call(*args, **allargs)
 
 class FileTreeView(TreeView):
     """specific version of the treeview to display file trees
@@ -51,8 +78,9 @@
     css_classes = 'treeview widget filetree'
     title = _('file tree view')
 
-    def call(self, subvid=None, treeid=None, initial_load=True):
-        super(FileTreeView, self).call(treeid=treeid, subvid='filetree-oneline', initial_load=initial_load)
+    def call(self, subvid=None, treeid=None, initial_load=True, **kwargs):
+        super(FileTreeView, self).call(treeid=treeid, subvid='filetree-oneline',
+                                       initial_load=initial_load, **kwargs)
 
 class FileItemInnerView(EntityView):
     """inner view used by the TreeItemView instead of oneline view
@@ -63,7 +91,7 @@
     id = 'filetree-oneline'
 
     def cell_call(self, row, col):
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         if ITree.is_implemented_by(entity.__class__) and not entity.is_leaf():
             self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline'))
         else:
@@ -77,7 +105,7 @@
 
     def cell_call(self, row, col, vid='oneline', parentvid='treeview', treeid=None):
         assert treeid is not None
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         itemview = self.view(vid, self.rset, row=row, col=col)
         if row == len(self.rset) - 1:
             self.w(u'<li class="last">%s</li>' % itemview)
@@ -91,18 +119,19 @@
     (each item should be expandable if it's not a tree leaf)
     """
     id = 'treeitemview'
-    __select__ = EntityView.__select__ & implements(ITree) # XXX
+    default_branch_state_is_open = False
+    __select__ = EntityView.__select__ & implements(ITree)
 
     def open_state(self, eeid, treeid):
         cookies = self.req.get_cookie()
         treestate = cookies.get(treecookiename(treeid))
         if treestate:
             return str(eeid) in treestate.value.split(';')
-        return False
+        return self.default_branch_state_is_open
 
-    def cell_call(self, row, col, treeid, vid='oneline', parentvid='treeview'):
+    def cell_call(self, row, col, treeid, vid='oneline', parentvid='treeview', **morekwargs):
         w = self.w
-        entity = self.entity(row, col)
+        entity = self.rset.get_entity(row, col)
         liclasses = []
         is_last = row == len(self.rset) - 1
         is_open = self.open_state(entity.eid, treeid)
@@ -114,10 +143,11 @@
         else:
             rql = entity.children_rql() % {'x': entity.eid}
             url = xml_escape(self.build_url('json', rql=rql, vid=parentvid,
-                                             pageid=self.req.pageid,
-                                             treeid=treeid,
-                                             fname='view',
-                                             treesubvid=vid))
+                                            pageid=self.req.pageid,
+                                            treeid=treeid,
+                                            fname='view',
+                                            treesubvid=vid,
+                                            morekwargs=json.dumps(morekwargs)))
             divclasses = ['hitarea']
             if is_open:
                 liclasses.append('collapsable')
@@ -136,8 +166,11 @@
                 w(u'<li class="%s">' % u' '.join(liclasses))
             else:
                 w(u'<li cubicweb:loadurl="%s" class="%s">' % (url, u' '.join(liclasses)))
-            divtail = """ onclick="asyncRemoteExec('node_clicked', '%s', '%s')" """ %\
-                (treeid, entity.eid)
+            if treeid.startswith('throw_away'):
+                divtail = ''
+            else:
+                divtail = """ onclick="asyncRemoteExec('node_clicked', '%s', '%s')" """ %\
+                    (treeid, entity.eid)
             w(u'<div class="%s"%s></div>' % (u' '.join(divclasses), divtail))
 
             # add empty <ul> because jquery's treeview plugin checks for
@@ -145,8 +178,8 @@
             if not is_open:
                 w(u'<ul class="placeholder"><li>place holder</li></ul>')
         # the local node info
-        self.wview(vid, self.rset, row=row, col=col)
+        self.wview(vid, self.rset, row=row, col=col, **morekwargs)
         if is_open and not is_leaf: #  => rql is defined
-            self.wview(parentvid, self.req.execute(rql), treeid=treeid, initial_load=False)
+            self.wview(parentvid, self.req.execute(rql), treeid=treeid, initial_load=False, **morekwargs)
         w(u'</li>')
 
--- a/web/views/urlpublishing.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/urlpublishing.py	Tue Sep 22 13:08:42 2009 +0200
@@ -28,8 +28,7 @@
 from rql import TypeResolverException
 
 from cubicweb import RegistryException, typed_eid
-from cubicweb.web import NotFound, Redirect
-from cubicweb.web.component import Component, Component
+from cubicweb.web import NotFound, Redirect, component
 
 
 class PathDontMatch(Exception):
@@ -37,7 +36,7 @@
     a path
     """
 
-class URLPublisherComponent(Component):
+class URLPublisherComponent(component.Component):
     """associate url's path to view identifier / rql queries,
     by applying a chain of urlpathevaluator components.
 
@@ -51,12 +50,14 @@
     something else than `PathDontMatch` will stop the handlers chain.
     """
     id = 'urlpublisher'
+    vreg = None # XXX necessary until property for deprecation warning is on appobject
 
-    def __init__(self, default_method='view'):
+    def __init__(self, vreg, default_method='view'):
         super(URLPublisherComponent, self).__init__()
+        self.vreg = vreg
         self.default_method = default_method
         evaluators = []
-        for evaluatorcls in self.vreg['components']['urlpathevaluator']:
+        for evaluatorcls in vreg['components']['urlpathevaluator']:
             # instantiation needed
             evaluator = evaluatorcls(self)
             evaluators.append(evaluator)
@@ -98,13 +99,14 @@
         return pmid, rset
 
 
-class URLPathEvaluator(Component):
+class URLPathEvaluator(component.Component):
     __abstract__ = True
     id = 'urlpathevaluator'
+    vreg = None # XXX necessary until property for deprecation warning is on appobject
 
     def __init__(self, urlpublisher):
         self.urlpublisher = urlpublisher
-
+        self.vreg = urlpublisher.vreg
 
 class RawPathEvaluator(URLPathEvaluator):
     """handle path of the form::
@@ -197,7 +199,7 @@
         evaluators = sorted(self.vreg['urlrewriting'].all_objects(),
                             key=lambda x: x.priority, reverse=True)
         for rewritercls in evaluators:
-            rewriter = rewritercls()
+            rewriter = rewritercls(req)
             try:
                 # XXX we might want to chain url rewrites
                 return rewriter.rewrite(req, uri)
@@ -217,6 +219,7 @@
             raise PathDontMatch()
         # remove last part and see if this is something like an actions
         # if so, call
+        # XXX bad smell: refactor to simpler code
         try:
             actionsreg = self.vreg['actions']
             requested = parts.pop(-1)
@@ -232,7 +235,7 @@
                 continue
             else:
                 try:
-                    action = actionsreg.select_best(actions, req, rset=rset)
+                    action = actionsreg._select_best(actions, req, rset=rset)
                 except RegistryException:
                     continue
                 else:
--- a/web/views/urlrewrite.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/urlrewrite.py	Tue Sep 22 13:08:42 2009 +0200
@@ -70,6 +70,9 @@
     id = 'simple'
 
     rules = [
+        ('/_', dict(vid='manage')),
+        ('/_registry', dict(vid='registry')),
+#        (rgx('/_([^/]+?)/?'), dict(vid=r'\1')),
         ('/schema',  dict(vid='schema')),
         ('/index', dict(vid='index')),
         ('/myprefs', dict(vid='propertiesform')),
--- a/web/views/workflow.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/workflow.py	Tue Sep 22 13:08:42 2009 +0200
@@ -15,54 +15,73 @@
 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')
+    __select__ = (one_line_rset() & 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))
+        # specify both rset/row/col and entity in case implements selector (and
+        # not entity_implements) is used on custom form
+        form = self.vreg['forms'].select(
+            'changestate', self.req, rset=self.rset, row=row, col=col,
+            entity=entity, transition=transition,
+            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''))
+        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()
@@ -110,14 +129,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):
@@ -130,32 +183,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):
@@ -163,7 +201,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'
@@ -177,9 +215,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
@@ -209,14 +249,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	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/views/xmlrss.py	Tue Sep 22 13:08:42 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)
@@ -117,7 +117,7 @@
     __select__ = non_final_entity() & one_line_rset()
 
     def feed_url(self):
-        return self.entity(0, 0).rss_feed_url()
+        return self.rset.get_entity(0, 0).rss_feed_url()
 
 
 class RSSIconBox(box.BoxTemplate):
--- a/web/wdoc/ChangeLog_en	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/wdoc/ChangeLog_en	Tue Sep 22 13:08:42 2009 +0200
@@ -1,27 +1,34 @@
 .. -*- coding: utf-8 -*-
-.. _`user preferences`: myprefs#fieldset_ui
-
-2008-09-25  --  2.50.0
-    * jQuery replaces MochiKit
-    * schema inheritance support
+.. _`user preferences`: myprefs
+.. _here: sparql
+.. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/
+.. _schema: schema
+.. _OWL: http://www.w3.org/TR/owl-features/
 
-2008-05-13  --  2.48.0
-    * web pages are now served with the ``xhtml+xml`` content type
+2009-08-07  --  3.4.0
+
+    * support for SPARQL_. Click here_ to test it.
 
-2008-03-27  --  2.47.0
-    * fckeditor is now integrated to edit rich text fields. If you don't see it,
-      check your `user preferences`_.
+    * and another step toward the semantic web: new `ExternalUri` entity type
+      with its associated `same_as` relation. See
+      http://www.w3.org/TR/owl-ref/#sameAs-def for more information and check
+      this instance schema_ to see on which entity types it may be applied
 
-2008-03-13  --  2.46.0
-    * new calendar and timetable views.
-    
-    * click-and-edit functionalities : if you see the text edit cursor when
-      you're over a fied, try to double-click!
-      
-    * automatic facets oriented search : a filter box should appear when you're
-      looking for something and more than one entity are displayed.
+    * new "view workflow" and "view history" items in the workflow
+      sub-menu of the actions box
+
+    * you can now edit comments of workflow transition afterward (if authorized,
+      of course)
+
+    * modification date of an entity is updated when its state is changed
+
 
-2008-02-15  --  2.44.0
-    * new internationalized online help system. Click the question mark on the
-      right top corner! Hopefuly some new documentation will appear as time is
-      going. 
+2009-02-26  --  3.1.0
+
+    * schema may be exported as OWL_
+
+    * new ajax interface for site configuration / `user preferences`_
+
+
+2008-10-24  --  2.99.0
+    * cubicweb is now open source !
--- a/web/wdoc/ChangeLog_fr	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/wdoc/ChangeLog_fr	Tue Sep 22 13:08:42 2009 +0200
@@ -1,31 +1,34 @@
 .. -*- coding: utf-8 -*-
 .. _`préférences utilisateurs`: myprefs#fieldset_ui
-
-2008-09-25  --  2.50.0
-    * jQuery remplace MochiKit
-    * support de l'héritage de schéma
+.. _ici: sparql
+.. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/
+.. _schema: schema
+.. _OWL: http://www.w3.org/TR/owl-features/
 
-2008-05-13  --  2.48.0
-    * les pages sont servies en tant que ``xhtml+xml`` pour certains navigateurs
+2009-08-07  --  3.4.0
 
-2008-03-27  --  2.47.0
-    * fckeditor est enfin intégré pour éditer les champs de type texte riche. Si
-      vous ne le voyez pas apparaître, vérifiez vos `préférences utilisateurs`_.
+    * support de SPARQL_. Cliquez ici_ pour le tester.
 
-2008-03-13  --  2.46.0
-    * nouvelle vues calendrier et emploi du temps
-    
-    * fonctionalité "click-et-édite" : si vous voyez apparaitre le curseur
-      d'édition de texte en survolant un champ, essayez de double-cliquer !
-      
-    * recherche par facettes : une boîte de filtrage devrait apparaitre
-      automatiquement lorsque vous effectuez une recherche qui ramène plus d'une
-      entité
+    * et encore un pas vers le web sémantique : un nouveau type d'entité
+      `ExternalUri` et la relation associée `same_as`. Voir
+      http://www.w3.org/TR/owl-ref/#sameAs-def pour plus d'information, ainsi
+      que le schema_ de cette instance pour voir à quels types d'entités cela
+      s'applique.
 
-2008-02-15  --  2.44.0
-    * nouveau système d'aide internationalisé. Cliquez sur le point
-      d'interrogation en haut à droite. Reste à enrichir le contenu de cette
-      documentation, mais cela devrait arriver avec le temps.
+    * nouveau liens "voir les états possibles" et "voir l'historique" dans le sous-menu
+      workflow de la boite actions
+
+    * vous pouvez dorénavant éditer les commentaires des passages de transition
+      depuis l'historique, pour peu que vous ayez les droits nécessaire bien sûr
+
+    * la date de modification d'une entité est mise à jour lorsque son état est changé
 
 
-      
+2009-02-26  --  3.1.0
+
+    * le schéma peut être exporté en OWL_
+
+    * nouvelle interface ajax pour la configuration du site et les `préférences utilisateurs`_
+
+
+
--- a/web/webconfig.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/webconfig.py	Tue Sep 22 13:08:42 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'])
 
--- a/web/webctl.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/web/webctl.py	Tue Sep 22 13:08:42 2009 +0200
@@ -8,8 +8,7 @@
 """
 __docformat__ = "restructuredtext en"
 
-from cubicweb import underline_title
-from cubicweb.toolsutils import CommandHandler
+from cubicweb.toolsutils import CommandHandler, underline_title
 from logilab.common.shellutils import ASK
 
 class WebCreateHandler(CommandHandler):
--- a/web/widgets.py	Wed Aug 05 09:15:56 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,930 +0,0 @@
-"""widgets for entity edition
-
-those are in cubicweb.common since we need to know available widgets at schema
-serialization time
-
-: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
-"""
-__docformat__ = "restructuredtext en"
-
-from datetime import datetime
-
-from logilab.mtconverter import xml_escape
-
-from yams.constraints import SizeConstraint, StaticVocabularyConstraint
-
-from cubicweb.common.uilib import toggle_action
-from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param
-
-def _format_attrs(kwattrs):
-    """kwattrs is the dictionary of the html attributes available for
-    the edited element
-    """
-    # sort for predictability (required for tests)
-    return u' '.join(sorted(u'%s="%s"' % item for item in kwattrs.iteritems()))
-
-def _value_from_values(values):
-    # take care, value may be 0, 0.0...
-    if values:
-        value = values[0]
-        if value is None:
-            value = u''
-    else:
-        value = u''
-    return value
-
-def _eclass_eschema(eschema_or_eclass):
-    try:
-        return eschema_or_eclass, eschema_or_eclass.e_schema
-    except AttributeError:
-        return None, eschema_or_eclass
-
-def checkbox(name, value, attrs='', checked=None):
-    if checked is None:
-        checked = value
-    checked = checked and 'checked="checked"' or ''
-    return u'<input type="checkbox" name="%s" value="%s" %s %s />' % (
-        name, value, checked, attrs)
-
-def widget(vreg, subjschema, rschema, objschema, role='object'):
-    """get a widget to edit the given relation"""
-    if rschema == 'eid':
-        # return HiddenWidget(vreg, subjschema, rschema, objschema)
-        return EidWidget(vreg, _eclass_eschema(subjschema)[1], rschema, objschema)
-    return widget_factory(vreg, subjschema, rschema, objschema, role=role)
-
-
-class Widget(object):
-    """abstract widget class"""
-    need_multipart = False
-    # generate the "id" attribute with the same value as the "name" (html) attribute
-    autoid = True
-    html_attributes = set(('id', 'class', 'tabindex', 'accesskey', 'onchange', 'onkeypress'))
-    cubicwebns_attributes = set()
-
-    def __init__(self, vreg, subjschema, rschema, objschema,
-                 role='subject', description=None,
-                 **kwattrs):
-        self.vreg = vreg
-        self.rschema = rschema
-        self.subjtype = subjschema
-        self.objtype = objschema
-        self.role = role
-        self.name = rschema.type
-        self.description = description
-        self.attrs = kwattrs
-        # XXX accesskey may not be unique
-        kwattrs['accesskey'] = self.name[0]
-
-    def copy(self):
-        """shallow copy (useful when you need to modify self.attrs
-        because widget instances are cached)
-        """
-        # brute force copy (subclasses don't have the
-        # same __init__ prototype)
-        widget = self.__new__(self.__class__)
-        widget.__dict__ = dict(self.__dict__)
-        widget.attrs = dict(widget.attrs)
-        return widget
-
-    @staticmethod
-    def size_constraint_attrs(attrs, maxsize):
-        """set html attributes in the attrs dict to consider maxsize"""
-        pass
-
-    def format_attrs(self):
-        """return a string with html attributes available for the edit input"""
-        # sort for predictability (required for tests)
-        attrs = []
-        for name, value in self.attrs.iteritems():
-            # namespace attributes have priority over standard xhtml ones
-            if name in self.cubicwebns_attributes:
-                attrs.append(u'cubicweb:%s="%s"' % (name, value))
-            elif name in self.html_attributes:
-                attrs.append(u'%s="%s"' % (name, value))
-        return u' '.join(sorted(attrs))
-
-    def required(self, entity):
-        """indicates if the widget needs a value to be filled in"""
-        card = self.rschema.cardinality(self.subjtype, self.objtype, self.role)
-        return card in '1+'
-
-    def input_id(self, entity):
-        try:
-            return self.rname
-        except AttributeError:
-            return eid_param(self.name, entity.eid)
-
-    def render_label(self, entity, label=None):
-        """render widget's label"""
-        label = label or self.rschema.display_name(entity.req, self.role)
-        forid = self.input_id(entity)
-        if forid:
-            forattr =  ' for="%s"' % forid
-        else:
-            forattr = ''
-        if self.required(entity):
-            label = u'<label class="required"%s>%s</label>' % (forattr, label)
-        else:
-            label = u'<label%s>%s</label>' % (forattr, label)
-        return label
-
-    def render_error(self, entity):
-        """return validation error for widget's field of the given entity, if
-        any
-        """
-        errex = entity.req.data.get('formerrors')
-        if errex and errex.eid == entity.eid and self.name in errex.errors:
-            entity.req.data['displayederrors'].add(self.name)
-            return u'<span class="error">%s</span>' % errex.errors[self.name]
-        return u''
-
-    def render_help(self, entity):
-        """render a help message about the (edited) field"""
-        req = entity.req
-        help = [u'<div class="helper">']
-        descr = self.description or self.rschema.rproperty(self.subjtype, self.objtype, 'description')
-        if descr:
-            help.append(u'<span>%s</span>' % req._(descr))
-        example = self.render_example(req)
-        if example:
-            help.append(u'<span>(%s: %s)</span>'
-                        % (req._('sample format'), example))
-        help.append(u'</div>')
-        return u'&nbsp;'.join(help)
-
-    def render_example(self, req):
-        return u''
-
-    def render(self, entity):
-        """render the widget for a simple view"""
-        if not entity.has_eid():
-            return u''
-        return entity.printable_value(self.name)
-
-    def edit_render(self, entity, tabindex=None,
-                    includehelp=False, useid=None, **kwargs):
-        """render the widget for edition"""
-        # this is necessary to handle multiple edition
-        self.rname = eid_param(self.name, entity.eid)
-        if useid:
-            self.attrs['id'] = useid
-        elif self.autoid:
-            self.attrs['id'] = self.rname
-        if tabindex is not None:
-            self.attrs['tabindex'] = tabindex
-        else:
-            self.attrs['tabindex'] = entity.req.next_tabindex()
-        output = self._edit_render(entity, **kwargs)
-        if includehelp:
-            output += self.render_help(entity)
-        return output
-
-    def _edit_render(self, entity):
-        """do the actual job to render the widget for edition"""
-        raise NotImplementedError
-
-    def current_values(self, entity):
-        """return the value of the field associated to this widget on the given
-        entity. always return a list of values, which'll have size equal to 1
-        if the field is monovalued (like all attribute fields, but not all non
-        final relation fields
-        """
-        if self.rschema.is_final():
-            return entity.attribute_values(self.name)
-        elif entity.has_eid():
-            return [row[0] for row in entity.related(self.name, self.role)]
-        return ()
-
-    def current_value(self, entity):
-        return _value_from_values(self.current_values(entity))
-
-    def current_display_values(self, entity):
-        """same as .current_values but consider values stored in session in case
-        of validation error
-        """
-        values = entity.req.data.get('formvalues')
-        if values is None:
-            return self.current_values(entity)
-        cdvalues = values.get(self.rname)
-        if cdvalues is None:
-            return self.current_values(entity)
-        if not isinstance(cdvalues, (list, tuple)):
-            cdvalues = (cdvalues,)
-        return cdvalues
-
-    def current_display_value(self, entity):
-        """same as .current_value but consider values stored in session in case
-        of validation error
-        """
-        return _value_from_values(self.current_display_values(entity))
-
-    def hidden_input(self, entity, qvalue):
-        """return an hidden field which
-        1. indicates that a field is edited
-        2. hold the old value to easily detect if the field has been modified
-
-        `qvalue` is the html quoted old value
-        """
-        if self.role == 'subject':
-            editmark = 'edits'
-        else:
-            editmark = 'edito'
-        if qvalue is None or not entity.has_eid():
-            qvalue = INTERNAL_FIELD_VALUE
-        return u'<input type="hidden" name="%s-%s" value="%s"/>\n' % (
-            editmark, self.rname, qvalue)
-
-class InputWidget(Widget):
-    """abstract class for input generating a <input> tag"""
-    input_type = None
-    html_attributes = Widget.html_attributes | set(('type', 'name', 'value'))
-
-    def _edit_render(self, entity):
-        value = self.current_value(entity)
-        dvalue = self.current_display_value(entity)
-        if isinstance(value, basestring):
-            value = xml_escape(value)
-        if isinstance(dvalue, basestring):
-            dvalue = xml_escape(dvalue)
-        return u'%s<input type="%s" name="%s" value="%s" %s/>' % (
-            self.hidden_input(entity, value), self.input_type,
-            self.rname, dvalue, self.format_attrs())
-
-class HiddenWidget(InputWidget):
-    input_type = 'hidden'
-    autoid = False
-    def __init__(self, vreg, subjschema, rschema, objschema,
-                 role='subject', **kwattrs):
-        InputWidget.__init__(self, vreg, subjschema, rschema, objschema,
-                             role='subject',
-                             **kwattrs)
-        # disable access key
-        del self.attrs['accesskey']
-
-    def current_value(self, entity):
-        value = InputWidget.current_value(self, entity)
-        return value or INTERNAL_FIELD_VALUE
-
-    def current_display_value(self, entity):
-        value = InputWidget.current_display_value(self, entity)
-        return value or INTERNAL_FIELD_VALUE
-
-    def render_label(self, entity, label=None):
-        """render widget's label"""
-        return u''
-
-    def render_help(self, entity):
-        return u''
-
-    def hidden_input(self, entity, value):
-        """no hidden input for hidden input"""
-        return ''
-
-
-class EidWidget(HiddenWidget):
-
-    def _edit_render(self, entity):
-        return u'<input type="hidden" name="eid" value="%s" />' % entity.eid
-
-
-class StringWidget(InputWidget):
-    input_type = 'text'
-    html_attributes = InputWidget.html_attributes | set(('size', 'maxlength'))
-    @staticmethod
-    def size_constraint_attrs(attrs, maxsize):
-        """set html attributes in the attrs dict to consider maxsize"""
-        attrs['size'] = min(maxsize, 40)
-        attrs['maxlength'] = maxsize
-
-
-class AutoCompletionWidget(StringWidget):
-    cubicwebns_attributes = (StringWidget.cubicwebns_attributes |
-                          set(('accesskey', 'size', 'maxlength')))
-    attrs = ()
-
-    wdgtype = 'SuggestField'
-
-    def current_value(self, entity):
-        value = StringWidget.current_value(self, entity)
-        return value or INTERNAL_FIELD_VALUE
-
-    def _get_url(self, entity):
-        return entity.req.build_url('json', fname=entity.autocomplete_initfuncs[self.rschema],
-                                pageid=entity.req.pageid, mode='remote')
-
-    def _edit_render(self, entity):
-        req = entity.req
-        req.add_js( ('cubicweb.widgets.js', 'jquery.autocomplete.js') )
-        req.add_css('jquery.autocomplete.css')
-        value = self.current_value(entity)
-        dvalue = self.current_display_value(entity)
-        if isinstance(value, basestring):
-            value = xml_escape(value)
-        if isinstance(dvalue, basestring):
-            dvalue = xml_escape(dvalue)
-        iid = self.attrs.pop('id')
-        if self.required(entity):
-            cssclass = u' required'
-        else:
-            cssclass = u''
-        dataurl = self._get_url(entity)
-        return (u'%(hidden)s<input type="text" name="%(iid)s" value="%(value)s" cubicweb:dataurl="%(url)s" class="widget%(required)s" id="%(iid)s" '
-                u'tabindex="%(tabindex)s" cubicweb:loadtype="auto" cubicweb:wdgtype="%(wdgtype)s"  %(attrs)s />' % {
-                    'iid': iid,
-                    'hidden': self.hidden_input(entity, value),
-                    'wdgtype': self.wdgtype,
-                    'url': xml_escape(dataurl),
-                    'tabindex': self.attrs.pop('tabindex'),
-                    'value': dvalue,
-                    'attrs': self.format_attrs(),
-                    'required' : cssclass,
-                    })
-
-class StaticFileAutoCompletionWidget(AutoCompletionWidget):
-    wdgtype = 'StaticFileSuggestField'
-
-    def _get_url(self, entity):
-        return entity.req.datadir_url + entity.autocomplete_initfuncs[self.rschema]
-
-class RestrictedAutoCompletionWidget(AutoCompletionWidget):
-    wdgtype = 'RestrictedSuggestField'
-
-
-class PasswordWidget(InputWidget):
-    input_type = 'password'
-
-    def required(self, entity):
-        if InputWidget.required(self, entity) and not entity.has_eid():
-            return True
-        return False
-
-    def current_values(self, entity):
-        # on existant entity, show password field has non empty (we don't have
-        # the actual value
-        if entity.has_eid():
-            return (INTERNAL_FIELD_VALUE,)
-        return super(PasswordWidget, self).current_values(entity)
-
-    def _edit_render(self, entity):
-        html = super(PasswordWidget, self)._edit_render(entity)
-        name = eid_param(self.name + '-confirm', entity.eid)
-        return u'%s<br/>\n<input type="%s" name="%s" id="%s" tabindex="%s"/>&nbsp;<span class="emphasis">(%s)</span>' % (
-            html, self.input_type, name, name, entity.req.next_tabindex(),
-            entity.req._('confirm password'))
-
-
-class TextWidget(Widget):
-    html_attributes = Widget.html_attributes | set(('rows', 'cols'))
-
-    @staticmethod
-    def size_constraint_attrs(attrs, maxsize):
-        """set html attributes in the attrs dict to consider maxsize"""
-        if 256 < maxsize < 513:
-            attrs['cols'], attrs['rows'] = 60, 5
-        else:
-            attrs['cols'], attrs['rows'] = 80, 10
-
-    def render(self, entity):
-        if not entity.has_eid():
-            return u''
-        return entity.printable_value(self.name)
-
-    def _edit_render(self, entity, with_format=True):
-        req = entity.req
-        editor = self._edit_render_textarea(entity, with_format)
-        value = self.current_value(entity)
-        if isinstance(value, basestring):
-            value = xml_escape(value)
-        return u'%s%s' % (self.hidden_input(entity, value), editor)
-
-    def _edit_render_textarea(self, entity, with_format):
-        self.attrs.setdefault('cols', 80)
-        self.attrs.setdefault('rows', 20)
-        dvalue = self.current_display_value(entity)
-        if isinstance(dvalue, basestring):
-            dvalue = xml_escape(dvalue)
-        if entity.use_fckeditor(self.name):
-            entity.req.fckeditor_config()
-            if with_format:
-                if entity.has_eid():
-                    format = entity.attr_metadata(self.name, 'format')
-                else:
-                    format = ''
-                frname = eid_param(self.name + '_format', entity.eid)
-                hidden = u'<input type="hidden" name="edits-%s" value="%s"/>\n'\
-                         '<input type="hidden" name="%s" value="text/html"/>\n' % (
-                    frname, format, frname)
-            return u'%s<textarea cubicweb:type="wysiwyg" onkeyup="autogrow(this)" name="%s" %s>%s</textarea>' % (
-                hidden, self.rname, self.format_attrs(), dvalue)
-        if with_format and entity.e_schema.has_metadata(self.name, 'format'):
-            fmtwdg = entity.get_widget(self.name + '_format')
-            fmtwdgstr = fmtwdg.edit_render(entity, tabindex=self.attrs['tabindex'])
-            self.attrs['tabindex'] = entity.req.next_tabindex()
-        else:
-            fmtwdgstr = ''
-        return u'%s<br/><textarea onkeyup="autogrow(this)" name="%s" %s>%s</textarea>' % (
-            fmtwdgstr, self.rname, self.format_attrs(), dvalue)
-
-
-class CheckBoxWidget(Widget):
-    html_attributes = Widget.html_attributes | set(('checked', ))
-    def _edit_render(self, entity):
-        value = self.current_value(entity)
-        dvalue = self.current_display_value(entity)
-        return self.hidden_input(entity, value) + checkbox(self.rname, 'checked', self.format_attrs(), dvalue)
-
-    def render(self, entity):
-        if not entity.has_eid():
-            return u''
-        if getattr(entity, self.name):
-            return entity.req._('yes')
-        return entity.req._('no')
-
-
-class YesNoRadioWidget(CheckBoxWidget):
-    html_attributes = Widget.html_attributes | set(('disabled',))
-    def _edit_render(self, entity):
-        value = self.current_value(entity)
-        dvalue = self.current_display_value(entity)
-        attrs1 = self.format_attrs()
-        del self.attrs['id'] # avoid duplicate id for xhtml compliance
-        attrs2 = self.format_attrs()
-        if dvalue:
-            attrs1 += ' checked="checked"'
-        else:
-            attrs2 += ' checked="checked"'
-        wdgs = [self.hidden_input(entity, value),
-                u'<input type="radio" name="%s" value="1" %s/>%s<br/>' % (self.rname, attrs1, entity.req._('yes')),
-                u'<input type="radio" name="%s" value="" %s/>%s<br/>' % (self.rname, attrs2, entity.req._('no'))]
-        return '\n'.join(wdgs)
-
-
-class FileWidget(Widget):
-    need_multipart = True
-    def _file_wdg(self, entity):
-        wdgs = [u'<input type="file" name="%s" %s/>' % (self.rname, self.format_attrs())]
-        req = entity.req
-        if (entity.e_schema.has_metadata(self.name, 'format')
-            or entity.e_schema.has_metadata(self.name, 'encoding')):
-            divid = '%s-%s-advanced' % (self.name, entity.eid)
-            wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
-                        (xml_escape(toggle_action(divid)),
-                         req._('show advanced fields'),
-                         xml_escape(req.build_url('data/puce_down.png')),
-                         req._('show advanced fields')))
-            wdgs.append(u'<div id="%s" class="hidden">' % divid)
-            for extraattr in ('_format', '_encoding'):
-                if entity.e_schema.has_subject_relation('%s%s' % (self.name, extraattr)):
-                    ewdg = entity.get_widget(self.name + extraattr)
-                    wdgs.append(ewdg.render_label(entity))
-                    wdgs.append(ewdg.edit_render(entity, includehelp=True))
-                    wdgs.append(u'<br/>')
-            wdgs.append(u'</div>')
-        if entity.has_eid():
-            if not self.required(entity):
-                # trick to be able to delete an uploaded file
-                wdgs.append(u'<br/>')
-                wdgs.append(checkbox(eid_param('__%s_detach' % self.rname, entity.eid), False))
-                wdgs.append(req._('detach attached file %s' % entity.dc_title()))
-            else:
-                wdgs.append(u'<br/>')
-                wdgs.append(req._('currently attached file: %s' % entity.dc_title()))
-        return '\n'.join(wdgs)
-
-    def _edit_render(self, entity):
-        return self.hidden_input(entity, None) + self._file_wdg(entity)
-
-
-class TextFileWidget(FileWidget):
-    def _edit_msg(self, entity):
-        if entity.has_eid() and not self.required(entity):
-            msg = entity.req._(
-                'You can either submit a new file using the browse button above'
-                ', or choose to remove already uploaded file by checking the '
-                '"detach attached file" check-box, or edit file content online '
-                'with the widget below.')
-        else:
-            msg = entity.req._(
-                'You can either submit a new file using the browse button above'
-                ', or edit file content online with the widget below.')
-        return msg
-
-    def _edit_render(self, entity):
-        wdgs = [self._file_wdg(entity)]
-        if entity.attr_metadata(self.name, 'format') in ('text/plain', 'text/html', 'text/rest'):
-            msg = self._edit_msg(entity)
-            wdgs.append(u'<p><b>%s</b></p>' % msg)
-            twdg = TextWidget(self.vreg, self.subjtype, self.rschema, self.objtype)
-            twdg.rname = self.rname
-            data = getattr(entity, self.name)
-            if data:
-                encoding = entity.attr_metadata(self.name, 'encoding')
-                try:
-                    entity[self.name] = unicode(data.getvalue(), encoding)
-                except UnicodeError:
-                    pass
-                else:
-                    wdgs.append(twdg.edit_render(entity, with_format=False))
-                    entity[self.name] = data # restore Binary value
-            wdgs.append(u'<br/>')
-        return '\n'.join(wdgs)
-
-
-class ComboBoxWidget(Widget):
-    html_attributes = Widget.html_attributes | set(('multiple', 'size'))
-
-    def __init__(self, vreg, subjschema, rschema, objschema,
-                 multiple=False, **kwattrs):
-        super(ComboBoxWidget, self).__init__(vreg, subjschema, rschema, objschema,
-                                             **kwattrs)
-        if multiple:
-            self.attrs['multiple'] = 'multiple'
-            if not 'size' in self.attrs:
-                self.attrs['size'] = '5'
-        # disable access key (dunno why but this is not allowed by xhtml 1.0)
-        del self.attrs['accesskey']
-
-    def vocabulary(self, entity):
-        raise NotImplementedError()
-
-    def form_value(self, entity, value, values):
-        if value in values:
-            flag = 'selected="selected"'
-        else:
-            flag = ''
-        return value, flag
-
-    def _edit_render(self, entity):
-        values = self.current_values(entity)
-        if values:
-            res = [self.hidden_input(entity, v) for v in values]
-        else:
-            res = [self.hidden_input(entity, INTERNAL_FIELD_VALUE)]
-        dvalues = self.current_display_values(entity)
-        res.append(u'<select name="%s" %s>' % (self.rname, self.format_attrs()))
-        for label, value in self.vocabulary(entity):
-            if value is None:
-                # handle separator
-                res.append(u'<optgroup label="%s"/>' % (label or ''))
-            else:
-                value, flag = self.form_value(entity, value, dvalues)
-                res.append(u'<option value="%s" %s>%s</option>' % (value, flag, xml_escape(label)))
-        res.append(u'</select>')
-        return '\n'.join(res)
-
-
-class StaticComboBoxWidget(ComboBoxWidget):
-
-    def __init__(self, vreg, subjschema, rschema, objschema,
-                 vocabfunc, multiple=False, sort=False, **kwattrs):
-        super(StaticComboBoxWidget, self).__init__(vreg, subjschema, rschema, objschema,
-                                                   multiple, **kwattrs)
-        self.sort = sort
-        self.vocabfunc = vocabfunc
-
-    def vocabulary(self, entity):
-        choices = self.vocabfunc(entity=entity)
-        if self.sort:
-            choices = sorted(choices)
-        if self.rschema.rproperty(self.subjtype, self.objtype, 'internationalizable'):
-            return zip((entity.req._(v) for v in choices), choices)
-        return zip(choices, choices)
-
-
-class EntityLinkComboBoxWidget(ComboBoxWidget):
-    """to be used be specific forms"""
-
-    def current_values(self, entity):
-        if entity.has_eid():
-            return [r[0] for r in entity.related(self.name, self.role)]
-        defaultmeth = 'default_%s_%s' % (self.role, self.name)
-        if hasattr(entity, defaultmeth):
-            return getattr(entity, defaultmeth)()
-        return ()
-
-    def vocabulary(self, entity):
-        return [('', INTERNAL_FIELD_VALUE)] + entity.vocabulary(self.rschema, self.role)
-
-
-class RawDynamicComboBoxWidget(EntityLinkComboBoxWidget):
-
-    def vocabulary(self, entity, limit=None):
-        req = entity.req
-        # first see if its specified by __linkto form parameters
-        linkedto = entity.linked_to(self.name, self.role)
-        if linkedto:
-            entities = (req.entity_from_eid(eid) for eid in linkedto)
-            return [(entity.view('combobox'), entity.eid) for entity in entities]
-        # it isn't, check if the entity provides a method to get correct values
-        if not self.required(entity):
-            res = [('', INTERNAL_FIELD_VALUE)]
-        else:
-            res = []
-        # vocabulary doesn't include current values, add them
-        if entity.has_eid():
-            rset = entity.related(self.name, self.role)
-            relatedvocab = [(e.view('combobox'), e.eid) for e in rset.entities()]
-        else:
-            relatedvocab = []
-        return res + entity.vocabulary(self.rschema, self.role) + relatedvocab
-
-
-class DynamicComboBoxWidget(RawDynamicComboBoxWidget):
-
-    def vocabulary(self, entity, limit=None):
-        return sorted(super(DynamicComboBoxWidget, self).vocabulary(entity, limit))
-
-
-class AddComboBoxWidget(DynamicComboBoxWidget):
-    def _edit_render(self, entity):
-        req = entity.req
-        req.add_js( ('cubicweb.ajax.js', 'jquery.js', 'cubicweb.widgets.js') )
-        values = self.current_values(entity)
-        if values:
-            res = [self.hidden_input(entity, v) for v in values]
-        else:
-            res = [self.hidden_input(entity, INTERNAL_FIELD_VALUE)]
-        dvalues = self.current_display_values(entity)
-        etype_from = entity.e_schema.subject_relation(self.name).objects(entity.e_schema)[0]
-        res.append(u'<select class="widget" cubicweb:etype_to="%s" cubicweb:etype_from="%s" cubicweb:loadtype="auto" cubicweb:wdgtype="AddComboBox" name="%s" %s>'
-                   % (entity.e_schema, etype_from, self.rname, self.format_attrs()))
-        for label, value in self.vocabulary(entity):
-            if value is None:
-                # handle separator
-                res.append(u'<optgroup label="%s"/>' % (label or ''))
-            else:
-                value, flag = self.form_value(entity, value, dvalues)
-                res.append(u'<option value="%s" %s>%s</option>' % (value, flag, xml_escape(label)))
-        res.append(u'</select>')
-        res.append(u'<div id="newvalue">')
-        res.append(u'<input type="text" id="newopt" />')
-        res.append(u'<a href="javascript:noop()" id="add_newopt">&nbsp;</a></div>')
-        return '\n'.join(res)
-
-
-class IntegerWidget(StringWidget):
-    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
-        kwattrs['size'] = 5
-        kwattrs['maxlength'] = 15
-        StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
-
-    def render_example(self, req):
-        return '23'
-
-
-class FloatWidget(StringWidget):
-    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
-        kwattrs['size'] = 5
-        kwattrs['maxlength'] = 15
-        StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
-
-    def render_example(self, req):
-        formatstr = req.property_value('ui.float-format')
-        return formatstr % 1.23
-
-    def current_values(self, entity):
-        values = entity.attribute_values(self.name)
-        if values:
-            formatstr = entity.req.property_value('ui.float-format')
-            value = values[0]
-            if value is not None:
-                value = float(value)
-            else:
-                return ()
-            return [formatstr % value]
-        return ()
-
-
-class DecimalWidget(StringWidget):
-    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
-        kwattrs['size'] = 5
-        kwattrs['maxlength'] = 15
-        StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
-
-    def render_example(self, req):
-        return '345.0300'
-
-
-class DateWidget(StringWidget):
-    format_key = 'ui.date-format'
-    monthnames = ('january', 'february', 'march', 'april',
-                  'may', 'june', 'july', 'august',
-                  'september', 'october', 'november', 'december')
-    daynames = ('monday', 'tuesday', 'wednesday', 'thursday',
-                'friday', 'saturday', 'sunday')
-
-    @classmethod
-    def add_localized_infos(cls, req):
-        """inserts JS variables defining localized months and days"""
-        # import here to avoid dependancy from cubicweb-common to simplejson
-        _ = req._
-        monthnames = [_(mname) for mname in cls.monthnames]
-        daynames = [_(dname) for dname in cls.daynames]
-        req.html_headers.define_var('MONTHNAMES', monthnames)
-        req.html_headers.define_var('DAYNAMES', daynames)
-
-    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
-        kwattrs.setdefault('size', 10)
-        kwattrs.setdefault('maxlength', 10)
-        StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
-
-    def current_values(self, entity):
-        values = entity.attribute_values(self.name)
-        if values and hasattr(values[0], 'strftime'):
-            formatstr = entity.req.property_value(self.format_key)
-            return [values[0].strftime(str(formatstr))]
-        return values
-
-    def render_example(self, req):
-        formatstr = req.property_value(self.format_key)
-        return datetime.now().strftime(str(formatstr))
-
-
-    def _edit_render(self, entity):
-        wdg = super(DateWidget, self)._edit_render(entity)
-        cal_button = self.render_calendar_popup(entity)
-        return wdg+cal_button
-
-    def render_help(self, entity):
-        """calendar popup widget"""
-        req = entity.req
-        help = [ u'<div class="helper">' ]
-        descr = self.rschema.rproperty(self.subjtype, self.objtype, 'description')
-        if descr:
-            help.append('<span>%s</span>' % req._(descr))
-        example = self.render_example(req)
-        if example:
-            help.append('<span>(%s: %s)</span>'
-                        % (req._('sample format'), example))
-        help.append(u'</div>')
-        return u'&nbsp;'.join(help)
-
-    def render_calendar_popup(self, entity):
-        """calendar popup widget"""
-        req = entity.req
-        self.add_localized_infos(req)
-        req.add_js(('cubicweb.ajax.js', 'cubicweb.calendar.js',))
-        req.add_css(('cubicweb.calendar_popup.css',))
-        inputid = self.attrs.get('id', self.rname)
-        helperid = "%shelper" % inputid
-        _today = datetime.now()
-        year = int(req.form.get('year', _today.year))
-        month = int(req.form.get('month', _today.month))
-
-        return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper">
-<img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""
-                % (helperid, inputid, year, month,
-                   req.external_resource('CALENDAR_ICON'), req._('calendar'), helperid) )
-
-class DateTimeWidget(DateWidget):
-    format_key = 'ui.datetime-format'
-
-    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
-        kwattrs['size'] = 16
-        kwattrs['maxlength'] = 16
-        DateWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
-
-    def render_example(self, req):
-        formatstr1 = req.property_value('ui.datetime-format')
-        formatstr2 = req.property_value('ui.date-format')
-        return req._('%(fmt1)s, or without time: %(fmt2)s') % {
-            'fmt1': datetime.now().strftime(str(formatstr1)),
-            'fmt2': datetime.now().strftime(str(formatstr2)),
-            }
-
-
-class TimeWidget(StringWidget):
-    format_key = 'ui.time-format'
-    def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs):
-        kwattrs['size'] = 5
-        kwattrs['maxlength'] = 5
-        StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs)
-
-
-class EmailWidget(StringWidget):
-
-    def render(self, entity):
-        email = getattr(entity, self.name)
-        if not email:
-            return u''
-        return u'<a href="mailto:%s">%s</a>' % (email, email)
-
-class URLWidget(StringWidget):
-
-    def render(self, entity):
-        url = getattr(entity, self.name)
-        if not url:
-            return u''
-        url = xml_escape(url)
-        return u'<a href="%s">%s</a>' % (url, url)
-
-class EmbededURLWidget(StringWidget):
-
-    def render(self, entity):
-        url = getattr(entity, self.name)
-        if not url:
-            return u''
-        aurl = xml_escape(entity.build_url('embed', url=url))
-        return u'<a href="%s">%s</a>' % (aurl, url)
-
-
-
-def widget_factory(vreg, subjschema, rschema, objschema, role='subject',
-                   **kwargs):
-    """return the most adapated widget to edit the relation
-    'subjschema rschema objschema' according to information found in the schema
-    """
-    if role == 'subject':
-        eclass, subjschema = _eclass_eschema(subjschema)
-    else:
-        eclass, objschema = _eclass_eschema(objschema)
-    if eclass is not None and rschema in getattr(eclass, 'widgets', ()):
-        wcls = WIDGETS[eclass.widgets[rschema]]
-    elif not rschema.is_final():
-        card = rschema.rproperty(subjschema, objschema, 'cardinality')
-        if role == 'object':
-            multiple = card[1] in '+*'
-        else: #if role == 'subject':
-            multiple = card[0] in '+*'
-        return DynamicComboBoxWidget(vreg, subjschema, rschema, objschema,
-                                     role=role, multiple=multiple)
-    else:
-        wcls = None
-    factory = FACTORIES.get(objschema, _default_widget_factory)
-    return factory(vreg, subjschema, rschema, objschema, wcls=wcls,
-                   role=role, **kwargs)
-
-
-# factories to find the most adapated widget according to a type and other constraints
-
-def _string_widget_factory(vreg, subjschema, rschema, objschema, wcls=None, **kwargs):
-    w = None
-    for c in rschema.rproperty(subjschema, objschema, 'constraints'):
-        if isinstance(c, StaticVocabularyConstraint):
-            # may have been set by a previous SizeConstraint but doesn't make sense
-            # here (even doesn't have the same meaning on a combobox actually)
-            kwargs.pop('size', None)
-            return (wcls or StaticComboBoxWidget)(vreg, subjschema, rschema, objschema,
-                                                  vocabfunc=c.vocabulary, **kwargs)
-        if isinstance(c, SizeConstraint) and c.max is not None:
-            # don't return here since a StaticVocabularyConstraint may
-            # follow
-            if wcls is None:
-                if c.max < 257:
-                    _wcls = StringWidget
-                else:
-                    _wcls = TextWidget
-            else:
-                _wcls = wcls
-            _wcls.size_constraint_attrs(kwargs, c.max)
-            w = _wcls(vreg, subjschema, rschema, objschema, **kwargs)
-    if w is None:
-        w = (wcls or TextWidget)(vreg, subjschema, rschema, objschema, **kwargs)
-    return w
-
-def _default_widget_factory(vreg, subjschema, rschema, objschema, wcls=None, **kwargs):
-    if wcls is None:
-        wcls = _WFACTORIES[objschema]
-    return wcls(vreg, subjschema, rschema, objschema, **kwargs)
-
-FACTORIES = {
-    'String' :  _string_widget_factory,
-    'Boolean':  _default_widget_factory,
-    'Bytes':    _default_widget_factory,
-    'Date':     _default_widget_factory,
-    'Datetime': _default_widget_factory,
-    'Float':    _default_widget_factory,
-    'Decimal':    _default_widget_factory,
-    'Int':      _default_widget_factory,
-    'Password': _default_widget_factory,
-    'Time':     _default_widget_factory,
-    }
-
-# default widget by entity's type
-_WFACTORIES = {
-    'Boolean':  YesNoRadioWidget,
-    'Bytes':    FileWidget,
-    'Date':     DateWidget,
-    'Datetime': DateTimeWidget,
-    'Int':      IntegerWidget,
-    'Float':    FloatWidget,
-    'Decimal':  DecimalWidget,
-    'Password': PasswordWidget,
-    'String' :  StringWidget,
-    'Time':     TimeWidget,
-    }
-
-# widgets registry
-WIDGETS = {}
-def register(widget_list):
-    for obj in widget_list:
-        if isinstance(obj, type) and issubclass(obj, Widget):
-            if obj is Widget or obj is ComboBoxWidget:
-                continue
-            WIDGETS[obj.__name__] = obj
-
-register(globals().values())
--- a/wsgi/handler.py	Wed Aug 05 09:15:56 2009 +0200
+++ b/wsgi/handler.py	Tue Sep 22 13:08:42 2009 +0200
@@ -97,7 +97,7 @@
 #         assert self.base_url[-1] == '/'
 #         self.https_url = config['https-url']
 #         assert not self.https_url or self.https_url[-1] == '/'
-        self.url_rewriter = self.appli.vreg.select_object('components', 'urlrewriter')
+        self.url_rewriter = self.appli.vreg['components'].select_or_none('urlrewriter')
 
     def _render(self, req):
         """this function performs the actual rendering