merge stable
authorAlexandre Fayolle <alexandre.fayolle@logilab.fr>
Tue, 06 Apr 2010 19:46:38 +0200
branchstable
changeset 5205 8b48add93b7e
parent 5168 1ab032df5ca3 (diff)
parent 5204 d175ce5c2e85 (current diff)
child 5206 b33469113dd1
merge
--- a/.hgtags	Thu Mar 04 17:56:45 2010 +0100
+++ b/.hgtags	Tue Apr 06 19:46:38 2010 +0200
@@ -104,3 +104,16 @@
 d2ba93fcb8da95ceab08f48f8149a480215f149c cubicweb-debian-version-3.6.0-1
 4ae30c9ca11b1edad67d25b76fce672171d02023 cubicweb-version-3.6.1
 b9cdfe3341d1228687515d9af8686971ad5e6f5c cubicweb-debian-version-3.6.1-1
+0a16f07112b90fb61d2e905855fece77e5a7e39c cubicweb-debian-version-3.6.1-2
+bfebe3d14d5390492925fc294dfdafad890a7104 cubicweb-version-3.6.2
+f3b4bb9121a0e7ee5961310ff79e61c890948a77 cubicweb-debian-version-3.6.2-1
+270aba1e6fa21dac6b070e7815e6d1291f9c87cd cubicweb-version-3.7.0
+0c9ff7e496ce344b7e6bf5c9dd2847daf9034e5e cubicweb-debian-version-3.7.0-1
+6b0832bbd1daf27c2ce445af5b5222e1e522fb90 cubicweb-version-3.7.1
+9194740f070e64da5a89f6a9a31050a8401ebf0c cubicweb-debian-version-3.7.1-1
+9c342fa4f1b73e06917d7dc675949baff442108b cubicweb-version-3.6.3
+f9fce56d6a0c2bc6c4b497b66039a8bbbbdc8074 cubicweb-debian-version-3.6.3-1
+d010f749c21d55cd85c5feb442b9cf816282953c cubicweb-version-3.7.2
+8fda29a6c2191ba3cc59242c17b28b34127c75fa cubicweb-debian-version-3.7.2-1
+768beb8e15f15e079f8ee6cfc35125e12b19e140 cubicweb-version-3.7.3
+44c7bf90df71dd562e5a7be5ced3019da603d24f cubicweb-debian-version-3.7.3-1
--- a/MANIFEST.in	Thu Mar 04 17:56:45 2010 +0100
+++ b/MANIFEST.in	Tue Apr 06 19:46:38 2010 +0200
@@ -15,7 +15,7 @@
 recursive-include etwist *.xml *.html
 
 recursive-include i18n *.pot *.po
-recursive-include schemas *.py *.sql.*
+recursive-include schemas *.py *.sql
 
 recursive-include entities/test/data *
 recursive-include sobjects/test/data *
--- a/__init__.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/__init__.py	Tue Apr 06 19:46:38 2010 +0200
@@ -112,7 +112,7 @@
 
 CW_EVENT_MANAGER = CubicWebEventManager()
 
-def onevent(event):
+def onevent(event, *args, **kwargs):
     """decorator to ease event / callback binding
 
     >>> from cubicweb import onevent
@@ -123,6 +123,6 @@
     >>>
     """
     def _decorator(func):
-        CW_EVENT_MANAGER.bind(event, func)
+        CW_EVENT_MANAGER.bind(event, func, *args, **kwargs)
         return func
     return _decorator
--- a/__pkginfo__.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/__pkginfo__.py	Tue Apr 06 19:46:38 2010 +0200
@@ -7,7 +7,7 @@
 distname = "cubicweb"
 modname = "cubicweb"
 
-numversion = (3, 6, 1)
+numversion = (3, 7, 3)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL'
@@ -30,7 +30,7 @@
 
 web = 'http://www.cubicweb.org'
 ftp = 'ftp://ftp.logilab.org/pub/cubicweb'
-pyversions = ['2.4', '2.5']
+pyversions = ['2.5', '2.6']
 
 classifiers = [
            'Environment :: Web Environment',
@@ -89,6 +89,8 @@
          [join(data_dir, fname) for fname in listdir(data_dir) if not isdir(join(data_dir, fname))]],
         [join('share', 'cubicweb', 'cubes', 'shared', 'data', 'timeline'),
          [join(data_dir, 'timeline', fname) for fname in listdir(join(data_dir, 'timeline'))]],
+        [join('share', 'cubicweb', 'cubes', 'shared', 'data', 'images'),
+         [join(data_dir, 'images', fname) for fname in listdir(join(data_dir, 'images'))]],
         [join('share', 'cubicweb', 'cubes', 'shared', 'wdoc'),
          [join(wdoc_dir, fname) for fname in listdir(wdoc_dir) if not isdir(join(wdoc_dir, fname))]],
         [join('share', 'cubicweb', 'cubes', 'shared', 'wdoc', 'images'),
--- a/_exceptions.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/_exceptions.py	Tue Apr 06 19:46:38 2010 +0200
@@ -45,17 +45,19 @@
 
 class ConnectionError(RepositoryError):
     """raised when a bad connection id is given or when an attempt to establish
-    a connection failed"""
+    a connection failed
+    """
 
 class AuthenticationError(ConnectionError):
-    """raised when a bad connection id is given or when an attempt to establish
-    a connection failed"""
+    """raised when when an attempt to establish a connection failed do to wrong
+    connection information (login / password or other authentication token)
+    """
+    def __init__(self, *args, **kwargs):
+        super(AuthenticationError, self).__init__(*args)
+        self.__dict__.update(kwargs)
 
 class BadConnectionId(ConnectionError):
-    """raised when a bad connection id is given or when an attempt to establish
-    a connection failed"""
-
-BadSessionId = BadConnectionId # XXX bw compat for pyro connections
+    """raised when a bad connection id is given"""
 
 class UnknownEid(RepositoryError):
     """the eid is not defined in the system tables"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/_gcdebug.py	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,87 @@
+
+import gc, types, weakref
+
+from cubicweb.schema import CubicWebRelationSchema, CubicWebEntitySchema
+
+listiterator = type(iter([]))
+
+IGNORE_CLASSES = (
+    type, tuple, dict, list, set, frozenset, type(len),
+    weakref.ref, weakref.WeakKeyDictionary,
+    listiterator,
+    property, classmethod,
+    types.ModuleType, types.FunctionType, types.MethodType,
+    types.MemberDescriptorType, types.GetSetDescriptorType,
+    )
+
+def _get_counted_class(obj, classes):
+    for cls in classes:
+        if isinstance(obj, cls):
+            return cls
+    raise AssertionError()
+
+def gc_info(countclasses,
+            ignoreclasses=IGNORE_CLASSES,
+            viewreferrersclasses=(), showobjs=False, maxlevel=1):
+    gc.collect()
+    gc.collect()
+    counters = {}
+    ocounters = {}
+    for obj in gc.get_objects():
+        if isinstance(obj, countclasses):
+            cls = _get_counted_class(obj, countclasses)
+            try:
+                counters[cls.__name__] += 1
+            except KeyError:
+                counters[cls.__name__] = 1
+        elif not isinstance(obj, ignoreclasses):
+            try:
+                key = '%s.%s' % (obj.__class__.__module__,
+                                 obj.__class__.__name__)
+            except AttributeError:
+                key = str(obj)
+            try:
+                ocounters[key] += 1
+            except KeyError:
+                ocounters[key] = 1
+        if isinstance(obj, viewreferrersclasses):
+            print '   ', obj, referrers(obj, showobjs, maxlevel)
+    return counters, ocounters, gc.garbage
+
+
+def referrers(obj, showobj=False, maxlevel=1):
+    objreferrers = _referrers(obj, maxlevel)
+    try:
+        return sorted(set((type(x), showobj and x or getattr(x, '__name__', '%#x' % id(x)))
+                          for x in objreferrers))
+    except TypeError:
+        s = set()
+        unhashable = []
+        for x in objreferrers:
+            try:
+                s.add(x)
+            except TypeError:
+                unhashable.append(x)
+        return sorted(s) + unhashable
+
+def _referrers(obj, maxlevel, _seen=None, _level=0):
+    interesting = []
+    if _seen is None:
+        _seen = set()
+    for x in gc.get_referrers(obj):
+        if id(x) in _seen:
+            continue
+        _seen.add(id(x))
+        if isinstance(x, types.FrameType):
+            continue
+        if isinstance(x, (CubicWebRelationSchema, CubicWebEntitySchema)):
+            continue
+        if isinstance(x, (list, tuple, set, dict, listiterator)):
+            if _level >= maxlevel:
+                pass
+                #interesting.append(x)
+            else:
+                interesting += _referrers(x, maxlevel, _seen, _level+1)
+        else:
+            interesting.append(x)
+    return interesting
--- a/appobject.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/appobject.py	Tue Apr 06 19:46:38 2010 +0200
@@ -1,11 +1,18 @@
-"""Base class for dynamically loaded objects accessible through the vregistry.
-
-You'll also find some convenience classes to build selectors.
+# :organization: Logilab
+# :copyright: 2001-2010 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
+"""
+The `AppObject` class
+---------------------
 
-:organization: Logilab
-:copyright: 2001-2010 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
+The AppObject class is the base class for all dynamically loaded objects
+(application objects) accessible through the vregistry.
+
+We can find a certain number of attributes and methods defined in this class and
+common to all the application objects.
+
+.. autoclass:: AppObject
 """
 __docformat__ = "restructuredtext en"
 
@@ -14,19 +21,24 @@
 from warnings import warn
 
 from logilab.common.deprecation import deprecated
+from logilab.common.decorators import classproperty
 from logilab.common.logging_ext import set_log_methods
 
 
 # selector base classes and operations ########################################
 
 def objectify_selector(selector_func):
-    """convenience decorator for simple selectors where a class definition
-    would be overkill::
+    """Most of the time, a simple score function is enough to build a selector.
+    The :func:`objectify_selector` decorator turn it into a proper selector
+    class::
 
         @objectify_selector
         def one(cls, *args, **kwargs):
             return 1
 
+        class MyView(View):
+            __select__ = View.__select__ & one()
+
     """
     return type(selector_func.__name__, (Selector,),
                 {'__doc__': selector_func.__doc__,
@@ -48,7 +60,7 @@
 
 class Selector(object):
     """base class for selector classes providing implementation
-    for operators ``&`` and ``|``
+    for operators ``&``, ``|`` and  ``~``
 
     This class is only here to give access to binary operators, the
     selector logic itself should be implemented in the __call__ method
@@ -204,47 +216,88 @@
     selected according to a context (usually at least a request and a result
     set).
 
-    Concrete application objects classes are designed to be loaded by the
-    vregistry and should be accessed through it, not by direct instantiation.
+    The following attributes should be set on concret appobject classes:
 
-    The following attributes should be set on concret appobject classes:
-    :__registry__:
+    :attr:`__registry__`
       name of the registry for this object (string like 'views',
       'templates'...)
-    :__regid__:
+
+    :attr:`__regid__`
       object's identifier in the registry (string like 'main',
       'primary', 'folder_box')
-    :__select__:
+
+    :attr:`__select__`
       class'selector
 
-    Moreover, the `__abstract__` attribute may be set to True to indicate
-    that a appobject is abstract and should not be registered.
+    Moreover, the `__abstract__` attribute may be set to True to indicate that a
+    class is abstract and should not be registered.
 
     At selection time, the following attributes are set on the instance:
 
-    :_cw:
+    :attr:`_cw`
       current request
-    :cw_extra_kwargs:
+    :attr:`cw_extra_kwargs`
       other received arguments
 
-    only if rset is found in arguments (in which case rset/row/col will be
-    removed from cwextra_kwargs):
+    And also the following, only if `rset` is found in arguments (in which case
+    rset/row/col will be removed from `cwextra_kwargs`):
 
-    :cw_rset:
+    :attr:`cw_rset`
       context result set or None
-    :cw_row:
+
+    :attr:`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
-    :cw_col:
+
+    :attr:`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
+
+
+    .. Note::
+
+      * do not inherit directly from this class but from a more specific class
+        such as `AnyEntity`, `EntityView`, `AnyRsetView`, `Action`...
+
+      * to be recordable, a subclass has to define its registry (attribute
+        `__registry__`) and its identifier (attribute `__regid__`). Usually
+        you don't have to take care of the registry since it's set by the base
+        class, only the identifier `id`
+
+      * application objects are designed to be loaded by the vregistry and
+        should be accessed through it, not by direct instantiation, besides
+        to use it as base classe.
+
+
+      * 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 must write::
+
+          class Truc(PrimaryView):
+              def f(self, arg1):
+                  super(Truc, self).f(arg1)
+
     """
     __registry__ = None
     __regid__ = None
     __select__ = yes()
 
+    @classproperty
+    def __registries__(cls):
+        if cls.__registry__ is None:
+            return ()
+        return (cls.__registry__,)
+
     @classmethod
     def __registered__(cls, registry):
         """called by the registry when the appobject has been registered.
@@ -258,7 +311,7 @@
         except AttributeError:
             pdefs = getattr(cls, 'cw_property_defs', {})
         else:
-            warn('property_defs is deprecated, use cw_property_defs in %s'
+            warn('[3.6] property_defs is deprecated, use cw_property_defs in %s'
                  % cls, DeprecationWarning)
         for propid, pdef in pdefs.items():
             pdef = pdef.copy() # may be shared
--- a/cwconfig.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/cwconfig.py	Tue Apr 06 19:46:38 2010 +0200
@@ -1,46 +1,42 @@
 # -*- coding: utf-8 -*-
-"""common configuration utilities for cubicweb
+#:organization: Logilab
+#:copyright: 2001-2010 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
+
+# docstring included in doc/book/en/admin/setup.rst
+"""
+.. _ResourceMode:
+
+Resource mode
+-------------
 
-:organization: Logilab
-:copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+A resource *mode* is a predifined set of settings for various resources
+directories, such as cubes, instances, etc. to ease development with the
+framework. There are two running modes with |cubicweb|:
+
+* 'user', resources are searched / created in the user home directory:
+
+  - instances are stored in :file:`~/etc/cubicweb.d`
+  - temporary files (such as pid file) in :file:`/tmp`
+
+* 'system', resources are searched / created in the system directories (eg
+  usually requiring root access):
+
+  - instances are stored in :file:`<INSTALL_PREFIX>/etc/cubicweb.d`
+  - temporary files (such as pid file) in :file:`/var/run/cubicweb`
+
+  where `<INSTALL_PREFIX>` is the detected installation prefix ('/usr/local' for
+  instance).
 
 
-If cubicweb is a mercurial checkout (eg `CWDEV` is true), located in
-`CW_SOFTWARE_ROOT`:
-
- * main cubes directory is `<CW_SOFTWARE_ROOT>/../cubes`. You can specify
-   another one with `CW_INSTANCES_DIR` environment variable or simply add some
-   other directories by using `CW_CUBES_PATH`.
-
- * cubicweb migration files are by default searched in
-   `<CW_SOFTWARE_ROOT>/misc/migration` instead of
-   `/usr/share/cubicweb/migration/`(unless another emplacement is specified
-   using `CW_MIGRATION_DIR`.
-
- * Cubicweb will start in 'user' mode (see below)
-
-
-On startup, Cubicweb is using a specific *mode*. A mode corresponds to some
-default setting for various resource directories. There are currently 2 main
-modes : 'system', for system wide installation, and 'user', fur user local
-installation (e.g. no root privileges).
-
-'user' mode is activated automatically when cubicweb is a mercurial checkout
-(e.g.  has a .hg directory). You can also force mode by using the `CW_MODE`
-environment variable, to:
-
-* use system wide installation but user specific instances and all, without root
-  privileges on the system (`export CW_MODE=user`)
-
-* use local checkout of cubicweb on system wide instances (requires root
-  privileges on the system (`export CW_MODE=system`)
-
- Here is the default resource directories settings according to mode:
+Notice that each resource path may be explicitly set using an environment
+variable if the default doesn't suit your needs. Here are the default resource
+directories that are affected according to mode:
 
 * 'system': ::
 
-        CW_INSTANCES_DIR = /etc/cubicweb.d/
+        CW_INSTANCES_DIR = <INSTALL_PREFIX>/etc/cubicweb.d/
         CW_INSTANCES_DATA_DIR = /var/lib/cubicweb/instances/
         CW_RUNTIME_DIR = /var/run/cubicweb/
 
@@ -50,27 +46,79 @@
         CW_INSTANCES_DATA_DIR = ~/etc/cubicweb.d/
         CW_RUNTIME_DIR = /tmp
 
+Cubes search path is also affected, see the :ref:Cube section.
+
+By default, the mode automatically set to 'user' if a :file:`.hg` directory is found
+in the cubicweb package, else it's set to 'system'. You can force this by setting
+the :envvar:`CW_MODE` environment variable to either 'user' or 'system' so you can
+easily:
+
+* use system wide installation but user specific instances and all, without root
+  privileges on the system (`export CW_MODE=user`)
+
+* use local checkout of cubicweb on system wide instances (requires root
+  privileges on the system (`export CW_MODE=system`)
+
+If you've a doubt about the mode you're currently running, check the first line
+outputed by the :command:`cubicweb-ctl list` command.
+
+Also, if cubicweb is a mercurial checkout located in `<CW_SOFTWARE_ROOT>`:
+
+* main cubes directory is `<CW_SOFTWARE_ROOT>/../cubes`. You can specify
+  another one with :envvar:`CW_INSTANCES_DIR` environment variable or simply
+  add some other directories by using :envvar:`CW_CUBES_PATH`
+
+* cubicweb migration files are searched in `<CW_SOFTWARE_ROOT>/misc/migration`
+  instead of `<INSTALL_PREFIX>/share/cubicweb/migration/`.
+
+
+.. _ConfigurationEnv:
+
+Environment configuration
+-------------------------
+
+Python
+``````
+
+If you installed |cubicweb| by cloning the Mercurial forest or from source
+distribution, then you will need to update the environment variable PYTHONPATH by
+adding the path to the forest `cubicweb`:
+
+Add the following lines to either :file:`.bashrc` or :file:`.bash_profile` to
+configure your development environment ::
+
+    export PYTHONPATH=/full/path/to/cubicweb-forest
+
+If you installed |cubicweb| with packages, no configuration is required and your
+new cubes will be placed in `/usr/share/cubicweb/cubes` and your instances will
+be placed in `/etc/cubicweb.d`.
+
+
+CubicWeb
+````````
+
+Here are all environment variables that may be used to configure |cubicweb|:
 
 .. envvar:: CW_MODE
-   Resource mode: user or system
+
+   Resource mode: user or system, as explained in :ref:ResourceMode.
 
 .. envvar:: CW_CUBES_PATH
-   Augments the default search path for cubes
+
+   Augments the default search path for cubes. You may specify several
+   directories using ':' as separator (';' under windows environment).
 
 .. envvar:: CW_INSTANCES_DIR
-   Directory where cubicweb instances will be found
+
+   Directory where cubicweb instances will be found.
 
 .. envvar:: CW_INSTANCES_DATA_DIR
-   Directory where cubicweb instances data will be written
+
+   Directory where cubicweb instances data will be written (backup file...)
 
 .. envvar:: CW_RUNTIME_DIR
+
    Directory where pid files will be written
-
-.. envvar:: CW_MIGRATION_DIR
-   Directory where cubicweb migration files will be found
-
-
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 __docformat__ = "restructuredtext en"
 _ = unicode
@@ -90,7 +138,8 @@
 from logilab.common.configuration import (Configuration, Method,
                                           ConfigurationMixIn, merge_options)
 
-from cubicweb import CW_SOFTWARE_ROOT, CW_MIGRATION_MAP, ConfigurationError
+from cubicweb import (CW_SOFTWARE_ROOT, CW_MIGRATION_MAP,
+                      ConfigurationError, Binary)
 from cubicweb.toolsutils import env_path, create_dir
 
 CONFIGURATIONS = []
@@ -115,7 +164,7 @@
 
 def possible_configurations(directory):
     """return a list of installed configurations in a directory
-    according to *-ctl files
+    according to \*-ctl files
     """
     return [name for name in ('repository', 'twisted', 'all-in-one')
             if exists(join(directory, '%s.conf' % name))]
@@ -217,7 +266,9 @@
 
     if os.environ.get('APYCOT_ROOT'):
         mode = 'test'
-        if CWDEV:
+        # allow to test cubes within apycot using cubicweb not installed by
+        # apycot
+        if __file__.startswith(os.environ['APYCOT_ROOT']):
             CUBES_DIR = '%(APYCOT_ROOT)s/local/share/cubicweb/cubes/' % os.environ
             # create __init__ file
             file(join(CUBES_DIR, '__init__.py'), 'w').close()
@@ -327,7 +378,7 @@
     def available_cubes(cls):
         cubes = set()
         for directory in cls.cubes_search_path():
-            if not os.path.exists(directory):
+            if not exists(directory):
                 cls.error('unexistant directory in cubes search path: %s'
                            % directory)
                 continue
@@ -638,16 +689,18 @@
     """base class for cubicweb server and web configurations"""
 
     INSTANCES_DATA_DIR = None
-    if CubicWebNoAppConfiguration.mode == 'test':
+    if os.environ.get('APYCOT_ROOT'):
         root = os.environ['APYCOT_ROOT']
         REGISTRY_DIR = '%s/etc/cubicweb.d/' % root
+        if not exists(REGISTRY_DIR):
+            os.makedirs(REGISTRY_DIR)
         RUNTIME_DIR = tempfile.gettempdir()
-        if CWDEV:
+        # allow to test cubes within apycot using cubicweb not installed by
+        # apycot
+        if __file__.startswith(os.environ['APYCOT_ROOT']):
             MIGRATION_DIR = '%s/local/share/cubicweb/migration/' % root
         else:
             MIGRATION_DIR = '/usr/share/cubicweb/migration/'
-        if not exists(REGISTRY_DIR):
-            os.makedirs(REGISTRY_DIR)
     else:
         if CubicWebNoAppConfiguration.mode == 'user':
             REGISTRY_DIR = expanduser('~/etc/cubicweb.d/')
@@ -664,8 +717,6 @@
 
     # for some commands (creation...) we don't want to initialize gettext
     set_language = True
-    # set this to true to avoid false error message while creating an instance
-    creating = False
     # set this to true to allow somethings which would'nt be possible
     repairing = False
 
@@ -914,7 +965,7 @@
         """return available translation for an instance, by looking for
         compiled catalog
 
-        take *args to be usable as a vocabulary method
+        take \*args to be usable as a vocabulary method
         """
         from glob import glob
         yield 'en' # ensure 'en' is yielded even if no .mo found
@@ -998,7 +1049,7 @@
 
 _EXT_REGISTERED = False
 def register_stored_procedures():
-    from logilab.common.adbh import FunctionDescr
+    from logilab.database import FunctionDescr
     from rql.utils import register_function, iter_funcnode_variables
 
     global _EXT_REGISTERED
@@ -1010,8 +1061,7 @@
         supported_backends = ('postgres', 'sqlite',)
         rtype = 'String'
 
-        @classmethod
-        def st_description(cls, funcnode, mainindex, tr):
+        def st_description(self, funcnode, mainindex, tr):
             return ', '.join(sorted(term.get_description(mainindex, tr)
                                     for term in iter_funcnode_variables(funcnode)))
 
@@ -1023,6 +1073,7 @@
 
     register_function(CONCAT_STRINGS) # XXX bw compat
 
+
     class GROUP_CONCAT(CONCAT_STRINGS):
         supported_backends = ('mysql', 'postgres', 'sqlite',)
 
@@ -1033,8 +1084,7 @@
         supported_backends = ('postgres', 'sqlite',)
         rtype = 'String'
 
-        @classmethod
-        def st_description(cls, funcnode, mainindex, tr):
+        def st_description(self, funcnode, mainindex, tr):
             return funcnode.children[0].get_description(mainindex, tr)
 
     register_function(LIMIT_SIZE)
@@ -1046,9 +1096,25 @@
     register_function(TEXT_LIMIT_SIZE)
 
 
+    class FSPATH(FunctionDescr):
+        """return path of some bytes attribute stored using the Bytes
+        File-System Storage (bfss)
+        """
+        rtype = 'Bytes' # XXX return a String? potential pb with fs encoding
 
-    class FSPATH(FunctionDescr):
-        supported_backends = ('postgres', 'sqlite',)
-        rtype = 'Bytes'
+        def update_cb_stack(self, stack):
+            assert len(stack) == 1
+            stack[0] = self.source_execute
+
+        def as_sql(self, backend, args):
+            raise NotImplementedError('source only callback')
+
+        def source_execute(self, source, value):
+            fpath = source.binary_to_str(value)
+            try:
+                return Binary(fpath)
+            except OSError, ex:
+                self.critical("can't open %s: %s", fpath, ex)
+                return None
 
     register_function(FSPATH)
--- a/cwctl.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/cwctl.py	Tue Apr 06 19:46:38 2010 +0200
@@ -383,7 +383,6 @@
         cubes = splitstrip(pop_arg(args, 1))
         appid = pop_arg(args)
         # get the configuration and helper
-        cwcfg.creating = True
         config = cwcfg.config_for(appid, configname)
         config.set_language = False
         cubes = config.expand_cubes(cubes)
@@ -797,7 +796,7 @@
         # handle i18n upgrade:
         # * install new languages
         # * recompile catalogs
-        # in the first componant given
+        # XXX search available language in the first cube given
         from cubicweb import i18n
         templdir = cwcfg.cube_dir(config.cubes()[0])
         langs = [lang for lang, _ in i18n.available_catalogs(join(templdir, 'i18n'))]
@@ -812,7 +811,12 @@
         print
         print '-> instance migrated.'
         if not (CWDEV or self.config.nostartstop):
-            StartInstanceCommand().start_instance(appid)
+            # restart instance through fork to get a proper environment, avoid
+            # uicfg pb (and probably gettext catalogs, to check...)
+            forkcmd = '%s start %s' % (sys.argv[0], appid)
+            status = system(forkcmd)
+            if status:
+                print '%s exited with status %s' % (forkcmd, status)
         print
 
 
@@ -932,20 +936,11 @@
     def i18ninstance_instance(appid):
         """recompile instance's messages catalogs"""
         config = cwcfg.config_for(appid)
-        try:
-            config.bootstrap_cubes()
-        except IOError, ex:
-            import errno
-            if ex.errno != errno.ENOENT:
-                raise
-            # bootstrap_cubes files doesn't exist
-            # notify this is not a regular start
-            config.repairing = True
-            # create an in-memory repository, will call config.init_cubes()
-            config.repository()
-        except AttributeError:
+        config.quick_start = True # notify this is not a regular start
+        repo = config.repository()
+        if config._cubes is None:
             # web only config
-            config.init_cubes(config.repository().get_cubes())
+            config.init_cubes(repo.get_cubes())
         errors = config.i18ncompile()
         if errors:
             print '\n'.join(errors)
--- a/cwvreg.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/cwvreg.py	Tue Apr 06 19:46:38 2010 +0200
@@ -1,9 +1,178 @@
-"""extend the generic VRegistry with some cubicweb specific stuff
+# :organization: Logilab
+# :copyright: 2001-2010 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
+""".. VRegistry:
+
+The `VRegistry`
+---------------
+
+The `VRegistry` can be seen as a two level dictionary. It contains all objects
+loaded dynamically to build a |cubicweb| application. Basically:
+
+* first level key return a *registry*. This key corresponds to the `__registry__`
+  attribute of application object classes
+
+* second level key return a list of application objects which share the same
+  identifier. This key corresponds to the `__regid__` attribute of application
+  object classes.
+
+A *registry* hold a specific kind of application objects. You've for instance
+a registry for entity classes, another for views, etc...
+
+The `VRegistry` has two main responsibilities:
+
+- being the access point to all registries
+
+- handling the registration process at startup time, and during automatic
+  reloading in debug mode.
+
+
+.. _AppObjectRecording:
+
+Details of the recording process
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. index::
+   vregistry: registration_callback
+
+On startup, |cubicweb| have to load application objects defined in its library
+and in cubes used by the instance. Application objects from the library are
+loaded first, then those provided by cubes are loaded in an ordered way (e.g. if
+your cube depends on an other, objects from the dependancy will be loaded
+first). Cube's modules or packages where appobject are looked at is explained in
+:ref:`cubelayout`.
+
+For each module:
+
+* by default all objects are registered automatically
+
+* if some objects have to replace other objects, or be included only if some
+  condition is true, you'll have to define a `registration_callback(vreg)`
+  function in your module and explicitly register **all objects** in this module,
+  using the api defined below.
+
+.. Note::
+    Once the function `registration_callback(vreg)` is implemented in a module,
+    all the objects from this module have to be explicitly registered as it
+    disables the automatic objects registration.
+
+
+API for objects registration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here are the registration methods that you can use in the `registration_callback`
+to register your objects to the `VRegistry` instance given as argument (usually
+named `vreg`):
+
+.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_all
+.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_and_replace
+.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register
+.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_if_interface_found
+.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.unregister
+
+
+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)
 
-:organization: Logilab
-:copyright: 2001-2010 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
+In this example, we register all application object classes defined in the module
+except `SeeAlsoVComponent`. This class is then registered only if the 'see_also'
+relation type is defined in the instance'schema.
+
+.. sourcecode:: python
+
+   # goa/appobjects/sessions.py
+   def registration_callback(vreg):
+      vreg.register(SessionsCleaner)
+      # replace AuthenticationManager by GAEAuthenticationManager
+      vreg.register_and_replace(GAEAuthenticationManager, AuthenticationManager)
+      # replace PersistentSessionManager by GAEPersistentSessionManager
+      vreg.register_and_replace(GAEPersistentSessionManager, PersistentSessionManager)
+
+In this example, we explicitly register classes one by one:
+
+* the `SessionCleaner` class
+* the `GAEAuthenticationManager` to replace the `AuthenticationManager`
+* the `GAEPersistentSessionManager` to replace the `PersistentSessionManager`
+
+If at some point we register a new appobject class in this module, it won't be
+registered at all without modification to the `registration_callback`
+implementation. The previous example will register it though, thanks to the call
+to the `register_all` method.
+
+
+.. _Selection:
+
+Runtime objects selection
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now that we've all application objects loaded, the question is : when I want some
+specific object, for instance the primary view for a given entity, how do I get
+the proper object ? This is what we call the **selection mechanism**.
+
+As explained in the :ref:`Concepts` section:
+
+* each application object has a **selector**, defined by its `__select__` class attribute
+
+* this selector is responsible to return a **score** for a given context
+
+  - 0 score means the object doesn't apply to this context
+
+  - else, the higher the score, the better the object suits the context
+
+* the object with the higher score is selected.
+
+.. Note::
+
+  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 silenced in production mode and one of the objects with
+  the higher score is picked.
+
+  In such cases you would need to review your design and make sure your selectors
+  or appobjects are properly defined.
+
+For instance, if you are selecting the primary (eg `__regid__ = 'primary'`) view (eg
+`__registry__ = 'views'`) 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
+
+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.
+
+.. _SelectionAPI:
+
+API for objects selections
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here is the selection API you'll get on every registry. Some of them, as the
+'etypes' registry, containing entity classes, extend it. In those methods,
+`*args, **kwargs` is what we call the **context**. Those arguments are given to
+selectors that will inspect there content and return a score accordingly.
+
+.. automethod:: cubicweb.vregistry.Registry.select
+
+.. automethod:: cubicweb.vregistry.Registry.select_or_none
+
+.. automethod:: cubicweb.vregistry.Registry.possible_objects
+
+.. automethod:: cubicweb.vregistry.Registry.object_by_id
 """
 __docformat__ = "restructuredtext en"
 _ = unicode
@@ -21,13 +190,10 @@
 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 searching for implements
     selectors, with a bw compat fallback to accepts_interfaces attribute
@@ -265,6 +431,11 @@
         self.schema = None
         self.initialized = False
         self.reset()
+        # XXX give force_reload (or refactor [re]loading...)
+        if self.config.mode != 'test':
+            # don't clear rtags during test, this may cause breakage with
+            # manually imported appobject modules
+            CW_EVENT_MANAGER.bind('before-registry-reload', clear_rtag_objects)
 
     def setdefault(self, regid):
         try:
@@ -312,9 +483,7 @@
         """set instance'schema and load application objects"""
         self._set_schema(schema)
         # now we can load application's web objects
-        searchpath = self.config.vregistry_path()
-        self.reset(searchpath, force_reload=False)
-        self.register_objects(searchpath, force_reload=False)
+        self._reload(self.config.vregistry_path(), force_reload=False)
         # map lowered entity type names to their actual name
         self.case_insensitive_etypes = {}
         for eschema in self.schema.entities():
@@ -323,6 +492,14 @@
             clear_cache(eschema, 'ordered_relations')
             clear_cache(eschema, 'meta_attributes')
 
+    def _reload(self, path, force_reload):
+        CW_EVENT_MANAGER.emit('before-registry-reload')
+        # modification detected, reset and reload
+        self.reset(path, force_reload)
+        super(CubicWebVRegistry, self).register_objects(
+            path, force_reload, self.config.extrapath)
+        CW_EVENT_MANAGER.emit('after-registry-reload')
+
     def _set_schema(self, schema):
         """set instance'schema"""
         self.schema = schema
@@ -339,8 +516,11 @@
                     obj.schema = schema
 
     def register_if_interface_found(self, obj, ifaces, **kwargs):
-        """register an object but remove it if no entity class implements one of
-        the given interfaces at the end of the registration process
+        """register `obj` but remove it if no entity class implements one of
+        the given `ifaces` interfaces at the end of the registration process.
+
+        Extra keyword arguments are given to the
+        :meth:`~cubicweb.cwvreg.CubicWebVRegistry.register` function.
         """
         self.register(obj, **kwargs)
         if not isinstance(ifaces,  (tuple, list)):
@@ -349,6 +529,13 @@
             self._needs_iface[obj] = ifaces
 
     def register(self, obj, *args, **kwargs):
+        """register `obj` application object into `registryname` or
+        `obj.__registry__` if not specified, with identifier `oid` or
+        `obj.__regid__` if not specified.
+
+        If `clear` is true, all objects with the same identifier will be
+        previously unregistered.
+        """
         super(CubicWebVRegistry, self).register(obj, *args, **kwargs)
         # XXX bw compat
         ifaces = use_interfaces(obj)
@@ -363,12 +550,7 @@
             super(CubicWebVRegistry, self).register_objects(
                 path, force_reload, self.config.extrapath)
         except RegistryOutOfDate:
-            CW_EVENT_MANAGER.emit('before-registry-reload')
-            # modification detected, reset and reload
-            self.reset(path, force_reload)
-            super(CubicWebVRegistry, self).register_objects(
-                path, force_reload, self.config.extrapath)
-            CW_EVENT_MANAGER.emit('after-registry-reload')
+            self._reload(path, force_reload)
 
     def initialization_completed(self):
         """cw specific code once vreg initialization is completed:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dataimport.py	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,695 @@
+# -*- coding: utf-8 -*-
+"""This module provides tools to import tabular data.
+
+:organization: Logilab
+:copyright: 2001-2010 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(cnx))
+  ctl.askerror = 1
+  ctl.generators = GENERATORS
+  ctl.data['utilisateurs'] = lazytable(utf8csvreader(open('users.csv')))
+  # run
+  ctl.run()
+
+.. BUG file with one column are not parsable
+.. TODO rollback() invocation is not possible yet
+"""
+__docformat__ = "restructuredtext en"
+
+import sys
+import csv
+import traceback
+import os.path as osp
+from StringIO import StringIO
+from copy import copy
+
+from logilab.common import shellutils
+from logilab.common.date import strptime
+from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated
+
+from cubicweb.server.utils import eschema_eid
+
+def ucsvreader_pb(filepath, encoding='utf-8', separator=',', quote='"',
+                  skipfirst=False, withpb=True):
+    """same as ucsvreader but a progress bar is displayed as we iter on rows"""
+    if not osp.exists(filepath):
+        raise Exception("file doesn't exists: %s" % filepath)
+    rowcount = int(shellutils.Execute('wc -l "%s"' % filepath).out.strip().split()[0])
+    if skipfirst:
+        rowcount -= 1
+    if withpb:
+        pb = shellutils.ProgressBar(rowcount, 50)
+    for urow in ucsvreader(file(filepath), encoding, separator, quote, skipfirst):
+        yield urow
+        if withpb:
+            pb.update()
+    print ' %s rows imported' % rowcount
+
+def ucsvreader(stream, encoding='utf-8', separator=',', quote='"',
+               skipfirst=False):
+    """A csv reader that accepts files with any encoding and outputs unicode
+    strings
+    """
+    it = iter(csv.reader(stream, delimiter=separator, quotechar=quote))
+    if skipfirst:
+        it.next()
+    for row in it:
+        yield [item.decode(encoding) for item in row]
+
+def commit_every(nbit, store, it):
+    for i, x in enumerate(it):
+        yield x
+        if nbit is not None and i % nbit:
+            store.commit()
+    if nbit is not None:
+        store.commit()
+
+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 mk_entity(row, map):
+    """Return a dict made from sanitized mapped values.
+
+    ValueError can be raised on unexpected values found in checkers
+
+    >>> row = {'myname': u'dupont'}
+    >>> map = [('myname', u'name', (call_transform_method('title'),))]
+    >>> mk_entity(row, map)
+    {'name': u'Dupont'}
+    >>> row = {'myname': u'dupont', 'optname': u''}
+    >>> map = [('myname', u'name', (call_transform_method('title'),)),
+    ...        ('optname', u'MARKER', (optional,))]
+    >>> mk_entity(row, map)
+    {'name': u'Dupont', 'optname': None}
+    """
+    res = {}
+    assert isinstance(row, dict)
+    assert isinstance(map, list)
+    for src, dest, funcs in map:
+        res[dest] = row[src]
+        try:
+            for func in funcs:
+                res[dest] = func(res[dest])
+                if res[dest] is None:
+                    break
+        except ValueError, err:
+            raise ValueError('error with %r field: %s' % (src, err))
+    return res
+
+
+# user interactions ############################################################
+
+def tell(msg):
+    print 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'
+
+
+class catch_error(object):
+    """Helper for @contextmanager decorator."""
+
+    def __init__(self, ctl, key='unexpected error', msg=None):
+        self.ctl = ctl
+        self.key = key
+        self.msg = msg
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        if type is not None:
+            if issubclass(type, (KeyboardInterrupt, SystemExit)):
+                return # re-raise
+            if self.ctl.catcherrors:
+                self.ctl.record_error(self.key, None, type, value, traceback)
+                return True # silent
+
+
+# base sanitizing/coercing functions ###########################################
+
+def optional(value):
+    """checker to filter optional field
+
+    If value is undefined (ex: empty string), return None that will
+    break the checkers validation chain
+
+    General use is to add 'optional' check in first condition to avoid
+    ValueError by further checkers
+
+    >>> MAPPER = [(u'value', 'value', (optional, int))]
+    >>> row = {'value': u'XXX'}
+    >>> mk_entity(row, MAPPER)
+    {'value': None}
+    >>> row = {'value': u'100'}
+    >>> mk_entity(row, MAPPER)
+    {'value': 100}
+    """
+    if value:
+        return value
+    return None
+
+def required(value):
+    """raise ValueError is value is empty
+
+    This check should be often found in last position in the chain.
+    """
+    if value:
+        return value
+    raise ValueError("required")
+
+def todatetime(format='%d/%m/%Y'):
+    """return a transformation function to turn string input value into a
+    `datetime.datetime` instance, using given format.
+
+    Follow it by `todate` or `totime` functions from `logilab.common.date` if
+    you want a `date`/`time` instance instead of `datetime`.
+    """
+    def coerce(value):
+        return strptime(value, format)
+    return coerce
+
+def call_transform_method(methodname, *args, **kwargs):
+    """return value returned by calling the given method on input"""
+    def coerce(value):
+        return getattr(value, methodname)(*args, **kwargs)
+    return coerce
+
+def call_check_method(methodname, *args, **kwargs):
+    """check value returned by calling the given method on input is true,
+    else raise ValueError
+    """
+    def check(value):
+        if getattr(value, methodname)(*args, **kwargs):
+            return value
+        raise ValueError('%s not verified on %r' % (methodname, value))
+    return check
+
+# base integrity checking functions ############################################
+
+def check_doubles(buckets):
+    """Extract the keys that have more than one item in their bucket."""
+    return [(k, len(v)) for k, v in buckets.items() if len(v) > 1]
+
+def check_doubles_not_none(buckets):
+    """Extract the keys that have more than one item in their bucket."""
+    return [(k, len(v)) for k, v in buckets.items()
+            if k is not None and len(v) > 1]
+
+
+# object stores #################################################################
+
+class ObjectStore(object):
+    """Store objects in memory for *faster* validation (development mode)
+
+    But it 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._commit = 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 is not a dict but a %s' % type(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, inlined=False):
+        """Add new relation"""
+        relation = eid_from, rtype, eid_to
+        self.relations.add(relation)
+        return relation
+
+    def commit(self):
+        """this commit method do nothing by default
+
+        This is voluntary to use the frequent autocommit feature in CubicWeb
+        when you are using hooks or another
+
+        If you want override commit method, please set it by the
+        constructor
+        """
+        pass
+
+    def rql(self, *args):
+        if self._rql is not None:
+            return self._rql(*args)
+
+    @property
+    def nb_inserted_entities(self):
+        return len(self.eids)
+    @property
+    def nb_inserted_types(self):
+        return len(self.types)
+    @property
+    def nb_inserted_relations(self):
+        return len(self.relations)
+
+    @deprecated("[3.7] index support will disappear")
+    def build_index(self, name, type, func=None, can_be_empty=False):
+        """build internal index for further search"""
+        index = {}
+        if func is None or not callable(func):
+            func = lambda x: x['eid']
+        for eid in self.types[type]:
+            index.setdefault(func(self.eids[eid]), []).append(eid)
+        if not can_be_empty:
+            assert index, "new index '%s' cannot be empty" % name
+        self.indexes[name] = index
+
+    @deprecated("[3.7] index support will disappear")
+    def build_rqlindex(self, name, type, key, rql, rql_params=False,
+                       func=None, can_be_empty=False):
+        """build an index by rql query
+
+        rql should return eid in first column
+        ctl.store.build_index('index_name', 'users', 'login', 'Any U WHERE U is CWUser')
+        """
+        self.types[type] = []
+        rset = self.rql(rql, rql_params or {})
+        if not can_be_empty:
+            assert rset, "new index type '%s' cannot be empty (0 record found)" % type
+        for entity in rset.entities():
+            getattr(entity, key) # autopopulate entity with key attribute
+            self.eids[entity.eid] = dict(entity)
+            if entity.eid not in self.types[type]:
+                self.types[type].append(entity.eid)
+
+        # Build index with specified key
+        func = lambda x: x[key]
+        self.build_index(name, type, func, can_be_empty=can_be_empty)
+
+    @deprecated("[3.7] index support will disappear")
+    def fetch(self, name, key, unique=False, decorator=None):
+        """index fetcher method
+
+        decorator is a callable method or an iterator of callable methods (usually a lambda function)
+        decorator=lambda x: x[:1] (first value is returned)
+        decorator=lambda x: x.lower (lowercased value is returned)
+
+        decorator is handy when you want to improve index keys but without
+        changing the original field
+
+        Same check functions can be reused here.
+        """
+        eids = self.indexes[name].get(key, [])
+        if decorator is not None:
+            if not hasattr(decorator, '__iter__'):
+                decorator = (decorator,)
+            for f in decorator:
+                eids = f(eids)
+        if unique:
+            assert len(eids) == 1, u'expected a single one value for key "%s" in index "%s". Got %i' % (key, name, len(eids))
+            eids = eids[0]
+        return eids
+
+    @deprecated("[3.7] index support will disappear")
+    def find(self, type, key, value):
+        for idx in self.types[type]:
+            item = self.items[idx]
+            if item[key] == value:
+                yield item
+
+    @deprecated("[3.7] checkpoint() deprecated. use commit() instead")
+    def checkpoint(self):
+        self.commit()
+
+
+class RQLObjectStore(ObjectStore):
+    """ObjectStore that works with an actual RQL repository (production mode)"""
+    _rql = None # bw compat
+
+    def __init__(self, session=None, commit=None):
+        ObjectStore.__init__(self)
+        if session is not None:
+            if not hasattr(session, 'set_pool'):
+                # connection
+                cnx = session
+                session = session.request()
+                session.set_pool = lambda : None
+                commit = commit or cnx.commit
+            else:
+                session.set_pool()
+            self.session = session
+            self._commit = commit or session.commit
+        elif commit is not None:
+            self._commit = commit
+            # XXX .session
+
+    @deprecated("[3.7] checkpoint() deprecated. use commit() instead")
+    def checkpoint(self):
+        self.commit()
+
+    def commit(self):
+        txuuid = self._commit()
+        self.session.set_pool()
+        return txuuid
+
+    def rql(self, *args):
+        if self._rql is not None:
+            return self._rql(*args)
+        return self.session.execute(*args)
+
+    def create_entity(self, *args, **kwargs):
+        entity = self.session.create_entity(*args, **kwargs)
+        self.eids[entity.eid] = entity
+        self.types.setdefault(args[0], []).append(entity.eid)
+        return entity
+
+    def _put(self, type, item):
+        query = ('INSERT %s X: ' % type) + ', '.join('X %s %%(%s)s' % (k, k)
+                                                     for k in item)
+        return self.rql(query, item)[0][0]
+
+    def relate(self, eid_from, rtype, eid_to, inlined=False):
+        eid_from, rtype, eid_to = super(RQLObjectStore, self).relate(
+            eid_from, rtype, eid_to)
+        self.rql('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
+                  {'x': int(eid_from), 'y': int(eid_to)}, ('x', 'y'))
+
+
+# the 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, askerror=0, catcherrors=None, tell=tell,
+                 commitevery=50):
+        self.store = store
+        self.generators = None
+        self.data = {}
+        self.errors = None
+        self.askerror = askerror
+        if  catcherrors is None:
+            catcherrors = askerror
+        self.catcherrors = catcherrors
+        self.commitevery = commitevery # set to None to do a single commit
+        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 record_error(self, key, msg=None, type=None, value=None, tb=None):
+        tmp = StringIO()
+        if type is None:
+            traceback.print_exc(file=tmp)
+        else:
+            traceback.print_exception(type, value, tb, file=tmp)
+        print tmp.getvalue()
+        # use a list to avoid counting a <nb lines> errors instead of one
+        errorlog = self.errors.setdefault(key, [])
+        if msg is None:
+            errorlog.append(tmp.getvalue().splitlines())
+        else:
+            errorlog.append( (msg, tmp.getvalue().splitlines()) )
+
+    def run(self):
+        self.errors = {}
+        for func, checks in self.generators:
+            self._checks = {}
+            func_name = func.__name__
+            self.tell("Run import function '%s'..." % func_name)
+            try:
+                func(self)
+            except:
+                if self.catcherrors:
+                    self.record_error(func_name, 'While calling %s' % func.__name__)
+                else:
+                    self._print_stats()
+                    raise
+            for key, func, title, help in checks:
+                buckets = self._checks.get(key)
+                if buckets:
+                    err = func(buckets)
+                    if err:
+                        self.errors[title] = (help, err)
+        txuuid = self.store.commit()
+        self._print_stats()
+        if self.errors:
+            if self.askerror == 2 or (self.askerror and confirm('Display errors ?')):
+                from pprint import pformat
+                for errkey, error in self.errors.items():
+                    self.tell("\n%s (%s): %d\n" % (error[0], errkey, len(error[1])))
+                    self.tell(pformat(sorted(error[1])))
+        if txuuid is not None:
+            print 'transaction id:', txuuid
+    def _print_stats(self):
+        nberrors = sum(len(err[1]) for err in self.errors.values())
+        self.tell('\nImport statistics: %i entities, %i types, %i relations and %i errors'
+                  % (self.store.nb_inserted_entities,
+                     self.store.nb_inserted_types,
+                     self.store.nb_inserted_relations,
+                     nberrors))
+
+    def get_data(self, key):
+        return self.data.get(key)
+
+    def index(self, name, key, value, unique=False):
+        """create a new index
+
+        If unique is set to True, only first occurence will be kept not the following ones
+        """
+        if unique:
+            try:
+                if value in self.store.indexes[name][key]:
+                    return
+            except KeyError:
+                # we're sure that one is the first occurence; so continue...
+                pass
+        self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value)
+
+    def tell(self, msg):
+        self._tell(msg)
+
+    def iter_and_commit(self, datakey):
+        """iter rows, triggering commit every self.commitevery iterations"""
+        return commit_every(self.commitevery, self.store, self.get_data(datakey))
+
+
+
+from datetime import datetime
+from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES
+
+
+class NoHookRQLObjectStore(RQLObjectStore):
+    """ObjectStore that works with an actual RQL repository (production mode)"""
+    _rql = None # bw compat
+
+    def __init__(self, session, metagen=None, baseurl=None):
+        super(NoHookRQLObjectStore, self).__init__(session)
+        self.source = session.repo.system_source
+        self.rschema = session.repo.schema.rschema
+        self.add_relation = self.source.add_relation
+        if metagen is None:
+            metagen = MetaGenerator(session, baseurl)
+        self.metagen = metagen
+        self._nb_inserted_entities = 0
+        self._nb_inserted_types = 0
+        self._nb_inserted_relations = 0
+        self.rql = session.execute
+        # deactivate security
+        session.set_read_security(False)
+        session.set_write_security(False)
+
+    def create_entity(self, etype, **kwargs):
+        for k, v in kwargs.iteritems():
+            kwargs[k] = getattr(v, 'eid', v)
+        entity, rels = self.metagen.base_etype_dicts(etype)
+        entity = copy(entity)
+        entity._related_cache = {}
+        self.metagen.init_entity(entity)
+        entity.update(kwargs)
+        entity.edited_attributes = set(entity)
+        session = self.session
+        self.source.add_entity(session, entity)
+        self.source.add_info(session, entity, self.source, None, complete=False)
+        for rtype, targeteids in rels.iteritems():
+            # targeteids may be a single eid or a list of eids
+            inlined = self.rschema(rtype).inlined
+            try:
+                for targeteid in targeteids:
+                    self.add_relation(session, entity.eid, rtype, targeteid,
+                                      inlined)
+            except TypeError:
+                self.add_relation(session, entity.eid, rtype, targeteids,
+                                  inlined)
+        self._nb_inserted_entities += 1
+        return entity
+
+    def relate(self, eid_from, rtype, eid_to):
+        assert not rtype.startswith('reverse_')
+        self.add_relation(self.session, eid_from, rtype, eid_to,
+                          self.rschema(rtype).inlined)
+        self._nb_inserted_relations += 1
+
+    @property
+    def nb_inserted_entities(self):
+        return self._nb_inserted_entities
+    @property
+    def nb_inserted_types(self):
+        return self._nb_inserted_types
+    @property
+    def nb_inserted_relations(self):
+        return self._nb_inserted_relations
+
+    def _put(self, type, item):
+        raise RuntimeError('use create entity')
+
+
+class MetaGenerator(object):
+    def __init__(self, session, baseurl=None):
+        self.session = session
+        self.source = session.repo.system_source
+        self.time = datetime.now()
+        if baseurl is None:
+            config = session.vreg.config
+            baseurl = config['base-url'] or config.default_base_url()
+        if not baseurl[-1] == '/':
+            baseurl += '/'
+        self.baseurl =  baseurl
+        # attributes/relations shared by all entities of the same type
+        self.etype_attrs = []
+        self.etype_rels = []
+        # attributes/relations specific to each entity
+        self.entity_attrs = ['cwuri']
+        #self.entity_rels = [] XXX not handled (YAGNI?)
+        schema = session.vreg.schema
+        rschema = schema.rschema
+        for rtype in META_RTYPES:
+            if rtype in ('eid', 'cwuri') or rtype in VIRTUAL_RTYPES:
+                continue
+            if rschema(rtype).final:
+                self.etype_attrs.append(rtype)
+            else:
+                self.etype_rels.append(rtype)
+        if not schema._eid_index:
+            # test schema loaded from the fs
+            self.gen_is = self.test_gen_is
+            self.gen_is_instance_of = self.test_gen_is_instanceof
+
+    @cached
+    def base_etype_dicts(self, etype):
+        entity = self.session.vreg['etypes'].etype_class(etype)(self.session)
+        # entity are "surface" copied, avoid shared dict between copies
+        del entity.cw_extra_kwargs
+        for attr in self.etype_attrs:
+            entity[attr] = self.generate(entity, attr)
+        rels = {}
+        for rel in self.etype_rels:
+            rels[rel] = self.generate(entity, rel)
+        return entity, rels
+
+    def init_entity(self, entity):
+        entity.eid = self.source.create_eid(self.session)
+        for attr in self.entity_attrs:
+            entity[attr] = self.generate(entity, attr)
+
+    def generate(self, entity, rtype):
+        return getattr(self, 'gen_%s' % rtype)(entity)
+
+    def gen_cwuri(self, entity):
+        return u'%seid/%s' % (self.baseurl, entity.eid)
+
+    def gen_creation_date(self, entity):
+        return self.time
+    def gen_modification_date(self, entity):
+        return self.time
+
+    def gen_is(self, entity):
+        return entity.e_schema.eid
+    def gen_is_instance_of(self, entity):
+        eids = []
+        for etype in entity.e_schema.ancestors() + [entity.e_schema]:
+            eids.append(entity.e_schema.eid)
+        return eids
+
+    def gen_created_by(self, entity):
+        return self.session.user.eid
+    def gen_owned_by(self, entity):
+        return self.session.user.eid
+
+    # implementations of gen_is / gen_is_instance_of to use during test where
+    # schema has been loaded from the fs (hence entity type schema eids are not
+    # known)
+    def test_gen_is(self, entity):
+        return eschema_eid(self.session, entity.e_schema)
+    def test_gen_is_instanceof(self, entity):
+        eids = []
+        for eschema in entity.e_schema.ancestors() + [entity.e_schema]:
+            eids.append(eschema_eid(self.session, eschema))
+        return eids
--- a/dbapi.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/dbapi.py	Tue Apr 06 19:46:38 2010 +0200
@@ -57,6 +57,7 @@
     etypescls = cwvreg.VRegistry.REGISTRY_FACTORY['etypes']
     etypescls.etype_class = etypescls.orig_etype_class
 
+
 class ConnectionProperties(object):
     def __init__(self, cnxtype=None, lang=None, close=True, log=False):
         self.cnxtype = cnxtype or 'pyro'
@@ -203,11 +204,6 @@
             self.pgettext = lambda x, y: y
         self.debug('request default language: %s', self.lang)
 
-    def decorate_rset(self, rset):
-        rset.vreg = self.vreg
-        rset.req = self
-        return rset
-
     def describe(self, eid):
         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
         return self.cnx.describe(eid)
@@ -242,7 +238,7 @@
     def get_session_data(self, key, default=None, pop=False):
         """return value associated to `key` in session data"""
         if self.cnx is None:
-            return None # before the connection has been established
+            return default # before the connection has been established
         return self.cnx.get_session_data(key, default, pop)
 
     def set_session_data(self, key, value):
@@ -371,6 +367,16 @@
             return '<Connection %s (anonymous)>' % self.sessionid
         return '<Connection %s>' % self.sessionid
 
+    def __enter__(self):
+        return self.cursor()
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        if exc_type is None:
+            self.commit()
+        else:
+            self.rollback()
+            return False #propagate the exception
+
     def request(self):
         return DBAPIRequest(self.vreg, self)
 
@@ -397,15 +403,21 @@
             pass
 
     def check(self):
-        """raise `BadSessionId` if the connection is no more valid"""
+        """raise `BadConnectionId` if the connection is no more valid"""
+        if self._closed is not None:
+            raise ProgrammingError('Closed connection')
         self._repo.check_session(self.sessionid)
 
     def set_session_props(self, **props):
-        """raise `BadSessionId` if the connection is no more valid"""
+        """raise `BadConnectionId` if the connection is no more valid"""
+        if self._closed is not None:
+            raise ProgrammingError('Closed connection')
         self._repo.set_session_props(self.sessionid, props)
 
     def get_shared_data(self, key, default=None, pop=False):
         """return value associated to `key` in shared data"""
+        if self._closed is not None:
+            raise ProgrammingError('Closed connection')
         return self._repo.get_shared_data(self.sessionid, key, default, pop)
 
     def set_shared_data(self, key, value, querydata=False):
@@ -416,6 +428,8 @@
         transaction, and won't be available through the connexion, only on the
         repository side.
         """
+        if self._closed is not None:
+            raise ProgrammingError('Closed connection')
         return self._repo.set_shared_data(self.sessionid, key, value, querydata)
 
     def get_schema(self):
@@ -501,6 +515,8 @@
     def user(self, req=None, props=None):
         """return the User object associated to this connection"""
         # cnx validity is checked by the call to .user_info
+        if self._closed is not None:
+            raise ProgrammingError('Closed connection')
         eid, login, groups, properties = self._repo.user_info(self.sessionid,
                                                               props)
         if req is None:
@@ -521,6 +537,8 @@
                 pass
 
     def describe(self, eid):
+        if self._closed is not None:
+            raise ProgrammingError('Closed connection')
         return self._repo.describe(self.sessionid, eid)
 
     def close(self):
@@ -535,19 +553,20 @@
         if self._closed:
             raise ProgrammingError('Connection is already closed')
         self._repo.close(self.sessionid)
+        del self._repo # necessary for proper garbage collection
         self._closed = 1
 
     def commit(self):
-        """Commit any pending transaction to the database. Note that if the
-        database supports an auto-commit feature, this must be initially off. An
-        interface method may be provided to turn it back on.
+        """Commit pending transaction for this connection to the repository.
 
-        Database modules that do not support transactions should implement this
-        method with void functionality.
+        may raises `Unauthorized` or `ValidationError` if we attempted to do
+        something we're not allowed to for security or integrity reason.
+
+        If the transaction is undoable, a transaction id will be returned.
         """
         if not self._closed is None:
             raise ProgrammingError('Connection is already closed')
-        self._repo.commit(self.sessionid)
+        return self._repo.commit(self.sessionid)
 
     def rollback(self):
         """This method is optional since not all databases provide transaction
@@ -574,6 +593,73 @@
             req = self.request()
         return self.cursor_class(self, self._repo, req=req)
 
+    # undo support ############################################################
+
+    def undoable_transactions(self, ueid=None, req=None, **actionfilters):
+        """Return a list of undoable transaction objects by the connection's
+        user, ordered by descendant transaction time.
+
+        Managers may filter according to user (eid) who has done the transaction
+        using the `ueid` argument. Others will only see their own transactions.
+
+        Additional filtering capabilities is provided by using the following
+        named arguments:
+
+        * `etype` to get only transactions creating/updating/deleting entities
+          of the given type
+
+        * `eid` to get only transactions applied to entity of the given eid
+
+        * `action` to get only transactions doing the given action (action in
+          'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
+          'D'.
+
+        * `public`: when additional filtering is provided, their are by default
+          only searched in 'public' actions, unless a `public` argument is given
+          and set to false.
+        """
+        txinfos = self._repo.undoable_transactions(self.sessionid, ueid,
+                                                   **actionfilters)
+        if req is None:
+            req = self.request()
+        for txinfo in txinfos:
+            txinfo.req = req
+        return txinfos
+
+    def transaction_info(self, txuuid, req=None):
+        """Return transaction object for the given uid.
+
+        raise `NoSuchTransaction` if not found or if session's user is not
+        allowed (eg not in managers group and the transaction doesn't belong to
+        him).
+        """
+        txinfo = self._repo.transaction_info(self.sessionid, txuuid)
+        if req is None:
+            req = self.request()
+        txinfo.req = req
+        return txinfo
+
+    def transaction_actions(self, txuuid, public=True):
+        """Return an ordered list of action effectued during that transaction.
+
+        If public is true, return only 'public' actions, eg not ones triggered
+        under the cover by hooks, else return all actions.
+
+        raise `NoSuchTransaction` if the transaction is not found or if
+        session's user is not allowed (eg not in managers group and the
+        transaction doesn't belong to him).
+        """
+        return self._repo.transaction_actions(self.sessionid, txuuid, public)
+
+    def undo_transaction(self, txuuid):
+        """Undo the given transaction. Return potential restoration errors.
+
+        raise `NoSuchTransaction` if not found or if session's user is not
+        allowed (eg not in managers group and the transaction doesn't belong to
+        him).
+        """
+        return self._repo.undo_transaction(self.sessionid, txuuid)
+
 
 # cursor object ###############################################################
 
@@ -646,11 +732,11 @@
         Return values are not defined by the DB-API, but this here it returns a
         ResultSet object.
         """
-        self._res = res = self._repo.execute(self._sessid, operation,
-                                             parameters, eid_key, build_descr)
-        self.req.decorate_rset(res)
+        self._res = rset = self._repo.execute(self._sessid, operation,
+                                              parameters, eid_key, build_descr)
+        rset.req = self.req
         self._index = 0
-        return res
+        return rset
 
 
     def executemany(self, operation, seq_of_parameters):
--- a/debian/changelog	Thu Mar 04 17:56:45 2010 +0100
+++ b/debian/changelog	Tue Apr 06 19:46:38 2010 +0200
@@ -1,3 +1,53 @@
+cubicweb (3.7.3-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 31 Mar 2010 14:55:21 +0200
+
+cubicweb (3.7.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 26 Mar 2010 15:53:01 +0100
+
+cubicweb (3.7.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 19 Mar 2010 14:47:23 +0100
+
+cubicweb (3.7.0-1) unstable; urgency=low
+
+  * remove postgresql-contrib from cubicweb dependency (using tsearch
+    which is included with postgres >= 8.3)
+  * add postgresql-client | mysql-client to cubicweb-server dependencies using two
+    new cubicweb-[postgresql|mysql]-support virtual packages (necessary for
+    dump/restore of database)
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Tue, 16 Mar 2010 17:55:37 +0100
+
+ cubicweb (3.6.3-1) unstable; urgency=low
+
+  * remove postgresql-contrib from cubicweb dependency (using tsearch
+    which is included with postgres >= 8.3)
+  * add postgresql-client | mysql-client to cubicweb-server dependencies using two
+    new cubicweb-[postgresql|mysql]-support virtual packages (necessary for
+    dump/restore of database)
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 24 Mar 2010 07:50:47 +0100
+
+cubicweb (3.6.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 11 Mar 2010 19:49:04 +0100
+
+cubicweb (3.6.1-2) unstable; urgency=low
+
+  * remove depends to python-elementtree (included in python>=2.5)
+
+ -- Pierre-Yves David <pierre-yves.david@logilab.fr>  Fri, 04 Mar 2010 14:43:01 +0100
+
 cubicweb (3.6.1-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Thu Mar 04 17:56:45 2010 +0100
+++ b/debian/control	Tue Apr 06 19:46:38 2010 +0200
@@ -7,16 +7,16 @@
            Adrien Di Mascio <Adrien.DiMascio@logilab.fr>,
            Aurélien Campéas <aurelien.campeas@logilab.fr>,
            Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 5), python-dev (>=2.4), python-central (>= 0.5)
+Build-Depends: debhelper (>= 5), python-dev (>=2.5), python-central (>= 0.5)
 Standards-Version: 3.8.0
 Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.4, << 2.6
+XS-Python-Version: >= 2.5, << 2.6
 
 Package: cubicweb
 Architecture: all
 XB-Python-Version: ${python:Versions}
 Depends: ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-twisted (= ${source:Version})
-XB-Recommends: (postgresql, postgresql-plpython, postgresql-contrib) | mysql | sqlite3
+XB-Recommends: (postgresql, postgresql-plpython) | mysql | sqlite3
 Recommends: postgresql | mysql | sqlite3
 Description: the complete CubicWeb framework
  CubicWeb is a semantic web application framework.
@@ -33,7 +33,7 @@
 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
+Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (=> 1.0.2), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
 Recommends: pyro, cubicweb-documentation (= ${source:Version})
 Description: server part of the CubicWeb framework
  CubicWeb is a semantic web application framework.
@@ -43,6 +43,26 @@
  This package provides the repository server part of the library and
  necessary shared data files such as the schema library.
 
+Package: cubicweb-postgresql-support
+Architecture: all
+# postgresql-client packages for backup/restore of non local database
+Depends: python-psycopg2, postgresql-client
+Description: postgres support for the CubicWeb framework
+ CubicWeb is a semantic web application framework.
+ .
+ This virtual package provides dependancies to use postgres for the
+ cubicweb repository.
+
+Package: cubicweb-mysql-support
+Architecture: all
+# mysql-client packages for backup/restore of non local database
+Depends: python-mysqldb, mysql-client
+Description: mysql support for the CubicWeb framework
+ CubicWeb is a semantic web application framework.
+ .
+ This virtual package provides dependancies to use mysql for the
+ cubicweb repository.
+
 
 Package: cubicweb-twisted
 Architecture: all
@@ -62,7 +82,7 @@
 Package: cubicweb-web
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3), python-elementtree
+Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3)
 Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-pysixt, fop, python-imaging
 Description: web interface library for the CubicWeb framework
  CubicWeb is a semantic web application framework.
@@ -77,7 +97,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.48.1), python-yams (>= 0.28.0), python-rql (>= 0.24.0), python-lxml
+Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.49.0), python-yams (>= 0.28.1), python-rql (>= 0.25.0), python-lxml
 Recommends: python-simpletal (>= 4.0), python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/devtools/__init__.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/devtools/__init__.py	Tue Apr 06 19:46:38 2010 +0200
@@ -81,7 +81,6 @@
     mode = 'test'
     set_language = False
     read_instance_schema = False
-    bootstrap_schema = False
     init_repository = True
     options = cwconfig.merge_options(ServerConfiguration.options + (
         ('anonymous-user',
@@ -106,8 +105,6 @@
         self.init_log(log_threshold, force=True)
         # need this, usually triggered by cubicweb-ctl
         self.load_cwctl_plugins()
-        self.global_set_option('anonymous-user', 'anon')
-        self.global_set_option('anonymous-password', 'anon')
 
     anonymous_user = TwistedConfiguration.anonymous_user.im_func
 
@@ -123,6 +120,8 @@
         super(TestServerConfiguration, self).load_configuration()
         self.global_set_option('anonymous-user', 'anon')
         self.global_set_option('anonymous-password', 'anon')
+        # no undo support in tests
+        self.global_set_option('undo-support', '')
 
     def main_config_file(self):
         """return instance's control configuration file"""
@@ -201,6 +200,8 @@
         init_test_database_sqlite(config)
     elif driver == 'postgres':
         init_test_database_postgres(config)
+    elif driver == 'sqlserver2005':
+        init_test_database_sqlserver2005(config, source)
     else:
         raise ValueError('no initialization function for driver %r' % driver)
     config._cubes = None # avoid assertion error
@@ -223,11 +224,19 @@
 ### postgres test database handling ############################################
 
 def init_test_database_postgres(config):
-    """initialize a fresh sqlite databse used for testing purpose"""
+    """initialize a fresh postgresql databse used for testing purpose"""
     if config.init_repository:
         from cubicweb.server import init_repository
         init_repository(config, interactive=False, drop=True)
 
+### sqlserver2005 test database handling ############################################
+
+def init_test_database_sqlserver2005(config):
+    """initialize a fresh sqlserver databse used for testing purpose"""
+     if config.init_repository:
+         from cubicweb.server import init_repository
+         init_repository(config, interactive=False, drop=True, vreg=vreg)
+
 
 ### sqlite test database handling ##############################################
 
--- a/devtools/dataimport.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/devtools/dataimport.py	Tue Apr 06 19:46:38 2010 +0200
@@ -1,547 +1,4 @@
-# -*- coding: utf-8 -*-
-"""This module provides tools to import tabular data.
-
-:organization: Logilab
-:copyright: 2001-2010 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 = 1
-  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)
-
-
-.. BUG fichier à une colonne pose un problème de parsing
-.. TODO rollback()
-"""
-__docformat__ = "restructuredtext en"
-
-import sys
-import csv
-import traceback
-import os.path as osp
-from StringIO import StringIO
-
-from logilab.common import shellutils
-from logilab.common.deprecation import deprecated
-
-def ucsvreader_pb(filepath, encoding='utf-8', separator=',', quote='"',
-                  skipfirst=False, withpb=True):
-    """same as ucsvreader but a progress bar is displayed as we iter on rows"""
-    if not osp.exists(filepath):
-        raise Exception("file doesn't exists: %s" % filepath)
-    rowcount = int(shellutils.Execute('wc -l "%s"' % filepath).out.strip().split()[0])
-    if skipfirst:
-        rowcount -= 1
-    if withpb:
-        pb = shellutils.ProgressBar(rowcount, 50)
-    for urow in ucsvreader(file(filepath), encoding, separator, quote, skipfirst):
-        yield urow
-        if withpb:
-            pb.update()
-    print ' %s rows imported' % rowcount
-
-def ucsvreader(stream, encoding='utf-8', separator=',', quote='"',
-               skipfirst=False):
-    """A csv reader that accepts files with any encoding and outputs unicode
-    strings
-    """
-    it = iter(csv.reader(stream, delimiter=separator, quotechar=quote))
-    if skipfirst:
-        it.next()
-    for row in it:
-        yield [item.decode(encoding) for item in row]
-
-utf8csvreader = deprecated('use ucsvreader instead')(ucsvreader)
-
-def commit_every(nbit, store, it):
-    for i, x in enumerate(it):
-        yield x
-        if nbit is not None and i % nbit:
-            store.checkpoint()
-    if nbit is not None:
-        store.checkpoint()
-
-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 mk_entity(row, map):
-    """Return a dict made from sanitized mapped values.
-
-    ValidationError can be raised on unexpected values found in checkers
-
-    >>> row = {'myname': u'dupont'}
-    >>> map = [('myname', u'name', (capitalize_if_unicase,))]
-    >>> mk_entity(row, map)
-    {'name': u'Dupont'}
-    >>> row = {'myname': u'dupont', 'optname': u''}
-    >>> map = [('myname', u'name', (capitalize_if_unicase,)),
-    ...        ('optname', u'MARKER', (optional,))]
-    >>> mk_entity(row, map)
-    {'name': u'Dupont'}
-    """
-    res = {}
-    assert isinstance(row, dict)
-    assert isinstance(map, list)
-    for src, dest, funcs in map:
-        assert not (required in funcs and optional in funcs), "optional and required checks are exclusive"
-        res[dest] = row[src]
-        try:
-            for func in funcs:
-                res[dest] = func(res[dest])
-            if res[dest] is None:
-                raise AssertionError('undetermined value')
-        except AssertionError, err:
-            if optional in funcs:
-                # Forget this field if exception is coming from optional function
-               del res[dest]
-            else:
-               raise AssertionError('error with "%s" field: %s' % (src, err))
-    return res
-
-
-# user interactions ############################################################
-
-def tell(msg):
-    print 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'
-
-
-class catch_error(object):
-    """Helper for @contextmanager decorator."""
-
-    def __init__(self, ctl, key='unexpected error', msg=None):
-        self.ctl = ctl
-        self.key = key
-        self.msg = msg
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, type, value, traceback):
-        if type is not None:
-            if issubclass(type, (KeyboardInterrupt, SystemExit)):
-                return # re-raise
-            if self.ctl.catcherrors:
-                self.ctl.record_error(self.key, None, type, value, traceback)
-                return True # silent
-
-
-# base sanitizing functions ####################################################
-
-def capitalize_if_unicase(txt):
-    if txt.isupper() or txt.islower():
-        return txt.capitalize()
-    return txt
-
-def uppercase(txt):
-    return txt.upper()
-
-def lowercase(txt):
-    return txt.lower()
-
-def no_space(txt):
-    return txt.replace(' ','')
-
-def no_uspace(txt):
-    return txt.replace(u'\xa0','')
-
-def no_dash(txt):
-    return txt.replace('-','')
-
-def decimal(value):
-    """cast to float but with comma replacement
-
-    We take care of some locale format as replacing ',' by '.'"""
-    value = value.replace(',', '.')
-    try:
-        return float(value)
-    except Exception, err:
-        raise AssertionError(err)
-
-def integer(value):
-    try:
-        return int(value)
-    except Exception, err:
-        raise AssertionError(err)
-
-def strip(txt):
-    return txt.strip()
-
-def yesno(value):
-    """simple heuristic that returns boolean value
-
-    >>> yesno("Yes")
-    True
-    >>> yesno("oui")
-    True
-    >>> yesno("1")
-    True
-    >>> yesno("11")
-    True
-    >>> yesno("")
-    False
-    >>> yesno("Non")
-    False
-    >>> yesno("blablabla")
-    False
-    """
-    if value:
-        return value.lower()[0] in 'yo1'
-    return False
-
-def isalpha(value):
-    if value.isalpha():
-        return value
-    raise AssertionError("not all characters in the string alphabetic")
-
-def optional(value):
-    """validation error will not been raised if you add this checker in chain"""
-    return value
-
-def required(value):
-    """raise AssertionError is value is empty
-
-    This check should be often found in last position in the chain.
-    """
-    if bool(value):
-        return value
-    raise AssertionError("required")
-
-@deprecated('use required(value)')
-def nonempty(value):
-    return required(value)
-
-@deprecated('use integer(value)')
-def alldigits(txt):
-    if txt.isdigit():
-        return txt
-    else:
-        return u''
-
-
-# base integrity checking functions ############################################
-
-def check_doubles(buckets):
-    """Extract the keys that have more than one item in their bucket."""
-    return [(k, len(v)) for k, v in buckets.items() if len(v) > 1]
-
-def check_doubles_not_none(buckets):
-    """Extract the keys that have more than one item in their bucket."""
-    return [(k, len(v)) for k, v in buckets.items()
-            if k is not None and len(v) > 1]
-
-
-# object stores #################################################################
-
-class ObjectStore(object):
-    """Store objects in memory for *faster* validation (development mode)
-
-    But it 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 is not a dict but a %s' % type(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):
-        """Add new relation (reverse type support is available)
-
-        >>> 1,2 = eid_from, eid_to
-        >>> self.relate(eid_from, 'in_group', eid_to)
-        1, 'in_group', 2
-        >>> self.relate(eid_from, 'reverse_in_group', eid_to)
-        2, 'in_group', 1
-        """
-        if rtype.startswith('reverse_'):
-            eid_from, eid_to = eid_to, eid_from
-            rtype = rtype[8:]
-        relation = eid_from, rtype, eid_to
-        self.relations.add(relation)
-        return relation
-
-    def build_index(self, name, type, func=None):
-        index = {}
-        if func is None or not callable(func):
-            func = lambda x: x['eid']
-        for eid in self.types[type]:
-            index.setdefault(func(self.eids[eid]), []).append(eid)
-        assert index, "new index '%s' cannot be empty" % name
-        self.indexes[name] = index
-
-    def build_rqlindex(self, name, type, key, rql, rql_params=False, func=None):
-        """build an index by rql query
-
-        rql should return eid in first column
-        ctl.store.build_index('index_name', 'users', 'login', 'Any U WHERE U is CWUser')
-        """
-        rset = self.rql(rql, rql_params or {})
-        for entity in rset.entities():
-            getattr(entity, key) # autopopulate entity with key attribute
-            self.eids[entity.eid] = dict(entity)
-            if entity.eid not in self.types.setdefault(type, []):
-                self.types[type].append(entity.eid)
-        assert self.types[type], "new index type '%s' cannot be empty (0 record found)" % type
-
-        # Build index with specified key
-        func = lambda x: x[key]
-        self.build_index(name, type, func)
-
-    @deprecated('get_many() deprecated. Use fetch() instead')
-    def get_many(self, name, key):
-        return self.fetch(name, key, unique=False)
-
-    @deprecated('get_one() deprecated. Use fetch(..., unique=True) instead')
-    def get_one(self, name, key):
-        return self.fetch(name, key, unique=True)
-
-    def fetch(self, name, key, unique=False, decorator=None):
-        """
-            decorator is a callable method or an iterator of callable methods (usually a lambda function)
-            decorator=lambda x: x[:1] (first value is returned)
-
-            We can use validation check function available in _entity
-        """
-        eids = self.indexes[name].get(key, [])
-        if decorator is not None:
-            if not hasattr(decorator, '__iter__'):
-                decorator = (decorator,)
-            for f in decorator:
-                eids = f(eids)
-        if unique:
-            assert len(eids) == 1, u'expected a single one value for key "%s" in index "%s". Got %i' % (key, name, len(eids))
-            eids = eids[0] # FIXME maybe it's better to keep an iterator here ?
-        return eids
-
-    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, *args):
-        if self._rql is not None:
-            return self._rql(*args)
-
-    def checkpoint(self):
-        pass
-
-
-class RQLObjectStore(ObjectStore):
-    """ObjectStore that works with an actual RQL repository (production mode)"""
-    _rql = None # bw compat
-
-    def __init__(self, session=None, checkpoint=None):
-        ObjectStore.__init__(self)
-        if session is not None:
-            if not hasattr(session, 'set_pool'):
-                # connection
-                cnx = session
-                session = session.request()
-                session.set_pool = lambda : None
-                checkpoint = checkpoint or cnx.commit
-            self.session = session
-            self.checkpoint = checkpoint or session.commit
-        elif checkpoint is not None:
-            self.checkpoint = checkpoint
-
-    def rql(self, *args):
-        if self._rql is not None:
-            return self._rql(*args)
-        self.session.set_pool()
-        return self.session.execute(*args)
-
-    def create_entity(self, *args, **kwargs):
-        self.session.set_pool()
-        entity = self.session.create_entity(*args, **kwargs)
-        self.eids[entity.eid] = entity
-        self.types.setdefault(args[0], []).append(entity.eid)
-        return entity
-
-    def _put(self, type, item):
-        query = ('INSERT %s X: ' % type) + ', '.join('X %s %%(%s)s' % (k, k)
-                                                     for k in item)
-        return self.rql(query, item)[0][0]
-
-    def relate(self, eid_from, rtype, eid_to):
-        # if reverse relation is found, eids are exchanged
-        eid_from, rtype, eid_to = super(RQLObjectStore, self).relate(
-            eid_from, rtype, eid_to)
-        self.rql('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
-                  {'x': int(eid_from), 'y': int(eid_to)}, ('x', 'y'))
-
-
-# the 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, askerror=0, catcherrors=None, tell=tell,
-                 commitevery=50):
-        self.store = store
-        self.generators = None
-        self.data = {}
-        self.errors = None
-        self.askerror = askerror
-        if  catcherrors is None:
-            catcherrors = askerror
-        self.catcherrors = catcherrors
-        self.commitevery = commitevery # set to None to do a single commit
-        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 record_error(self, key, msg=None, type=None, value=None, tb=None):
-        tmp = StringIO()
-        if type is None:
-            traceback.print_exc(file=tmp)
-        else:
-            traceback.print_exception(type, value, tb, file=tmp)
-        print tmp.getvalue()
-        # use a list to avoid counting a <nb lines> errors instead of one
-        errorlog = self.errors.setdefault(key, [])
-        if msg is None:
-            errorlog.append(tmp.getvalue().splitlines())
-        else:
-            errorlog.append( (msg, tmp.getvalue().splitlines()) )
-
-    def run(self):
-        self.errors = {}
-        for func, checks in self.generators:
-            self._checks = {}
-            func_name = func.__name__[4:]  # XXX
-            self.tell("Import '%s'..." % func_name)
-            try:
-                func(self)
-            except:
-                if self.catcherrors:
-                    self.record_error(func_name, 'While calling %s' % func.__name__)
-                else:
-                    raise
-            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()
-        nberrors = sum(len(err[1]) for err in self.errors.values())
-        self.tell('\nImport completed: %i entities, %i types, %i relations and %i errors'
-                  % (len(self.store.eids), len(self.store.types),
-                     len(self.store.relations), nberrors))
-        if self.errors:
-            if self.askerror == 2 or (self.askerror and confirm('Display errors ?')):
-                from pprint import pformat
-                for errkey, error in self.errors.items():
-                    self.tell("\n%s (%s): %d\n" % (error[0], errkey, len(error[1])))
-                    self.tell(pformat(sorted(error[1])))
-
-    def get_data(self, key):
-        return self.data.get(key)
-
-    def index(self, name, key, value, unique=False):
-        """create a new index
-
-        If unique is set to True, only first occurence will be kept not the following ones
-        """
-        if unique:
-            try:
-                if value in self.store.indexes[name][key]:
-                    return
-            except KeyError:
-                # we're sure that one is the first occurence; so continue...
-                pass
-        self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value)
-
-    def tell(self, msg):
-        self._tell(msg)
-
-    def iter_and_commit(self, datakey):
-        """iter rows, triggering commit every self.commitevery iterations"""
-        return commit_every(self.commitevery, self.store, self.get_data(datakey))
+# pylint: disable-msg=W0614,W0401
+from warnings import warn
+warn('moved to cubicweb.dataimport', DeprecationWarning, stacklevel=2)
+from cubicweb.dataimport import *
--- a/devtools/fake.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/devtools/fake.py	Tue Apr 06 19:46:38 2010 +0200
@@ -7,9 +7,7 @@
 """
 __docformat__ = "restructuredtext en"
 
-from logilab.common.adbh import get_adv_func_helper
-
-from indexer import get_indexer
+from logilab.database import get_db_helper
 
 from cubicweb.req import RequestSessionBase
 from cubicweb.cwvreg import CubicWebVRegistry
@@ -118,17 +116,6 @@
     def validate_cache(self):
         pass
 
-    # session compatibility (in some test are using this class to test server
-    # side views...)
-    def actual_session(self):
-        """return the original parent session if any, else self"""
-        return self
-
-    def unsafe_execute(self, *args, **kwargs):
-        """return the original parent session if any, else self"""
-        kwargs.pop('propagate', None)
-        return self.execute(*args, **kwargs)
-
 
 class FakeUser(object):
     login = 'toto'
@@ -138,18 +125,19 @@
 
 
 class FakeSession(RequestSessionBase):
+    read_security = write_security = True
+    set_read_security = set_write_security = lambda *args, **kwargs: None
+
     def __init__(self, repo=None, user=None):
         self.repo = repo
         self.vreg = getattr(self.repo, 'vreg', CubicWebVRegistry(FakeConfig(), initlog=False))
         self.pool = FakePool()
         self.user = user or FakeUser()
         self.is_internal_session = False
-        self.is_super_session = self.user.eid == -1
         self.transaction_data = {}
 
-    def execute(self, *args):
+    def execute(self, *args, **kwargs):
         pass
-    unsafe_execute = execute
 
     def commit(self, *args):
         self.transaction_data.clear()
@@ -158,11 +146,6 @@
     def system_sql(self, sql, args=None):
         pass
 
-    def decorate_rset(self, rset, propagate=False):
-        rset.vreg = self.vreg
-        rset.req = self
-        return rset
-
     def set_entity_cache(self, entity):
         pass
 
@@ -200,12 +183,7 @@
 
 
 class FakeSource(object):
-    dbhelper = get_adv_func_helper('sqlite')
-    indexer = get_indexer('sqlite', 'UTF8')
-    dbhelper.fti_uid_attr = indexer.uid_attr
-    dbhelper.fti_table = indexer.table
-    dbhelper.fti_restriction_sql = indexer.restriction_sql
-    dbhelper.fti_need_distinct_query = indexer.need_distinct
+    dbhelper = get_db_helper('sqlite')
     def __init__(self, uri):
         self.uri = uri
 
--- a/devtools/repotest.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/devtools/repotest.py	Tue Apr 06 19:46:38 2010 +0200
@@ -95,6 +95,31 @@
     def __iter__(self):
         return iter(sorted(self.origdict, key=self.sortkey))
 
+def schema_eids_idx(schema):
+    """return a dictionary mapping schema types to their eids so we can reread
+    it from the fs instead of the db (too costly) between tests
+    """
+    schema_eids = {}
+    for x in schema.entities():
+        schema_eids[x] = x.eid
+    for x in schema.relations():
+        schema_eids[x] = x.eid
+        for rdef in x.rdefs.itervalues():
+            schema_eids[(rdef.subject, rdef.rtype, rdef.object)] = rdef.eid
+    return schema_eids
+
+def restore_schema_eids_idx(schema, schema_eids):
+    """rebuild schema eid index"""
+    for x in schema.entities():
+        x.eid = schema_eids[x]
+        schema._eid_index[x.eid] = x
+    for x in schema.relations():
+        x.eid = schema_eids[x]
+        schema._eid_index[x.eid] = x
+        for rdef in x.rdefs.itervalues():
+            rdef.eid = schema_eids[(rdef.subject, rdef.rtype, rdef.object)]
+            schema._eid_index[rdef.eid] = rdef
+
 
 from logilab.common.testlib import TestCase
 from rql import RQLHelper
@@ -150,17 +175,23 @@
         self.pool = self.session.set_pool()
         self.maxeid = self.get_max_eid()
         do_monkey_patch()
+        self._dumb_sessions = []
 
     def get_max_eid(self):
-        return self.session.unsafe_execute('Any MAX(X)')[0][0]
+        return self.session.execute('Any MAX(X)')[0][0]
     def cleanup(self):
-        self.session.unsafe_execute('DELETE Any X WHERE X eid > %s' % self.maxeid)
+        self.session.set_pool()
+        self.session.execute('DELETE Any X WHERE X eid > %s' % self.maxeid)
 
     def tearDown(self):
         undo_monkey_patch()
         self.session.rollback()
         self.cleanup()
         self.commit()
+        # properly close dumb sessions
+        for session in self._dumb_sessions:
+            session.rollback()
+            session.close()
         self.repo._free_pool(self.pool)
         assert self.session.user.eid != -1
 
@@ -198,6 +229,8 @@
         u._groups = set(groups)
         s = Session(u, self.repo)
         s._threaddata.pool = self.pool
+        # register session to ensure it gets closed
+        self._dumb_sessions.append(s)
         return s
 
     def execute(self, rql, args=None, eid_key=None, build_descr=True):
@@ -223,6 +256,7 @@
         self.sources = self.o._repo.sources
         self.system = self.sources[-1]
         do_monkey_patch()
+        self._dumb_sessions = [] # by hi-jacked parent setup
 
     def add_source(self, sourcecls, uri):
         self.sources.append(sourcecls(self.repo, self.o.schema,
@@ -237,6 +271,9 @@
             del self.repo.sources_by_uri[source.uri]
             self.newsources -= 1
         undo_monkey_patch()
+        for session in self._dumb_sessions:
+            session._threaddata.pool = None
+            session.close()
 
     def _prepare_plan(self, rql, kwargs=None):
         rqlst = self.o.parse(rql, annotate=True)
--- a/devtools/test/unittest_testlib.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/devtools/test/unittest_testlib.py	Tue Apr 06 19:46:38 2010 +0200
@@ -9,12 +9,12 @@
 from cStringIO import StringIO
 from unittest import TestSuite
 
-
-from logilab.common.testlib import (TestCase, unittest_main, 
+from logilab.common.testlib import (TestCase, unittest_main,
                                     SkipAwareTextTestRunner)
 
 from cubicweb.devtools import htmlparser
 from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.pytestconf import clean_repo_test_cls
 
 class WebTestTC(TestCase):
 
@@ -37,7 +37,7 @@
         self.assertEquals(result.testsRun, 2)
         self.assertEquals(len(result.errors), 0)
         self.assertEquals(len(result.failures), 1)
-
+        clean_repo_test_cls(MyWebTest)
 
 
 HTML_PAGE = u"""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
--- a/devtools/testlib.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/devtools/testlib.py	Tue Apr 06 19:46:38 2010 +0200
@@ -12,6 +12,7 @@
 import re
 from urllib import unquote
 from math import log
+from contextlib import contextmanager
 
 import simplejson
 
@@ -207,6 +208,7 @@
     def _build_repo(cls):
         cls.repo, cls.cnx = devtools.init_test_database(config=cls.config)
         cls.init_config(cls.config)
+        cls.repo.hm.call_hooks('server_startup', repo=cls.repo)
         cls.vreg = cls.repo.vreg
         cls._orig_cnx = cls.cnx
         cls.config.repository = lambda x=None: cls.repo
@@ -228,7 +230,9 @@
     @property
     def session(self):
         """return current server side session (using default manager account)"""
-        return self.repo._sessions[self.cnx.sessionid]
+        session = self.repo._sessions[self.cnx.sessionid]
+        session.set_pool()
+        return session
 
     @property
     def adminsession(self):
@@ -245,7 +249,14 @@
 
     def setUp(self):
         pause_tracing()
-        self._init_repo()
+        previous_failure = self.__class__.__dict__.get('_repo_init_failed')
+        if previous_failure is not None:
+            self.skip('repository is not initialised: %r' % previous_failure)
+        try:
+            self._init_repo()
+        except Exception, ex:
+            self.__class__._repo_init_failed = ex
+            raise
         resume_tracing()
         self.setup_database()
         self.commit()
@@ -265,20 +276,20 @@
             return req.user
 
     def create_user(self, login, groups=('users',), password=None, req=None,
-                    commit=True):
+                    commit=True, **kwargs):
         """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')
+        if req is None:
+            req = self._orig_cnx.request()
+        user = req.create_entity('CWUser', login=unicode(login),
+                                 upassword=password, **kwargs)
+        req.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()
+            req.cnx.commit()
         return user
 
     def login(self, login, **kwargs):
@@ -319,7 +330,10 @@
 
     @nocoverage
     def commit(self):
-        self.cnx.commit()
+        try:
+            return self.cnx.commit()
+        finally:
+            self.session.set_pool() # ensure pool still set after commit
 
     @nocoverage
     def rollback(self):
@@ -327,6 +341,8 @@
             self.cnx.rollback()
         except ProgrammingError:
             pass
+        finally:
+            self.session.set_pool() # ensure pool still set after commit
 
     # # server side db api #######################################################
 
@@ -339,6 +355,17 @@
     def entity(self, rql, args=None, eidkey=None, req=None):
         return self.execute(rql, args, eidkey, req=req).get_entity(0, 0)
 
+    @contextmanager
+    def temporary_appobjects(self, *appobjects):
+        self.vreg._loadedmods.setdefault(self.__module__, {})
+        for obj in appobjects:
+            self.vreg.register(obj)
+        try:
+            yield
+        finally:
+            for obj in appobjects:
+                self.vreg.unregister(obj)
+
     # vregistry inspection utilities ###########################################
 
     def pviews(self, req, rset):
@@ -484,7 +511,8 @@
             else:
                 cleanup = lambda p: (p[0], unquote(p[1]))
                 params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
-            path = path[len(req.base_url()):]
+            if path.startswith(req.base_url()): # may be relative
+                path = path[len(req.base_url()):]
             return path, params
         else:
             self.fail('expected a Redirect exception')
@@ -503,7 +531,7 @@
         req.cnx = None
         sh = self.app.session_handler
         authm = sh.session_manager.authmanager
-        authm.authinforetreivers[-1].anoninfo = self.vreg.config.anonymous_user()
+        authm.anoninfo = self.vreg.config.anonymous_user()
         # not properly cleaned between tests
         self.open_sessions = sh.session_manager._sessions = {}
         return req, origcnx
--- a/doc/book/en/admin/additional-tips.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/admin/additional-tips.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -9,12 +9,10 @@
 Backup, backup, backup
 ``````````````````````
 
-It is always a good idea to backup. If your system does not do that,
-you should set it up. Note that whenever you do an upgrade,
-`cubicweb-ctl` offers you to backup your database.
-
-There are a number of ways for doing backups. Before you go ahead,
-make sure the following permissions are correct ::
+It is always a good idea to backup. If your system does not do that, you should
+set it up. Note that whenever you do an upgrade, `cubicweb-ctl` offers you to
+backup your database.  There are a number of ways for doing backups. Before you
+go ahead, make sure the following permissions are correct ::
 
    # chgrp postgres /var/lib/cubicweb/backup
 
@@ -24,31 +22,36 @@
 
    # chmod g+r /etc/cubicweb.d/*<instance>*/sources
 
-**Classic way**
+**Classic way on PostgreSQL server**
 
-Simply use the pg_dump in a cron ::
+Simply use the pg_dump in a cron installed for `postgres` user on the database server::
 
-    su -c "pg_dump -Fc --username=cubicweb --no-owner" postgres > <your-instance>-$(date '+%Y-%m-%d_%H:%M:%S').dump
+    # m h  dom mon dow   command
+    0 2 * * * pg_dump -Fc --username=cubicweb --no-owner <instance> > /var/backups/<instance>-$(date '+%Y-%m-%d_%H:%M:%S').dump
 
 **CubicWeb way**
 
-The CubicWeb way is to use the `db-dump` command. For that, you have to put your passwords in a user-only-readable file at the
-root of the postgres user. The file is `.pgpass` (`chmod 0600`), in this case for a socket run connection to postgres ::
+The CubicWeb way is to use the `db-dump` command. For that, you have to put
+your passwords in a user-only-readable file at the home directory of root user.
+The file is `.pgpass` (`chmod 0600`), in this case for a socket run connection
+to PostgreSQL ::
 
-    /var/run/postgresql:5432:<instance>:cubicweb:<password>
+    /var/run/postgresql:5432:<instance>:<database user>:<database password>
 
 The postgres documentation for the `.pgpass` format can be found `here`_
 
-Then add the following command to the crontab of the postgres user (`su posgres 'crontab -e'`)::
+Then add the following command to the crontab of the user (`crontab -e`)::
 
     # m h  dom mon dow   command
     0 2 * * * cubicweb-ctl db-dump <instance>
 
 **The automated sysadmin way**
 
-You can use a combination `backup-ninja`_ (which has a postgres script in the example directory), `backuppc`)_ (for versionning).
+You can use a combination `backup-ninja`_ (which has a postgres script in the
+example directory), `backuppc`)_ (for versionning).
 
-Please note that in the *CubicWeb way* it adds a second location for your password which is error-prone.
+Please note that in the *CubicWeb way* it adds a second location for your
+password which is error-prone.
 
 .. _`here` : http://www.postgresql.org/docs/current/static/libpq-pgpass.html
 .. _`backup-ninja` : https://labs.riseup.net/code/projects/show/backupninja/
--- a/doc/book/en/admin/instance-config.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/admin/instance-config.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -49,13 +49,13 @@
    and `https://localhost/demo` and actually running on port 8080, it
    takes to the http:::
 
-     RewriteCond %(REQUEST_URI) ^/demo
+     RewriteCond %{REQUEST_URI} ^/demo
      RewriteRule ^/demo$ /demo/
      RewriteRule ^/demo/(.*) http://127.0.0.1:8080/$1 [L,P]
 
    and for the https:::
 
-     RewriteCond %(REQUEST_URI) ^/ demo
+     RewriteCond %{REQUEST_URI} ^/ demo
      RewriteRule ^/demo$/demo/
      RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P]
 
@@ -63,7 +63,7 @@
    and we will file in the all-in-one.conf of the instance:::
 
      base-url = http://localhost/demo
-     https-url = `https://localhost/demo`
+     https-url = https://localhost/demo
 
 Setting up the web client
 -------------------------
--- a/doc/book/en/admin/ldap.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/admin/ldap.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -1,3 +1,5 @@
+.. _LDAP:
+
 LDAP integration
 ================
 
--- a/doc/book/en/admin/setup.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/admin/setup.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -8,16 +8,17 @@
 Installation of `Cubicweb` and its dependencies
 -----------------------------------------------
 
-*CubicWeb* is packaged for Debian and Ubuntu, but can be installed from source
+|cubicweb| is packaged for Debian and Ubuntu, but can be installed from source
 using a tarball or the Mercurial version control system.
 
+
 .. _DebianInstallation:
 
 Debian and Ubuntu packages
 ```````````````````````````
 
-Depending on the distribution you are using, add the appropriate line to your list
-of sources (for example by editing ``/etc/apt/sources.list``).
+Depending on the distribution you are using, add the appropriate line to your
+list of sources (for example by editing ``/etc/apt/sources.list``).
 
 For Debian Lenny::
 
@@ -37,21 +38,26 @@
   apt-get update
   apt-get install cubicweb cubicweb-dev
 
-`cubicweb` installs the framework itself, allowing you to create
-new instances.
+
+`cubicweb` installs the framework itself, allowing you to create new instances.
+
+`cubicweb-dev` installs the development environment allowing you to develop new
+cubes.
 
-`cubicweb-dev` installs the development environment allowing you to
-develop new cubes.
+There is also a wide variety of cubes listed on the `CubicWeb.org Forge`_
+available as debian packages and tarball.
 
-There is also a wide variety of cubes listed on http://www.cubicweb.org/Project available as debian packages and tarball.
+The repositories are signed with `Logilab's gnupg key`_. To avoid warning on
+"apt-get update":
 
-The repositories are signed with `Logilab's gnupg key`_. To avoid warning on "apt-get update":
 1. become root using sudo
 2. download http://ftp.logilab.org/dists/logilab-dists-key.asc using e.g. wget
 3. run "apt-key add logilab-dists-key.asc"
 4. re-run apt-get update (manually or through the package manager, whichever you prefer)
 
 .. _`Logilab's gnupg key`: http://ftp.logilab.org/dists/logilab-dists-key.asc
+.. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
+
 
 .. _SourceInstallation:
 
@@ -66,6 +72,11 @@
 
 Make sure you have installed the dependencies (see appendixes for the list).
 
+|cubicweb| should soon be pip_ installable, stay tuned (expected in 3.8).
+
+.. _pip: http://pypi.python.org/pypi/pip
+
+
 Install from version control system
 ```````````````````````````````````
 
@@ -85,32 +96,31 @@
 
 Make sure you have installed the dependencies (see appendixes for the list).
 
+
 .. _WindowsInstallation:
 
 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.
+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::
+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).
+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::
+Then you must grab Twisted. There is a windows installer directly available from
+this page::
 
   http://twistedmatrix.com/trac/
 
@@ -129,11 +139,9 @@
 
   http://www.stickpeople.com/projects/python/win-psycopg/#Version2
 
-Please be careful to select the right python (2.5) and postgres (8.4)
-versions.
+Please be careful to select the right python (2.5) and postgres (8.4) versions.
 
-Pyro enables remote access to cubicweb repository instances. Get it
-there::
+Pyro enables remote access to cubicweb repository instances. Get it there::
 
   http://sourceforge.net/projects/pyro/files/
 
@@ -144,26 +152,26 @@
 
 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::
+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::
+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)::
+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::
+If you need to peruse mercurial over ssh, it can be helpful to get an ssh client
+like Putty::
 
   http://www.putty.org/
 
@@ -173,10 +181,9 @@
   http://www.vectrace.com/mercurialeclipse/
 
 Setting up the sources
-______________________
+~~~~~~~~~~~~~~~~~~~~~~
 
-You need to enable the mercurial forest extension. To do this, edit
-the file::
+You need to enable the mercurial forest extension. To do this, edit the file::
 
   C:\Program Files\TortoiseHg\Mercurial.ini
 
@@ -185,8 +192,8 @@
   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`.
+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:
 
@@ -194,28 +201,26 @@
 
 * 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.
+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).
+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.
+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::
+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
@@ -231,13 +236,13 @@
 ... and get a meaningful output.
 
 Running an instance as a service
---------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-This currently assumes that the instances configurations is located
-at C:\\etc\\cubicweb.d.
+This currently assumes that the instances configurations is located at
+C:\\etc\\cubicweb.d.
 
-For a cube 'my_cube', you will then find C:\\etc\\cubicweb.d\\my_cube\\win32svc.py
-that has to be used thusly::
+For a cube 'my_cube', you will then find
+C:\\etc\\cubicweb.d\\my_cube\\win32svc.py that has to be used thusly::
 
   win32svc install
 
@@ -248,17 +253,6 @@
 should start the service.
 
 
-PostgreSQL installation
-```````````````````````
-
-Please refer to the `PostgreSQL project online documentation`_.
-
-.. _`PostgreSQL project online documentation`: http://www.postgresql.org/
-
-You need to install the three following packages: `postgresql-8.3`,
-`postgresql-contrib-8.3` and `postgresql-plpython-8.3`.
-
-
 Other dependencies
 ``````````````````
 
@@ -271,103 +265,115 @@
 
 * `python-ldap` if you plan to use a LDAP source on the server
 
-.. _ConfigurationEnv:
 
-Environment configuration
--------------------------
-
-If you installed *CubicWeb* by cloning the Mercurial forest, then you
-will need to update the environment variable PYTHONPATH by adding
-the path to the forest ``cubicweb``:
-
-Add the following lines to either `.bashrc` or `.bash_profile` to configure
-your development environment ::
-
-    export PYTHONPATH=/full/path/to/cubicweb-forest
-
-If you installed *CubicWeb* with packages, no configuration is required and your
-new cubes will be placed in `/usr/share/cubicweb/cubes` and your instances
-will be placed in `/etc/cubicweb.d`.
-
-You may run a system-wide install of *CubicWeb* in "user mode" and use it for
-development by setting the following environment variable::
-
-    export CW_MODE=user
-    export CW_CUBES_PATH=~/lib/cubes
-    export CW_INSTANCES_DIR=~/etc/cubicweb.d/
-    export CW_INSTANCES_DATA_DIR=$CW_INSTANCES_DIR
-    export CW_RUNTIME_DIR=/tmp
-
-.. note::
-    The values given above are our suggestions but of course
-    can be different.
-
+.. _DatabaseInstallation:
 
 Databases configuration
 -----------------------
 
-.. _ConfigurationPostgresql:
+Whatever the backend used, database connection information are stored in the
+instance's :file:`sources` file. Currently cubicweb has been tested using
+Postgresql (recommanded), MySQL, SQLServer and SQLite.
+
+.. _PostgresqlConfiguration:
 
 PostgreSQL configuration
 ````````````````````````
 
-.. note::
-    If you already have an existing cluster and PostgreSQL server
-    running, you do not need to execute the initilization step
-    of your PostgreSQL database.
+For installation, please refer to the `PostgreSQL project online documentation`_.
+
+.. _`PostgreSQL project online documentation`: http://www.postgresql.org/
+
+You need to install the three following packages: `postgresql-8.X`,
+`postgresql-client-8.X`, and `postgresql-plpython-8.X`. If you run postgres
+version prior to 8.3, you'll also need the `postgresql-contrib-8.X` package for
+full-text search extension.
 
-* First, initialize the database PostgreSQL with the command ``initdb``.
+If you run postgres on another host than the |cubicweb| repository, you should
+install the `postgresql-client` package on the |cubicweb| host, and others on the
+database host.
+
+.. Note::
+
+    If you already have an existing cluster and PostgreSQL server running, you do
+    not need to execute the initilization step of your PostgreSQL database unless
+    you want a specific cluster for |cubicweb| databases or if your existing
+    cluster doesn't use the UTF8 encoding (see note below).
+
+* First, initialize a PostgreSQL cluster with the command ``initdb``.
   ::
 
-    $ initdb -D /path/to/pgsql
+    $ initdb -E UTF8 -D /path/to/pgsql
+
+  Notice the encoding specification. This is necessary since |cubicweb| usually
+  want UTF8 encoded database. If you use a cluster with the wrong encoding, you'll
+  get error like::
 
-  Once initialized, start the database server PostgreSQL
-  with the command::
+    new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII)
+    HINT:  Use the same encoding as in the template database, or use template0 as template.
+
+
+  Once initialized, start the database server PostgreSQL with the command::
 
     $ postgres -D /path/to/psql
 
-  If you cannot execute this command due to permission issues, please
-  make sure that your username has write access on the database.
-  ::
+  If you cannot execute this command due to permission issues, please make sure
+  that your username has write access on the database.  ::
 
     $ chown username /path/to/pgsql
 
-* The database authentication can be either set to `ident sameuser`
-  or `md5`.
-  If set to `md5`, make sure to use an existing user
-  of your database.
-  If set to `ident sameuser`, make sure that your
-  client's operating system user name has a matching user in
-  the database. If not, please do as follow to create a user::
+* The database authentication can be either set to `ident sameuser` or `md5`.  If
+  set to `md5`, make sure to use an existing user of your database.  If set to
+  `ident sameuser`, make sure that your client's operating system user name has a
+  matching user in the database. If not, please do as follow to create a user::
 
     $ su
     $ su - postgres
     $ createuser -s -P username
 
-  The option `-P` (for password prompt), will encrypt the password with
-  the method set in the configuration file ``pg_hba.conf``.
-  If you do not use this option `-P`, then the default value will be null
-  and you will need to set it with::
+  The option `-P` (for password prompt), will encrypt the password with the
+  method set in the configuration file :file:`pg_hba.conf`.  If you do not use this
+  option `-P`, then the default value will be null and you will need to set it
+  with::
 
     $ su postgres -c "echo ALTER USER username WITH PASSWORD 'userpasswd' | psql"
 
-  This login/password will be requested when you will create an
-  instance with `cubicweb-ctl create` to initialize the database of
-  your instance.
-
-.. note::
-    The authentication method can be configured in ``pg_hba.conf``.
+.. Note::
+    The authentication method can be configured in file:`pg_hba.conf`.
 
 
-.. FIXME Are these steps really necessary? It seemed to work without.
+The above login/password will be requested when you will create an instance with
+`cubicweb-ctl create` to initialize the database of your instance.
 
-* Installation of plain-text index extension ::
+Notice that the `cubicweb-ctl db-create` does database initialization that
+may requires a postgres superuser. That's why a login/password is explicitly asked
+at this step, so you can use there a superuser without using this user when running
+the instance. Things that require special privileges at this step:
+
+* database creation, require the 'create database' permission
+* install the plpython extension language (require superuser)
+* install the tsearch extension for postgres version prior to 8.3 (require superuser)
 
-    cat /usr/share/postgresql/8.3/contrib/tsearch2.sql | psql -U username template1
+To avoid using a super user each time you create an install, a nice trick is to
+install plpython (and tsearch when needed) on the special `template1` database,
+so they will be installed automatically when cubicweb databases are created
+without even with needs for special access rights. To do so, run ::
+
+  # Installation of plpythonu language by default ::
+  $ createlang -U pgadmin plpythonu template1
+  $ psql -U pgadmin template1
+  template1=# update pg_language set lanpltrusted=TRUE where lanname='plpythonu';
 
-* Installation of plpythonu language by default ::
+Where `pgadmin` is a postgres superuser. The last command is necessary since by
+default plpython is an 'untrusted' language and as such can't be used by non
+superuser. This update fix that problem by making it trusted.
 
-    createlang -U pgadmin plpythonu template1
+To install the tsearch plain-text index extension on postgres prior to 8.3, run::
+
+    cat /usr/share/postgresql/8.X/contrib/tsearch2.sql | psql -U username template1
+
+
+.. _MySqlConfiguration:
 
 MySql configuration
 ```````````````````
@@ -378,19 +384,22 @@
     default-character-set=utf8
     max_allowed_packet = 128M
 
-.. note::
+.. Note::
     It is unclear whether mysql supports indexed string of arbitrary lenght or
     not.
 
+
+.. _SQLServerConfiguration:
+
 SQLServer configuration
------------------------
+```````````````````````
 
-As of this writing, sqlserver support is in progress. You should be
-able to connect, create a database and go quite far, but some of the
-generated SQL is still currently not accepted by the backend.
+As of this writing, sqlserver support is in progress. You should be able to
+connect, create a database and go quite far, but some of the generated SQL is
+still currently not accepted by the backend.
 
-The `source` configuration file may look like this (specific parts
-only are shown)::
+The `source` configuration file may look like this (specific parts only are
+shown)::
 
   [system]
   db-driver=sqlserver2005
@@ -402,17 +411,43 @@
   db-encoding=utf8
 
 
+
+.. _SQLiteConfiguration:
+
+SQLite configuration
+````````````````````
+SQLite has the great advantage of requiring almost no configuration. Simply
+use 'sqlite' as db-driver, and set path to the dabase as db-name. Don't specify
+anything for db-user and db-password, they will be ignore anyway.
+
+.. Note::
+  SQLite is great for testing and to play with cubicweb but is not suited for
+  production environments.
+
+
+.. _PyroConfiguration:
+
 Pyro configuration
 ------------------
 
-If you use Pyro, it is required to have a name server Pyro running on your
-network (by default it is detected by a broadcast request).
+If you want to use Pyro to access your instance remotly, or to have multi-source
+or distributed configuration, it is required to have a name server Pyro running
+on your network. By by default it is detected by a broadcast request, but you can
+specify a location in the instance's configuration file.
 
 To do so, you need to :
 
-* launch the server manually before starting cubicweb as a server with
-  `pyro-nsd start`
+* launch the server manually before starting cubicweb as a server with `pyro-nsd
+  start`
+
+* under debian, edit the file :file:`/etc/default/pyro-nsd` so that the name
+  server pyro will be launched automatically when the machine fire up
+
 
-* edit the file ``/etc/default/pyro-nsd`` so that the name server pyro
-  will be launched automatically when the machine fire up
+Cubicweb resources configuration
+--------------------------------
 
+.. autodocstring:: cubicweb.cwconfig
+
+
+.. |cubicweb| replace:: *CubicWeb*
--- a/doc/book/en/annexes/cubicweb-ctl.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/annexes/cubicweb-ctl.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -104,7 +104,7 @@
 * ``i18ninstance``, recompiles the messages catalogs of an instance.
   This is automatically done while upgrading.
 
-See also chapter :ref:`internationalisation`.
+See also chapter :ref:`internationalization`.
 
 Other commands
 --------------
@@ -119,4 +119,4 @@
 specific Google AppEgine database, they are not available for now
 in cubicweb-ctl, but they are available in the instance created.
 
-For more details, please see :ref:`gaecontents` .
+For more details, please see :ref:`GoogleAppEngineSource` .
--- a/doc/book/en/annexes/faq.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/annexes/faq.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -150,7 +150,7 @@
 expression for a relation) instead of an ERQLExpression (rql
 expression for an entity).
 
-You can find additional information in the section :ref:`security`.
+You can find additional information in the section :ref:`securitymodel`.
 
 
 What is `Error while publishing rest text ...` ?
--- a/doc/book/en/conf.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/conf.py	Tue Apr 06 19:46:38 2010 +0200
@@ -19,6 +19,7 @@
 # serve to show the default value.
 
 import sys, os
+
 from cubicweb import __pkginfo__ as cw
 
 # If your extensions are in another directory, add it here. If the directory
@@ -31,7 +32,7 @@
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc']
+extensions = ['sphinx.ext.autodoc', 'logilab.common.sphinx_ext']
 autoclass_content = 'both'
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['.templates']
--- a/doc/book/en/development/datamodel/define-workflows.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/datamodel/define-workflows.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -1,5 +1,7 @@
 .. -*- coding: utf-8 -*-
 
+.. _Workflow:
+
 Define a Workflow
 =================
 
--- a/doc/book/en/development/datamodel/definition.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/datamodel/definition.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -11,21 +11,20 @@
 
 At this point, it is important to make clear the difference between
 *relation type* and *relation definition*: a *relation type* is only a relation
-name with potentially other additionnal properties (see below), whereas a
+name with potentially other additional properties (see below), whereas a
 *relation definition* is a complete triplet
 "<subject entity type> <relation type> <object entity type>".
-A relation type could have been implied if none is related to a
-relation definition of the schema.
 
-Also, it should be clear that to properly handle data migration, an instance'schema
-is stored in the database, so the python schema file used to defined it are only readen
+Also, it should be clear that to properly handle data migration, an
+instance's schema
+is stored in the database, so the python schema file used to defined it is only read
 when the instance is created or upgraded.
 
-The following built-in types are available : `String`, `Int`, `Float`,
+The following built-in types are available: `String`, `Int`, `Float`,
 `Decimal`, `Boolean`, `Date`, `Datetime`, `Time`, `Interval`, `Byte`
 and `Password`.
 
-You'll also have access to :ref:`base cubicweb entity types <CWBaseEntityTypes>`.
+You'll also have access to :ref:`base CubicWeb entity types <CWBaseEntityTypes>`.
 
 The instance schema is accessible through the .schema attribute of the
 `vregistry`.  It's an instance of :class:`cubicweb.schema.Schema`, which
@@ -33,117 +32,127 @@
 
 :note:
   In previous yams versions, almost all classes where available without
-  any import, but the should now be explicitely imported.
+  any import, but the should now be explicitly imported.
 
 
 Entity type
 ~~~~~~~~~~~
-It's an instance of :class:`yams.schema.EntitySchema`. Each entity types has
-a set of attributes and relation and some permissions, defining who can add, read,
+An entity type is an instance of :class:`yams.schema.EntitySchema`. Each entity type has
+a set of attributes and relations, and some permissions which define who can add, read,
 update or delete entities of this type.
 
 XXX yams inheritance
 
 Relation type
 ~~~~~~~~~~~~~
-It's an instance of :class:`yams.schema.RelationSchema`. A relation type is simply
-a semantic definition of a kind of relationship that may occurs in your application.
+A relation type is an instance of :class:`yams.schema.RelationSchema`. A relation type is simply
+a semantic definition of a kind of relationship that may occur in an application.
 
-It's important to choose a good name, at least to avoid conflicts with some semantically
-different relation defined in other cubes (since we've no namespace yet).
+It is important to choose a good name, at least to avoid conflicts with some semantically
+different relation defined in other cubes (since we've no name space yet).
 
-A relation type hold the following properties (which are hence shared between all
+A relation type holds the following properties (which are hence shared between all
 relation definitions of that type):
 
-* `inlined` : boolean handling the physical optimization for archiving
+* `inlined`: boolean handling the physical optimization for archiving
   the relation in the subject entity table, instead of creating a specific
   table for the relation. This applies to relations where cardinality
   of subject->relation->object is 0..1 (`?`) or 1..1 (`1`) for *all* its relation
   definitions.
 
-* `symmetric` : boolean indicating that the relation is symmetrical, which
+* `symmetric`: boolean indicating that the relation is symmetrical, which
   means that `X relation Y` implies `Y relation X`.
 
 
 Relation definition
 ~~~~~~~~~~~~~~~~~~~
-It's an instance of :class:`yams.schema.RelationDefinition`. It is a complete triplet
+A relation definition is an instance of :class:`yams.schema.RelationDefinition`. It is a complete triplet
 "<subject entity type> <relation type> <object entity type>".
 
+When creating a new instance of that class, the corresponding
+:class:`RelationType` instance is created on the fly if necessary.
+
+
 Properties
 ``````````
 
-* Optional properties for attributes and relations :
+* Optional properties for attributes and relations:
 
-  - `description` : a string describing an attribute or a relation. By default
+  - `description`: a string describing an attribute or a relation. By default
     this string will be used in the editing form of the entity, which means
     that it is supposed to help the end-user and should be flagged by the
     function `_` to be properly internationalized.
 
-  - `constraints` : a list of conditions/constraints that the relation has to
+  - `constraints`: a list of conditions/constraints that the relation has to
     satisfy (c.f. `Constraints`_)
 
-  - `cardinality` : a two character string which specify the cardinality of the
+  - `cardinality`: a two character string specifying the cardinality of the
     relation. The first character defines the cardinality of the relation on
     the subject, and the second on the object. When a relation can have
     multiple subjects or objects, the cardinality applies to all,
     not on a one-to-one basis (so it must be consistent...). The possible
-    values are inspired from regular expression syntax :
+    values are inspired from regular expression syntax:
 
     * `1`: 1..1
     * `?`: 0..1
     * `+`: 1..n
     * `*`: 0..n
 
-* optional properties for attributes :
+* optional properties for attributes:
 
-  - `unique` : boolean indicating if the value of the attribute has to be unique
+  - `unique`: boolean indicating if the value of the attribute has to be unique
     or not within all entities of the same type (false by default)
 
-  - `indexed` : boolean indicating if an index needs to be created for this
+  - `indexed`: boolean indicating if an index needs to be created for this
     attribute in the database (false by default). This is useful only if
     you know that you will have to run numerous searches on the value of this
     attribute.
 
-  - `default` : default value of the attribute. In case of date types, the values
+  - `default`: default value of the attribute. In case of date types, the values
     which could be used correspond to the RQL keywords `TODAY` and `NOW`.
 
-* optional properties of type `String` :
+* optional properties for type `String` attributes:
 
-  - `fulltextindexed` : boolean indicating if the attribute is part of
+  - `fulltextindexed`: boolean indicating if the attribute is part of
     the full text index (false by default) (*applicable on the type `Byte`
     as well*)
 
-  - `internationalizable` : boolean indicating if the value of the attribute
+  - `internationalizable`: boolean indicating if the value of the attribute
     is internationalizable (false by default)
 
-* optional properties for relations :
+* optional properties for relations:
 
-  - `composite` : string indicating that the subject (composite == 'subject')
+  - `composite`: string indicating that the subject (composite == 'subject')
     is composed of the objects of the relations. For the opposite case (when
     the object is composed of the subjects of the relation), we just set
     'object' as value. The composition implies that when the relation
     is deleted (so when the composite is deleted, at least), the composed are also deleted.
 
-  - `fti_container`: XXX feed me
+  - `fulltext_container`: string indicating if the value if the full text
+    indexation of the entity on one end of the relation should be used
+    to find the entity on the other end. The possible values are
+    'subject' or 'object'. For instance the use_email relation has
+    that property set to 'subject', since when performing a full text
+    search people want to find the entity using an email address, and not
+    the entity representing the email address.
 
 Constraints
 ```````````
 
-By default, the available constraint types are :
+By default, the available constraint types are:
 
 General Constraints
 ......................
 
-* `SizeConstraint` : allows to specify a minimum and/or maximum size on
+* `SizeConstraint`: allows to specify a minimum and/or maximum size on
   string (generic case of `maxsize`)
 
-* `BoundConstraint` : allows to specify a minimum and/or maximum value on
+* `BoundConstraint`: allows to specify a minimum and/or maximum value on
   numeric types
 
-* `UniqueConstraint` : identical to "unique=True"
+* `UniqueConstraint`: identical to "unique=True"
 
-* `StaticVocabularyConstraint` : identical to "vocabulary=(...)"
+* `StaticVocabularyConstraint`: identical to "vocabulary=(...)"
 
 XXX Attribute, TODAY, NOW
 
@@ -159,39 +168,19 @@
 displayed when the constraint fails. As RQLVocabularyConstraint never fails the
 third argument is not available.
 
-* `RQLConstraint` : allows to specify a RQL query that has to be satisfied
+* `RQLConstraint`: allows to specify a RQL query that has to be satisfied
   by the subject and/or the object of relation. In this query the variables
-  `S` and `O` are reserved for the entities subject and object of the
-  relation.
+  `S` and `O` are reserved for the relation subject and object entities.
 
-* `RQLVocabularyConstraint` : similar to the previous type of constraint except
+* `RQLVocabularyConstraint`: similar to the previous type of constraint except
   that it does not express a "strong" constraint, which means it is only used to
   restrict the values listed in the drop-down menu of editing form, but it does
   not prevent another entity to be selected.
 
-* `RQLUniqueConstraint` : allows to the specify a RQL query that ensure that an
+* `RQLUniqueConstraint`: allows to the specify a RQL query that ensure that an
   attribute is unique in a specific context. The Query must **never** return more
   than a single result to be satisfied. In this query the variables `S` is
-  reserved for the entity subject of the relation. The other variable should be
-  specified with the second constructor argument (mainvars). This constraints
-  should be used when UniqueConstraint doesn't fit. Here is a simple example ::
-
-    # Check that in the same Workflow each state's name is unique.  Using
-    # UniqueConstraint (or unique=True) here would prevent states in different
-    # workflows to have the same name.
-
-    # With: State S, Workflow W, String N ; S state_of W, S name N
-
-    RQLUniqueConstraint('S name N, S state_of WF, Y state_of WF, Y name N',
-                        mainvars='Y',
-                        msg=_('workflow already have a state of that name'))
-
-
-
-* `RQLUniqueConstraint` : allows to the specify a RQL query that ensure that an
-  attribute is unique in a specific context. The Query must **never** return more
-  than a single result to be satisfied. In this query the variables `S` is
-  reserved for the entity subject of the relation. The other variable should be
+  reserved for the relation subject entity. The other variables should be
   specified with the second constructor argument (mainvars). This constraints
   should be used when UniqueConstraint doesn't fit. Here is a simple example ::
 
@@ -209,11 +198,13 @@
 
 XXX note about how to add new constraint
 
+.. _securitymodel:
+
 
 The security model
 ~~~~~~~~~~~~~~~~~~
 
-The security model of `cubicWeb` is based on `Access Control List`.
+The security model of `CubicWeb` is based on `Access Control List`.
 The main principles are:
 
 * users and groups of users
@@ -223,24 +214,25 @@
 
 For *CubicWeb* in particular:
 
-* we associate rights at the enttities/relations schema level
-* for each entity, we distinguish four kind of permissions: read,
-  add, update and delete
-* for each relation, we distinguish three kinds of permissions: read,
-  add and delete (we can not modify a relation)
-* the basic groups are: Administrators, Users and Guests
-* by default, users belong to the group Users
-* there is a virtual group called `Owners` to which we
-  can associate only deletion and update permissions
-* we can not add users to the `Owners` group, they are
-  implicitly added to it according to the context of the objects
-  they own
-* the permissions of this group are only checked on update/deletion
-  actions if all the other groups the user belongs to does not provide
-  those permissions
+* we associate rights at the entities/relations schema level
+* for each entity, we distinguish four kinds of permissions: `read`,
+  `add`, `update` and `delete`
+* for each relation, we distinguish three kinds of permissions: `read`,
+  `add` and `delete` (it is not possible to `modify` a relation)
+* the default groups are: `administrators`, `users` and `guests`
+* by default, users belong to the `users` group
+* there is a virtual group called `owners` to which we
+  can associate only `delete` and `update` permissions
+
+  * we can not add users to the `Owners` group, they are
+    implicitly added to it according to the context of the objects
+    they own
+  * the permissions of this group are only checked on `update`/`delete`
+    actions if all the other groups the user belongs to do not provide
+    those permissions
 
 Setting permissions is done with the attribute `__permissions__` of entities and
-relation types. It defines a dictionary where the keys are the access types
+relation types. The value of this attribute is a dictionary where the keys are the access types
 (action), and the values are the authorized groups or expressions.
 
 For an entity type, the possible actions are `read`, `add`, `update` and
@@ -250,7 +242,7 @@
 
 For each access type, a tuple indicates the name of the authorized groups and/or
 one or multiple RQL expressions to satisfy to grant access. The access is
-provided if the user is in one of the listed groups or one of if the RQL condition
+provided if the user is in one of the listed groups or if one of the RQL condition
 is satisfied.
 
 The standard user groups
@@ -262,14 +254,14 @@
 
 * `managers`
 
-* `owners` : virtual group corresponding to the entity's owner.
+* `owners`: virtual group corresponding to the entity's owner.
   This can only be used for the actions `update` and `delete` of an entity
   type.
 
 It is also possible to use specific groups if they are defined in the
-precreate of the cube (``migration/precreate.py``). Defining groups in
-postcreate or even later makes them NOT available for security
-purposes (in this case, an `sync_schema_props_perms` command have to
+precreate script of the cube (``migration/precreate.py``). Defining groups in
+postcreate script or later makes them unavailable for security
+purposes (in this case, an `sync_schema_props_perms` command has to
 be issued in a CubicWeb shell).
 
 
@@ -278,13 +270,13 @@
 It is possible to define RQL expression to provide update permission
 (`add`, `delete` and `update`) on relation and entity types.
 
-RQL expression for entity type permission :
+RQL expression for entity type permission:
 
 * you have to use the class `ERQLExpression`
 
 * the used expression corresponds to the WHERE statement of an RQL query
 
-* in this expression, the variables X and U are pre-defined references
+* in this expression, the variables `X` and `U` are pre-defined references
   respectively on the current entity (on which the action is verified) and
   on the user who send the request
 
@@ -295,19 +287,19 @@
   to this variable
 
 For RQL expressions on a relation type, the principles are the same except
-for the following :
+for the following:
 
 * you have to use the class `RRQLExpression` in the case of a non-final relation
 
-* in the expression, the variables S, O and U are pre-defined references
+* in the expression, the variables `S`, `O` and `U` are pre-defined references
   to respectively the subject and the object of the current relation (on
   which the action is being verified) and the user who executed the query
 
 * we can also define rights over attributes of an entity (non-final relation),
-  knowing that :
+  knowing that:
 
   - to define RQL expression, we have to use the class `ERQLExpression`
-    in which X represents the entity the attribute belongs to
+    in which `X` represents the entity the attribute belongs to
 
   - the permissions `add` and `delete` are equivalent. Only `add`/`read`
     are actually taken in consideration.
@@ -331,7 +323,7 @@
 Use of RQL expression for reading rights
 ````````````````````````````````````````
 
-The principles are the same but with the following restrictions :
+The principles are the same but with the following restrictions:
 
 * we can not use `RRQLExpression` on relation types for reading
 
@@ -346,19 +338,28 @@
 Entity type definition
 ~~~~~~~~~~~~~~~~~~~~~~
 
-An entity type is defined by a Python class which inherits from `EntityType`.
-The class definition contains the description of attributes and relations
-for the defined entity type.
-The class name corresponds to the entity type name. It is exepected to be
-defined in the module ``mycube.schema``.
+An entity type is defined by a Python class which inherits from
+:class:`yams.buildobjs.EntityType`.  The class definition contains the
+description of attributes and relations for the defined entity type.
+The class name corresponds to the entity type name. It is expected to
+be defined in the module ``mycube.schema``.
+
+:Note on schema definition:
+
+ The code in ``mycube.schema`` is not meant to be executed. The class
+ EntityType mentioned above is different from the EntitySchema class
+ described in the previous chapter. EntityType is a helper class to
+ make Entity definition easier. Yams will process EntityType classes
+ and create EntitySchema instances from these class definitions. Similar
+ manipulation happen for relations.
 
 When defining a schema using python files, you may use the following shortcuts:
 
-- `required` : boolean indicating if the attribute is required, eg subject cardinality is '1'
+- `required`: boolean indicating if the attribute is required, ed subject cardinality is '1'
 
-- `vocabulary` : specify static possible values of an attribute
+- `vocabulary`: specify static possible values of an attribute
 
-- `maxsize` : integer providing the maximum size of a string (no limit by default)
+- `maxsize`: integer providing the maximum size of a string (no limit by default)
 
 For example:
 
@@ -380,6 +381,20 @@
 birth and a relation that connects a `Person` to another entity of type
 `Company` through the semantic `works_for`.
 
+:Naming convention:
+
+ Entity class names must start with an uppercase letter. The common
+ usage is to use ``CamelCase`` names.
+
+ Attribute and relation names must start with a lowercase letter. The
+ common usage is to use ``underscore_separated_words``. Attribute and
+ relation names starting with a single underscore are permitted, to
+ denote a somewhat "protected" or "private" attribute.
+
+ In any case, identifiers starting with "CW" or "cw" are reserved for
+ internal use by the framework.
+
+
 The name of the Python attribute corresponds to the name of the attribute
 or the relation in *CubicWeb* application.
 
@@ -388,29 +403,16 @@
     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 satisfy (see `Properties`_
 for more details).
 
-
-* relations can be defined by using `ObjectRelation` or `SubjectRelation`.
-  The first argument of `SubjectRelation` or `ObjectRelation` gives respectively
-  the object/subject entity type of the relation. This could be :
-
-  * a string corresponding to an entity type
-
-  * a tuple of string corresponding to multiple entity types
-
-  * special string such as follows :
-
-    - "**" : all types of entities
-    - "*" : all types of non-meta entities
-    - "@" : all types of meta entities but not system entities (e.g. used for
-      the basic schema description)
-
 * it is possible to use the attribute `meta` to flag an entity type as a `meta`
   (e.g. used to describe/categorize other entities)
 
-*Note* : if you end up with an `if` in the definition of your entity, this probably
+.. XXX the paragraph below needs clarification and / or moving out in
+.. another place
+
+*Note*: if you end up with an `if` in the definition of your entity, this probably
 means that you need two separate entities that implement the `ITree` interface and
 get the result from `.children()` which ever entity is concerned.
 
@@ -438,20 +440,49 @@
     subject = '*'
     object = 'CWUser'
 
-In the case of simultaneous relations definitions, `subject` and `object`
-can both be equal to the value of the first argument of `SubjectRelation`
-and `ObjectRelation`.
+If provided, the `subject` and `object` attributes denote the subject
+and object of the various relation definitions related to the relation
+type. Allowed values for these attributes are:
+
+* a string corresponding to an entity type
+* a tuple of string corresponding to multiple entity types
+* special string such as follows:
+
+  - "**": all types of entities
+  - "*": all types of non-meta entities
+  - "@": all types of meta entities but not system entities (e.g. used for
+    the basic schema description)
 
 When a relation is not inlined and not symmetrical, and it does not require
-specific permissions, its definition (by using `SubjectRelation` and
-`ObjectRelation`) is all we need.
+specific permissions, it can be defined using a `SubjectRelation`
+attribute in the EntityType class. The first argument of `SubjectRelation` gives
+the entity type for the object of the relation.
+
+:Naming convention:
+
+ Although this way of defining relations uses a Python class, the
+ naming convention defined earlier prevails over the PEP8 conventions
+ used in the framework: relation type class names use
+ ``underscore_separated_words``. 
 
+:Historical note:
+
+   It has been historically possible to use `ObjectRelation` which
+   defines a relation in the opposite direction. This feature is soon to be
+   deprecated and therefore should not be used in newly written code.
+
+:Future deprecation note:
+
+  In an even more remote future, it is quite possible that the
+  SubjectRelation shortcut will become deprecated, in favor of the
+  RelationType declaration which offers some advantages in the context
+  of reusable cubes.
 
 Definition of permissions
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 The entity type `CWPermission` from the standard library
 allows to build very complex and dynamic security architectures. The schema of
-this entity type is as follow :
+this entity type is as follow:
 
 .. sourcecode:: python
 
@@ -459,12 +490,12 @@
         """entity type that may be used to construct some advanced security configuration
         """
         name = String(required=True, indexed=True, internationalizable=True, maxsize=100)
- require_group = SubjectRelation('CWGroup', cardinality='+*',
+        require_group = SubjectRelation('CWGroup', cardinality='+*',
                                         description=_('groups to which the permission is granted'))
- require_state = SubjectRelation('State',
+        require_state = SubjectRelation('State',
                                         description=_("entity's state in which the permission is applicable"))
         # can be used on any entity
- require_permission = ObjectRelation('**', cardinality='*1', composite='subject',
+        require_permission = ObjectRelation('**', cardinality='*1', composite='subject',
                                             description=_("link a permission to the entity. This "
                                                           "permission should be used in the security "
                                                           "definition of the entity's type to be useful."))
@@ -500,7 +531,7 @@
 
 This configuration indicates that an entity `CWPermission` named
 "add_version" can be associated to a project and provides rights to create
-new versions on this project to specific groups. It is important to notice that :
+new versions on this project to specific groups. It is important to notice that:
 
 * in such case, we have to protect both the entity type "Version" and the relation
   associating a version to a project ("version_of")
--- a/doc/book/en/development/devcore/appobject.rst	Thu Mar 04 17:56:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-
-
-The `AppObject` class
-~~~~~~~~~~~~~~~~~~~~~
-
-In general:
-
-* we do not inherit directly from this class but from a more specific
-  class such as `AnyEntity`, `EntityView`, `AnyRsetView`,
-  `Action`...
-
-* to be recordable, a subclass has to define its own register (attribute
-  `__registry__`) and its identifier (attribute `id`). Usually we do not have
-  to take care of the register, only the identifier `id`.
-
-We can find a certain number of attributes and methods defined in this class
-and common to all the application objects.
-
-At recording time, the following attributes are dynamically added to
-the *subclasses*:
-
-* `vreg`, the `vregistry` of the instance
-* `schema`, the instance schema
-* `config`, the instance configuration
-
-We also find on instances, the following attributes:
-
-* ._cw`, `Request` instance
-* `rset`, the *result set* associated to the object if necessary
-
-:URL handling:
-  * `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:
-
-  * `entity(row, col=0)`, returns the entity corresponding to the data position
-    in the *result set* associated to the object
-
-  * `complete_entity(row, col=0, skip_bytes=True)`, is equivalent to `entity` but
-    also call the method `complete()` on the entity before returning it
-
-:Data formatting:
-  * `format_date(date, date_format=None, time=False)` returns a string for a
-    date time according to instance's configuration
-  * `format_time(time)` returns a string for a date time according to
-    instance's configuration
-
-:And more...:
-
-  * `tal_render(template, variables)`, renders a precompiled page template with
-    variables in the given dictionary as context
-
-.. note::
-  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 must write: ::
-
-      class Truc(PrimaryView):
-          def f(self, arg1):
-              super(Truc, self).f(arg1)
--- a/doc/book/en/development/devcore/cwconfig.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/devcore/cwconfig.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -1,5 +1,5 @@
 :mod:`Configuration <cubicweb.cwconfig>`
 ----------------------------------------
 
-.. automodule:: cubicweb.cwconfig
-   :members:
+.. .. automodule:: cubicweb.cwconfig
+..   :members:
--- a/doc/book/en/development/devcore/index.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/devcore/index.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -4,9 +4,7 @@
 .. toctree::
    :maxdepth: 1
 
-   vreg.rst
-   appobject.rst
-   selectors.rst
    dbapi.rst
+   reqbase.rst
    cwconfig.rst
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/development/devcore/reqbase.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,30 @@
+
+Those are methods you'll find on both request objects and on repository session:
+
+:URL handling:
+  * `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 formatting:
+  * `format_date(date, date_format=None, time=False)` returns a string for a
+    date time according to instance's configuration
+
+  * `format_time(time)` returns a string for a date time according to
+    instance's configuration
+
+:And more...:
+
+  * `tal_render(template, variables)`, renders a precompiled page template with
+    variables in the given dictionary as context
+
+
+Result set methods:
+
+  * `get_entity(row, col)`, returns the entity corresponding to the data position
+    in the *result set*
+
+  * `complete_entity(row, col, skip_bytes=True)`, is equivalent to `get_entity` but
+    also call the method `complete()` on the entity before returning it
+
+
--- a/doc/book/en/development/devcore/selectors.rst	Thu Mar 04 17:56:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-Base selectors
---------------
-
-Selectors are scoring functions that are called by the registry to tell whenever
-an appobject can be selected in a given context. Selector sets are for instance
-the glue that tie views to the data model. Using them appropriately is an
-essential part of the construction of well behaved cubes.
-
-Of course you may have to write your own set of selectors as your needs grows and
-you get familiar with the framework (see :ref:CustomSelectors).
-
-Here is a description of generic selectors provided by CubicWeb that should suit
-most of your needs.
-
-Bare selectors
-~~~~~~~~~~~~~~
-Those selectors are somewhat dumb, which doesn't mean they're not (very) useful.
-
-.. autoclass:: cubicweb.appobject.yes
-.. autoclass:: cubicweb.selectors.match_kwargs
-.. autoclass:: cubicweb.selectors.appobject_selectable
-
-
-Result set selectors
-~~~~~~~~~~~~~~~~~~~~~
-Those selectors are looking for a result set in the context ('rset' argument or
-the input context) and match or not according to its shape. Some of these
-selectors have different behaviour if a particular cell of the result set is
-specified using 'row' and 'col' arguments of the input context or not.
-
-.. autoclass:: cubicweb.selectors.none_rset
-.. autoclass:: cubicweb.selectors.any_rset
-.. autoclass:: cubicweb.selectors.nonempty_rset
-.. autoclass:: cubicweb.selectors.empty_rset
-.. autoclass:: cubicweb.selectors.one_line_rset
-.. autoclass:: cubicweb.selectors.multi_lines_rset
-.. autoclass:: cubicweb.selectors.multi_columns_rset
-.. autoclass:: cubicweb.selectors.paginated_rset
-.. autoclass:: cubicweb.selectors.sorted_rset
-.. autoclass:: cubicweb.selectors.one_etype_rset
-.. autoclass:: cubicweb.selectors.multi_etypes_rset
-
-
-Entity selectors
-~~~~~~~~~~~~~~~~
-Those selectors are looking for either an `entity` argument in the input context,
-or entity found in the result set ('rset' argument or the input context) and
-match or not according to entity's (instance or class) properties.
-
-.. autoclass:: cubicweb.selectors.non_final_entity
-.. autoclass:: cubicweb.selectors.implements
-.. autoclass:: cubicweb.selectors.score_entity
-.. autoclass:: cubicweb.selectors.rql_condition
-.. autoclass:: cubicweb.selectors.relation_possible
-.. autoclass:: cubicweb.selectors.partial_relation_possible
-.. autoclass:: cubicweb.selectors.has_related_entities
-.. autoclass:: cubicweb.selectors.partial_has_related_entities
-.. autoclass:: cubicweb.selectors.has_permission
-.. autoclass:: cubicweb.selectors.has_add_permission
-
-
-Logged user selectors
-~~~~~~~~~~~~~~~~~~~~~
-Those selectors are looking for properties of the user issuing the request.
-
-.. autoclass:: cubicweb.selectors.anonymous_user
-.. autoclass:: cubicweb.selectors.authenticated_user
-.. autoclass:: cubicweb.selectors.match_user_groups
-
-
-Web request selectors
-~~~~~~~~~~~~~~~~~~~~~
-Those selectors are looking for properties of *web* request, they can not be
-used on the data repository side.
-
-.. autoclass:: cubicweb.selectors.match_form_params
-.. autoclass:: cubicweb.selectors.match_search_state
-.. autoclass:: cubicweb.selectors.match_context_prop
-.. autoclass:: cubicweb.selectors.match_view
-.. autoclass:: cubicweb.selectors.primary_view
-.. autoclass:: cubicweb.selectors.specified_etype_implements
-
-
-Other selectors
-~~~~~~~~~~~~~~~
-.. autoclass:: cubicweb.selectors.match_transition
-
-You'll also find some other (very) specific selectors hidden in other modules
-than :module:`cubicweb.selectors`.
\ No newline at end of file
--- a/doc/book/en/development/devcore/vreg.rst	Thu Mar 04 17:56:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,220 +0,0 @@
-The VRegistry
---------------
-
-The recording process on startup
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Details of the recording process
-````````````````````````````````
-
-.. index::
-   vregistry: registration_callback
-
-On startup, |cubicweb| have to fill the vregistry with appobjects defined
-in its library and in cubes used by the instance. Appobjects from the library
-are loaded first, then appobjects provided by cubes are loaded in an ordered
-way (e.g. if your cube depends on an other, appobjects from the dependancy will
-be loaded first). Cube's modules or packages where appobject are looked at is explained
-in :ref:`cubelayout`.
-
-For each module:
-
-* by default all objects are registered automatically
-
-* if some objects have to replace other objects or be included only if a
-  condition is true, you'll have to define a `registration_callback(vreg)`
-  function in your module and explicitly register *all objects* in this
-  module, using the vregistry api defined below.
-
-.. note::
-    Once the function `registration_callback(vreg)` is implemented, all the objects
-    have to be explicitly registered as it disables the automatic object registering.
-
-
-API d'enregistrement des objets
-```````````````````````````````
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_all
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_and_replace
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_if_interface_found
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.unregister
-
-
-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)
-
-   # goa/appobjects/sessions.py
-   def registration_callback(vreg):
-      vreg.register(SessionsCleaner)
-      # replace AuthenticationManager by GAEAuthenticationManager 
-      vreg.register_and_replace(GAEAuthenticationManager, AuthenticationManager)
-      # replace PersistentSessionManager by GAEPersistentSessionManager
-      vreg.register_and_replace(GAEPersistentSessionManager, PersistentSessionManager)
-
-
-Runtime objects selection
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Using and combining existant selectors
-``````````````````````````````````````
-
-The object's selector is defined by its `__select__` class attribute.
-
-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
-means that one of them should return a positive score. On success, the first
-positive score is returned.
-
-You can also "negate"  a selector by precedeing it by the `~` operator.
-
-Of course you can use paren to balance expressions.
-
-
-For instance, if you are selecting the primary (eg `__regid__ = '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
-
-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
-````````
-
-The goal: when on a Blog, one wants the RSS link to refer to blog
-entries, not to the blog entity itself.
-
-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.
-
-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).
-
-Hence we have two cases here, one for a single-entity rsets, the other
-for multi-entities rsets.
-
-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:
-
-* the inherited selection criteria (one has to look them up in the
-  class hierarchy to know the details)
-
-* non_final_entity, which filters on rsets containing non final
-  entities (a 'final entity' being synonym for entity attribute)
-
-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.
-
-Of course, once this is done, you have to:
-
-* fill in the call method of EntityRSSIconBox
-
-* provide the default implementation of the method returning the RSS
-  stream url on AnyEntity
-
-* redefine this method on Blog.
-
-When to use selectors?
-``````````````````````
-
-Selectors are to be used whenever arises the need of dispatching on the shape or
-content of a result set or whatever else context (value in request form params,
-authenticated user groups, etc...). That is, almost all the time.
-
-XXX add and example of a single view w/ big "if" inside splitted into two views
-with appropriate selectors.
-
-
-.. CustomSelectors_
-
-Defining your own selectors
-```````````````````````````
-.. autoclass:: cubicweb.appobject.Selector
-   :members: __call__
-
-.. autofunction:: cubicweb.appobject.objectify_selector
-.. autofunction:: cubicweb.selectors.lltrace
-
-Selectors __call__ should *always* return a positive integer, and shall never
-return `None`.
-
-Useful abstract base classes for 'entity' selectors:
-
-.. autoclass:: cubicweb.selectors.EClassSelector
-.. autoclass:: cubicweb.selectors.EntitySelector
-
-
-Debugging
-`````````
-
-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, *if you're running your instance in debug mode*.
-
-Here is an example:
-
-.. sourcecode:: python
-
-     from cubicweb.selectors import traced_selection
-     with traced_selection():
-         mycomp = self._cw.vreg['views'].select('wfhistory', self._cw, rset=rset)
-
-Don't forget the 'from __future__ import with_statement' at the module
-top-level if you're using python 2.5.
-
-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'>
-
-You can also give to traced_selection the registry ids of objects on which to debug
-you want to debug selection ('wfhistory' in the example above).
-
-Also, if you're using python 2.4, which as no 'with' yet, you'll have to to it
-the following way:
-
-.. sourcecode:: python
-
-         from cubicweb import selectors
-         selectors.TRACED_OIDS = ('wfhistory',)
-         mycomp = self._cw.vreg['views'].select('wfhistory', self._cw, rset=rset)
-         selectors.TRACED_OIDS = ()
--- a/doc/book/en/development/devweb/index.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/devweb/index.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -12,7 +12,6 @@
    property
    rtags
    views
-   gettingdata
    form
    facets
    httpcaching
--- a/doc/book/en/development/devweb/internationalization.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/devweb/internationalization.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -15,7 +15,7 @@
 
 * in your Python code and cubicweb-tal templates : mark translatable strings
 
-* in your instance : handle the translation catalog
+* in your instance : handle the translation catalog, edit translations
 
 String internationalization
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -50,8 +50,9 @@
 itself, but not its translation (it's actually another name for the
 `unicode` builtin).
 
-In the other hand the request's method `self._cw._` is meant to retrieve the
-proper translation of translation strings in the requested language.
+In the other hand the request's method `self._cw._` is also meant to
+retrieve the proper translation of translation strings in the
+requested language.
 
 Finally you can also use the `__` attribute of request object to get a
 translation for a string *which should not itself added to the catalog*,
@@ -62,14 +63,15 @@
 In this example ._cw.__` is used instead of ._cw._` so we don't have 'This %s' in
 messages catalogs.
 
-
 Translations in cubicweb-tal template can also be done with TAL tags
 `i18n:content` and `i18n:replace`.
 
-
 If you need to add messages on top of those that can be found in the source,
 you can create a file named `i18n/static-messages.pot`.
 
+You could put there messages not found in the python sources or
+overrides for some messages of used cubes.
+
 Generated string
 ````````````````
 
@@ -80,20 +82,20 @@
 For exemple the following schema ::
 
   Class EntityA(EntityType):
-      relationa2b = SubjectRelation('EntityB')
+      relation_a2b = SubjectRelation('EntityB')
 
   class EntityB(EntityType):
       pass
 
 May generate the following message ::
 
-  add Execution has_export File subject
+  add EntityA relation_a2b EntityB subject
 
 This message will be used in views of ``EntityA`` for creation of a new
 ``EntityB`` with a preset relation ``relation_a2b`` between the current
 ``EntityA`` and the new ``EntityB``. The opposite message ::
 
-  add Execution has_export File object
+  add EntityA relation_a2b EntityB object
 
 Is used for similar creation of an ``EntityA`` from a view of ``EntityB``. The
 title of they respective creation form will be ::
@@ -105,8 +107,8 @@
 In the translated string you can use ``%(linkto)s`` for reference to the source
 ``entity``.
 
-Handle the translation catalog
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Handling the translation catalog
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Once the internationalization is done in your code, you need to populate and
 update the translation catalog. Cubicweb provides the following commands for this
@@ -117,11 +119,11 @@
   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
-  executed you must update the translation files *.po* in the "i18n"
-  directory of your template. This command will of course not remove
-  existing translations still in use.
+* `i18ncube` updates the translation catalogs of *one particular cube*
+  (or of all cubes). After this command is executed you must update
+  the translation files *.po* in the "i18n" directory of your
+  cube. This command will of course not remove existing translations
+  still in use. It will mark unused translation but not remove them.
 
 * `i18ninstance` recompiles the translation catalogs of *one particular
   instance* (or of all instances) after the translation catalogs of
@@ -134,6 +136,7 @@
 
 Example
 ```````
+
 You have added and/or modified some translation strings in your cube
 (after creating a new view or modifying the cube's schema for exemple).
 To update the translation catalogs you need to do:
@@ -143,3 +146,77 @@
 3. `hg ci -m "updated i18n catalogs"`
 4. `cubicweb-ctl i18ninstance <myinstance>`
 
+Editing po files
+~~~~~~~~~~~~~~~~
+
+Using a PO aware editor
+````````````````````````
+
+Many tools exist to help maintain .po (PO) files. Common editors or
+development environment provides modes for these. One can also find
+dedicated PO files editor, such as `poedit`_.
+
+.. _`poedit`:  http://www.poedit.net/
+
+While usage of such a tool is commendable, PO files are perfectly
+editable with a (unicode aware) plain text editor. It is also useful
+to know their structure for troubleshooting purposes.
+
+Structure of a PO file
+``````````````````````
+
+In this section, we selectively quote passages of the `GNU gettext`_
+manual chapter on PO files, available there::
+
+ http://www.gnu.org/software/hello/manual/gettext/PO-Files.html
+
+One PO file entry has the following schematic structure::
+
+     white-space
+     #  translator-comments
+     #. extracted-comments
+     #: reference...
+     #, flag...
+     #| msgid previous-untranslated-string
+     msgid untranslated-string
+     msgstr translated-string
+
+
+A simple entry can look like this::
+
+     #: lib/error.c:116
+     msgid "Unknown system error"
+     msgstr "Error desconegut del sistema"
+
+It is also possible to have entries with a context specifier. They
+look like this::
+
+     white-space
+     #  translator-comments
+     #. extracted-comments
+     #: reference...
+     #, flag...
+     #| msgctxt previous-context
+     #| msgid previous-untranslated-string
+     msgctxt context
+     msgid untranslated-string
+     msgstr translated-string
+
+
+The context serves to disambiguate messages with the same
+untranslated-string. It is possible to have several entries with the
+same untranslated-string in a PO file, provided that they each have a
+different context. Note that an empty context string and an absent
+msgctxt line do not mean the same thing.
+
+Contexts and CubicWeb
+`````````````````````
+
+CubicWeb PO files have both non-contextual and contextual msgids.
+
+Contextual entries are automatically used in some cases. For instance,
+entity.dc_type(), eschema.display_name(req) or display_name(etype,
+req, form, context) methods/function calls will use them.
+
+It is also possible to explicitly use the with _cw.pgettext(context,
+msgid).
--- a/doc/book/en/development/devweb/js.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/devweb/js.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -40,11 +40,238 @@
   snippet inline in the html headers. This is quite useful for setting
   up early jQuery(document).ready(...) initialisations.
 
-Overview of what's available
+CubicWeb javascript events
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ``server-response``: this event is triggered on HTTP responses (both
+  standard and ajax). The two following extra parameters are passed
+  to callbacks :
+
+  - ``ajax``: a boolean that says if the reponse was issued by an
+    ajax request
+
+  - ``node``: the DOM node returned by the server in case of an
+    ajax request, otherwise the document itself for standard HTTP
+    requests.
+
+Important AJAX APIS
+~~~~~~~~~~~~~~~~~~~
+
+* `jQuery.fn.loadxhtml` is an important extension to jQuery which
+  allow proper loading and in-place DOM update of xhtml views. It is
+  suitably augmented to trigger necessary events, and process CubicWeb
+  specific elements such as the facet system, fckeditor, etc.
+
+* `asyncRemoteExec` and `remoteExec` are the base building blocks for
+  doing arbitrary async (resp. sync) communications with the server
+
+A simple example with asyncRemoteExec
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the python side, we have to extend the BaseController class. The
+@jsonize decorator ensures that the `return value` of the method is
+encoded as JSON data. By construction, the JSonController inputs
+everything in JSON format.
+
+.. sourcecode: python
+
+    from cubicweb.web.views.basecontrollers import JSonController, jsonize
+
+    @monkeypatch(JSonController)
+    @jsonize
+    def js_say_hello(self, name):
+        return u'hello %s' % name
+
+In the javascript side, we do the asynchronous call. Notice how it
+creates a `deferred` object. Proper treatment of the return value or
+error handling has to be done through the addCallback and addErrback
+methods.
+
+.. sourcecode: javascript
+
+    function async_hello(name) {
+        var deferred = asyncRemoteExec('say_hello', name);
+        deferred.addCallback(function (response) {
+            alert(response);
+        });
+        deferred.addErrback(function () {
+            alert('something fishy happened');
+        });
+     }
+
+     function sync_hello(name) {
+         alert( remoteExec('say_hello', name) );
+     }
+
+A simple example with loadxhtml
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here we are concerned with the retrieval of a specific view to be
+injected in the live DOM. The view will be of course selected
+server-side using an entity eid provided by the client side.
+
+.. sourcecode: python
+
+    from cubicweb import typed_eid
+    from cubicweb.web.views.basecontrollers import JSonController, xhtmlize
+
+    @monkeypatch(JSonController)
+    @xhtmlize
+    def js_frob_status(self, eid, frobname):
+        entity = self._cw.entity_from_eid(typed_eid(eid))
+        return entity.view('frob', name=frobname)
+
+.. sourcecode: javascript
+
+    function update_some_div(divid, eid, frobname) {
+        var params = {fname:'frob_status', eid: eid, frobname:frobname};
+        jQuery('#'+divid).loadxhtml(JSON_BASE_URL, params, 'post');
+     }
+
+In this example, the url argument is the base json url of a cube
+instance (it should contain something like
+`http://myinstance/json?`). The actual JSonController method name is
+encoded in the `params` dictionnary using the `fname` key.
+
+A more real-life example from CubicWeb
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A frequent use case of Web 2 applications is the delayed (or
+on-demand) loading of pieces of the DOM. This is typically achieved
+using some preparation of the initial DOM nodes, jQuery event handling
+and proper use of loadxhtml.
+
+We present here a skeletal version of the mecanism used in CubicWeb
+and available in web/views/tabs.py, in the `LazyViewMixin` class.
+
+.. sourcecode: python
+
+    def lazyview(self, vid, rql=None):
+        """ a lazy version of wview """
+        w = self.w
+        self._cw.add_js('cubicweb.lazy.js')
+        urlparams = {'vid' : vid, 'fname' : 'view'}
+        if rql is not None:
+            urlparams['rql'] = rql
+        w(u'<div id="lazy-%s" cubicweb:loadurl="%s">' % (
+            vid, xml_escape(self._cw.build_url('json', **urlparams))))
+        w(u'</div>')
+        self._cw.add_onload(u"""
+            jQuery('#lazy-%(vid)s').bind('%(event)s', function() {
+                   load_now('#lazy-%(vid)s');});"""
+            % {'event': 'load_%s' % vid, 'vid': vid})
+
+This creates a `div` with an specific event associated to it.
+
+The full version deals with:
+
+* optional parameters such as an entity eid, an rset
+
+* the ability to further reload the fragment
+
+* the ability to display a spinning wheel while the fragment is still
+  not loaded
+
+* handling of browsers that do not support ajax (search engines,
+  text-based browsers such as lynx, etc.)
+
+The javascript side is quite simple, due to loadxhtml awesomeness.
+
+.. sourcecode: javascript
+
+    function load_now(eltsel) {
+        var lazydiv = jQuery(eltsel);
+        lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
+    }
+
+This is all significantly different of the previous `simple example`
+(albeit this example actually comes from real-life code).
+
+Notice how the `cubicweb:loadurl` is used to convey the url
+information. The base of this url is similar to the global javascript
+JSON_BASE_URL. According to the pattern described earlier,
+the `fname` parameter refers to the standard `js_view` method of the
+JSonController. This method renders an arbitrary view provided a view
+id (or `vid`) is provided, and most likely an rql expression yielding
+a result set against which a proper view instance will be selected.
+
+The `cubicweb:loadurl` is one of the 29 attributes extensions to XHTML
+in a specific cubicweb namespace. It is a means to pass information
+without breaking HTML nor XHTML compliance and without resorting to
+ungodly hacks.
+
+Given all this, it is easy to add a small nevertheless useful feature
+to force the loading of a lazy view (for instance, a very
+computation-intensive web page could be scinded into one fast-loading
+part and a delayed part).
+
+In the server side, a simple call to a javascript function is
+sufficient.
+
+.. sourcecode: python
+
+    def forceview(self, vid):
+        """trigger an event that will force immediate loading of the view
+        on dom readyness
+        """
+        self._cw.add_onload("trigger_load('%s');" % vid)
+
+The browser-side definition follows.
+
+.. sourcecode: javascript
+
+    function trigger_load(divid) {
+        jQuery('#lazy-' + divd).trigger('load_' + divid);
+    }
+
+
+Anatomy of a lodxhtml call
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The loadxhtml extension to jQuery accept many parameters with rich
+semantics. Let us detail these.
+
+* `url` (mandatory) should be a complete url, typically based on the
+  JSonController, but this is not strictly mandatory
+
+* `data` (optional) is a dictionnary of values given to the
+  controller specified through an `url` argument; some keys may have a
+  special meaning depending on the choosen controller (such as `fname`
+  for the JSonController); the `callback` key, if present, must refer
+  to a function to be called at the end of loadxhtml (more on this
+  below)
+
+* `reqtype` (optional) specifies the request method to be used (get or
+  post); if the argument is 'post', then the post method is used,
+  otherwise the get method is used
+
+* `mode` (optional) is one of `replace` (the default) which means the
+  loaded node will replace the current node content, `swap` to replace
+  the current node with the loaded node, and `append` which will
+  append the loaded node to the current node content
+
+
+About the `callback` option:
+
+* it is called with two parameters: the current node, and a list
+  containing the loaded (and post-processed node)
+
+* whenever is returns another function, this function is called in
+  turn with the same parameters as above
+
+This mecanism allows callback chaining.
+
+
+Javascript library: overview
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 * jquery.* : jquery and jquery UI library
 
+* 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.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
@@ -54,11 +281,6 @@
   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)
 
@@ -68,5 +290,6 @@
 
 * cubicweb.facets.js : used by the facets mechanism
 
-xxx massmailing, gmap, fckcwconfig, timeline-bundle, timeline-ext,
-calendar, goa, flotn tazy, tabs, bookmarks
+There is also javascript support for massmailing, gmap (google maps),
+fckcwconfig (fck editor), timeline, calendar, goa (CubicWeb over
+AppEngine), flot (charts drawing), tabs and bookmarks.
--- a/doc/book/en/development/devweb/rtags.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/devweb/rtags.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -2,13 +2,13 @@
 -----------------------------------
 
 
-The "Relation tags" structure
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Relation tags
+~~~~~~~~~~~~~
 .. automodule:: cubicweb.rtags
-   :members:
 
 
-The `uicfg` module (:mod:`cubicweb.web.uicfg`)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``uicfg`` module
+~~~~~~~~~~~~~~~~~~~~
 .. automodule:: cubicweb.web.uicfg
-   :members:
+
--- a/doc/book/en/development/devweb/views.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/devweb/views.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -211,10 +211,10 @@
 **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**
+**Instead of merely using type based dispatch, we do predicate dispatch
+which is quite more powerful.**
 
 Assuming we added entries to the blog titled `MyLife`, displaying it
 now allows to read its description and all its entries.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/development/entityclasses/application-logic.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,166 @@
+How to use entities objects
+---------------------------
+
+The previous chapters detailed the classes and methods available to
+the developper at the so-called `ORM`_ level. However they say little
+about the common patterns of usage of these objects.
+
+.. _`ORM`: http://en.wikipedia.org/wiki/Object-relational_mapping
+
+Entities objects are used in the repository and web sides of
+CubicWeb. In the repository side of things, one should manipulate them
+in Hooks and Operations.
+
+Hooks and Operations provide support for the implementation of rules
+such as computed attributes, coherency invariants, etc (they play the
+same role as database triggers, but in a way that is independant of
+the actual data sources).
+
+So a lot of an application's business rules will be written in Hooks
+(or Operations).
+
+In the web side, views also typically operate using entity
+objects. Obvious entity methods for use in views are the dublin code
+method like dc_title, etc. For separation of concerns reasons, one
+should ensure no ui logic pervades the entities level, and also no
+business logic should creep into the views.
+
+In the duration of a transaction, entities objects can be instantiated
+many times, in views and hooks, even for the same database entity. For
+instance, in a classic CubicWeb deployment setup, the repository and
+the web frontend are separated process communicating over the
+wire. There is no way state can be shared between these processes
+(there is a specific API for that). Hence, it is not possible to use
+entity objects as messengers between these components of an
+application. It means that an attribute set as in `obj.x = 42`,
+whether or not x is actually an entity schema attribute, has a short
+life span, limited to the hook, operation or view within which the
+object was built.
+
+Setting an attribute or relation value can be done in the context of a
+Hook/Operation, using the obj.set_attributes(x=42) notation or a plain
+RQL SET expression.
+
+In views, it would be preferable to encapsulate the necessary logic in
+a method of the concerned entity class(es). But of course, this advice
+is also reasonnable for Hooks/Operations, though the separation of
+concerns here is less stringent than in the case of views.
+
+This leads to the practical role of entity objects: it's where an
+important part of the application logic lie (the other part being
+located in the Hook/Operations).
+
+Anatomy of an entity class
+--------------------------
+
+We can look now at a real life example coming from the `tracker`_
+cube. Let us begin to study the entities/project.py content.
+
+.. sourcecode:: python
+
+    class Project(TreeMixIn, AnyEntity):
+        __regid__ = 'Project'
+        __implements__ = AnyEntity.__implements__ + (ITree,)
+        fetch_attrs, fetch_order = fetch_config(('name', 'description',
+                                                 'description_format', 'summary'))
+
+        TICKET_DEFAULT_STATE_RESTR = 'S name IN ("created","identified","released","scheduled")'
+
+        tree_attribute = 'subproject_of'
+        parent_target = 'subject'
+        children_target = 'object'
+
+        def dc_title(self):
+            return self.name
+
+First we see that it uses an ITree interface and the TreeMixIn default
+implementation. The attributes `tree_attribute`, `parent_target` and
+`children_target` are used by the TreeMixIn code. This is typically
+used in views concerned with the representation of tree-like
+structures (CubicWeb provides several such views).
+
+It is important that the views themselves try not to implement this
+logic, not only because such views would be hardly applyable to other
+tree-like relations, but also because it is perfectly fine and useful
+to use such an interface in Hooks.
+
+In fact, Tree nature is a property of the data model that cannot be
+fully and portably expressed at the level of database entities (think
+about the transitive closure of the child relation). This is a further
+argument to implement it at entity class level.
+
+The `dc_title` method provides a (unicode string) value likely to be
+consummed by views, but note that here we do not care about output
+encodings. We care about providing data in the most universal format
+possible, because the data could be used by a web view (which would be
+responsible of ensuring XHTML compliance), or a console or file
+oriented output (which would have the necessary context about the
+needed byte stream encoding).
+
+The fetch_attrs, fetch_order class attributes are parameters of the
+`ORM`_ layer. They tell which attributes should be loaded at once on
+entity object instantiation (by default, only the eid is known, other
+attributes are loaded on demand), and which attribute is to be used to
+order the .related() and .unrelated() methods output.
+
+Finally, we can observe the big TICKET_DEFAULT_STATE_RESTR is a pure
+application domain piece of data. There is, of course, no limitation
+to the amount of class attributes of this kind.
+
+Let us now dig into more substantial pieces of code.
+
+.. sourcecode:: python
+
+    def latest_version(self, states=('published',), reverse=None):
+        """returns the latest version(s) for the project in one of the given
+        states.
+
+        when no states specified, returns the latest published version.
+        """
+        order = 'DESC'
+        if reverse is not None:
+            warn('reverse argument is deprecated',
+                 DeprecationWarning, stacklevel=1)
+            if reverse:
+                order = 'ASC'
+        rset = self.versions_in_state(states, order, True)
+        if rset:
+            return rset.get_entity(0, 0)
+        return None
+
+    def versions_in_state(self, states, order='ASC', limit=False):
+        """returns version(s) for the project in one of the given states, sorted
+        by version number.
+
+        If limit is true, limit result to one version.
+        If reverse, versions are returned from the smallest to the greatest.
+        """
+        if limit:
+            order += ' LIMIT 1'
+        rql = 'Any V,N ORDERBY version_sort_value(N) %s ' \
+              'WHERE V num N, V in_state S, S name IN (%s), ' \
+              'V version_of P, P eid %%(p)s' % (order, ','.join(repr(s) for s in states))
+        return self._cw.execute(rql, {'p': self.eid})
+
+.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker/
+
+These few lines exhibit the important properties we want to outline:
+
+* entity code is concerned with the application domain
+
+* it is NOT concerned with database coherency (this is the realm of
+  Hooks/Operations); in other words, it assumes a coherent world
+
+* it is NOT concerned with end-user interfaces
+
+* however it can be used in both contexts
+
+* it does not create or manipulate the internal object's state
+
+* it plays freely with RQL expression as needed
+
+* it is not concerned with internationalization
+
+* it does not raise exceptions
+
+
--- a/doc/book/en/development/entityclasses/data-as-objects.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/entityclasses/data-as-objects.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -17,6 +17,7 @@
 :Formatting and output generation:
 
   * `view(vid, **kwargs)`, applies the given view to the entity
+    (and returns an unicode string)
 
   * `absolute_url(**kwargs)`, returns an absolute URL to access the primary view
     of an entity
@@ -32,22 +33,30 @@
   * `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 all at once
-    all the missing attributes of an entity
+  * `complete(skip_bytes=True)`, executes a request that recovers at
+    once all the missing attributes of an entity
 
   * `get_value(name)`, returns the value associated to the attribute name given
     in parameter
 
-  * `related(rtype, x='subject', limit=None, entities=False)`, returns a list
-    of entities related to the current entity by the relation given in parameter
+  * `related(rtype, role='subject', limit=None, entities=False)`,
+    returns a list of entities related to the current entity by the
+    relation given in parameter
 
-  * `unrelated(rtype, targettype, x='subject', limit=None)`, returns a result set
-    corresponding to the entities not related to the current entity by the
-    relation given in parameter and satisfying its constraints
+  * `unrelated(rtype, targettype, role='subject', limit=None)`,
+    returns a result set corresponding to the entities not (yet)
+    related to the current entity by the relation given in parameter
+    and satisfying its constraints
 
   * `set_attributes(**kwargs)`, updates the attributes list with the corresponding
     values given named parameters
 
+  * `set_relations(**kwargs)`, add relations to the given object. To
+     set a relation where this entity is the object of the relation,
+     use 'reverse_'<relation> as argument name.  Values may be an
+     entity, a list of entities, or None (meaning that all relations of
+     the given type from or to this object should be deleted).
+
   * `copy_relations(ceid)`, copies the relations of the entities having the eid
     given in the parameters on the current entity
 
@@ -66,8 +75,10 @@
 and helps specializing (by further subclassing) the handling of a
 given entity type.
 
-The methods defined for `AnyEntity`, in addition to `Entity`, are the
-following ones:
+Most methods defined for `AnyEntity`, in addition to `Entity`, add
+support for the `Dublin Core`_ metadata.
+
+.. _`Dublin Core`: http://dublincore.org/
 
 :Standard meta-data (Dublin Core):
 
@@ -85,12 +96,26 @@
   * `dc_authors()`, returns a unicode string corresponding to the meta-data
     `Authors` (owners by default)
 
+  * `dc_creator()`, returns a unicode string corresponding to the
+    creator of the entity
+
   * `dc_date(date_format=None)`, returns a unicode string corresponding to
     the meta-data `Date` (update date by default)
 
   * `dc_type(form='')`, returns a string to display the entity type by
     specifying the preferred form (`plural` for a plural form)
 
+  * `dc_language()`, returns the language used by the entity
+
+
+:Misc methods:
+
+  * `after_deletion_path`, return (path, parameters) which should be
+     used as redirect information when this entity is being deleted
+
+  * `pre_web_edit`, callback called by the web editcontroller when an
+    entity will be created/modified, to let a chance to do some entity
+    specific stuff (does nothing by default)
 
 Inheritance
 -----------
--- a/doc/book/en/development/entityclasses/index.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/entityclasses/index.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -10,4 +10,4 @@
    data-as-objects
    load-sort
    interfaces
-   more
+   application-logic
--- a/doc/book/en/development/entityclasses/interfaces.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/entityclasses/interfaces.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -1,7 +1,9 @@
 Interfaces
 ----------
 
-Same thing as object-oriented programming interfaces.
+This is the same thing as object-oriented programming `interfaces`_.
+
+.. _`interfaces`: http://java.sun.com/docs/books/tutorial/java/concepts/interface.html
 
 Definition of an interface is quite trivial. An example from cubicweb
 itself (found in cubicweb/interfaces.py):
@@ -17,7 +19,7 @@
             """returns the item's children"""
 
         def children_rql(self):
-            """XXX returns RQL to get children"""
+            """returns RQL to get children"""
 
         def iterchildren(self):
             """iterates over the item's children"""
@@ -53,7 +55,7 @@
 Interfaces (and some implementations as mixins) defined in the library
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. automodule:: cubicweb.interface
+.. automodule:: cubicweb.interfaces
    :members:
 
 .. automodule:: cubicweb.mixins
--- a/doc/book/en/development/entityclasses/more.rst	Thu Mar 04 17:56:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-Navigation on deletion
-----------------------
-
-XXX after_deletion_path, pre_web_edit
-
-Controlling output url
------------------------
-
-XXX write me
-
-Controling notification references
-----------------------------------
-
-XXX write me
--- a/doc/book/en/development/index.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/index.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -11,12 +11,13 @@
    :numbered:
 
    cubes/index
+   vreg.rst
    datamodel/index
    entityclasses/index
    devcore/index
    devweb/index
    devrepo/index
-   testing/index
-   migration/index
+   testing.rst
+   migration.rst
    webstdlib/index
-   profiling/index
+   profiling.rst
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/development/migration.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,198 @@
+.. -*- coding: utf-8 -*-
+
+.. _migration:
+
+Migration
+=========
+
+One of the main design goals of *CubicWeb* was to support iterative and agile
+development. For this purpose, multiple actions are provided to facilitate the
+improvement of an instance, and in particular to handle the changes to be
+applied to the data model, without loosing existing data.
+
+The current version of a cube (and of cubicweb itself) is provided in the file
+`__pkginfo__.py` as a tuple of 3 integers.
+
+Migration scripts management
+----------------------------
+
+Migration scripts has to be located in the directory `migration` of your
+cube and named accordingly:
+
+::
+
+  <version n° X.Y.Z>[_<description>]_<mode>.py
+
+in which :
+
+* X.Y.Z is the model version number to which the script enables to migrate.
+
+* *mode* (between the last "_" and the extension ".py") is used for
+  distributed installation. It indicates to which part
+  of the application (RQL server, web server) the script applies.
+  Its value could be :
+
+  * `common`, applies to the RQL server as well as the web server and updates
+    files on the hard drive (configuration files migration for example).
+
+  * `web`, applies only to the web server and updates files on the hard drive.
+
+  * `repository`, applies only to the RQL server and updates files on the
+    hard drive.
+
+  * `Any`, applies only to the RQL server and updates data in the database
+    (schema and data migration for example).
+
+Again in the directory `migration`, the file `depends.map` allows to indicate
+that for the migration to a particular model version, you always have to first
+migrate to a particular *CubicWeb* version. This file can contain comments (lines
+starting by `#`) and a dependancy is listed as follows: ::
+
+  <model version n° X.Y.Z> : <cubicweb version n° X.Y.Z>
+
+For example: ::
+
+  0.12.0: 2.26.0
+  0.13.0: 2.27.0
+  # 0.14 works with 2.27 <= cubicweb <= 2.28 at least
+  0.15.0: 2.28.0
+
+Base context
+------------
+
+The following identifiers are pre-defined in migration scripts:
+
+* `config`, instance configuration
+
+* `interactive_mode`, boolean indicating that the script is executed in
+  an interactive mode or not
+
+* `versions_map`, dictionary of migrated versions  (key are cubes
+  names, including 'cubicweb', values are (from version, to version)
+
+* `confirm(question)`, function asking the user and returning true
+  if the user answers yes, false otherwise (always returns true in
+  non-interactive mode)
+
+* the function `_`, it is equivalent to `unicode` allowing to flag the strings
+  to internationalize in the migration scripts.
+
+In the `repository` scripts, the following identifiers are also defined:
+
+* `checkpoint`, request confirming and executing a "commit" at checking point
+
+* `schema`, instance schema (readen from the database)
+
+* `fsschema`, installed schema on the file system (e.g. schema of
+  the updated model and cubicweb)
+
+* `repo`, repository object
+
+* `session`, repository session object
+
+
+Schema migration
+----------------
+The following functions for schema migration are available in `repository`
+scripts:
+
+* `add_attribute(etype, attrname, attrtype=None, commit=True)`, adds a new
+  attribute to an existing entity type. If the attribute type is not specified,
+  then it is extracted from the updated schema.
+
+* `drop_attribute(etype, attrname, commit=True)`, removes an attribute from an
+  existing entity type.
+
+* `rename_attribute(etype, oldname, newname, commit=True)`, renames an attribute
+
+* `add_entity_type(etype, auto=True, commit=True)`, adds a new entity type.
+  If `auto` is True, all the relations using this entity type and having a known
+  entity type on the other hand will automatically be added.
+
+* `drop_entity_type(etype, commit=True)`, removes an entity type and all the
+  relations using it.
+
+* `rename_entity_type(oldname, newname, commit=True)`, renames an entity type
+
+* `add_relation_type(rtype, addrdef=True, commit=True)`, adds a new relation
+  type. If `addrdef` is True, all the relations definitions of this type will
+  be added.
+
+* `drop_relation_type(rtype, commit=True)`, removes a relation type and all the
+  definitions of this type.
+
+* `rename_relation(oldname, newname, commit=True)`, renames a relation.
+
+* `add_relation_definition(subjtype, rtype, objtype, commit=True)`, adds a new
+  relation definition.
+
+* `drop_relation_definition(subjtype, rtype, objtype, commit=True)`, removes
+  a relation definition.
+
+* `sync_schema_props_perms(ertype=None, syncperms=True, syncprops=True, syncrdefs=True, commit=True)`,
+  synchronizes properties and/or permissions on:
+  - the whole schema if ertype is None
+  - an entity or relation type schema if ertype is a string
+  - a relation definition  if ertype is a 3-uple (subject, relation, object)
+
+* `change_relation_props(subjtype, rtype, objtype, commit=True, **kwargs)`, changes
+  properties of a relation definition by using the named parameters of the properties
+  to change.
+
+* `set_widget(etype, rtype, widget, commit=True)`, changes the widget used for the
+  relation <rtype> of entity type <etype>.
+
+* `set_size_constraint(etype, rtype, size, commit=True)`, changes the size constraints
+  for the relation <rtype> of entity type <etype>.
+
+Data migration
+--------------
+The following functions for data migration are available in `repository` scripts:
+
+* `rql(rql, kwargs=None, cachekey=None, ask_confirm=True)`, executes an arbitrary RQL
+  query, either to interrogate or update. A result set object is returned.
+
+* `add_entity(etype, *args, **kwargs)`, adds a nes entity type of the given
+  type. The attribute and relation values are specified using the named and
+  positionned parameters.
+
+Workflow creation
+-----------------
+
+The following functions for workflow creation are available in `repository`
+scripts:
+
+* `add_workflow(label, workflowof, initial=False, commit=False, **kwargs)`, adds a new workflow
+  for a given type(s)
+
+You can find more details about workflows in the chapter :ref:`Workflow` .
+
+Configuration migration
+-----------------------
+
+The following functions for configuration migration are available in all
+scripts:
+
+* `option_renamed(oldname, newname)`, indicates that an option has been renamed
+
+* `option_group_change(option, oldgroup, newgroup)`, indicates that an option does not
+  belong anymore to the same group.
+
+* `option_added(oldname, newname)`, indicates that an option has been added.
+
+* `option_removed(oldname, newname)`, indicates that an option has been deleted.
+
+
+Others migration functions
+--------------------------
+Those functions are only used for low level operations that could not be
+accomplished otherwise or to repair damaged databases during interactive
+session. They are available in `repository` scripts:
+
+* `sql(sql, args=None, ask_confirm=True)`, executes an arbitrary SQL query on the system source
+* `add_entity_type_table(etype, commit=True)`
+* `add_relation_type_table(rtype, commit=True)`
+* `uninline_relation(rtype, commit=True)`
+
+
+[FIXME] Add explanation on how to use cubicweb-ctl shell
--- a/doc/book/en/development/migration/index.rst	Thu Mar 04 17:56:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,198 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _migration:
-
-Migration
-=========
-
-One of the main design goals of *CubicWeb* was to support iterative and agile
-development. For this purpose, multiple actions are provided to facilitate the
-improvement of an instance, and in particular to handle the changes to be
-applied to the data model, without loosing existing data.
-
-The current version of a cube (and of cubicweb itself) is provided in the file
-`__pkginfo__.py` as a tuple of 3 integers.
-
-Migration scripts management
-----------------------------
-
-Migration scripts has to be located in the directory `migration` of your
-cube and named accordingly:
-
-::
-
-  <version n° X.Y.Z>[_<description>]_<mode>.py
-
-in which :
-
-* X.Y.Z is the model version number to which the script enables to migrate.
-
-* *mode* (between the last "_" and the extension ".py") is used for
-  distributed installation. It indicates to which part
-  of the application (RQL server, web server) the script applies.
-  Its value could be :
-
-  * `common`, applies to the RQL server as well as the web server and updates
-    files on the hard drive (configuration files migration for example).
-
-  * `web`, applies only to the web server and updates files on the hard drive.
-
-  * `repository`, applies only to the RQL server and updates files on the
-    hard drive.
-
-  * `Any`, applies only to the RQL server and updates data in the database
-    (schema and data migration for example).
-
-Again in the directory `migration`, the file `depends.map` allows to indicate
-that for the migration to a particular model version, you always have to first
-migrate to a particular *CubicWeb* version. This file can contain comments (lines
-starting by `#`) and a dependancy is listed as follows: ::
-
-  <model version n° X.Y.Z> : <cubicweb version n° X.Y.Z>
-
-For example: ::
-
-  0.12.0: 2.26.0
-  0.13.0: 2.27.0
-  # 0.14 works with 2.27 <= cubicweb <= 2.28 at least
-  0.15.0: 2.28.0
-
-Base context
-------------
-
-The following identifiers are pre-defined in migration scripts:
-
-* `config`, instance configuration
-
-* `interactive_mode`, boolean indicating that the script is executed in
-  an interactive mode or not
-
-* `versions_map`, dictionary of migrated versions  (key are cubes
-  names, including 'cubicweb', values are (from version, to version)
-
-* `confirm(question)`, function asking the user and returning true
-  if the user answers yes, false otherwise (always returns true in
-  non-interactive mode)
-
-* the function `_`, it is equivalent to `unicode` allowing to flag the strings
-  to internationalize in the migration scripts.
-
-In the `repository` scripts, the following identifiers are also defined:
-
-* `checkpoint`, request confirming and executing a "commit" at checking point
-
-* `schema`, instance schema (readen from the database)
-
-* `fsschema`, installed schema on the file system (e.g. schema of
-  the updated model and cubicweb)
-
-* `repo`, repository object
-
-* `session`, repository session object
-
-
-Schema migration
-----------------
-The following functions for schema migration are available in `repository`
-scripts:
-
-* `add_attribute(etype, attrname, attrtype=None, commit=True)`, adds a new
-  attribute to an existing entity type. If the attribute type is not specified,
-  then it is extracted from the updated schema.
-
-* `drop_attribute(etype, attrname, commit=True)`, removes an attribute from an
-  existing entity type.
-
-* `rename_attribute(etype, oldname, newname, commit=True)`, renames an attribute
-
-* `add_entity_type(etype, auto=True, commit=True)`, adds a new entity type.
-  If `auto` is True, all the relations using this entity type and having a known
-  entity type on the other hand will automatically be added.
-
-* `drop_entity_type(etype, commit=True)`, removes an entity type and all the
-  relations using it.
-
-* `rename_entity_type(oldname, newname, commit=True)`, renames an entity type
-
-* `add_relation_type(rtype, addrdef=True, commit=True)`, adds a new relation
-  type. If `addrdef` is True, all the relations definitions of this type will
-  be added.
-
-* `drop_relation_type(rtype, commit=True)`, removes a relation type and all the
-  definitions of this type.
-
-* `rename_relation(oldname, newname, commit=True)`, renames a relation.
-
-* `add_relation_definition(subjtype, rtype, objtype, commit=True)`, adds a new
-  relation definition.
-
-* `drop_relation_definition(subjtype, rtype, objtype, commit=True)`, removes
-  a relation definition.
-
-* `sync_schema_props_perms(ertype=None, syncperms=True, syncprops=True, syncrdefs=True, commit=True)`,
-  synchronizes properties and/or permissions on:
-  - the whole schema if ertype is None
-  - an entity or relation type schema if ertype is a string
-  - a relation definition  if ertype is a 3-uple (subject, relation, object)
-
-* `change_relation_props(subjtype, rtype, objtype, commit=True, **kwargs)`, changes
-  properties of a relation definition by using the named parameters of the properties
-  to change.
-
-* `set_widget(etype, rtype, widget, commit=True)`, changes the widget used for the
-  relation <rtype> of entity type <etype>.
-
-* `set_size_constraint(etype, rtype, size, commit=True)`, changes the size constraints
-  for the relation <rtype> of entity type <etype>.
-
-Data migration
---------------
-The following functions for data migration are available in `repository` scripts:
-
-* `rql(rql, kwargs=None, cachekey=None, ask_confirm=True)`, executes an arbitrary RQL
-  query, either to interrogate or update. A result set object is returned.
-
-* `add_entity(etype, *args, **kwargs)`, adds a nes entity type of the given
-  type. The attribute and relation values are specified using the named and
-  positionned parameters.
-
-Workflow creation
------------------
-
-The following functions for workflow creation are available in `repository`
-scripts:
-
-* `add_workflow(label, workflowof, initial=False, commit=False, **kwargs)`, adds a new workflow
-  for a given type(s)
-
-You can find more details about workflows in the chapter :ref:`Workflow` .
-
-Configuration migration
------------------------
-
-The following functions for configuration migration are available in all
-scripts:
-
-* `option_renamed(oldname, newname)`, indicates that an option has been renamed
-
-* `option_group_change(option, oldgroup, newgroup)`, indicates that an option does not
-  belong anymore to the same group.
-
-* `option_added(oldname, newname)`, indicates that an option has been added.
-
-* `option_removed(oldname, newname)`, indicates that an option has been deleted.
-
-
-Others migration functions
---------------------------
-Those functions are only used for low level operations that could not be
-accomplished otherwise or to repair damaged databases during interactive
-session. They are available in `repository` scripts:
-
-* `sql(sql, args=None, ask_confirm=True)`, executes an arbitrary SQL query on the system source
-* `add_entity_type_table(etype, commit=True)`
-* `add_relation_type_table(rtype, commit=True)`
-* `uninline_relation(rtype, commit=True)`
-
-
-[FIXME] Add explanation on how to use cubicweb-ctl shell
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/development/profiling.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,55 @@
+Profiling and performance
+=========================
+
+If you feel that one of your pages takes more time than it should to be
+generated, chances are that you're making too many RQL queries.  Obviously,
+there are other reasons but experience tends to show this is the first thing to
+track down. Luckily, CubicWeb provides a configuration option to log RQL
+queries. In your ``all-in-one.conf`` file, set the **query-log-file** option::
+
+    # web application query log file
+    query-log-file=~/myapp-rql.log
+
+Then restart your application, reload your page and stop your application.
+The file ``myapp-rql.log`` now contains the list of RQL queries that were
+executed during your test. It's a simple text file containing lines such as::
+
+    Any A WHERE X eid %(x)s, X lastname A {'x': 448} -- (0.002 sec, 0.010 CPU sec)
+    Any A WHERE X eid %(x)s, X firstname A {'x': 447} -- (0.002 sec, 0.000 CPU sec)
+
+The structure of each line is::
+
+    <RQL QUERY> <QUERY ARGS IF ANY> -- <TIME SPENT>
+
+CubicWeb also provides the **exlog** command to examine and summarize data found
+in such a file:
+
+.. sourcecode:: sh
+
+    $ cubicweb-ctl exlog < ~/myapp-rql.log
+    0.07 50 Any A WHERE X eid %(x)s, X firstname A {}
+    0.05 50 Any A WHERE X eid %(x)s, X lastname A {}
+    0.01 1 Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, E employees X, X modification_date AA {}
+    0.01 1 Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s {, }
+    0.01 1 Any B,T,P ORDERBY lower(T) WHERE B is Bookmark,B title T, B path P, B bookmarked_by U, U eid %(x)s {}
+    0.01 1 Any A,B,C,D WHERE A eid %(x)s,A name B,A creation_date C,A modification_date D {}
+
+This command sorts and uniquifies queries so that it's easy to see where
+is the hot spot that needs optimization.
+
+Do not neglect to set the **fetch_attrs** attribute you can define in your
+entity classes because it can greatly reduce the number of queries executed (see
+:ref:`FetchAttrs`).
+
+You should also know about the **profile** option in the ``all-in-on.conf``. If
+set, this option will make your application run in an `hotshot`_ session and
+store the results in the specified file.
+
+.. _hotshot: http://docs.python.org/library/hotshot.html#module-hotshot
+
+Last but no least, if you're using the PostgreSQL database backend, VACUUMing
+your database can significantly improve the performance of the queries (by
+updating the statistics used by the query optimizer). Nowadays, this is done
+automatically from time to time, but if you've just imported a large amount of
+data in your db, you will want to vacuum it (with the analyse option on). Read
+the documentation of your database for more information.
--- a/doc/book/en/development/profiling/index.rst	Thu Mar 04 17:56:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-Profiling and performance
-=========================
-
-If you feel that one of your pages takes more time than it should to be
-generated, chances are that you're making too many RQL queries.  Obviously,
-there are other reasons but experience tends to show this is the first thing to
-track down. Luckily, CubicWeb provides a configuration option to log RQL
-queries. In your ``all-in-one.conf`` file, set the **query-log-file** option::
-
-    # web application query log file
-    query-log-file=~/myapp-rql.log
-
-Then restart your application, reload your page and stop your application.
-The file ``myapp-rql.log`` now contains the list of RQL queries that were
-executed during your test. It's a simple text file containing lines such as::
-
-    Any A WHERE X eid %(x)s, X lastname A {'x': 448} -- (0.002 sec, 0.010 CPU sec)
-    Any A WHERE X eid %(x)s, X firstname A {'x': 447} -- (0.002 sec, 0.000 CPU sec)
-
-The structure of each line is::
-
-    <RQL QUERY> <QUERY ARGS IF ANY> -- <TIME SPENT>
-
-CubicWeb also provides the **exlog** command to examine and summarize data found
-in such a file:
-
-.. sourcecode:: sh
-
-    $ cubicweb-ctl exlog < ~/myapp-rql.log
-    0.07 50 Any A WHERE X eid %(x)s, X firstname A {}
-    0.05 50 Any A WHERE X eid %(x)s, X lastname A {}
-    0.01 1 Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, E employees X, X modification_date AA {}
-    0.01 1 Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s {, }
-    0.01 1 Any B,T,P ORDERBY lower(T) WHERE B is Bookmark,B title T, B path P, B bookmarked_by U, U eid %(x)s {}
-    0.01 1 Any A,B,C,D WHERE A eid %(x)s,A name B,A creation_date C,A modification_date D {}
-
-This command sorts and uniquifies queries so that it's easy to see where
-is the hot spot that needs optimization.
-
-Do not neglect to set the **fetch_attrs** attribute you can define in your
-entity classes because it can greatly reduce the number of queries executed (see
-:ref:`FetchAttrs`).
-
-You should also know about the **profile** option in the ``all-in-on.conf``. If
-set, this option will make your application run in an `hotshot`_ session and
-store the results in the specified file.
-
-.. _hotshot: http://docs.python.org/library/hotshot.html#module-hotshot
-
-Last but no least, if you're using the PostgreSQL database backend, VACUUMing
-your database can significantly improve the performance of the queries (by
-updating the statistics used by the query optimizer). Nowadays, this is done
-automatically from time to time, but if you've just imported a large amount of
-data in your db, you will want to vacuum it (with the analyse option on). Read
-the documentation of your database for more information.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/development/testing.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,78 @@
+.. -*- coding: utf-8 -*-
+
+Tests
+=====
+
+.. toctree::
+   :maxdepth: 1
+
+
+Unit tests
+----------
+
+*CubicWeb* framework provides essentially two Python test classes in the
+module `cubicweb.devtools.apptest`:
+
+* `EnvBasedTC`, to simulate a complete environment (web + repository)
+* `RepositoryBasedTC`, to simulate a repository environment only
+
+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()
+
+Do not use the references kept to the entities created with a connection from another.
+
+
+Email notifications tests
+-------------------------
+When running tests potentially generated e-mails are not really
+sent but is found in the list `MAILBOX` of module `cubicweb.devtools.apptest`.
+This list is reset at each test *setUp* (by the setUp of classes `EnvBasedTC`
+and `RepositoryBasedTC`).
+
+
+You can test your notifications by analyzing the contents of this list, which
+contains objects with two attributes:
+* `recipients`, the list of recipients
+* `msg`, object email.Message
+
+
+Automatic testing
+-----------------
+XXXFILLME
--- a/doc/book/en/development/testing/index.rst	Thu Mar 04 17:56:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-Tests
-=====
-
-.. toctree::
-   :maxdepth: 1
-
-
-Unit tests
-----------
-
-*CubicWeb* framework provides essentially two Python test classes in the
-module `cubicweb.devtools.apptest`:
-
-* `EnvBasedTC`, to simulate a complete environment (web + repository)
-* `RepositoryBasedTC`, to simulate a repository environment only
-
-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()
-
-Do not use the references kept to the entities created with a connection from another.
-
-
-Email notifications tests
--------------------------
-When running tests potentially generated e-mails are not really
-sent but is found in the list `MAILBOX` of module `cubicweb.devtools.apptest`.
-This list is reset at each test *setUp* (by the setUp of classes `EnvBasedTC`
-and `RepositoryBasedTC`).
-
-
-You can test your notifications by analyzing the contents of this list, which
-contains objects with two attributes:
-* `recipients`, the list of recipients
-* `msg`, object email.Message
-
-
-Automatic testing
------------------
-XXXFILLME
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/development/vreg.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,110 @@
+The VRegistry, selectors and application objects
+================================================
+
+This chapter talk about core concepts of the |cubicweb| framework, that make it
+different from other framework (and probably not easy to grasp at a first
+glance). You won't be able to do advanced development with |cubicweb| without
+a good understanding of what's explain below.
+
+This chapter goes deep into details. You don't have to remember them all but keep
+it in mind so you can go back there later...
+
+.. toctree::
+   :maxdepth: 1
+
+.. autodocstring:: cubicweb.cwvreg
+.. autodocstring:: cubicweb.selectors
+.. automodule:: cubicweb.appobject
+
+Base selectors
+--------------
+
+Selectors are scoring functions that are called by the registry to tell whenever
+an appobject can be selected in a given context. Selector sets are for instance
+the glue that tie views to the data model. Using them appropriately is an
+essential part of the construction of well behaved cubes.
+
+Of course you may have to write your own set of selectors as your needs grows and
+you get familiar with the framework (see :ref:`CustomSelectors`).
+
+Here is a description of generic selectors provided by CubicWeb that should suit
+most of your needs.
+
+Bare selectors
+~~~~~~~~~~~~~~
+Those selectors are somewhat dumb, which doesn't mean they're not (very) useful.
+
+.. autoclass:: cubicweb.appobject.yes
+.. autoclass:: cubicweb.selectors.match_kwargs
+.. autoclass:: cubicweb.selectors.appobject_selectable
+
+
+Result set selectors
+~~~~~~~~~~~~~~~~~~~~~
+Those selectors are looking for a result set in the context ('rset' argument or
+the input context) and match or not according to its shape. Some of these
+selectors have different behaviour if a particular cell of the result set is
+specified using 'row' and 'col' arguments of the input context or not.
+
+.. autoclass:: cubicweb.selectors.none_rset
+.. autoclass:: cubicweb.selectors.any_rset
+.. autoclass:: cubicweb.selectors.nonempty_rset
+.. autoclass:: cubicweb.selectors.empty_rset
+.. autoclass:: cubicweb.selectors.one_line_rset
+.. autoclass:: cubicweb.selectors.multi_lines_rset
+.. autoclass:: cubicweb.selectors.multi_columns_rset
+.. autoclass:: cubicweb.selectors.paginated_rset
+.. autoclass:: cubicweb.selectors.sorted_rset
+.. autoclass:: cubicweb.selectors.one_etype_rset
+.. autoclass:: cubicweb.selectors.multi_etypes_rset
+
+
+Entity selectors
+~~~~~~~~~~~~~~~~
+Those selectors are looking for either an `entity` argument in the input context,
+or entity found in the result set ('rset' argument or the input context) and
+match or not according to entity's (instance or class) properties.
+
+.. autoclass:: cubicweb.selectors.non_final_entity
+.. autoclass:: cubicweb.selectors.implements
+.. autoclass:: cubicweb.selectors.score_entity
+.. autoclass:: cubicweb.selectors.rql_condition
+.. autoclass:: cubicweb.selectors.relation_possible
+.. autoclass:: cubicweb.selectors.partial_relation_possible
+.. autoclass:: cubicweb.selectors.has_related_entities
+.. autoclass:: cubicweb.selectors.partial_has_related_entities
+.. autoclass:: cubicweb.selectors.has_permission
+.. autoclass:: cubicweb.selectors.has_add_permission
+
+
+Logged user selectors
+~~~~~~~~~~~~~~~~~~~~~
+Those selectors are looking for properties of the user issuing the request.
+
+.. autoclass:: cubicweb.selectors.anonymous_user
+.. autoclass:: cubicweb.selectors.authenticated_user
+.. autoclass:: cubicweb.selectors.match_user_groups
+
+
+Web request selectors
+~~~~~~~~~~~~~~~~~~~~~
+Those selectors are looking for properties of *web* request, they can not be
+used on the data repository side.
+
+.. autoclass:: cubicweb.selectors.match_form_params
+.. autoclass:: cubicweb.selectors.match_search_state
+.. autoclass:: cubicweb.selectors.match_context_prop
+.. autoclass:: cubicweb.selectors.match_view
+.. autoclass:: cubicweb.selectors.primary_view
+.. autoclass:: cubicweb.selectors.specified_etype_implements
+
+
+Other selectors
+~~~~~~~~~~~~~~~
+.. autoclass:: cubicweb.selectors.match_transition
+
+You'll also find some other (very) specific selectors hidden in other modules
+than :mod:`cubicweb.selectors`.
+
+
+.. |cubicweb| replace:: *CubicWeb*
--- a/doc/book/en/development/webstdlib/primary.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/development/webstdlib/primary.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -1,3 +1,5 @@
+.. _primary:
+
 The default 'primary' view (:mod:`cubicweb.web.views.primary`)
 ---------------------------------------------------------------
 
Binary file doc/book/en/images/primaryview_template.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/images/primaryview_template.svg	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,254 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="1036.6421"
+   height="845.07812"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.46"
+   sodipodi:docname="primaryview_template.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   version="1.0"
+   inkscape:export-filename="/home/steph/local/fcubicweb/cubicweb/doc/book/en/images/primaryview_template.png"
+   inkscape:export-xdpi="43.451603"
+   inkscape:export-ydpi="43.451603">
+  <defs
+     id="defs4">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective10" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.49497475"
+     inkscape:cx="432.61573"
+     inkscape:cy="370.11733"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="824"
+     inkscape:window-height="1094"
+     inkscape:window-x="0"
+     inkscape:window-y="45" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Calque 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(162.2968,90.697922)">
+    <rect
+       style="fill:#ffffff;fill-rule:evenodd;stroke:#ff0000;stroke-width:3.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3301"
+       width="842.59973"
+       height="562.81085"
+       x="28.555748"
+       y="53.761448" />
+    <rect
+       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect2383"
+       width="629.62366"
+       height="54.69112"
+       x="54.112095"
+       y="73.831123" />
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="500.19885"
+       y="104.27108"
+       id="text2385"><tspan
+         sodipodi:role="line"
+         x="500.19885"
+         y="104.27108"
+         id="tspan3163">navcontenttop</tspan></text>
+    <rect
+       style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3167"
+       width="628.2298"
+       height="165.69759"
+       x="54.809006"
+       y="142.37053" />
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="311.65164"
+       y="283.88312"
+       id="text3169"><tspan
+         sodipodi:role="line"
+         x="311.65164"
+         y="283.88312"
+         id="tspan3171">view.render_entity_attributes()</tspan></text>
+    <rect
+       style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3173"
+       width="627.95807"
+       height="193.9873"
+       x="56.373432"
+       y="320.51138" />
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="320.29282"
+       y="488.45456"
+       id="text3175"><tspan
+         sodipodi:role="line"
+         x="320.29282"
+         y="488.45456"
+         id="tspan3177">view.render_entity_relations()</tspan></text>
+    <rect
+       style="fill:#ffd5d5;fill-rule:evenodd;stroke:#000000;stroke-width:1.76090598;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3185"
+       width="145.94266"
+       height="499.44452"
+       x="702.43958"
+       y="70.384262" />
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="74.177475"
+       y="-823.00977"
+       id="text3187"
+       transform="matrix(0,1,-1,0,0,0)"><tspan
+         sodipodi:role="line"
+         x="74.177475"
+         y="-823.00977"
+         id="tspan3189">view.render_side_boxes()</tspan></text>
+    <rect
+       style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:2.50000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3191"
+       width="629.62366"
+       height="45.386246"
+       x="54.112095"
+       y="524.98816" />
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="456.07504"
+       y="561.66559"
+       id="text3181"><tspan
+         sodipodi:role="line"
+         x="456.07504"
+         y="561.66559"
+         id="tspan3183">navcontentbottom</tspan></text>
+    <rect
+       style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.50000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3240"
+       width="1031.1713"
+       height="55.714287"
+       x="-161.0468"
+       y="-89.447922" />
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="757.85767"
+       y="-51.771908"
+       id="text3264"><tspan
+         sodipodi:role="line"
+         x="757.85767"
+         y="-51.771908"
+         id="tspan3266">header</tspan></text>
+    <rect
+       style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.76090598;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3270"
+       width="167.89117"
+       height="696.74976"
+       x="-160.03128"
+       y="-13.70227" />
+    <rect
+       style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3279"
+       width="842.59979"
+       height="55.714287"
+       x="28.328695"
+       y="-13.41731" />
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="668.23749"
+       y="25.321617"
+       id="text3281"><tspan
+         sodipodi:role="line"
+         x="668.23749"
+         y="25.321617"
+         id="tspan3283">contentheader</tspan></text>
+    <rect
+       style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3285"
+       width="1032.5997"
+       height="55.714287"
+       x="-159.50443"
+       y="697.41589" />
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="770.28204"
+       y="736.52045"
+       id="text3287"><tspan
+         sodipodi:role="line"
+         x="770.28204"
+         y="736.52045"
+         id="tspan3289">footer</tspan></text>
+    <rect
+       style="fill:#dfdfdf;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect3291"
+       width="844.62012"
+       height="55.714287"
+       x="27.850754"
+       y="627.44647" />
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="680.66193"
+       y="669.04254"
+       id="text3293"><tspan
+         sodipodi:role="line"
+         x="680.66193"
+         y="669.04254"
+         id="tspan3295">contentfooter</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="-130.25735"
+       y="24.239677"
+       id="text3297"><tspan
+         sodipodi:role="line"
+         x="-130.25735"
+         y="24.239677"
+         id="tspan3299">leftcolumn</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:23.38711166px;font-style:normal;font-weight:normal;fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;font-family:Bitstream Vera Sans"
+       x="373.18518"
+       y="610.24188"
+       id="text3351"><tspan
+         sodipodi:role="line"
+         x="373.18518"
+         y="610.24188"
+         id="tspan3353">view.render()</tspan></text>
+  </g>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/intro/concepts.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,343 @@
+.. -*- coding: utf-8 -*-
+
+.. _Concepts:
+
+The Core Concepts of |cubicweb|
+===============================
+
+This section defines some terms and core concepts of the |cubicweb| framework. To
+avoid confusion while reading this book, take time to go through the following
+definitions and use this section as a reference during your reading.
+
+
+.. _Cube:
+
+Cubes
+-----
+
+A cube is a software component made of three parts: its data model
+(:file:`schema`), its logic (:file:`entities`) and its user interface
+(:file:`views`).
+
+A cube can use other cubes as building blocks and assemble them to provide a
+whole with richer functionnalities than its parts. The cubes `cubicweb-blog`_ and
+`cubicweb-comment`_ could be used to make a cube named *myblog* with commentable
+blog entries.
+
+The `CubicWeb.org Forge`_ offers a large number of cubes developed by the community
+and available under a free software license.
+
+The command :command:`cubicweb-ctl list` displays the list of cubes installed on
+your system.
+
+On a Unix system, the available cubes are usually stored in the directory
+:file:`/usr/share/cubicweb/cubes`. If you're using the cubicweb forest
+(:ref:SourceInstallation), the cubes are searched in the directory
+:file:`/path/to/cubicweb_forest/cubes`. The environment variable
+:envvar:`CW_CUBES_PATH` gives additionnal locations where to search for cubes.
+
+.. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
+.. _`cubicweb-blog`: http://www.cubicweb.org/project/cubicweb-blog
+.. _`cubicweb-comment`: http://www.cubicweb.org/project/cubicweb-comment
+
+
+.. _Instance:
+
+Instances
+---------
+
+An instance is a runnable application installed on a computer and based on a
+cube.
+
+The instance directory contains the configuration files. Several instances can be
+created and based on the same cube. For exemple, several software forges can be
+set up on one computer system based on the `cubicweb-forge`_ cube.
+
+.. _`cubicweb-forge`: http://www.cubicweb.org/project/cubicweb-forge
+
+Instances can be of three different types: all-in-one, web engine or data
+repository. For applications that support high traffic, several web (front-end)
+and data (back-end) instances can be set-up to share the load.
+
+.. image:: ../../images/archi_globale.en.png
+
+The command :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.
+
+
+.. Note::
+
+  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.
+
+
+.. _RepositoryIntro:
+
+Data Repository
+---------------
+
+The data repository [1]_ provides access to one or more data sources (including
+SQL databases, LDAP repositories, other |cubicweb| instance repositories, GAE's
+DataStore, etc).
+
+All interactions with the repository are done using the Relation Query Language
+(:ref:`RQL`). The repository federates the data sources and hides them from the
+querier, which does not realize when a query spans accross several data sources
+and requires running sub-queries and merges to complete.
+
+It is common to run the web engine and the repository in the same process (see
+instances of type all-in-one above), but this is not a requirement. A repository
+can be set up to be accessed remotely using Pyro (`Python Remote Objects`_) and
+act as a server. However, it's important to know if code you're writing is
+executed on the repository side, on our client side (the web engine being a
+client for instance): you don't have the same abilities on both side. On the
+repository side, you can for instance by-pass security checks, which isn't
+possible from client code.
+
+Some logic can be attached to events that happen in the repository, like
+creation of entities, deletion of relations, etc. This is used for example to
+send email notifications when the state of an object changes. See :ref:`HookIntro` below.
+
+.. [1] not to be confused with a Mercurial repository or a Debian repository.
+.. _`Python Remote Objects`: http://pyro.sourceforge.net/
+
+
+.. _WebEngineIntro:
+
+Web Engine
+----------
+
+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 `CRUD`_ 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.
+
+.. _`CRUD`: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete
+
+.. _SchemaIntro:
+
+Schema (Data Model)
+-------------------
+
+The data model of a cube is described as an entity-relationship schema using a
+comprehensive language made of Python classes imported from the yams_ library.
+
+.. _yams: http://www.logilab.org/project/yams/
+
+An `entity type` defines a set of attributes and is used in some relations.
+Attributes may be of the following types: `String`, `Int`, `Float`, `Boolean`,
+`Date`, `Time`, `Datetime`, `Interval`, `Password`, `Bytes`, `RichString`.
+
+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`.
+
+A `relation definition` is a triple (*subject entity type*, *relation type*, *object
+entity type*) associated with a set of properties such as cardinality,
+constraints, etc.
+
+Permissions can be set on entity types and relation definition to control who
+will be able to create, read, update or delete entities and relations. Permissions
+are granted to groups (to which users may belong) or using rql expression (if the
+rql expression returns some results, the permission is granted).
+
+Some meta-data necessary to the system is added to the data model. That includes
+entities like users and groups, the entities used to store the data model
+itself and attributes like unique identifier, creation date, creator, etc.
+
+When you create a new |cubicweb| instance, the schema is stored in the database.
+When the cubes the instance is based on evolve, they may change their data model
+and provide migration scripts that will be executed when the administrator will
+run the upgrade process for the instance.
+
+
+.. _VRegistryIntro:
+
+Registries and application objects
+----------------------------------
+
+Application objects
+~~~~~~~~~~~~~~~~~~~
+
+Beside a few core functionalities, almost every feature of the framework is
+achieved by dynamic objects (`application objects` or `appobjects`) stored in a
+two-levels registry (the `vregistry`). Each object is affected to a registry with
+an identifier in this registry. You may have more than one object sharing an
+identifier in the same registry, At runtime, appobjects are selected in a
+registry according to the context. Selection is done by comparing *score*
+returned by each appobject's *selector*.
+
+Application objects are stored in the vregistry using a two-level hierarchy :
+
+  object's `__registry__` : object's `__regid__` : [list of app objects]
+
+E.g. The `vregistry` contains several registries which hold a list of
+appobjects associated to an identifier.
+
+The base class of appobjects is :class:`cubicweb.appobject.AppObject`.
+
+Selectors
+~~~~~~~~~
+
+Each appobject has a selector, that is used to compute how well the object fits a
+given context. The better the object fits the context, the higher the score. They
+are the glue that tie appobjects to the data model. Using them appropriately is
+an essential part of the construction of well behaved cubes.
+
+|cubicweb| provides a set of basic selectors that may be parametrized.  Also,
+selectors can be combined with the `~` unary operator (negation) and the binary
+operators `&` and `|` (respectivly 'and' and 'or') to build more complex
+selector. Of course complex selector may be combined too. Last but not least, you
+can write your own selectors.
+
+The `vregistry`
+~~~~~~~~~~~~~~~
+
+At startup, the `vregistry` inspects a number of directories looking for
+compatible classes definition. After a recording process, the objects are
+assigned to registries so that they can be selected dynamically while the
+instance is running.
+
+In a cube, application object classes are looked in the following modules or
+packages:
+
+- `entities`
+- `views`
+- `sobjects`
+- `hooks`
+
+
+Once initialized, there are three common ways to retrieve some application object
+from a registry:
+
+* get the most appropriate object by specifying an identifier. In that case, the
+  object with the greatest score is selected. There should always be a single
+  appobject with a greater score than others for a particular context.
+
+* get all objects applying to a context by specifying a registry. In that case, a
+  list of objects will be returned containing the object with the highest score
+  (> 0) for each identifier in that registry.
+
+* get the object within a particular registry/identifier. In that case no
+  selection process is involved, the vregistry will expect to find a single
+  object in that cell.
+
+
+.. _RQLIntro:
+
+The RQL query language
+----------------------
+
+**No need for a complicated ORM when you have a powerful query language**
+
+All the persistent data in a |cubicweb| instance is retrieved and modified by
+using the Relation Query Language.
+
+This query language is inspired by SQL but is on a higher level in order to
+emphasize browsing relations.
+
+
+db-api
+~~~~~~
+
+The repository exposes a `db-api`_ like api but using the RQL instead of SQL.
+
+You basically get a connection using :func:`cubicweb.dbapi.connect` , then
+get a cursor to call its `execute` method which will return result set for the
+given rql query.
+
+You can also get additional information through the connection, such as the
+repository'schema, version configuration, etc.
+
+
+Result set
+~~~~~~~~~~
+
+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.
+
+
+.. _ViewIntro:
+
+Views
+-----
+
+**CubicWeb is data driven**
+
+The view system is loosely coupled to data through the selection system explained
+above. Views are application objects with a dedicated interface to 'render'
+something, eg producing some html, text, xml, pdf, or whatsover that can be
+displayed to a user.
+
+The two main entry points of a view are:
+
+* `call()`, used to render a view on a context with no result set, or on a whole
+  result set
+
+* `cell_call(row, col)`, used to render a view on a the cell with index `row` and
+  `col` of the context's result set (remember result set may be seen as a two
+  dimensions array).
+
+Then view may gets refined into different kind of objects such as `template`,
+`boxes`, `components`, which are more high-level abstraction useful to build
+the user interface in an object oriented way.
+
+
+.. _HookIntro:
+
+Hooks and operations
+--------------------
+
+**CubicWeb provides an extensible data repository**
+
+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 also application objects registered on events such as after/before
+add/update/delete on entities/relations, server startup or shutdown, etc. As all
+application objects, they have a selector defining when they should be called or
+not.
+
+`Operations` may be instantiated by hooks to do further processing at different
+steps of the transaction's commit / rollback, which usually can not be done
+safely at the hook execution time.
+
+Hooks and operation are an essential building block of any moderately complicated
+cubicweb application.
+
+.. Note:
+   RQL queries executed in hooks and operations are *unsafe* by default, e.g. the
+   read and write security is deactivated unless explicitly asked.
+  
+.. |cubicweb| replace:: *CubicWeb*
--- a/doc/book/en/intro/concepts/index.rst	Thu Mar 04 17:56:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,317 +0,0 @@
-.. -*- coding: utf-8 -*-
-
-.. _Concepts:
-
-The Core Concepts of |cubicweb|
-===============================
-
-This section defines some terms and core concepts of the |cubicweb|
-framework. To avoid confusion while reading this book, take time to go through
-the following definitions and use this section as a reference during your
-reading.
-
-.. _Cube:
-
-Cubes
------
-
-A cube is a software component made of three parts: its data model
-(:file:`schema`), its logic (:file:`entities`) and its user interface
-(:file:`views`).
-
-A cube can use other cubes as building blocks and assemble them to provide
-a whole with richer functionnalities than its parts. The cubes `cubicweb-blog`_
-and `cubicweb-comment`_ could be used to make a cube named *myblog* with
-commentable blog entries.
-
-The `|cubicweb| Forge`_ offers a large number of cubes developed by the community
-and available under a free software license.
-
-The command :command:`cubicweb-ctl list` displays the list of cubes installed on your
-system.
-
-On a Unix system, the available cubes are usually stored in the directory
-:file:`/usr/share/cubicweb/cubes`. If you're using the cubicweb forest
-(:ref:SourceInstallation), the cubes are searched in the directory
-:file:`/path/to/cubicweb_forest/cubes`. The environment variable
-:envvar:`CW_CUBES_PATH` gives additionnal locations where to search for cubes.
-
-.. _`|cubicweb| Forge`: http://www.cubicweb.org/project/
-.. _`cubicweb-blog`: http://www.cubicweb.org/project/cubicweb-blog
-.. _`cubicweb-comment`: http://www.cubicweb.org/project/cubicweb-comment
-
-
-Instances
----------
-
-An instance is a runnable application installed on a computer and based on a
-cube.
-
-The instance directory contains the configuration files. Several instances can
-be created and based on the same cube. For exemple, several software forges can
-be set up on one computer system based on the `cubicweb-forge`_ cube.
-
-.. _`cubicweb-forge`: http://www.cubicweb.org/project/cubicweb-forge
-
-Instances can be of three different types: all-in-one, web engine or data
-repository. For applications that support high traffic, several web (front-end)
-and data (back-end) instances can be set-up to share the load.
-
-.. image:: ../../images/archi_globale.en.png
-
-The command :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 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.
-
-Data Repository
----------------
-
-The data repository [1]_ provides access to one or more data sources (including
-SQL databases, LDAP repositories, Mercurial or Subversion version control
-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
-querier, which does not realize when a query spans accross several data sources
-and requires running sub-queries and merges to complete.
-
-It is common to run the web engine and the repository in the same process (see
-instances of type all-in-one above), but this is not a requirement. A repository
-can be set up to be accessed remotely using Pyro (`Python Remote Objects`_) and
-act as a server.
-
-Some logic can be attached to events that happen in the repository, like
-creation of entities, deletion of relations, etc. This is used for example to
-send email notifications when the state of an object changes. See `Hooks` below.
-
-.. [1] not to be confused with a Mercurial repository or a Debian repository.
-.. _`Python Remote Objects`: http://pyro.sourceforge.net/
-
-Web Engine
-----------
-
-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 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)
--------------------
-
-The data model of a cube is described as an entity-relationship schema using a
-comprehensive language made of Python classes imported from the yams_ library.
-
-.. _yams: http://www.logilab.org/project/yams/
-
-An `entity type` defines a set of attributes and is used in some relations.
-Attributes may be of the following types: `String`, `Int`, `Float`, `Boolean`,
-`Date`, `Time`, `Datetime`, `Interval`, `Password`, `Bytes`, `RichString`. See
-:ref:`yams.BASE_TYPES` for details.
-
-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`.
-
-A `relation definition` is a triple (*subject entity type*, *relation type*, *object
-entity type*) associated with a set of properties such as cardinality,
-constraints, etc.
-
-Permissions can be set on entity types and relation types to control who will be
-able to create, read, update or delete entities and relations.
-
-Some meta-data necessary to the system is added to the data model. That includes
-entities like users and groups, the entities used to store the data model
-itself and attributes like unique identifier, creation date, creator, etc.
-
-When you create a new |cubicweb| instance, the schema is stored in the database.
-When the cubes the instance is based on evolve, they may change their data model
-and provide migration scripts that will be executed when the administrator will
-run the upgrade process for the instance.
-
-Registries and Objects
-----------------------
-
-Application objects
-~~~~~~~~~~~~~~~~~~~
-
-Beside a few core functionalities, almost every feature of the framework is
-achieved by dynamic objects (`application objects` or `appobjects`) stored in a
-two-levels registry (the `vregistry`). Each object is affected to a registry with
-an identifier in this registry. You may have more than one object sharing an
-identifier in the same registry, At runtime, appobjects are selected in the
-vregistry according to the context.
-
-Application objects are stored in the registry using a two-level hierarchy :
-
-  object's `__registry__` : object's `id` : [list of app objects]
-
-The base class of appobjects is `AppObject` (module `cubicweb.appobject`).
-
-The `vregistry`
-~~~~~~~~~~~~~~~
-
-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
-~~~~~~~~~
-
-Each appobject has a selector, that is used to compute how well the object fits
-a given context. The better the object fits the context, the higher the score.
-
-|cubicweb| provides a set of basic selectors that may be parametrized. Selectors
-can be combined with the binary operators `&` and `|` to build more complex
-selector that can be combined too.
-
-There are three common ways to retrieve some appobject from the repository:
-
-* get the most appropriate objects by specifying a registry and an identifier. In
-  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
-  that case, every object with the a postive score is selected.
-
-* get the object within a particular registry/identifier. In that case no
-  selection process is involved, the vregistry will expect to find a single
-  object in that cell.
-
-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 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.
-
-If no object is found for a particular registry and identifier,
-``ObjectNotFound`` exception is raised.
-
-In such cases you would need to review your design and make sure your views are
-properly defined.
-
-
-
-The RQL query language
-----------------------
-
-**No need for a complicated ORM when you have a powerful query language**
-
-All the persistent data in a |cubicweb| instance is retrieved and modified by using the
-Relation Query Language.
-
-This query language is inspired by SQL but is on a higher level in order to
-emphasize browsing relations.
-
-db-api
-~~~~~~
-
-The repository exposes a `db-api`_ like api but using the RQL instead of SQL.
-XXX feed me
-
-Result set
-~~~~~~~~~~
-
-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
------
-
-**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.
-
-
-Hooks
------
-
-**CubicWeb provides an extensible data repository**
-
-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.
-
-
-.. _RunMode:
-
-Running mode
-------------
-
-A running mode is a predifined set of configuration telling where it should look
-for various resources, such as cubes, instances, etc. To ease development with
-the framework, there are two running modes with |cubicweb|:
-
-* 'user', resources are searched / created in the user home directory:
-  - instances are stored in :file:`~/etc/cubicweb.d`
-  - temporary files (such as pid file) in :file:`/tmp`
-
-* 'system', resources are searched / created in the system directories (eg usually requiring root access):
-  - instances are stored in :file:`/etc/cubicweb.d`
-  - temporary files (such as pid file) in :file:`/var/run/cubicweb`
-
-Cubes search path is also affected, see the :ref:Cube section.
-
-By default, the mode automatically set to 'user' if a :file:`.hg` directory is found
-in the cubicweb package, else it's set to 'system'. You can force this by setting
-the :envvar:`CW_MODE` environment variable to either 'user' or 'system'.
-
-If you've a doubt about the mode you're currently running, check the first line
-outputed by the :command:`cubicweb-ctl list` command.
-
-Notice that each resource path may be explicitly set using an environment
-variable if the default doesn't suit your needs.
-
-.. |cubicweb| replace:: *CubicWeb*
\ No newline at end of file
--- a/doc/book/en/intro/history.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/intro/history.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -19,10 +19,10 @@
 *CubicWeb* has been developed by Logilab_ and used in-house for many years
 before it was first installed for its clients in 2006 as version 2.
 
-In 2008, *CubicWeb* version 3 became downloadable for free under the terms of
-the LGPL license. Its community is now steadily growing as changes can occur
-rapidly thanks to the time and energy originally put in the design of the
-framework.
+In 2008, *CubicWeb* version 3 became downloadable for free under the
+terms of the LGPL license. Its community is now steadily growing
+without hampering the fast-paced stream of changes thanks to the time
+and energy originally put in the design of the framework.
 
 
 .. _Narval: http://www.logilab.org/project/narval
--- a/doc/book/en/intro/index.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/intro/index.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -17,5 +17,5 @@
 
    book-map
    history
-   concepts/index
+   concepts.rst
    tutorial/index
--- a/doc/book/en/intro/tutorial/create-cube.rst	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/book/en/intro/tutorial/create-cube.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -340,7 +340,7 @@
 example HTML output.
 
 .. note::
-   You can find more details about views and selectors in :ref:`ViewDefinition`.
+   You can find more details about views and selectors in :ref:`Views`.
 
 
 .. _DefineEntities:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/features_list.rst	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,224 @@
+=================
+CubicWeb features
+=================
+
+This page  tries to resume features found in the bare cubicweb framework,
+how mature and documented they are.
+
+:code maturity (CM):
+
+  - 0: experimental, not ready at all for production, may be killed
+
+  - 1: draft / unsatisfying, api may change in a near future, much probably in long
+       term
+
+  - 2: good enough, api sounds good but will probably evolve a bit with more
+    hindsight
+
+  - 3: mature, backward incompatible changes unexpected (may still evolve though,
+    of course)
+
+
+:documentation level (DL):
+
+  - 0: no documentation
+
+  - 1: poor documentation
+
+  - 2: some valuable documentation but some parts keep uncovered
+
+  - 3: good / complete documentation
+
+
+Instance configuration and maintainance
+=======================================
+
++====================================================================+====+====+
+|  FEATURE                                                           | CM | DL |
++====================================================================+====+====+
+| setup - installation                                               | 2  | 3  |
+| setup - environment variables                                      | 3  | 2  |
+| setup - running modes                                              | 2  | 2  |
+| setup - administration tasks                                       | 2  | 2  |
+| setup - configuration file                                         | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| configuration - user / groups handling                             | 3  | 1  |
+| configuration - site configuration                                 | 3  | 1  |
+| configuration - distributed configuration                          | 2  | 1  |
+| configuration - pyro                                               | 2  | 2  |
++--------------------------------------------------------------------+----+----+
+| multi-sources - capabilities                                       | NA | 0  |
+| multi-sources - configuration                                      | 2  | 0  |
+| multi-sources - ldap integration                                   | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| usage - custom ReST markup                                         | 2  | 0  |
+| usage - personal preferences                                       | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+
+
+Core development
+================
+
++====================================================================+====+====+
+|  FEATURE                                                           | CM | DL |
++====================================================================+====+====+
+| base - concepts                                                    | NA | 3  |
+| base - security model                                              | NA | 2  |
+| base - database initialization                                     | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| rql - base                                                         | 2  | 2  |
+| rql - write                                                        | 2  | 2  |
+| rql - function                                                     | 2  | 0  |
+| rql - outer joins                                                  | 2  | 1  |
+| rql - aggregates                                                   | 2  | 1  |
+| rql - subqueries                                                   | 2  | 0  |
++--------------------------------------------------------------------+----+----+
+| schema - base                                                      | 2  | 3  |
+| schema - constraints                                               | 3  | 2  |
+| schema - security                                                  | 2  | 2  |
+| schema - inheritance                                               | 1  | 1  |
+| schema - customization                                             | 1  | 1  |
+| schema - introspection                                             | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| vregistry - appobject                                              | 2  | 2  |
+| vregistry - registration                                           | 2  | 2  |
+| vregistry - selection                                              | 3  | 2  |
+| vregistry - core selectors                                         | 3  | 3  |
+| vregistry - custom selectors                                       | 2  | 1  |
+| vregistry - debugging selection                                    | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| entities - interfaces                                              | 2  | ?  |
+| entities - customization (dc_,...)                                 | 2  | ?  |
+| entities - app logic                                               | 2  | 2  |
+| entities - orm configuration                                       | 2  | 1  |
+| entities - pluggable mixins                                        | 1  | 0  |
+| entities - workflow                                                | 3  | 2  |
++--------------------------------------------------------------------+----+----+
+| dbapi - connection                                                 | 3  | 1  |
+| dbapi - data management                                            | 1  | 1  |
+| dbapi - result set                                                 | 3  | 1  |
+| dbapi - transaction, undo                                          | 2  | 0  |
++--------------------------------------------------------------------+----+----+
+| cube - layout                                                      | 2  | 3  |
+| cube - new cube                                                    | 2  | 2  |
++--------------------------------------------------------------------+----+----+
+| migration - context                                                | 2  | 1  |
+| migration - commands                                               | 2  | 2  |
++--------------------------------------------------------------------+----+----+
+| testlib - CubicWebTC                                               | 2  | 1  |
+| testlib - automatic tests                                          | 2  | 2  |
++--------------------------------------------------------------------+----+----+
+| i18n - mark string                                                 | 3  | 2  |
+| i18n - customize strings from other cubes / cubicweb               | 3  | 1  |
+| i18n - update catalog                                              | 3  | 2  |
++--------------------------------------------------------------------+----+----+
+| more - reloading tips                                              | NA | 0  |
+| more - site_cubicweb                                               | 2  | ?  |
+| more - adding options in configuration file                        | 3  | 0  |
+| more - adding options in site configuration / preferences          | 3  | ?  |
+| more - optimizing / profiling                                      | 2  | 1  |
+| more - c-c plugins                                                 | 3  | 0  |
+| more - crypto services                                             | 0  | 0  |
+| more - massive import                                              | 2  | 0  |
+| more - mime type based conversion                                  | 2  | 0  |
+| more - CWCache                                                     | 1  | 0  |
++--------------------------------------------------------------------+----+----+
+
+
+Web UI development
+==================
+
++====================================================================+====+====+
+|  FEATURE                                                           | CM | DL |
++====================================================================+====+====+
+| base - web request                                                 | 2  | 2  |
+| base - exceptions                                                  | 2  | 0  |
+| base - session, authentication                                     | 1  | 0  |
+| base - http caching                                                | 2  | 1  |
+| base - external resources                                          | 2  | 2  |
+| base - static files                                                | 2  | ?  |
+| base - data sharing                                                | 2  | 2  |
+| base - graphical chart customization                               | 1  | 1  |
++--------------------------------------------------------------------+----+----+
+| publishing - cycle                                                 | 2  | 2  |
+| publishing - error handling                                        | 2  | 1  |
+| publishing - transactions                                          | NA | ?  |
++--------------------------------------------------------------------+----+----+
+| controller - base                                                  | 2  | 2  |
+| controller - view                                                  | 2  | 1  |
+| controller - edit                                                  | 2  | 1  |
+| controller - json                                                  | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| views - base                                                       | 2  | 2  |
+| views - templates                                                  | 2  | 2  |
+| views - boxes                                                      | 2  | 1  |
+| views - components                                                 | 2  | 1  |
+| views - primary                                                    | 2  | 1  |
+| views - tabs                                                       | 2  | 1  |
+| views - xml                                                        | 2  | 0  |
+| views - text                                                       | 2  | 1  |
+| views - table                                                      | 2  | 1  |
+| views - plot                                                       | 2  | 0  |
+| views - navigation                                                 | 2  | 0  |
+| views - calendar, timeline                                         | 2  | 0  |
+| views - index                                                      | 2  | 2  |
+| views - breadcrumbs                                                | 2  | 1  |
+| views - actions                                                    | 2  | 1  |
+| views - debugging                                                  | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+| form - base                                                        | 2  | 1  |
+| form - fields                                                      | 2  | 1  |
+| form - widgets                                                     | 2  | 1  |
+| form - captcha                                                     | 2  | 0  |
+| form - renderers                                                   | 2  | 0  |
+| form - validation error handling                                   | 2  | 0  |
+| form - autoform                                                    | 2  | 2  |
+| form - reledit                                                     | 2  | 0  |
++--------------------------------------------------------------------+----+----+
+| facets - base                                                      | 2  | ?  |
+| facets - configuration                                             | 2  | 1  |
+| facets - custom facets                                             | 2  | 0  |
++--------------------------------------------------------------------+----+----+
+| css - base                                                         | 1  | 1  |
+| css - customization                                                | 1  | 1  |
++--------------------------------------------------------------------+----+----+
+| js - base                                                          | 1  | 1  |
+| js - jquery                                                        | 1  | 1  |
+| js - base functions                                                | 1  | 0  |
+| js - ajax                                                          | 1  | 0  |
+| js - widgets                                                       | 1  | 1  |
++--------------------------------------------------------------------+----+----+
+| other - page template                                              | 0  | 0  |
+| other - inline doc (wdoc)                                          | 2  | 0  |
+| other - magic search                                               | 2  | 0  |
+| other - url mapping                                                | 1  | 1  |
+| other - apache style url rewrite                                   | 1  | 1  |
+| other - sparql                                                     | 1  | 0  |
+| other - bookmarks                                                  | 2  | 1  |
++--------------------------------------------------------------------+----+----+
+
+
+Repository development
+======================
+
++====================================================================+====+====+
+|  FEATURE                                                           | CM | DL |
++====================================================================+====+====+
+| base - session                                                     | 2  | 2  |
+| base - more security control                                       | 2  | 0  |
+| base - debugging                                                   | 2  | 0  |
++--------------------------------------------------------------------+----+----+
+| hooks - development                                                | 2  | 2  |
+| hooks - abstract hooks                                             | 2  | 0  |
+| hooks - core hooks                                                 | 2  | 0  |
+| hooks - control                                                    | 2  | 0  |
+| hooks - operation                                                  | 2  | 2  |
++--------------------------------------------------------------------+----+----+
+| notification - sending email                                       | 2  | ?  |
+| notification - base views                                          | 1  | ?  |
+| notification - supervisions                                        | 1  | 0  |
++--------------------------------------------------------------------+----+----+
+| source - storages                                                  | 2  | 0  |
+| source - authentication plugins                                    | 2  | 0  |
+| source - custom sources                                            | 2  | 0  |
++--------------------------------------------------------------------+----+----+
--- a/doc/tools/generate_modules.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/doc/tools/generate_modules.py	Tue Apr 06 19:46:38 2010 +0200
@@ -16,7 +16,7 @@
     cw_gen = ModuleGenerator('cubicweb', '../..')
     cw_gen.generate("../book/en/annexes/api_cubicweb.rst",
                     EXCLUDE_DIRS + ('cwdesklets', 'misc', 'skel', 'skeleton'))
-    for modname in ('indexer', 'logilab', 'rql', 'yams'):
+    for modname in ('logilab', 'rql', 'yams'):
         cw_gen = ModuleGenerator(modname, '../../../' + modname)
         cw_gen.generate("../book/en/annexes/api_%s.rst" % modname,
                         EXCLUDE_DIRS + ('tools',))
--- a/entities/authobjs.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/entities/authobjs.py	Tue Apr 06 19:46:38 2010 +0200
@@ -93,15 +93,10 @@
         return self.groups == frozenset(('guests', ))
 
     def owns(self, eid):
-        if hasattr(self._cw, 'unsafe_execute'):
-            # use unsafe_execute on the repository side, in case
-            # session's user doesn't have access to CWUser
-            execute = self._cw.unsafe_execute
-        else:
-            execute = self._cw.execute
         try:
-            return execute('Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s',
-                           {'x': eid, 'u': self.eid}, 'x')
+            return self._cw.execute(
+                'Any X WHERE X eid %(x)s, X owned_by U, U eid %(u)s',
+                {'x': eid, 'u': self.eid}, 'x')
         except Unauthorized:
             return False
     owns = cached(owns, keyarg=1)
--- a/entities/schemaobjs.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/entities/schemaobjs.py	Tue Apr 06 19:46:38 2010 +0200
@@ -9,6 +9,8 @@
 
 from logilab.common.decorators import cached
 
+from yams.schema import role_name
+
 from cubicweb import ValidationError
 from cubicweb.schema import ERQLExpression, RRQLExpression
 
@@ -56,30 +58,22 @@
             return u'%s <<%s>>' % (self.dc_title(), ', '.join(stereotypes))
         return self.dc_title()
 
-    def inlined_changed(self, inlined):
-        """check inlining is necessary and possible:
-
-        * return False if nothing has changed
-        * raise ValidationError if inlining is'nt possible
-        * eventually return True
+    def check_inlined_allowed(self):
+        """check inlining is possible, raise ValidationError if not possible
         """
-        rschema = self._cw.vreg.schema.rschema(self.name)
-        if inlined == rschema.inlined:
-            return False
-        if inlined:
-            # 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._cw._("can't set inlined=%(inlined)s, "
-                                     "%(stype)s %(rtype)s %(otype)s "
-                                     "has cardinality=%(card)s")
-                    raise ValidationError(self.eid, {'inlined': msg % locals()})
-        return True
+        # 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':
+                qname = role_name('inlined', 'subject')
+                rtype = self.name
+                stype = rdef.stype
+                otype = rdef.otype
+                msg = self._cw._("can't set inlined=%(inlined)s, "
+                                 "%(stype)s %(rtype)s %(otype)s "
+                                 "has cardinality=%(card)s")
+                raise ValidationError(self.eid, {qname: msg % locals()})
 
     def db_key_name(self):
         """XXX goa specific"""
--- a/entities/test/unittest_wfobjs.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/entities/test/unittest_wfobjs.py	Tue Apr 06 19:46:38 2010 +0200
@@ -1,5 +1,7 @@
+from __future__ import with_statement
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import ValidationError
+from cubicweb.server.session import security_enabled
 
 def add_wf(self, etype, name=None, default=False):
     if name is None:
@@ -37,7 +39,7 @@
         self.commit()
         wf.add_state(u'foo')
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name': 'workflow already have a state of that name'})
+        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a state of that name'})
         # no pb if not in the same workflow
         wf2 = add_wf(self, 'Company')
         foo = wf2.add_state(u'foo', initial=True)
@@ -47,7 +49,7 @@
         self.commit()
         bar.set_attributes(name=u'foo')
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name': 'workflow already have a state of that name'})
+        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a state of that name'})
 
     def test_duplicated_transition(self):
         wf = add_wf(self, 'Company')
@@ -56,7 +58,7 @@
         wf.add_transition(u'baz', (foo,), bar, ('managers',))
         wf.add_transition(u'baz', (bar,), foo)
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name': 'workflow already have a transition of that name'})
+        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
         # no pb if not in the same workflow
         wf2 = add_wf(self, 'Company')
         foo = wf.add_state(u'foo', initial=True)
@@ -68,7 +70,7 @@
         self.commit()
         biz.set_attributes(name=u'baz')
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name': 'workflow already have a transition of that name'})
+        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
 
 
 class WorkflowTC(CubicWebTC):
@@ -126,10 +128,11 @@
         wf = add_wf(self, 'CWUser')
         s = wf.add_state(u'foo', initial=True)
         self.commit()
-        ex = self.assertRaises(ValidationError, self.session.unsafe_execute,
+        with security_enabled(self.session, write=False):
+            ex = self.assertRaises(ValidationError, self.session.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. "
+            self.assertEquals(ex.errors, {'in_state-subject': "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):
@@ -172,7 +175,7 @@
         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"})
+        self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
         cnx.close()
         cnx = self.login('member')
         req = self.request()
@@ -181,7 +184,7 @@
         cnx.commit()
         ex = self.assertRaises(ValidationError,
                                member.fire_transition, 'activate')
-        self.assertEquals(ex.errors, {'by_transition': "transition may not be fired"})
+        self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
 
     def test_fire_transition_owned_by(self):
         self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", '
@@ -254,7 +257,7 @@
         # 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 workflow"})
+        self.assertEquals(ex.errors, {'to_state-subject': "state doesn't belong to entity's workflow"})
         self.rollback()
         # force back to state1
         self.group.change_state('state1', u'gadget')
@@ -290,7 +293,7 @@
         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"})
+        self.assertEquals(ex.errors, {'subworkflow_exit-subject': u"can't have multiple exits on the same state"})
 
     def test_swf_fire_in_a_row(self):
         # sub-workflow
@@ -403,7 +406,7 @@
         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'})
+        self.assertEquals(ex.errors, {'custom_workflow-subject': 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"""
@@ -412,7 +415,7 @@
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': self.member.eid}, 'x')
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'custom_workflow': 'workflow isn\'t a workflow for this type'})
+        self.assertEquals(ex.errors, {'custom_workflow-subject': 'workflow isn\'t a workflow for this type'})
 
     def test_del_custom_wf(self):
         """member in some state shared by the new workflow, nothing has to be
@@ -440,19 +443,21 @@
 
 class AutoTransitionTC(CubicWebTC):
 
-    def setup_database(self):
-        self.wf = add_wf(self, 'CWUser')
-        asleep = self.wf.add_state('asleep', initial=True)
-        dead = self.wf.add_state('dead')
-        self.wf.add_transition('rest', asleep, asleep)
-        self.wf.add_transition('sick', asleep, dead, type=u'auto',
-                               conditions=({'expr': u'U surname "toto"',
-                                            'mainvars': u'U'},))
+    def setup_custom_wf(self):
+        wf = add_wf(self, 'CWUser')
+        asleep = wf.add_state('asleep', initial=True)
+        dead = wf.add_state('dead')
+        wf.add_transition('rest', asleep, asleep)
+        wf.add_transition('sick', asleep, dead, type=u'auto',
+                          conditions=({'expr': u'X surname "toto"',
+                                       'mainvars': u'X'},))
+        return wf
 
     def test_auto_transition_fired(self):
+        wf = self.setup_custom_wf()
         user = self.create_user('member')
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
-                     {'wf': self.wf.eid, 'x': user.eid})
+                     {'wf': wf.eid, 'x': user.eid})
         self.commit()
         user.clear_all_caches()
         self.assertEquals(user.state, 'asleep')
@@ -466,7 +471,7 @@
                           ['rest'])
         self.assertEquals(parse_hist(user.workflow_history),
                           [('asleep', 'asleep', 'rest', None)])
-        self.request().user.set_attributes(surname=u'toto') # fulfill condition
+        user.set_attributes(surname=u'toto') # fulfill condition
         self.commit()
         user.fire_transition('rest')
         self.commit()
@@ -477,6 +482,26 @@
                            ('asleep', 'asleep', 'rest', None),
                            ('asleep', 'dead', 'sick', None),])
 
+    def test_auto_transition_custom_initial_state_fired(self):
+        wf = self.setup_custom_wf()
+        user = self.create_user('member', surname=u'toto')
+        self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
+                     {'wf': wf.eid, 'x': user.eid})
+        self.commit()
+        self.assertEquals(user.state, 'dead')
+
+    def test_auto_transition_initial_state_fired(self):
+        wf = self.execute('Any WF WHERE ET default_workflow WF, '
+                          'ET name %(et)s', {'et': 'CWUser'}).get_entity(0, 0)
+        dead = wf.add_state('dead')
+        wf.add_transition('sick', wf.state_by_name('activated'), dead,
+                          type=u'auto', conditions=({'expr': u'X surname "toto"',
+                                                     'mainvars': u'X'},))
+        self.commit()
+        user = self.create_user('member', surname=u'toto')
+        self.commit()
+        self.assertEquals(user.state, 'dead')
+
 
 class WorkflowHooksTC(CubicWebTC):
 
@@ -505,7 +530,7 @@
                      {'wf': self.wf.eid})
         self.commit()
 
-    # XXX currently, we've to rely on hooks to set initial state, or to use unsafe_execute
+    # XXX currently, we've to rely on hooks to set initial state, or to use execute
     # def test_initial_state(self):
     #     cnx = self.login('stduser')
     #     cu = cnx.cursor()
@@ -532,7 +557,7 @@
         user = cnx.user(self.session)
         ex = self.assertRaises(ValidationError,
                                user.fire_transition, 'activate')
-        self.assertEquals(self._cleanup_msg(ex.errors['by_transition']),
+        self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
 
@@ -541,7 +566,7 @@
         user = cnx.user(self.session)
         ex = self.assertRaises(ValidationError,
                                user.fire_transition, 'dummy')
-        self.assertEquals(self._cleanup_msg(ex.errors['by_transition']),
+        self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
 
@@ -554,7 +579,7 @@
         session.set_pool()
         ex = self.assertRaises(ValidationError,
                                user.fire_transition, 'deactivate')
-        self.assertEquals(self._cleanup_msg(ex.errors['by_transition']),
+        self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                                             u"transition isn't allowed from")
         # get back now
         user.fire_transition('activate')
--- a/entities/wfobjs.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/entities/wfobjs.py	Tue Apr 06 19:46:38 2010 +0200
@@ -158,7 +158,7 @@
             todelstate = self.state_by_name(todelstate)
         if not hasattr(replacement, 'eid'):
             replacement = self.state_by_name(replacement)
-        execute = self._cw.unsafe_execute
+        execute = self._cw.execute
         execute('SET X in_state S WHERE S eid %(s)s', {'s': todelstate.eid}, 's')
         execute('SET X from_state NS WHERE X to_state OS, OS eid %(os)s, NS eid %(ns)s',
                 {'os': todelstate.eid, 'ns': replacement.eid}, 's')
--- a/entity.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/entity.py	Tue Apr 06 19:46:38 2010 +0200
@@ -20,6 +20,7 @@
 from cubicweb.rset import ResultSet
 from cubicweb.selectors import yes
 from cubicweb.appobject import AppObject
+from cubicweb.req import _check_cw_unsafe
 from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint
 from cubicweb.rqlrewrite import RQLRewriter
 
@@ -59,7 +60,7 @@
     :cvar skip_copy_for: a list of relations that should be skipped when copying
                          this kind of entity. Note that some relations such
                          as composite relations or relations that have '?1' as object
-                         cardinality are always skipped. 
+                         cardinality are always skipped.
     """
     __registry__ = 'etypes'
     __select__ = yes()
@@ -224,6 +225,93 @@
     def __cmp__(self, other):
         raise NotImplementedError('comparison not implemented for %s' % self.__class__)
 
+    def __getitem__(self, key):
+        if key == 'eid':
+            warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
+                 DeprecationWarning, stacklevel=2)
+            return self.eid
+        return super(Entity, self).__getitem__(key)
+
+    def __setitem__(self, attr, value):
+        """override __setitem__ to update self.edited_attributes.
+
+        Typically, a before_[update|add]_hook could do::
+
+            entity['generated_attr'] = generated_value
+
+        and this way, edited_attributes will be updated accordingly. Also, add
+        the attribute to skip_security since we don't want to check security
+        for such attributes set by hooks.
+        """
+        if attr == 'eid':
+            warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
+                 DeprecationWarning, stacklevel=2)
+            self.eid = value
+        else:
+            super(Entity, self).__setitem__(attr, value)
+            # don't add attribute into skip_security if already in edited
+            # attributes, else we may accidentaly skip a desired security check
+            if hasattr(self, 'edited_attributes') and \
+                   attr not in self.edited_attributes:
+                self.edited_attributes.add(attr)
+                self.skip_security_attributes.add(attr)
+
+    def __delitem__(self, attr):
+        """override __delitem__ to update self.edited_attributes on cleanup of
+        undesired changes introduced in the entity's dict. For example, see the
+        code snippet below from the `forge` cube:
+
+        .. sourcecode:: python
+
+            edited = self.entity.edited_attributes
+            has_load_left = 'load_left' in edited
+            if 'load' in edited and self.entity.load_left is None:
+                self.entity.load_left = self.entity['load']
+            elif not has_load_left and edited:
+                # cleanup, this may cause undesired changes
+                del self.entity['load_left']
+
+        """
+        super(Entity, self).__delitem__(attr)
+        if hasattr(self, 'edited_attributes'):
+            self.edited_attributes.remove(attr)
+
+    def setdefault(self, attr, default):
+        """override setdefault to update self.edited_attributes"""
+        super(Entity, self).setdefault(attr, default)
+        # don't add attribute into skip_security if already in edited
+        # attributes, else we may accidentaly skip a desired security check
+        if hasattr(self, 'edited_attributes') and \
+               attr not in self.edited_attributes:
+            self.edited_attributes.add(attr)
+            self.skip_security_attributes.add(attr)
+
+    def pop(self, attr, default=_marker):
+        """override pop to update self.edited_attributes on cleanup of
+        undesired changes introduced in the entity's dict. See `__delitem__`
+        """
+        if default is _marker:
+            value = super(Entity, self).pop(attr)
+        else:
+            value = super(Entity, self).pop(attr, default)
+        if hasattr(self, 'edited_attributes') and attr in self.edited_attributes:
+            self.edited_attributes.remove(attr)
+        return value
+
+    def update(self, values):
+        """override update to update self.edited_attributes. See `__setitem__`
+        """
+        for attr, value in values.items():
+            self[attr] = value # use self.__setitem__ implementation
+
+    def rql_set_value(self, attr, value):
+        """call by rql execution plan when some attribute is modified
+
+        don't use dict api in such case since we don't want attribute to be
+        added to skip_security_attributes.
+        """
+        super(Entity, self).__setitem__(attr, value)
+
     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
@@ -234,7 +322,7 @@
         return self
 
     def set_eid(self, eid):
-        self.eid = self['eid'] = eid
+        self.eid = eid
 
     def has_eid(self):
         """return True if the entity has an attributed eid (False
@@ -440,7 +528,8 @@
         """returns a resultset containing `self` information"""
         rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
                          {'x': self.eid}, [(self.__regid__,)])
-        return self._cw.decorate_rset(rset)
+        rset.req = self._cw
+        return rset
 
     def to_complete_relations(self):
         """by default complete final relations to when calling .complete()"""
@@ -459,7 +548,7 @@
                    all(matching_groups(e.get_groups('read')) for e in targets):
                     yield rschema, 'subject'
 
-    def to_complete_attributes(self, skip_bytes=True):
+    def to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
         for rschema, attrschema in self.e_schema.attribute_definitions():
             # skip binary data by default
             if skip_bytes and attrschema.type == 'Bytes':
@@ -470,13 +559,13 @@
             # password retreival is blocked at the repository server level
             rdef = rschema.rdef(self.e_schema, attrschema)
             if not self._cw.user.matching_groups(rdef.get_groups('read')) \
-                   or attrschema.type == 'Password':
+                   or (attrschema.type == 'Password' and skip_pwd):
                 self[attr] = None
                 continue
             yield attr
 
     _cw_completed = False
-    def complete(self, attributes=None, skip_bytes=True):
+    def complete(self, attributes=None, skip_bytes=True, skip_pwd=True):
         """complete this entity by adding missing attributes (i.e. query the
         repository to fill the entity)
 
@@ -493,7 +582,7 @@
         V = varmaker.next()
         rql = ['WHERE %s eid %%(x)s' % V]
         selected = []
-        for attr in (attributes or self.to_complete_attributes(skip_bytes)):
+        for attr in (attributes or self.to_complete_attributes(skip_bytes, skip_pwd)):
             # if attribute already in entity, nothing to do
             if self.has_key(attr):
                 continue
@@ -531,8 +620,8 @@
             # if some outer join are included to fetch inlined relations
             rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
                                     ','.join(rql))
-            execute = getattr(self._cw, 'unsafe_execute', self._cw.execute)
-            rset = execute(rql, {'x': self.eid}, 'x', build_descr=False)[0]
+            rset = self._cw.execute(rql, {'x': self.eid}, 'x',
+                                    build_descr=False)[0]
             # handle attributes
             for i in xrange(1, lastattr):
                 self[str(selected[i-1][0])] = rset[i]
@@ -542,7 +631,7 @@
                 value = rset[i]
                 if value is None:
                     rrset = ResultSet([], rql, {'x': self.eid})
-                    self._cw.decorate_rset(rrset)
+                    rrset.req = self._cw
                 else:
                     rrset = self._cw.eid_rset(value)
                 self.set_related_cache(rtype, role, rrset)
@@ -560,11 +649,8 @@
             if not self.is_saved():
                 return None
             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
-            # XXX should we really use unsafe_execute here? I think so (syt),
-            # see #344874
-            execute = getattr(self._cw, 'unsafe_execute', self._cw.execute)
             try:
-                rset = execute(rql, {'x': self.eid}, 'x')
+                rset = self._cw.execute(rql, {'x': self.eid}, 'x')
             except Unauthorized:
                 self[name] = value = None
             else:
@@ -595,10 +681,7 @@
             pass
         assert self.has_eid()
         rql = self.related_rql(rtype, role)
-        # XXX should we really use unsafe_execute here? I think so (syt),
-        # see #344874
-        execute = getattr(self._cw, 'unsafe_execute', self._cw.execute)
-        rset = execute(rql, {'x': self.eid}, 'x')
+        rset = self._cw.execute(rql, {'x': self.eid}, 'x')
         self.set_related_cache(rtype, role, rset)
         return self.related(rtype, role, limit, entities)
 
@@ -785,10 +868,6 @@
         haseid = 'eid' in self
         self._cw_completed = False
         self.clear()
-        # set eid if it was in, else we may get nasty error while editing this
-        # entity if it's bound to a repo session
-        if haseid:
-            self['eid'] = self.eid
         # clear relations cache
         for rschema, _, role in self.e_schema.relation_definitions():
             self.clear_related_cache(rschema.type, role)
@@ -800,63 +879,68 @@
 
     # raw edition utilities ###################################################
 
-    def set_attributes(self, _cw_unsafe=False, **kwargs):
+    def set_attributes(self, **kwargs):
+        _check_cw_unsafe(kwargs)
         assert kwargs
+        assert self._is_saved, "should not call set_attributes while entity "\
+               "hasn't been saved yet"
         relations = []
         for key in kwargs:
             relations.append('X %s %%(%s)s' % (key, key))
-        # update current local object
-        self.update(kwargs)
         # and now update the database
         kwargs['x'] = self.eid
-        if _cw_unsafe:
-            self._cw.unsafe_execute(
-                'SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs, 'x')
-        else:
-            self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
-                             kwargs, 'x')
+        self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
+                         kwargs, 'x')
+        kwargs.pop('x')
+        # update current local object _after_ the rql query to avoid
+        # interferences between the query execution itself and the
+        # edited_attributes / skip_security_attributes machinery
+        self.update(kwargs)
 
-    def set_relations(self, _cw_unsafe=False, **kwargs):
+    def set_relations(self, **kwargs):
         """add relations to the given object. To set a relation where this entity
         is the object of the relation, use 'reverse_'<relation> as argument name.
 
-        Values may be an entity, a list of entity, or None (meaning that all
+        Values may be an entity, a list of entities, or None (meaning that all
         relations of the given type from or to this object should be deleted).
         """
-        if _cw_unsafe:
-            execute = self._cw.unsafe_execute
-        else:
-            execute = self._cw.execute
         # XXX update cache
+        _check_cw_unsafe(kwargs)
         for attr, values in kwargs.iteritems():
             if attr.startswith('reverse_'):
                 restr = 'Y %s X' % attr[len('reverse_'):]
             else:
                 restr = 'X %s Y' % attr
             if values is None:
-                execute('DELETE %s WHERE X eid %%(x)s' % restr,
-                        {'x': self.eid}, 'x')
+                self._cw.execute('DELETE %s WHERE X eid %%(x)s' % restr,
+                                 {'x': self.eid}, 'x')
                 continue
             if not isinstance(values, (tuple, list, set, frozenset)):
                 values = (values,)
-            execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
+            self._cw.execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
                 restr, ','.join(str(r.eid) for r in values)),
-                    {'x': self.eid}, 'x')
+                             {'x': self.eid}, 'x')
 
-    def delete(self):
+    def delete(self, **kwargs):
         assert self.has_eid(), self.eid
         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
-                         {'x': self.eid})
+                         {'x': self.eid}, **kwargs)
 
     # server side utilities ###################################################
 
+    @property
+    def skip_security_attributes(self):
+        try:
+            return self._skip_security_attributes
+        except:
+            self._skip_security_attributes = set()
+            return self._skip_security_attributes
+
     def set_defaults(self):
         """set default values according to the schema"""
-        self._default_set = set()
         for attr, value in self.e_schema.defaults():
             if not self.has_key(attr):
                 self[str(attr)] = value
-                self._default_set.add(attr)
 
     def check(self, creation=False):
         """check this entity against its schema. Only final relation
@@ -868,7 +952,18 @@
             _ = unicode
         else:
             _ = self._cw._
-        self.e_schema.check(self, creation=creation, _=_)
+        if creation:
+            # on creations, we want to check all relations, especially
+            # required attributes
+            relations = [rschema for rschema in self.e_schema.subject_relations()
+                         if rschema.final and rschema.type != 'eid']
+        elif hasattr(self, 'edited_attributes'):
+            relations = [self._cw.vreg.schema.rschema(rtype)
+                         for rtype in self.edited_attributes]
+        else:
+            relations = None
+        self.e_schema.check(self, creation=creation, _=_,
+                            relations=relations)
 
     def fti_containers(self, _done=None):
         if _done is None:
@@ -876,7 +971,6 @@
         _done.add(self.eid)
         containers = tuple(self.e_schema.fulltext_containers())
         if containers:
-            yielded = False
             for rschema, target in containers:
                 if target == 'object':
                     targets = getattr(self, rschema.type)
@@ -888,8 +982,6 @@
                     for container in entity.fti_containers(_done):
                         yield container
                         yielded = True
-            if not yielded:
-                yield self
         else:
             yield self
 
@@ -897,12 +989,12 @@
         """used by the full text indexer to get words to index
 
         this method should only be used on the repository side since it depends
-        on the indexer package
+        on the logilab.database package
 
         :rtype: list
         :return: the list of indexable word of this entity
         """
-        from indexer.query_objects import tokenize
+        from logilab.database.fti import tokenize
         # take care to cases where we're modyfying the schema
         pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
         words = []
@@ -919,7 +1011,6 @@
                 continue
             if value:
                 words += tokenize(value)
-
         for rschema, role in self.e_schema.fulltext_relations():
             if role == 'subject':
                 for entity in getattr(self, rschema.type):
@@ -946,8 +1037,6 @@
 
     def __set__(self, eobj, value):
         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	Thu Mar 04 17:56:45 2010 +0100
+++ b/etwist/server.py	Tue Apr 06 19:46:38 2010 +0200
@@ -11,7 +11,6 @@
 import os
 import select
 import errno
-import hotshot
 from time import mktime
 from datetime import date, timedelta
 from urlparse import urlsplit, urlunsplit
@@ -99,15 +98,11 @@
     def __init__(self, config, debug=None):
         self.debugmode = debug
         self.config = config
-        self.base_url = config['base-url'] or config.default_base_url()
-        if self.base_url[-1] != '/':
-            self.base_url += '/'
-        self.https_url = config['https-url']
-        if self.https_url and self.https_url[-1] != '/':
-            self.https_url += '/'
         # instantiate publisher here and not in init_publisher to get some
         # checks done before daemonization (eg versions consistency)
         self.appli = CubicWebPublisher(config, debug=self.debugmode)
+        self.base_url = config['base-url']
+        self.https_url = config['https-url']
         self.versioned_datadir = 'data%s' % config.instance_md5_version()
 
     def init_publisher(self):
@@ -117,8 +112,6 @@
         if config.repo_method == 'inmemory':
             reactor.addSystemEventTrigger('before', 'shutdown',
                                           self.shutdown_event)
-            # monkey patch start_looping_task to get proper reactor integration
-            #self.appli.repo.__class__.start_looping_tasks = start_looping_tasks
             if config.pyro_enabled():
                 # if pyro is enabled, we have to register to the pyro name
                 # server, create a pyro daemon, and create a task to handle pyro
@@ -171,7 +164,7 @@
                         datadir = self.config.locate_resource(segments[1])
                         if datadir is None:
                             return None, []
-                    self.info('static file %s from %s', segments[-1], datadir)
+                    self.debug('static file %s from %s', segments[-1], datadir)
                     if segments[0] == 'data':
                         return static.File(str(datadir)), segments[1:]
                     else:
@@ -250,21 +243,12 @@
                                  headers=req.headers_out or None)
         except ExplicitLogin:  # must be before AuthenticationError
             return self.request_auth(req)
-        except AuthenticationError:
-            if self.config['auth-mode'] == 'cookie':
-                # in cookie mode redirecting to the index view is enough :
-                # either anonymous connection is allowed and the page will
-                # be displayed or we'll be redirected to the login form
-                msg = req._('you have been logged out')
-                if req.https:
-                    req._base_url =  self.base_url
-                    req.https = False
-                url = req.build_url('view', vid='index', __message=msg)
-                return self.redirect(req, url)
-            else:
-                # in http we have to request auth to flush current http auth
-                # information
-                return self.request_auth(req, loggedout=True)
+        except AuthenticationError, ex:
+            if self.config['auth-mode'] == 'cookie' and getattr(ex, 'url', None):
+                return self.redirect(req, ex.url)
+            # in http we have to request auth to flush current http auth
+            # information
+            return self.request_auth(req, loggedout=True)
         except Redirect, ex:
             return self.redirect(req, ex.location)
         # request may be referenced by "onetime callback", so clear its entity
@@ -350,32 +334,131 @@
 set_log_methods(CubicWebRootResource, getLogger('cubicweb.twisted'))
 
 
+listiterator = type(iter([]))
 
-def _gc_debug():
+def _gc_debug(all=True):
     import gc
     from pprint import pprint
     from cubicweb.appobject import AppObject
     gc.collect()
     count = 0
     acount = 0
+    fcount = 0
+    rcount = 0
+    ccount = 0
+    scount = 0
     ocount = {}
+    from rql.stmts import Union
+    from cubicweb.schema import CubicWebSchema
+    from cubicweb.rset import ResultSet
+    from cubicweb.dbapi import Connection, Cursor
+    from cubicweb.req import RequestSessionBase
+    from cubicweb.server.repository import Repository
+    from cubicweb.server.sources.native import NativeSQLSource
+    from cubicweb.server.session import Session
+    from cubicweb.devtools.testlib import CubicWebTC
+    from logilab.common.testlib import TestSuite
+    from optparse import Values
+    import types, weakref
     for obj in gc.get_objects():
-        if isinstance(obj, CubicWebTwistedRequestAdapter):
+        if isinstance(obj, RequestSessionBase):
             count += 1
+            if isinstance(obj, Session):
+                print '   session', obj, referrers(obj, True)
         elif isinstance(obj, AppObject):
             acount += 1
-        else:
+        elif isinstance(obj, ResultSet):
+            rcount += 1
+            #print '   rset', obj, referrers(obj)
+        elif isinstance(obj, Repository):
+            print '   REPO', obj, referrers(obj, True)
+        #elif isinstance(obj, NativeSQLSource):
+        #    print '   SOURCe', obj, referrers(obj)
+        elif isinstance(obj, CubicWebTC):
+            print '   TC', obj, referrers(obj)
+        elif isinstance(obj, TestSuite):
+            print '   SUITE', obj, referrers(obj)
+        #elif isinstance(obj, Values):
+        #    print '   values', '%#x' % id(obj), referrers(obj, True)
+        elif isinstance(obj, Connection):
+            ccount += 1
+            #print '   cnx', obj, referrers(obj)
+        #elif isinstance(obj, Cursor):
+        #    ccount += 1
+        #    print '   cursor', obj, referrers(obj)
+        elif isinstance(obj, file):
+            fcount += 1
+        #    print '   open file', file.name, file.fileno
+        elif isinstance(obj, CubicWebSchema):
+            scount += 1
+            print '   schema', obj, referrers(obj)
+        elif not isinstance(obj, (type, tuple, dict, list, set, frozenset,
+                                  weakref.ref, weakref.WeakKeyDictionary,
+                                  listiterator,
+                                  property, classmethod,
+                                  types.ModuleType, types.MemberDescriptorType,
+                                  types.FunctionType, types.MethodType)):
             try:
                 ocount[obj.__class__] += 1
             except KeyError:
                 ocount[obj.__class__] = 1
             except AttributeError:
                 pass
-    print 'IN MEM REQUESTS', count
-    print 'IN MEM APPOBJECTS', acount
-    ocount = sorted(ocount.items(), key=lambda x: x[1], reverse=True)[:20]
-    pprint(ocount)
-    print 'UNREACHABLE', gc.garbage
+    if count:
+        print ' NB REQUESTS/SESSIONS', count
+    if acount:
+        print ' NB APPOBJECTS', acount
+    if ccount:
+        print ' NB CONNECTIONS', ccount
+    if rcount:
+        print ' NB RSETS', rcount
+    if scount:
+        print ' NB SCHEMAS', scount
+    if fcount:
+        print ' NB FILES', fcount
+    if all:
+        ocount = sorted(ocount.items(), key=lambda x: x[1], reverse=True)[:20]
+        pprint(ocount)
+    if gc.garbage:
+        print 'UNREACHABLE', gc.garbage
+
+def referrers(obj, showobj=False):
+    try:
+        return sorted(set((type(x), showobj and x or getattr(x, '__name__', '%#x' % id(x)))
+                          for x in _referrers(obj)))
+    except TypeError:
+        s = set()
+        unhashable = []
+        for x in _referrers(obj):
+            try:
+                s.add(x)
+            except TypeError:
+                unhashable.append(x)
+        return sorted(s) + unhashable
+
+def _referrers(obj, seen=None, level=0):
+    import gc, types
+    from cubicweb.schema import CubicWebRelationSchema, CubicWebEntitySchema
+    interesting = []
+    if seen is None:
+        seen = set()
+    for x in gc.get_referrers(obj):
+        if id(x) in seen:
+            continue
+        seen.add(id(x))
+        if isinstance(x, types.FrameType):
+            continue
+        if isinstance(x, (CubicWebRelationSchema, CubicWebEntitySchema)):
+            continue
+        if isinstance(x, (list, tuple, set, dict, listiterator)):
+            if level >= 5:
+                pass
+                #interesting.append(x)
+            else:
+                interesting += _referrers(x, seen, level+1)
+        else:
+            interesting.append(x)
+    return interesting
 
 def run(config, debug):
     # create the site
@@ -410,7 +493,7 @@
     root_resource.start_service()
     logger.info('instance started on %s', root_resource.base_url)
     if config['profile']:
-        prof = hotshot.Profile(config['profile'])
-        prof.runcall(reactor.run)
+        import cProfile
+        cProfile.runctx('reactor.run()', globals(), locals(), config['profile'])
     else:
         reactor.run()
--- a/etwist/service.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/etwist/service.py	Tue Apr 06 19:46:38 2010 +0200
@@ -17,7 +17,6 @@
 os.environ['CW_INSTANCES_DIR'] = r'C:\etc\cubicweb.d'
 os.environ['USERNAME'] = 'cubicweb'
 
-
 class CWService(object, win32serviceutil.ServiceFramework):
     _svc_name_ = None
     _svc_display_name_ = None
@@ -25,7 +24,6 @@
 
     def __init__(self, *args, **kwargs):
         win32serviceutil.ServiceFramework.__init__(self, *args, **kwargs)
-        self._stop_event = win32event.CreateEvent(None, 0, 0, None)
         cwcfg.load_cwctl_plugins()
         set_log_methods(CubicWebRootResource, logger)
         server.parsePOSTData = parsePOSTData
@@ -33,8 +31,8 @@
     def SvcStop(self):
         self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
         logger.info('stopping %s service' % self.instance)
-        win32event.SetEvent(self._stop_event)
-        self.ReportServiceStatus(win32service.SERVICE_STOPPED)
+        reactor.stop()
+        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
 
     def SvcDoRun(self):
         self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
@@ -57,5 +55,4 @@
         except Exception, e:
             logger.error('service %s stopped (cause: %s)' % (self.instance, e))
             logger.exception('what happened ...')
-            self.SvcStop()
-
+        self.ReportServiceStatus(win32service.SERVICE_STOPPED)
--- a/ext/html4zope.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/ext/html4zope.py	Tue Apr 06 19:46:38 2010 +0200
@@ -24,12 +24,13 @@
 
 __docformat__ = 'reStructuredText'
 
+import os
+
 from logilab.mtconverter import xml_escape
 
 from docutils import nodes
 from docutils.writers.html4css1 import Writer as CSS1Writer
 from docutils.writers.html4css1 import HTMLTranslator as CSS1HTMLTranslator
-import os
 
 default_level = int(os.environ.get('STX_DEFAULT_LEVEL', 3))
 
--- a/ext/rest.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/ext/rest.py	Tue Apr 06 19:46:38 2010 +0200
@@ -25,7 +25,7 @@
 from os.path import join
 
 from docutils import statemachine, nodes, utils, io
-from docutils.core import publish_string
+from docutils.core import Publisher
 from docutils.parsers.rst import Parser, states, directives
 from docutils.parsers.rst.roles import register_canonical_role, set_classes
 
@@ -92,14 +92,15 @@
     in `docutils.parsers.rst.directives.misc`
     """
     context = state.document.settings.context
+    cw = context._cw
     source = state_machine.input_lines.source(
         lineno - state_machine.input_offset - 1)
     #source_dir = os.path.dirname(os.path.abspath(source))
     fid = arguments[0]
-    for lang in chain((context._cw.lang, context.vreg.property_value('ui.language')),
-                      context.config.available_languages()):
+    for lang in chain((cw.lang, cw.vreg.property_value('ui.language')),
+                      cw.vreg.config.available_languages()):
         rid = '%s_%s.rst' % (fid, lang)
-        resourcedir = context.config.locate_doc_file(rid)
+        resourcedir = cw.vreg.config.locate_doc_file(rid)
         if resourcedir:
             break
     else:
@@ -196,6 +197,15 @@
         self.finish_parse()
 
 
+# XXX docutils keep a ref on context, can't find a correct way to remove it
+class CWReSTPublisher(Publisher):
+    def __init__(self, context, settings, **kwargs):
+        Publisher.__init__(self, **kwargs)
+        self.set_components('standalone', 'restructuredtext', 'pseudoxml')
+        self.process_programmatic_settings(None, settings, None)
+        self.settings.context = context
+
+
 def rest_publish(context, data):
     """publish a string formatted as ReStructured Text to HTML
 
@@ -218,7 +228,7 @@
         # remove unprintable characters unauthorized in xml
         data = data.translate(ESC_CAR_TABLE)
     settings = {'input_encoding': encoding, 'output_encoding': 'unicode',
-                'warning_stream': StringIO(), 'context': context,
+                'warning_stream': StringIO(),
                 # dunno what's the max, severe is 4, and we never want a crash
                 # (though try/except may be a better option...)
                 'halt_level': 10,
@@ -233,9 +243,17 @@
     else:
         base_url = None
     try:
-        return publish_string(writer=Writer(base_url=base_url),
-                              parser=CubicWebReSTParser(), source=data,
-                              settings_overrides=settings)
+        pub = CWReSTPublisher(context, settings,
+                              parser=CubicWebReSTParser(),
+                              writer=Writer(base_url=base_url),
+                              source_class=io.StringInput,
+                              destination_class=io.StringOutput)
+        pub.set_source(data)
+        pub.set_destination()
+        res = pub.publish(enable_exit_status=None)
+        # necessary for proper garbage collection, else a ref is kept somewhere in docutils...
+        del pub.settings.context
+        return res
     except Exception:
         LOGGER.exception('error while publishing ReST text')
         if not isinstance(data, unicode):
--- a/goa/appobjects/dbmgmt.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/goa/appobjects/dbmgmt.py	Tue Apr 06 19:46:38 2010 +0200
@@ -172,7 +172,7 @@
     skip_etypes = ('CWGroup', 'CWUser')
 
     def call(self):
-        # XXX should use unsafe_execute with all hooks deactivated
+        # XXX should use unsafe execute with all hooks deactivated
         # XXX step by catching datastore errors?
         for eschema in self.schema.entities():
             if eschema.final or eschema in self.skip_etypes:
--- a/goa/db.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/goa/db.py	Tue Apr 06 19:46:38 2010 +0200
@@ -86,7 +86,7 @@
                 entity = vreg.etype_class(eschema.type)(req, rset, i, j)
                 rset._get_entity_cache_ = {(i, j): entity}
     rset.rowcount = len(rows)
-    req.decorate_rset(rset)
+    rset.req = req
     return rset
 
 
--- a/goa/dbinit.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/goa/dbinit.py	Tue Apr 06 19:46:38 2010 +0200
@@ -84,7 +84,7 @@
             Put(gaeentity)
 
 def init_persistent_schema(ssession, schema):
-    execute = ssession.unsafe_execute
+    execute = ssession.execute
     rql = ('INSERT CWEType X: X name %(name)s, X description %(descr)s,'
            'X final FALSE')
     eschema = schema.eschema('CWEType')
@@ -96,7 +96,7 @@
                       'descr': unicode(eschema.description)})
 
 def insert_versions(ssession, config):
-    execute = ssession.unsafe_execute
+    execute = ssession.execute
     # insert versions
     execute('INSERT CWProperty X: X pkey %(pk)s, X value%(v)s',
             {'pk': u'system.version.cubicweb',
--- a/goa/gaesource.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/goa/gaesource.py	Tue Apr 06 19:46:38 2010 +0200
@@ -255,10 +255,11 @@
                 if asession.user.eid == entity.eid:
                     asession.user.update(dict(gaeentity))
 
-    def delete_entity(self, session, etype, eid):
+    def delete_entity(self, session, entity):
         """delete an entity from the source"""
         # do not delay delete_entity as other modifications to ensure
         # consistency
+        eid = entity.eid
         key = Key(eid)
         Delete(key)
         session.clear_datastore_cache(key)
--- a/goa/goaconfig.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/goa/goaconfig.py	Tue Apr 06 19:46:38 2010 +0200
@@ -86,7 +86,7 @@
     cube_appobject_path = WebConfiguration.cube_appobject_path | ServerConfiguration.cube_appobject_path
 
     # use file system schema
-    bootstrap_schema = read_instance_schema = False
+    read_instance_schema = False
     # schema is not persistent, don't load schema hooks (unavailable)
     schema_hooks = False
     # no user workflow for now
--- a/goa/goactl.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/goa/goactl.py	Tue Apr 06 19:46:38 2010 +0200
@@ -15,38 +15,40 @@
                                  create_dir)
 from cubicweb.cwconfig import CubicWebConfiguration
 
-from logilab import common as lgc
-from logilab import constraint as lgcstr
-from logilab import mtconverter as lgmtc
-import rql, yams, yapps, simplejson, docutils, roman
 
-SLINK_DIRECTORIES = [
-    (lgc.__path__[0], 'logilab/common'),
-    (lgmtc.__path__[0], 'logilab/mtconverter'),
-    (lgcstr.__path__[0], 'logilab/constraint'),
-    (rql.__path__[0], 'rql'),
-    (simplejson.__path__[0], 'simplejson'),
-    (yams.__path__[0], 'yams'),
-    (yapps.__path__[0], 'yapps'),
-    (docutils.__path__[0], 'docutils'),
-    (roman.__file__.replace('.pyc', '.py'), 'roman.py'),
+def slink_directories():
+    import rql, yams, yapps, simplejson, docutils, roman
+    from logilab import common as lgc
+    from logilab import constraint as lgcstr
+    from logilab import mtconverter as lgmtc
+    dirs = [
+        (lgc.__path__[0], 'logilab/common'),
+        (lgmtc.__path__[0], 'logilab/mtconverter'),
+        (lgcstr.__path__[0], 'logilab/constraint'),
+        (rql.__path__[0], 'rql'),
+        (simplejson.__path__[0], 'simplejson'),
+        (yams.__path__[0], 'yams'),
+        (yapps.__path__[0], 'yapps'),
+        (docutils.__path__[0], 'docutils'),
+        (roman.__file__.replace('.pyc', '.py'), 'roman.py'),
 
-    ('/usr/share/fckeditor/', 'fckeditor'),
+        ('/usr/share/fckeditor/', 'fckeditor'),
+
+        (join(CW_SOFTWARE_ROOT, 'web', 'data'), join('cubes', 'shared', 'data')),
+        (join(CW_SOFTWARE_ROOT, 'web', 'wdoc'), join('cubes', 'shared', 'wdoc')),
+        (join(CW_SOFTWARE_ROOT, 'i18n'), join('cubes', 'shared', 'i18n')),
+        (join(CW_SOFTWARE_ROOT, 'goa', 'tools'), 'tools'),
+        (join(CW_SOFTWARE_ROOT, 'goa', 'bin'), 'bin'),
+        ]
 
-    (join(CW_SOFTWARE_ROOT, 'web', 'data'), join('cubes', 'shared', 'data')),
-    (join(CW_SOFTWARE_ROOT, 'web', 'wdoc'), join('cubes', 'shared', 'wdoc')),
-    (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
+    try:
+        import dateutil
+        import vobject
+        dirs.extend([ (dateutil.__path__[0], 'dateutil'),
+                      (vobject.__path__[0], 'vobject') ] )
+    except ImportError:
+        pass
+    return dirs
 
 COPY_CW_FILES = (
     '__init__.py',
@@ -194,7 +196,7 @@
         copy_skeleton(join(CW_SOFTWARE_ROOT, 'goa', 'skel'),
                       appldir, context, askconfirm=True)
         # cubicweb core dependancies
-        for directory, subdirectory in SLINK_DIRECTORIES:
+        for directory, subdirectory in slink_directories():
             subdirectory = join(appldir, subdirectory)
             if not exists(split(subdirectory)[0]):
                 create_dir(split(subdirectory)[0])
--- a/hooks/__init__.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/hooks/__init__.py	Tue Apr 06 19:46:38 2010 +0200
@@ -1,1 +1,36 @@
-"""core hooks"""
+"""core hooks
+
+:organization: Logilab
+:copyright: 2009-2010 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 timedelta, datetime
+from cubicweb.server import hook
+
+class ServerStartupHook(hook.Hook):
+    """task to cleanup expirated auth cookie entities"""
+    __regid__ = 'cw_cleanup_transactions'
+    events = ('server_startup',)
+
+    def __call__(self):
+        # XXX use named args and inner functions to avoid referencing globals
+        # which may cause reloading pb
+        lifetime = timedelta(days=self.repo.config['keep-transaction-lifetime'])
+        def cleanup_old_transactions(repo=self.repo, lifetime=lifetime):
+            mindate = datetime.now() - lifetime
+            session = repo.internal_session()
+            try:
+                session.system_sql(
+                    'DELETE FROM transactions WHERE tx_time < %(time)s',
+                    {'time': mindate})
+                # cleanup deleted entities
+                session.system_sql(
+                    'DELETE FROM deleted_entities WHERE dtime < %(time)s',
+                    {'time': mindate})
+                session.commit()
+            finally:
+                session.close()
+        self.repo.looping_task(60*60*24, cleanup_old_transactions, self.repo)
--- a/hooks/email.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/hooks/email.py	Tue Apr 06 19:46:38 2010 +0200
@@ -26,7 +26,7 @@
 
     def precommit_event(self):
         if self.condition():
-            self.session.unsafe_execute(
+            self.session.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')
 
--- a/hooks/integrity.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/hooks/integrity.py	Tue Apr 06 19:46:38 2010 +0200
@@ -10,11 +10,14 @@
 
 from threading import Lock
 
+from yams.schema import role_name
+
 from cubicweb import ValidationError
 from cubicweb.schema import RQLConstraint, RQLUniqueConstraint
 from cubicweb.selectors import implements
 from cubicweb.uilib import soup2xhtml
 from cubicweb.server import hook
+from cubicweb.server.hook import set_operation
 
 # special relations that don't have to be checked for integrity, usually
 # because they are handled internally by hooks (so we trust ourselves)
@@ -35,13 +38,12 @@
     RQLUniqueConstraint in two different transactions, as explained in
     http://intranet.logilab.fr/jpl/ticket/36564
     """
-    asession = session.actual_session()
-    if 'uniquecstrholder' in asession.transaction_data:
+    if 'uniquecstrholder' in session.transaction_data:
         return
     _UNIQUE_CONSTRAINTS_LOCK.acquire()
-    asession.transaction_data['uniquecstrholder'] = True
+    session.transaction_data['uniquecstrholder'] = True
     # register operation responsible to release the lock on commit/rollback
-    _ReleaseUniqueConstraintsOperation(asession)
+    _ReleaseUniqueConstraintsOperation(session)
 
 def _release_unique_cstr_lock(session):
     if 'uniquecstrholder' in session.transaction_data:
@@ -61,50 +63,48 @@
     """checking relation cardinality has to be done after commit in
     case the relation is being replaced
     """
-    eid, rtype = None, None
+    role = key = base_rql = None
 
     def precommit_event(self):
-        # recheck pending eids
-        if self.session.deleted_in_transaction(self.eid):
-            return
-        if self.rtype in self.session.transaction_data.get('pendingrtypes', ()):
-            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()
+        session =self.session
+        pendingeids = session.transaction_data.get('pendingeids', ())
+        pendingrtypes = session.transaction_data.get('pendingrtypes', ())
+        # poping key is not optional: if further operation trigger new deletion
+        # of relation, we'll need a new operation
+        for eid, rtype in session.transaction_data.pop(self.key):
+            # recheck pending eids / relation types
+            if eid in pendingeids:
+                continue
+            if rtype in pendingrtypes:
+                continue
+            if not session.execute(self.base_rql % rtype, {'x': eid}, 'x'):
+                etype = session.describe(eid)[0]
+                _ = session._
+                msg = _('at least one relation %(rtype)s is required on '
+                        '%(etype)s (%(eid)s)')
+                msg %= {'rtype': _(rtype), 'etype': _(etype), 'eid': eid}
+                raise ValidationError(eid, {role_name(rtype, self.role): msg})
 
 
 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'
-
+    role = 'subject'
+    key = '_cwisrel'
+    base_rql = 'Any O WHERE S eid %%(x)s, S %s O'
 
 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'
+    role = 'object'
+    key = '_cwiorel'
+    base_rql = 'Any S WHERE O eid %%(x)s, S %s O'
 
 
 class IntegrityHook(hook.Hook):
     __abstract__ = True
     category = 'integrity'
 
-class UserIntegrityHook(IntegrityHook):
-    __abstract__ = True
-    __select__ = IntegrityHook.__select__ & hook.regular_session()
 
-
-class CheckCardinalityHook(UserIntegrityHook):
+class CheckCardinalityHook(IntegrityHook):
     """check cardinalities are satisfied"""
     __regid__ = 'checkcard'
     events = ('after_add_entity', 'before_delete_relation')
@@ -112,14 +112,6 @@
     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
@@ -127,10 +119,14 @@
             # skip automatically handled relations
             if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
                 continue
-            opcls = role == 'subject' and _CheckSRelationOp or _CheckORelationOp
             rdef = rschema.role_rdef(eschema, targetschemas[0], role)
             if rdef.role_cardinality(role) in '1+':
-                self.checkrel_if_necessary(opcls, rschema.type, eid)
+                if role == 'subject':
+                    set_operation(self._cw, '_cwisrel', (eid, rschema.type),
+                                  _CheckSRelationOp)
+                else:
+                    set_operation(self._cw, '_cwiorel', (eid, rschema.type),
+                                  _CheckORelationOp)
 
     def before_delete_relation(self):
         rtype = self.rtype
@@ -138,14 +134,16 @@
             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
+        card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
         if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
-            self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom)
+            set_operation(self._cw, '_cwisrel', (eidfrom, rtype),
+                          _CheckSRelationOp)
         if card[1] in '1+' and not session.deleted_in_transaction(eidto):
-            self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto)
+            set_operation(self._cw, '_cwiorel', (eidto, rtype),
+                          _CheckORelationOp)
 
 
 class _CheckConstraintsOp(hook.LateOperation):
@@ -176,7 +174,7 @@
         pass
 
 
-class CheckConstraintHook(UserIntegrityHook):
+class CheckConstraintHook(IntegrityHook):
     """check the relation satisfy its constraints
 
     this is delayed to a precommit time operation since other relation which
@@ -194,7 +192,7 @@
                                rdef=(self.eidfrom, self.rtype, self.eidto))
 
 
-class CheckAttributeConstraintHook(UserIntegrityHook):
+class CheckAttributeConstraintHook(IntegrityHook):
     """check the attribute relation satisfy its constraints
 
     this is delayed to a precommit time operation since other relation which
@@ -214,7 +212,7 @@
                                         rdef=(self.entity.eid, attr, None))
 
 
-class CheckUniqueHook(UserIntegrityHook):
+class CheckUniqueHook(IntegrityHook):
     __regid__ = 'checkunique'
     events = ('before_add_entity', 'before_update_entity')
 
@@ -227,49 +225,11 @@
                 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})
+                rset = self._cw.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
-    """
-    __regid__ = 'deletecomposite'
-    events = ('before_delete_relation',)
-
-    def __call__(self):
-        # if the relation is being delete, don't delete composite's components
-        # automatically
-        pendingrdefs = self._cw.transaction_data.get('pendingrdefs', ())
-        if (self._cw.describe(self.eidfrom)[0], self.rtype,
-            self._cw.describe(self.eidto)[0]) in pendingrdefs:
-            return
-        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)
+                    qname = role_name(attr, 'subject')
+                    raise ValidationError(entity.eid, {qname: msg % val})
 
 
 class DontRemoveOwnersGroupHook(IntegrityHook):
@@ -281,16 +241,20 @@
 
     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:
+            msg = self._cw._('can\'t be deleted')
+            raise ValidationError(self.entity.eid, {None: msg})
+        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')})
+                qname = role_name('name', 'subject')
+                msg = self._cw._('can\'t be changed')
+                raise ValidationError(self.entity.eid, {qname: msg})
             self.entity['name'] = newname
 
 
-class TidyHtmlFields(UserIntegrityHook):
+class TidyHtmlFields(IntegrityHook):
     """tidy HTML in rich text strings"""
     __regid__ = 'htmltidy'
     events = ('before_add_entity', 'before_update_entity')
@@ -319,3 +283,59 @@
         user = self.entity
         if 'login' in user.edited_attributes and user.login:
             user.login = user.login.strip()
+
+
+# 'active' integrity hooks: you usually don't want to deactivate them, they are
+# not really integrity check, they maintain consistency on changes
+
+class _DelayedDeleteOp(hook.Operation):
+    """delete the object of composite relation except if the relation has
+    actually been redirected to another composite
+    """
+    key = base_rql = None
+
+    def precommit_event(self):
+        session = self.session
+        pendingeids = session.transaction_data.get('pendingeids', ())
+        neweids = session.transaction_data.get('neweids', ())
+        # poping key is not optional: if further operation trigger new deletion
+        # of composite relation, we'll need a new operation
+        for eid, rtype in session.transaction_data.pop(self.key):
+            # don't do anything if the entity is being created or deleted
+            if not (eid in pendingeids or eid in neweids):
+                etype = session.describe(eid)[0]
+                session.execute(self.base_rql % (etype, rtype), {'x': eid}, 'x')
+
+class _DelayedDeleteSEntityOp(_DelayedDeleteOp):
+    """delete orphan subject entity of a composite relation"""
+    key = '_cwiscomp'
+    base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT X %s Y'
+
+class _DelayedDeleteOEntityOp(_DelayedDeleteOp):
+    """check required object relation"""
+    key = '_cwiocomp'
+    base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT Y %s X'
+
+
+class DeleteCompositeOrphanHook(hook.Hook):
+    """delete the composed of a composite relation when this relation is deleted
+    """
+    __regid__ = 'deletecomposite'
+    events = ('before_delete_relation',)
+    category = 'activeintegrity'
+
+    def __call__(self):
+        # if the relation is being delete, don't delete composite's components
+        # automatically
+        pendingrdefs = self._cw.transaction_data.get('pendingrdefs', ())
+        if (self._cw.describe(self.eidfrom)[0], self.rtype,
+            self._cw.describe(self.eidto)[0]) in pendingrdefs:
+            return
+        composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
+                                              'composite')
+        if composite == 'subject':
+            set_operation(self._cw, '_cwiocomp', (self.eidto, self.rtype),
+                          _DelayedDeleteOEntityOp)
+        elif composite == 'object':
+            set_operation(self._cw, '_cwiscomp', (self.eidfrom, self.rtype),
+                          _DelayedDeleteSEntityOp)
--- a/hooks/metadata.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/hooks/metadata.py	Tue Apr 06 19:46:38 2010 +0200
@@ -12,19 +12,7 @@
 
 from cubicweb.selectors import 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
+from cubicweb.server.utils import eschema_eid
 
 
 class MetaDataHook(hook.Hook):
@@ -76,7 +64,10 @@
 
 
 class SetIsHook(MetaDataHook):
-    """create a new entity -> set is relation"""
+    """create a new entity -> set is and is_instance_of relations
+
+    those relations are inserted using sql so they are not hookable.
+    """
     __regid__ = 'setis'
     events = ('after_add_entity',)
 
@@ -86,18 +77,14 @@
         session = self._cw
         entity = self.entity
         try:
-            #session.add_relation(entity.eid, 'is',
-            #                     eschema_type_eid(session, entity.__regid__))
             session.system_sql('INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)'
-                           % (entity.eid, eschema_type_eid(session, entity.__regid__)))
+                           % (entity.eid, eschema_eid(session, entity.e_schema)))
         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))
+        for eschema in entity.e_schema.ancestors() + [entity.e_schema]:
             session.system_sql('INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)'
-                               % (entity.eid, eschema_type_eid(session, etype)))
+                               % (entity.eid, eschema_eid(session, eschema)))
 
 
 class SetOwnershipHook(MetaDataHook):
@@ -106,18 +93,17 @@
     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)
+        if not self._cw.is_internal_session:
+            self._cw.add_relation(self.entity.eid, 'owned_by', self._cw.user.eid)
+            _SetCreatorOp(self._cw, 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'))
+        self.session.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):
@@ -150,7 +136,8 @@
 
 
 class UpdateFTIHook(MetaDataHook):
-    """sync fulltext index when relevant relation is added / removed
+    """sync fulltext index text index container when a relation with
+    fulltext_container set is added / removed
     """
     __regid__ = 'updateftirel'
     events = ('after_add_relation', 'after_delete_relation')
@@ -158,15 +145,19 @@
     def __call__(self):
         rtype = self.rtype
         session = self._cw
+        ftcontainer = session.vreg.schema.rschema(rtype).fulltext_container
         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))
+                session.repo.system_source.index_entity(
+                    session, session.entity_from_eid(self.eidfrom))
             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))
+                session.repo.system_source.index_entity(
+                    session, session.entity_from_eid(self.eidto))
+        # after delete relation
+        elif ftcontainer == 'subject':
+            session.repo.system_source.index_entity(
+                session, entity=session.entity_from_eid(self.eidfrom))
+        elif ftcontainer == 'object':
+            session.repo.system_source.index_entity(
+                session, entity=session.entity_from_eid(self.eidto))
 
--- a/hooks/notification.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/hooks/notification.py	Tue Apr 06 19:46:38 2010 +0200
@@ -103,29 +103,28 @@
 class EntityUpdateHook(NotificationHook):
     __regid__ = 'notifentityupdated'
     __abstract__ = True # do not register by default
-
+    __select__ = NotificationHook.__select__ & hook.from_dbapi_query()
     events = ('before_update_entity',)
     skip_attrs = set()
 
     def __call__(self):
         session = self._cw
-        if self.entity.eid in session.transaction_data.get('neweids', ()):
+        if session.added_in_transaction(self.entity.eid):
             return # entity is being created
-        if session.is_super_session:
-            return # ignore changes triggered by hooks
         # then compute changes
+        attrs = [k for k in self.entity.edited_attributes
+                 if not k in self.skip_attrs]
+        if not attrs:
+            return
         changes = session.transaction_data.setdefault('changes', {})
         thisentitychanges = changes.setdefault(self.entity.eid, set())
-        attrs = [k for k in self.entity.edited_attributes if not k in self.skip_attrs]
-        if not attrs:
-            return
         rqlsel, rqlrestr = [], ['X eid %(x)s']
         for i, attr in enumerate(attrs):
             var = chr(65+i)
             rqlsel.append(var)
             rqlrestr.append('X %s %s' % (attr, var))
         rql = 'Any %s WHERE %s' % (','.join(rqlsel), ','.join(rqlrestr))
-        rset = session.unsafe_execute(rql, {'x': self.entity.eid}, 'x')
+        rset = session.execute(rql, {'x': self.entity.eid}, 'x')
         for i, attr in enumerate(attrs):
             oldvalue = rset[0][i]
             newvalue = self.entity[attr]
@@ -139,13 +138,11 @@
 
 class SomethingChangedHook(NotificationHook):
     __regid__ = 'supervising'
+    __select__ = NotificationHook.__select__ & hook.from_dbapi_query()
     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
--- a/hooks/security.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/hooks/security.py	Tue Apr 06 19:46:38 2010 +0200
@@ -9,27 +9,32 @@
 __docformat__ = "restructuredtext en"
 
 from cubicweb import Unauthorized
+from cubicweb.selectors import objectify_selector, lltrace
 from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook
 
 
 def check_entity_attributes(session, entity, editedattrs=None):
     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', ())
+    # .skip_security_attributes is there to bypass security for attributes
+    # set by hooks by modifying the entity's dictionnary
+    dontcheck = entity.skip_security_attributes
     if editedattrs is None:
         try:
             editedattrs = entity.edited_attributes
         except AttributeError:
-            editedattrs = entity
+            editedattrs = entity # XXX unexpected
     for attr in editedattrs:
-        if attr in defaults:
+        if attr in dontcheck:
             continue
         rdef = eschema.rdef(attr)
         if rdef.final: # non final relation are checked by other hooks
             # add/delete should be equivalent (XXX: unify them into 'update' ?)
             rdef.check_perm(session, 'update', eid=eid)
+    # don't update dontcheck until everything went fine: see usage in
+    # after_update_entity, where if we got an Unauthorized at hook time, we will
+    # retry and commit time
+    dontcheck |= frozenset(editedattrs)
 
 
 class _CheckEntityPermissionOp(hook.LateOperation):
@@ -53,10 +58,17 @@
         pass
 
 
+@objectify_selector
+@lltrace
+def write_security_enabled(cls, req, **kwargs):
+    if req is None or not req.write_security:
+        return 0
+    return 1
+
 class SecurityHook(hook.Hook):
     __abstract__ = True
     category = 'security'
-    __select__ = hook.Hook.__select__ & hook.regular_session()
+    __select__ = hook.Hook.__select__ & write_security_enabled()
 
 
 class AfterAddEntitySecurityHook(SecurityHook):
--- a/hooks/storages.py	Thu Mar 04 17:56:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-"""hooks to handle attributes mapped to a custom storage
-"""
-from cubicweb.server.hook import Hook
-from cubicweb.server.sources.storages import ETYPE_ATTR_STORAGE
-
-
-class BFSSHook(Hook):
-    """abstract class for bytes file-system storage hooks"""
-    __abstract__ = True
-    category = 'bfss'
-
-
-class PreAddEntityHook(BFSSHook):
-    """"""
-    __regid__ = 'bfss_add_entity'
-    events = ('before_add_entity', )
-
-    def __call__(self):
-        etype = self.entity.__regid__
-        for attr in ETYPE_ATTR_STORAGE.get(etype, ()):
-            ETYPE_ATTR_STORAGE[etype][attr].entity_added(self.entity, attr)
-
-class PreUpdateEntityHook(BFSSHook):
-    """"""
-    __regid__ = 'bfss_update_entity'
-    events = ('before_update_entity', )
-
-    def __call__(self):
-        etype = self.entity.__regid__
-        for attr in ETYPE_ATTR_STORAGE.get(etype, ()):
-            ETYPE_ATTR_STORAGE[etype][attr].entity_updated(self.entity, attr)
-
-class PreDeleteEntityHook(BFSSHook):
-    """"""
-    __regid__ = 'bfss_delete_entity'
-    events = ('before_delete_entity', )
-
-    def __call__(self):
-        etype = self.entity.__regid__
-        for attr in ETYPE_ATTR_STORAGE.get(etype, ()):
-            ETYPE_ATTR_STORAGE[etype][attr].entity_deleted(self.entity, attr)
--- a/hooks/syncschema.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/hooks/syncschema.py	Tue Apr 06 19:46:38 2010 +0200
@@ -12,11 +12,12 @@
 """
 __docformat__ = "restructuredtext en"
 
+from copy import copy
 from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema
-from yams.buildobjs import EntityType, RelationType, RelationDefinition
-from yams.schema2sql import eschema2sql, rschema2sql, type_from_constraints
+from yams import buildobjs as ybo, schema2sql as y2sql
 
 from logilab.common.decorators import clear_cache
+from logilab.common.testlib import mock_object
 
 from cubicweb import ValidationError
 from cubicweb.selectors import implements
@@ -187,11 +188,12 @@
         # every schema operation is triggering a schema update
         MemSchemaNotifyChanges(session)
 
-    def prepare_constraints(self, subjtype, rtype, objtype):
-        rdef = rtype.rdef(subjtype, objtype)
-        constraints = rdef.constraints
-        self.constraints = list(constraints)
-        rdef.constraints = self.constraints
+    def prepare_constraints(self, rdef):
+        # if constraints is already a list, reuse it (we're updating multiple
+        # constraints of the same rdef in the same transactions)
+        if not isinstance(rdef.constraints, list):
+            rdef.constraints = list(rdef.constraints)
+        self.constraints = rdef.constraints
 
 
 class MemSchemaEarlyOperation(MemSchemaOperation):
@@ -226,18 +228,26 @@
 
 class SourceDbCWRTypeUpdate(hook.Operation):
     """actually update some properties of a relation definition"""
-    rschema = values = entity = None # make pylint happy
+    rschema = entity = values = None # make pylint happy
 
     def precommit_event(self):
+        rschema = self.rschema
+        if rschema.final:
+            return
         session = self.session
-        rschema = self.rschema
-        if rschema.final or not 'inlined' in self.values:
+        if 'fulltext_container' in self.values:
+            ftiupdates = session.transaction_data.setdefault(
+                'fti_update_etypes', set())
+            for subjtype, objtype in rschema.rdefs:
+                ftiupdates.add(subjtype)
+                ftiupdates.add(objtype)
+            UpdateFTIndexOp(session)
+        if 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
+        if inlined:
+            self.entity.check_inlined_allowed()
         # inlined changed, make necessary physical changes!
         sqlexec = self.session.system_sql
         rtype = rschema.type
@@ -246,7 +256,7 @@
             # 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)
+                tablesql = y2sql.rschema2sql(rschema)
                 # create the necessary table
                 for sql in tablesql.split(';'):
                     if sql.strip():
@@ -314,13 +324,13 @@
         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)
+        rdef = ybo.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
 
@@ -338,8 +348,8 @@
                  'internationalizable': entity.internationalizable}
         rdef = self.init_rdef(**props)
         sysource = session.pool.source('system')
-        attrtype = type_from_constraints(sysource.dbhelper, rdef.object,
-                                         rdef.constraints)
+        attrtype = y2sql.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
@@ -370,12 +380,13 @@
                 self.error('error while creating index for %s.%s: %s',
                            table, column, ex)
         # final relations are not infered, propagate
+        schema = session.vreg.schema
         try:
-            eschema = session.vreg.schema.eschema(rdef.subject)
+            eschema = schema.eschema(rdef.subject)
         except KeyError:
             return # entity type currently being added
         # propagate attribute to children classes
-        rschema = session.vreg.schema.rschema(rdef.name)
+        rschema = schema.rschema(rdef.name)
         # if relation type has been inserted in the same transaction, its final
         # attribute is still set to False, so we've to ensure it's False
         rschema.final = True
@@ -385,15 +396,19 @@
                       'cardinality': rdef.cardinality,
                       'constraints': rdef.constraints,
                       'permissions': rdef.get_permissions(),
-                      'order': rdef.order})
+                      'order': rdef.order,
+                      'infered': False, 'eid': None
+                      })
+        cstrtypemap = ss.cstrtype_mapping(session)
         groupmap = group_mapping(session)
+        object = schema.eschema(rdef.object)
         for specialization in eschema.specialized_by(False):
             if (specialization, rdef.object) in rschema.rdefs:
                 continue
-            sperdef = RelationDefinitionSchema(specialization, rschema, rdef.object, props)
-            for rql, args in ss.rdef2rql(rschema, str(specialization),
-                                         rdef.object, sperdef, groupmap=groupmap):
-                session.execute(rql, args)
+            sperdef = RelationDefinitionSchema(specialization, rschema,
+                                               object, props)
+            ss.execschemarql(session.execute, sperdef,
+                             ss.rdef2rql(sperdef, cstrtypemap, groupmap))
         # set default value, using sql for performance and to avoid
         # modification_date update
         if default:
@@ -442,13 +457,13 @@
                     rtype in session.transaction_data.get('createdtables', ())):
                 try:
                     rschema = schema.rschema(rtype)
-                    tablesql = rschema2sql(rschema)
+                    tablesql = y2sql.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 = schema.add_relation_type(rdef)
-                    tablesql = rschema2sql(rschema)
+                    tablesql = y2sql.rschema2sql(rschema)
                     schema.del_relation_type(rtype)
                 # create the necessary table
                 for sql in tablesql.split(';'):
@@ -463,33 +478,34 @@
     rschema = values = None # make pylint happy
 
     def precommit_event(self):
+        session = self.session
         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')
+            sysource = session.pool.source('system')
             if self.values['indexed']:
-                sysource.create_index(self.session, table, column)
+                sysource.create_index(session, table, column)
             else:
-                sysource.drop_index(self.session, table, column)
+                sysource.drop_index(session, table, column)
         if 'cardinality' in self.values and self.rschema.final:
-            adbh = self.session.pool.source('system').dbhelper
+            adbh = 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.rdef(etype, atype).constraints
-            coltype = type_from_constraints(adbh, atype, constraints,
-                                            creating=False)
+            coltype = y2sql.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)
+            notnull = self.values['cardinality'][0] != '1'
+            sql = adbh.sql_set_null_allowed(table, column, coltype, notnull)
+            session.system_sql(sql)
         if 'fulltextindexed' in self.values:
-            UpdateFTIndexOp(self.session)
-            self.session.transaction_data.setdefault('fti_update_etypes',
-                                                     set()).add(etype)
+            UpdateFTIndexOp(session)
+            session.transaction_data.setdefault(
+                'fti_update_etypes', set()).add(etype)
 
 
 class SourceDbCWConstraintAdd(hook.Operation):
@@ -517,8 +533,8 @@
             oldcstr is None or oldcstr.max != newcstr.max):
             adbh = self.session.pool.source('system').dbhelper
             card = rtype.rdef(subjtype, objtype).cardinality
-            coltype = type_from_constraints(adbh, objtype, [newcstr],
-                                            creating=False)
+            coltype = y2sql.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)
@@ -534,7 +550,7 @@
 
 class SourceDbCWConstraintDel(hook.Operation):
     """actually remove a constraint of a relation definition"""
-    rtype = subjtype = objtype = None # make pylint happy
+    rtype = subjtype = None # make pylint happy
 
     def precommit_event(self):
         cstrtype = self.cstr.type()
@@ -656,10 +672,9 @@
             self.cancelled = True
             return
         rdef = self.session.vreg.schema.schema_by_eid(rdef.eid)
-        subjtype, rtype, objtype = rdef.as_triple()
-        self.prepare_constraints(subjtype, rtype, objtype)
+        self.prepare_constraints(rdef)
         cstrtype = self.entity.type
-        self.cstr = rtype.rdef(subjtype, objtype).constraint_by_type(cstrtype)
+        self.cstr = rdef.constraint_by_type(cstrtype)
         self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
         self.newcstr.eid = self.entity.eid
 
@@ -679,7 +694,7 @@
     """
     rtype = subjtype = objtype = None # make pylint happy
     def precommit_event(self):
-        self.prepare_constraints(self.subjtype, self.rtype, self.objtype)
+        self.prepare_constraints(self.rdef)
 
     def commit_event(self):
         self.constraints.remove(self.cstr)
@@ -787,7 +802,7 @@
         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)
+        self._cw.execute('DELETE %s X' % name)
         DropTable(self._cw, table=SQL_PREFIX + name)
         MemSchemaCWETypeDel(self._cw, name)
 
@@ -819,23 +834,26 @@
             return
         schema = self._cw.vreg.schema
         name = entity['name']
-        etype = EntityType(name=name, description=entity.get('description'),
-                           meta=entity.get('meta')) # don't care about final
+        etype = ybo.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.vreg.schema
         eschema = schema.add_entity_type(etype)
         # generate table sql and rql to add metadata
-        tablesql = eschema2sql(self._cw.pool.source('system').dbhelper, eschema,
-                               prefix=SQL_PREFIX)
-        relrqls = []
+        tablesql = y2sql.eschema2sql(self._cw.pool.source('system').dbhelper,
+                                     eschema, prefix=SQL_PREFIX)
+        rdefrqls = []
+        gmap = group_mapping(self._cw)
+        cmap = ss.cstrtype_mapping(self._cw)
         for rtype in (META_RTYPES - VIRTUAL_RTYPES):
             rschema = schema[rtype]
             sampletype = rschema.subjects()[0]
             desttype = rschema.objects()[0]
-            props = rschema.rdef(sampletype, desttype)
-            relrqls += list(ss.rdef2rql(rschema, name, desttype, props,
-                                        groupmap=group_mapping(self._cw)))
+            rdef = copy(rschema.rdef(sampletype, desttype))
+            rdef.subject = mock_object(eid=entity.eid)
+            mock = mock_object(eid=None)
+            rdefrqls.append( (mock, tuple(ss.rdef2rql(rdef, cmap, gmap))) )
         # now remove it !
         schema.del_entity_type(name)
         # create the necessary table
@@ -848,8 +866,8 @@
         etype.eid = entity.eid
         MemSchemaCWETypeAdd(self._cw, etype)
         # add meta relations
-        for rql, kwargs in relrqls:
-            self._cw.execute(rql, kwargs)
+        for rdef, relrqls in rdefrqls:
+            ss.execschemarql(self._cw.execute, rdef, relrqls)
 
 
 class BeforeUpdateCWETypeHook(DelCWETypeHook):
@@ -906,39 +924,34 @@
 
     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),
-                             symmetric=entity.get('symmetric', False),
-                             eid=entity.eid)
+        rtype = ybo.RelationType(name=entity.name,
+                                 description=entity.get('description'),
+                                 meta=entity.get('meta', False),
+                                 inlined=entity.get('inlined', False),
+                                 symmetric=entity.get('symmetric', False),
+                                 eid=entity.eid)
         MemSchemaCWRTypeAdd(self._cw, rtype)
 
 
 class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
     """check name change, handle final"""
-    __regid__ = 'checkupdatecwrtype'
+    __regid__ = 'syncupdatecwrtype'
     events = ('before_update_entity',)
 
     def __call__(self):
-        check_valid_changes(self._cw, self.entity)
-
-
-class AfterUpdateCWRTypeHook(DelCWRTypeHook):
-    __regid__ = 'syncupdatecwrtype'
-    events = ('after_update_entity',)
-
-    def __call__(self):
         entity = self.entity
-        rschema = self._cw.vreg.schema.rschema(entity.name)
+        check_valid_changes(self._cw, entity)
         newvalues = {}
-        for prop in ('meta', 'symmetric', 'inlined'):
-            if prop in entity:
-                newvalues[prop] = entity[prop]
+        for prop in ('symmetric', 'inlined', 'fulltext_container'):
+            if prop in entity.edited_attributes:
+                old, new = hook.entity_oldnewvalue(entity, prop)
+                if old != new:
+                    newvalues[prop] = entity[prop]
         if newvalues:
+            rschema = self._cw.vreg.schema.rschema(entity.name)
+            SourceDbCWRTypeUpdate(self._cw, rschema=rschema, entity=entity,
+                                  values=newvalues)
             MemSchemaCWRTypeUpdate(self._cw, rschema=rschema, values=newvalues)
-            SourceDbCWRTypeUpdate(self._cw, rschema=rschema, values=newvalues,
-                                  entity=entity)
 
 
 class AfterDelRelationTypeHook(SyncSchemaHook):
@@ -970,7 +983,7 @@
             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
+        execute = session.execute
         rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
                        'R eid %%(x)s' % rdeftype, {'x': self.eidto})
         lastrel = rset[0][0] == 0
@@ -1017,8 +1030,8 @@
 class AfterUpdateCWRDefHook(SyncSchemaHook):
     __regid__ = 'syncaddcwattribute'
     __select__ = SyncSchemaHook.__select__ & implements('CWAttribute',
-                                                               'CWRelation')
-    events = ('after_update_entity',)
+                                                        'CWRelation')
+    events = ('before_update_entity',)
 
     def __call__(self):
         entity = self.entity
@@ -1033,7 +1046,9 @@
             if prop == 'order':
                 prop = 'ordernum'
             if prop in entity.edited_attributes:
-                newvalues[prop] = entity[prop]
+                old, new = hook.entity_oldnewvalue(entity, prop)
+                if old != new:
+                    newvalues[prop] = entity[prop]
         if newvalues:
             subjtype = entity.stype.name
             MemSchemaRDefUpdate(self._cw, kobj=(subjtype, desttype),
@@ -1079,11 +1094,9 @@
         except IndexError:
             self._cw.critical('constraint type no more accessible')
         else:
-            subjtype, rtype, objtype = rdef.as_triple()
-            SourceDbCWConstraintDel(self._cw, subjtype=subjtype, rtype=rtype,
-                                    objtype=objtype, cstr=cstr)
-            MemSchemaCWConstraintDel(self._cw, subjtype=subjtype, rtype=rtype,
-                                     objtype=objtype, cstr=cstr)
+            SourceDbCWConstraintDel(self._cw, cstr=cstr,
+                                    subjtype=rdef.subject, rtype=rdef.rtype)
+            MemSchemaCWConstraintDel(self._cw, rdef=rdef, cstr=cstr)
 
 
 # permissions synchronization hooks ############################################
@@ -1148,14 +1161,11 @@
                       len(rset), etype)
             still_fti = list(schema[etype].indexable_attributes())
             for entity in rset.entities():
-                try:
-                    source.fti_unindex_entity(session, entity.eid)
-                    for container in entity.fti_containers():
-                        if still_fti or container is not entity:
-                            session.repo.index_entity(session, container)
-                except Exception:
-                    self.critical('Error while updating Full Text Index for'
-                                  ' entity %s', entity.eid, exc_info=True)
+                source.fti_unindex_entity(session, entity.eid)
+                for container in entity.fti_containers():
+                    if still_fti or container is not entity:
+                        source.fti_unindex_entity(session, entity.eid)
+                        source.fti_index_entity(session, container)
         if len(to_reindex):
             # Transaction have already been committed
             session.pool.commit()
--- a/hooks/syncsession.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/hooks/syncsession.py	Tue Apr 06 19:46:38 2010 +0200
@@ -7,6 +7,7 @@
 """
 __docformat__ = "restructuredtext en"
 
+from yams.schema import role_name
 from cubicweb import UnknownProperty, ValidationError, BadConnectionId
 from cubicweb.selectors import implements
 from cubicweb.server import hook
@@ -147,11 +148,13 @@
         try:
             value = session.vreg.typed_value(key, value)
         except UnknownProperty:
+            qname = role_name('pkey', 'subject')
             raise ValidationError(self.entity.eid,
-                                  {'pkey': session._('unknown property key')})
+                                  {qname: session._('unknown property key')})
         except ValueError, ex:
+            qname = role_name('value', 'subject')
             raise ValidationError(self.entity.eid,
-                                  {'value': session._(str(ex))})
+                                  {qname: session._(str(ex))})
         if not session.user.matching_groups('managers'):
             session.add_relation(self.entity.eid, 'for_user', session.user.eid)
         else:
@@ -174,7 +177,8 @@
         except UnknownProperty:
             return
         except ValueError, ex:
-            raise ValidationError(entity.eid, {'value': session._(str(ex))})
+            qname = role_name('value', 'subject')
+            raise ValidationError(entity.eid, {qname: 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,
@@ -214,8 +218,10 @@
         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']:
+            qname = role_name('for_user', 'subject')
+            msg = session._("site-wide property can't be set for user")
             raise ValidationError(eidfrom,
-                                  {'for_user': session._("site-wide property can't be set for user")})
+                                  {qname: msg})
         for session_ in get_user_sessions(session.repo, self.eidto):
             _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
                               key=key, value=value)
--- a/hooks/test/unittest_hooks.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/hooks/test/unittest_hooks.py	Tue Apr 06 19:46:38 2010 +0200
@@ -102,9 +102,9 @@
                               'WHERE FE name "CWUser", RT name "in_group", TE name "String"')[0][0]
         self.execute('SET X read_permission Y WHERE X eid %(x)s, Y name "managers"',
                      {'x': releid}, 'x')
-        ex = self.assertRaises(ValidationError,
-                               self.commit)
-        self.assertEquals(ex.errors, {'to_entity': 'RQLConstraint O final FALSE failed'})
+        ex = self.assertRaises(ValidationError, self.commit)
+        self.assertEquals(ex.errors,
+                          {'to_entity-object': 'RQLConstraint O final FALSE failed'})
 
     def test_html_tidy_hook(self):
         req = self.request()
@@ -217,23 +217,23 @@
     def test_unexistant_eproperty(self):
         ex = self.assertRaises(ValidationError,
                           self.execute, 'INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U')
-        self.assertEquals(ex.errors, {'pkey': 'unknown property key'})
+        self.assertEquals(ex.errors, {'pkey-subject': 'unknown property key'})
         ex = self.assertRaises(ValidationError,
                           self.execute, 'INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
-        self.assertEquals(ex.errors, {'pkey': 'unknown property key'})
+        self.assertEquals(ex.errors, {'pkey-subject': 'unknown property key'})
 
     def test_site_wide_eproperty(self):
         ex = self.assertRaises(ValidationError,
                                self.execute, 'INSERT CWProperty X: X pkey "ui.site-title", X value "hop", X for_user U')
-        self.assertEquals(ex.errors, {'for_user': "site-wide property can't be set for user"})
+        self.assertEquals(ex.errors, {'for_user-subject': "site-wide property can't be set for user"})
 
     def test_bad_type_eproperty(self):
         ex = self.assertRaises(ValidationError,
                                self.execute, 'INSERT CWProperty X: X pkey "ui.language", X value "hop", X for_user U')
-        self.assertEquals(ex.errors, {'value': u'unauthorized value'})
+        self.assertEquals(ex.errors, {'value-subject': u'unauthorized value'})
         ex = self.assertRaises(ValidationError,
                           self.execute, 'INSERT CWProperty X: X pkey "ui.language", X value "hop"')
-        self.assertEquals(ex.errors, {'value': u'unauthorized value'})
+        self.assertEquals(ex.errors, {'value-subject': u'unauthorized value'})
 
 
 class SchemaHooksTC(CubicWebTC):
@@ -253,7 +253,7 @@
             self.execute('INSERT CWUser X: X login "admin"')
         except ValidationError, ex:
             self.assertIsInstance(ex.entity, int)
-            self.assertEquals(ex.errors, {'login': 'the value "admin" is already used, use another one'})
+            self.assertEquals(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
 
 
 if __name__ == '__main__':
--- a/hooks/test/unittest_syncschema.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/hooks/test/unittest_syncschema.py	Tue Apr 06 19:46:38 2010 +0200
@@ -3,9 +3,11 @@
 from cubicweb import ValidationError
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.sqlutils import SQL_PREFIX
-
+from cubicweb.devtools.repotest import schema_eids_idx, restore_schema_eids_idx
 
-SCHEMA_EIDS = {}
+def teardown_module(*args):
+    del SchemaModificationHooksTC.schema_eids
+
 class SchemaModificationHooksTC(CubicWebTC):
     reset_schema = True
 
@@ -15,29 +17,12 @@
         # we have to read schema from the database to get eid for schema entities
         config._cubes = None
         cls.repo.fill_schema()
-        # remember them so we can reread it from the fs instead of the db (too
-        # costly) between tests
-        for x in cls.repo.schema.entities():
-            SCHEMA_EIDS[x] = x.eid
-        for x in cls.repo.schema.relations():
-            SCHEMA_EIDS[x] = x.eid
-            for rdef in x.rdefs.itervalues():
-                SCHEMA_EIDS[(rdef.subject, rdef.rtype, rdef.object)] = rdef.eid
+        cls.schema_eids = schema_eids_idx(cls.repo.schema)
 
     @classmethod
     def _refresh_repo(cls):
         super(SchemaModificationHooksTC, cls)._refresh_repo()
-        # rebuild schema eid index
-        schema = cls.repo.schema
-        for x in schema.entities():
-            x.eid = SCHEMA_EIDS[x]
-            schema._eid_index[x.eid] = x
-        for x in cls.repo.schema.relations():
-            x.eid = SCHEMA_EIDS[x]
-            schema._eid_index[x.eid] = x
-            for rdef in x.rdefs.itervalues():
-                rdef.eid = SCHEMA_EIDS[(rdef.subject, rdef.rtype, rdef.object)]
-                schema._eid_index[rdef.eid] = rdef
+        restore_schema_eids_idx(cls.repo.schema, cls.schema_eids)
 
     def index_exists(self, etype, attr, unique=False):
         self.session.set_pool()
@@ -271,20 +256,57 @@
         self.execute('Any X WHERE X is_instance_of BaseTransition, X messageid "hop"')
 
     def test_change_fulltextindexed(self):
-        target = self.request().create_entity(u'EmailAddress', address=u'rick.roll@dance.com')
+        req = self.request()
+        target = req.create_entity(u'Email', messageid=u'1234',
+                                   subject=u'rick.roll@dance.com')
         self.commit()
-        rset = self.execute('Any X Where X has_text "rick.roll"')
+        rset = req.execute('Any X WHERE X has_text "rick.roll"')
+        self.assertIn(target.eid, [item[0] for item in rset])
+        assert req.execute('SET A fulltextindexed FALSE '
+                            'WHERE E is CWEType, E name "Email", A is CWAttribute,'
+                            'A from_entity E, A relation_type R, R name "subject"')
+        self.commit()
+        rset = req.execute('Any X WHERE X has_text "rick.roll"')
+        self.failIf(rset)
+        assert req.execute('SET A fulltextindexed TRUE '
+                           'WHERE A from_entity E, A relation_type R, '
+                           'E name "Email", R name "subject"')
+        self.commit()
+        rset = req.execute('Any X WHERE X has_text "rick.roll"')
         self.assertIn(target.eid, [item[0] for item in rset])
 
-        assert self.execute('''SET A fulltextindexed False
-                        WHERE E is CWEType,
-                              E name "EmailAddress",
-                              A is CWAttribute,
-                              A from_entity E,
-                              A relation_type R,
-                              R name "address"
-                    ''')
+    def test_change_fulltext_container(self):
+        req = self.request()
+        target = req.create_entity(u'EmailAddress', address=u'rick.roll@dance.com')
+        target.set_relations(reverse_use_email=req.user)
+        self.commit()
+        rset = req.execute('Any X WHERE X has_text "rick.roll"')
+        self.assertIn(req.user.eid, [item[0] for item in rset])
+        assert self.execute('SET R fulltext_container NULL '
+                            'WHERE R name "use_email"')
+        self.commit()
+        rset = self.execute('Any X WHERE X has_text "rick.roll"')
+        self.assertIn(target.eid, [item[0] for item in rset])
+        assert self.execute('SET R fulltext_container "subject" '
+                            'WHERE R name "use_email"')
         self.commit()
-        rset = self.execute('Any X Where X has_text "rick.roll"')
-        self.assertNotIn(target.eid, [item[0] for item in rset])
+        rset = req.execute('Any X WHERE X has_text "rick.roll"')
+        self.assertIn(req.user.eid, [item[0] for item in rset])
 
+    def test_update_constraint(self):
+        rdef = self.schema['Transition'].rdef('type')
+        cstr = rdef.constraint_by_type('StaticVocabularyConstraint')
+        if not getattr(cstr, 'eid', None):
+            self.skip('start me alone') # bug in schema reloading, constraint's eid not restored
+        self.execute('SET X value %(v)s WHERE X eid %(x)s',
+                     {'x': cstr.eid, 'v': u"u'normal', u'auto', u'new'"}, 'x')
+        self.execute('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X '
+                     'WHERE CT name %(ct)s, EDEF eid %(x)s',
+                     {'ct': 'SizeConstraint', 'value': u'max=10', 'x': rdef.eid}, 'x')
+        self.commit()
+        cstr = rdef.constraint_by_type('StaticVocabularyConstraint')
+        self.assertEquals(cstr.values, (u'normal', u'auto', u'new'))
+        self.execute('INSERT Transition T: T name "hop", T type "new"')
+
+if __name__ == '__main__':
+    unittest_main()
--- a/hooks/workflow.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/hooks/workflow.py	Tue Apr 06 19:46:38 2010 +0200
@@ -9,6 +9,8 @@
 
 from datetime import datetime
 
+from yams.schema import role_name
+
 from cubicweb import RepositoryError, ValidationError
 from cubicweb.interfaces import IWorkflowable
 from cubicweb.selectors import implements
@@ -19,8 +21,8 @@
     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
+    # delete previous state first 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'):
@@ -42,10 +44,8 @@
                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)
-
+                session.add_relation(entity.eid, 'in_state', state.eid)
+                _FireAutotransitionOp(session, entity=entity)
 
 class _FireAutotransitionOp(hook.Operation):
     """try to fire auto transition after state changes"""
@@ -75,8 +75,9 @@
         if mainwf.eid == self.wfeid:
             deststate = mainwf.initial
             if not deststate:
+                qname = role_name('custom_workflow', 'subject')
                 msg = session._('workflow has no initial state')
-                raise ValidationError(entity.eid, {'custom_workflow': msg})
+                raise ValidationError(entity.eid, {qname: msg})
             if mainwf.state_by_eid(entity.current_state.eid):
                 # nothing to do
                 return
@@ -85,6 +86,7 @@
                 if entity.current_state.eid != deststate.eid:
                     _change_state(session, entity.eid,
                                   entity.current_state.eid, deststate.eid)
+                    _FireAutotransitionOp(session, entity=entity)
                 return
             msg = session._('workflow changed to "%s"')
             msg %= session._(mainwf.name)
@@ -99,8 +101,9 @@
         outputs = set()
         for ep in tr.subworkflow_exit:
             if ep.subwf_state.eid in outputs:
+                qname = role_name('subworkflow_exit', 'subject')
                 msg = self.session._("can't have multiple exits on the same state")
-                raise ValidationError(self.treid, {'subworkflow_exit': msg})
+                raise ValidationError(self.treid, {qname: msg})
             outputs.add(ep.subwf_state.eid)
 
 
@@ -114,6 +117,7 @@
         wftr = forentity.subworkflow_input_transition()
         if wftr is None:
             # inconsistency detected
+            qname = role_name('to_state', 'subject')
             msg = session._("state doesn't belong to entity's current workflow")
             raise ValidationError(self.trinfo.eid, {'to_state': msg})
         tostate = wftr.get_exit_point(forentity, trinfo['to_state'])
@@ -122,14 +126,7 @@
             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
+            forentity.change_state(tostate, msg, u'text/plain', tr=wftr)
 
 
 # hooks ########################################################################
@@ -175,8 +172,9 @@
         try:
             foreid = entity['wf_info_for']
         except KeyError:
+            qname = role_name('wf_info_for', 'subject')
             msg = session._('mandatory relation')
-            raise ValidationError(entity.eid, {'wf_info_for': msg})
+            raise ValidationError(entity.eid, {qname: 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
@@ -195,7 +193,8 @@
             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
+        cowpowers = ('managers' in session.user.groups
+                     or not session.write_security)
         # no investigate the requested state change...
         try:
             treid = entity['by_transition']
@@ -203,41 +202,47 @@
             # no transition set, check user is a manager and destination state
             # is specified (and valid)
             if not cowpowers:
+                qname = role_name('by_transition', 'subject')
                 msg = session._('mandatory relation')
-                raise ValidationError(entity.eid, {'by_transition': msg})
+                raise ValidationError(entity.eid, {qname: msg})
             deststateeid = entity.get('to_state')
             if not deststateeid:
+                qname = role_name('by_transition', 'subject')
                 msg = session._('mandatory relation')
-                raise ValidationError(entity.eid, {'by_transition': msg})
+                raise ValidationError(entity.eid, {qname: msg})
             deststate = wf.state_by_eid(deststateeid)
             if deststate is None:
+                qname = role_name('to_state', 'subject')
                 msg = session._("state doesn't belong to entity's workflow")
-                raise ValidationError(entity.eid, {'to_state': msg})
+                raise ValidationError(entity.eid, {qname: 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:
+                qname = role_name('by_transition', 'subject')
                 if tr is None:
                     msg = session._("transition doesn't belong to entity's workflow")
-                    raise ValidationError(entity.eid, {'by_transition': msg})
+                    raise ValidationError(entity.eid, {qname: msg})
                 if not tr.has_input_state(fromstate):
                     msg = session._("transition %(tr)s isn't allowed from %(st)s") % {
                         'tr': session._(tr.name), 'st': session._(fromstate.name)}
-                    raise ValidationError(entity.eid, {'by_transition': msg})
+                    raise ValidationError(entity.eid, {qname: msg})
                 if not tr.may_be_fired(foreid):
                     msg = session._("transition may not be fired")
-                    raise ValidationError(entity.eid, {'by_transition': msg})
+                    raise ValidationError(entity.eid, {qname: msg})
             if entity.get('to_state'):
                 deststateeid = entity['to_state']
                 if not cowpowers and deststateeid != tr.destination(forentity).eid:
+                    qname = role_name('by_transition', 'subject')
                     msg = session._("transition isn't allowed")
-                    raise ValidationError(entity.eid, {'by_transition': msg})
+                    raise ValidationError(entity.eid, {qname: msg})
                 if swtr is None:
                     deststate = session.entity_from_eid(deststateeid)
                     if not cowpowers and deststate is None:
+                        qname = role_name('to_state', 'subject')
                         msg = session._("state doesn't belong to entity's workflow")
-                        raise ValidationError(entity.eid, {'to_state': msg})
+                        raise ValidationError(entity.eid, {qname: msg})
             else:
                 deststateeid = tr.destination(forentity).eid
         # everything is ok, add missing information on the trinfo entity
@@ -266,7 +271,7 @@
 
 
 class CheckInStateChangeAllowed(WorkflowHook):
-    """check state apply, in case of direct in_state change using unsafe_execute
+    """check state apply, in case of direct in_state change using unsafe execute
     """
     __regid__ = 'wfcheckinstate'
     __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
@@ -287,12 +292,14 @@
             if wf.state_by_eid(self.eidto):
                 break
         else:
+            qname = role_name('in_state', 'subject')
             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})
+            raise ValidationError(self.eidfrom, {qname: msg})
         if entity.current_workflow and wf.eid != entity.current_workflow.eid:
+            qname = role_name('in_state', 'subject')
             msg = session._("state doesn't belong to entity's current workflow")
-            raise ValidationError(self.eidfrom, {'in_state': msg})
+            raise ValidationError(self.eidfrom, {qname: msg})
 
 
 class SetModificationDateOnStateChange(WorkflowHook):
@@ -307,8 +314,7 @@
             return
         entity = self._cw.entity_from_eid(self.eidfrom)
         try:
-            entity.set_attributes(modification_date=datetime.now(),
-                                  _cw_unsafe=True)
+            entity.set_attributes(modification_date=datetime.now())
         except RepositoryError, ex:
             # usually occurs if entity is coming from a read-only source
             # (eg ldap user)
--- a/i18n/en.po	Thu Mar 04 17:56:45 2010 +0100
+++ b/i18n/en.po	Tue Apr 06 19:46:38 2010 +0200
@@ -308,6 +308,42 @@
 msgid "CWUser_plural"
 msgstr "Users"
 
+#, python-format
+msgid ""
+"Can't restore %(role)s relation %(rtype)s to entity %(eid)s which is already "
+"linked using this relation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Can't restore relation %(rtype)s between %(subj)s and %(obj)s, that relation "
+"does not exists anymore in the schema."
+msgstr ""
+
+#, python-format
+msgid ""
+"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exists anymore in the schema."
+msgstr ""
+
+#, python-format
+msgid ""
+"Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
+"anymore."
+msgstr ""
+
+#, python-format
+msgid ""
+"Can't undo addition of relation %(rtype)s from %(subj)s to %(obj)s, doesn't "
+"exist anymore"
+msgstr ""
+
+#, python-format
+msgid ""
+"Can't undo creation of entity %(eid)s of type %(etype)s, type no more "
+"supported"
+msgstr ""
+
 msgid "Date"
 msgstr "Date"
 
@@ -369,6 +405,9 @@
 msgid "From:"
 msgstr ""
 
+msgid "Garbage collection information"
+msgstr ""
+
 msgid "Help"
 msgstr ""
 
@@ -390,6 +429,12 @@
 msgid "Interval_plural"
 msgstr "Intervals"
 
+msgid "Looked up classes"
+msgstr ""
+
+msgid "Most referenced classes"
+msgstr ""
+
 msgid "New BaseTransition"
 msgstr "XXX"
 
@@ -648,6 +693,9 @@
 msgid "UniqueConstraint"
 msgstr "unique constraint"
 
+msgid "Unreachable objects"
+msgstr ""
+
 msgid "Update permissions"
 msgstr ""
 
@@ -2095,9 +2143,15 @@
 msgid "entity created"
 msgstr ""
 
+msgid "entity creation"
+msgstr ""
+
 msgid "entity deleted"
 msgstr ""
 
+msgid "entity deletion"
+msgstr ""
+
 msgid "entity edited"
 msgstr ""
 
@@ -2118,6 +2172,9 @@
 msgid "entity types which may use this workflow"
 msgstr ""
 
+msgid "entity update"
+msgstr ""
+
 msgid "error while embedding page"
 msgstr ""
 
@@ -2469,6 +2526,9 @@
 msgid "incontext"
 msgstr "in-context"
 
+msgid "incorrect captcha value"
+msgstr ""
+
 #, python-format
 msgid "incorrect value (%(value)s) for type \"%(type)s\""
 msgstr ""
@@ -2717,6 +2777,9 @@
 msgid "may"
 msgstr ""
 
+msgid "memory leak debugging"
+msgstr ""
+
 msgid "milestone"
 msgstr ""
 
@@ -3084,6 +3147,12 @@
 msgid "relation %(relname)s of %(ent)s"
 msgstr ""
 
+msgid "relation add"
+msgstr ""
+
+msgid "relation removal"
+msgstr ""
+
 msgid "relation_type"
 msgstr "relation type"
 
@@ -3298,6 +3367,12 @@
 msgid "site-wide property can't be set for user"
 msgstr ""
 
+msgid "some errors occured:"
+msgstr ""
+
+msgid "some later transaction(s) touch entity, undo them first"
+msgstr ""
+
 msgid "sorry, the server is unable to handle this query"
 msgstr ""
 
@@ -3569,6 +3644,9 @@
 msgid "toggle check boxes"
 msgstr ""
 
+msgid "transaction undoed"
+msgstr ""
+
 #, python-format
 msgid "transition %(tr)s isn't allowed from %(st)s"
 msgstr ""
@@ -3661,12 +3739,18 @@
 msgid "ui.time-format"
 msgstr "time format"
 
+msgid "unable to check captcha, please try again"
+msgstr ""
+
 msgid "unaccessible"
 msgstr ""
 
 msgid "unauthorized value"
 msgstr ""
 
+msgid "undo"
+msgstr ""
+
 msgid "unique identifier used to connect to the application"
 msgstr ""
 
--- a/i18n/es.po	Thu Mar 04 17:56:45 2010 +0100
+++ b/i18n/es.po	Tue Apr 06 19:46:38 2010 +0200
@@ -316,6 +316,42 @@
 msgid "CWUser_plural"
 msgstr "Usuarios"
 
+#, python-format
+msgid ""
+"Can't restore %(role)s relation %(rtype)s to entity %(eid)s which is already "
+"linked using this relation."
+msgstr ""
+
+#, python-format
+msgid ""
+"Can't restore relation %(rtype)s between %(subj)s and %(obj)s, that relation "
+"does not exists anymore in the schema."
+msgstr ""
+
+#, python-format
+msgid ""
+"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exists anymore in the schema."
+msgstr ""
+
+#, python-format
+msgid ""
+"Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
+"anymore."
+msgstr ""
+
+#, python-format
+msgid ""
+"Can't undo addition of relation %(rtype)s from %(subj)s to %(obj)s, doesn't "
+"exist anymore"
+msgstr ""
+
+#, python-format
+msgid ""
+"Can't undo creation of entity %(eid)s of type %(etype)s, type no more "
+"supported"
+msgstr ""
+
 msgid "Date"
 msgstr "Fecha"
 
@@ -377,6 +413,9 @@
 msgid "From:"
 msgstr "De: "
 
+msgid "Garbage collection information"
+msgstr ""
+
 msgid "Help"
 msgstr ""
 
@@ -398,6 +437,12 @@
 msgid "Interval_plural"
 msgstr "Duraciones"
 
+msgid "Looked up classes"
+msgstr ""
+
+msgid "Most referenced classes"
+msgstr ""
+
 msgid "New BaseTransition"
 msgstr ""
 
@@ -656,6 +701,9 @@
 msgid "UniqueConstraint"
 msgstr ""
 
+msgid "Unreachable objects"
+msgstr ""
+
 msgid "Update permissions"
 msgstr "Autorización de modificar"
 
@@ -2140,9 +2188,15 @@
 msgid "entity created"
 msgstr "entidad creada"
 
+msgid "entity creation"
+msgstr ""
+
 msgid "entity deleted"
 msgstr "Entidad eliminada"
 
+msgid "entity deletion"
+msgstr ""
+
 msgid "entity edited"
 msgstr "entidad modificada"
 
@@ -2165,6 +2219,9 @@
 msgid "entity types which may use this workflow"
 msgstr ""
 
+msgid "entity update"
+msgstr ""
+
 msgid "error while embedding page"
 msgstr "Error durante la inclusión de la página"
 
@@ -2527,6 +2584,9 @@
 msgid "incontext"
 msgstr "En el contexto"
 
+msgid "incorrect captcha value"
+msgstr ""
+
 #, python-format
 msgid "incorrect value (%(value)s) for type \"%(type)s\""
 msgstr "valor %(value)s incorrecto para el tipo \"%(type)s\""
@@ -2785,6 +2845,9 @@
 msgid "may"
 msgstr "Mayo"
 
+msgid "memory leak debugging"
+msgstr ""
+
 msgid "milestone"
 msgstr "Milestone"
 
@@ -3157,6 +3220,12 @@
 msgid "relation %(relname)s of %(ent)s"
 msgstr "relación %(relname)s de %(ent)s"
 
+msgid "relation add"
+msgstr ""
+
+msgid "relation removal"
+msgstr ""
+
 msgid "relation_type"
 msgstr "tipo de relación"
 
@@ -3379,6 +3448,12 @@
 msgstr ""
 "una propiedad especifica para el sitio no puede establecerse para el usuario"
 
+msgid "some errors occured:"
+msgstr ""
+
+msgid "some later transaction(s) touch entity, undo them first"
+msgstr ""
+
 msgid "sorry, the server is unable to handle this query"
 msgstr "lo sentimos, el servidor no puede manejar esta consulta"
 
@@ -3650,6 +3725,9 @@
 msgid "toggle check boxes"
 msgstr "cambiar valor"
 
+msgid "transaction undoed"
+msgstr ""
+
 #, python-format
 msgid "transition %(tr)s isn't allowed from %(st)s"
 msgstr ""
@@ -3742,12 +3820,18 @@
 msgid "ui.time-format"
 msgstr ""
 
+msgid "unable to check captcha, please try again"
+msgstr ""
+
 msgid "unaccessible"
 msgstr "inaccesible"
 
 msgid "unauthorized value"
 msgstr "valor no permitido"
 
+msgid "undo"
+msgstr ""
+
 msgid "unique identifier used to connect to the application"
 msgstr "identificador unico utilizado para conectar a la aplicación"
 
--- a/i18n/fr.po	Thu Mar 04 17:56:45 2010 +0100
+++ b/i18n/fr.po	Tue Apr 06 19:46:38 2010 +0200
@@ -315,6 +315,54 @@
 msgid "CWUser_plural"
 msgstr "Utilisateurs"
 
+#, python-format
+msgid ""
+"Can't restore %(role)s relation %(rtype)s to entity %(eid)s which is already "
+"linked using this relation."
+msgstr ""
+"Ne peut restaurer la relation %(role)s %(rtype)s vers l'entité %(eid)s qui "
+"est déja lié à une autre entité par cette relation."
+
+#, python-format
+msgid ""
+"Can't restore relation %(rtype)s between %(subj)s and %(obj)s, that relation "
+"does not exists anymore in the schema."
+msgstr ""
+"Ne peut restaurer la relation %(rtype)s entre %(subj)s et  %(obj)s, cette "
+"relation n'existe plus dans le schéma."
+
+#, python-format
+msgid ""
+"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exists anymore in the schema."
+msgstr ""
+"Ne peut restaurer la relation %(rtype)s de l'entité %(eid)s, cette "
+"relationn'existe plus dans le schéma"
+
+#, python-format
+msgid ""
+"Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
+"anymore."
+msgstr ""
+"Ne peut restaurer la relation %(rtype)s, l'entité %(role)s %(eid)s n'existe "
+"plus."
+
+#, python-format
+msgid ""
+"Can't undo addition of relation %(rtype)s from %(subj)s to %(obj)s, doesn't "
+"exist anymore"
+msgstr ""
+"Ne peut annuler l'ajout de relation %(rtype)s de %(subj)s vers %(obj)s, "
+"cette relation n'existe plus"
+
+#, python-format
+msgid ""
+"Can't undo creation of entity %(eid)s of type %(etype)s, type no more "
+"supported"
+msgstr ""
+"Ne peut annuler la création de l'entité %(eid)s de type %(etype)s, ce type "
+"n'existe plus"
+
 msgid "Date"
 msgstr "Date"
 
@@ -376,6 +424,9 @@
 msgid "From:"
 msgstr "De :"
 
+msgid "Garbage collection information"
+msgstr "Information sur le ramasse-miette"
+
 msgid "Help"
 msgstr "Aide"
 
@@ -397,6 +448,12 @@
 msgid "Interval_plural"
 msgstr "Durées"
 
+msgid "Looked up classes"
+msgstr "Classes recherchées"
+
+msgid "Most referenced classes"
+msgstr "Classes les plus référencées"
+
 msgid "New BaseTransition"
 msgstr "XXX"
 
@@ -655,6 +712,9 @@
 msgid "UniqueConstraint"
 msgstr "contrainte d'unicité"
 
+msgid "Unreachable objects"
+msgstr "Objets inacessible"
+
 msgid "Update permissions"
 msgstr "Permissions de modifier"
 
@@ -2163,9 +2223,15 @@
 msgid "entity created"
 msgstr "entité créée"
 
+msgid "entity creation"
+msgstr "création d'entité"
+
 msgid "entity deleted"
 msgstr "entité supprimée"
 
+msgid "entity deletion"
+msgstr "suppression d'entité"
+
 msgid "entity edited"
 msgstr "entité éditée"
 
@@ -2187,6 +2253,9 @@
 msgid "entity types which may use this workflow"
 msgstr "types d'entité pouvant utiliser ce workflow"
 
+msgid "entity update"
+msgstr "mise à jour d'entité"
+
 msgid "error while embedding page"
 msgstr "erreur pendant l'inclusion de la page"
 
@@ -2552,6 +2621,9 @@
 msgid "incontext"
 msgstr "dans le contexte"
 
+msgid "incorrect captcha value"
+msgstr "valeur de captcha incorrecte"
+
 #, python-format
 msgid "incorrect value (%(value)s) for type \"%(type)s\""
 msgstr "valeur %(value)s incorrecte pour le type \"%(type)s\""
@@ -2811,6 +2883,9 @@
 msgid "may"
 msgstr "mai"
 
+msgid "memory leak debugging"
+msgstr "Déboguage des fuites de mémoire"
+
 msgid "milestone"
 msgstr "jalon"
 
@@ -3181,6 +3256,12 @@
 msgid "relation %(relname)s of %(ent)s"
 msgstr "relation %(relname)s de %(ent)s"
 
+msgid "relation add"
+msgstr "ajout de relation"
+
+msgid "relation removal"
+msgstr "suppression de relation"
+
 msgid "relation_type"
 msgstr "type de relation"
 
@@ -3403,6 +3484,13 @@
 msgid "site-wide property can't be set for user"
 msgstr "une propriété spécifique au site ne peut être propre à un utilisateur"
 
+msgid "some errors occured:"
+msgstr "des erreurs sont survenues"
+
+msgid "some later transaction(s) touch entity, undo them first"
+msgstr ""
+"des transactions plus récentes modifient cette entité, annulez les d'abord"
+
 msgid "sorry, the server is unable to handle this query"
 msgstr "désolé, le serveur ne peut traiter cette requête"
 
@@ -3679,6 +3767,9 @@
 msgid "toggle check boxes"
 msgstr "inverser les cases à cocher"
 
+msgid "transaction undoed"
+msgstr "transaction annulées"
+
 #, python-format
 msgid "transition %(tr)s isn't allowed from %(st)s"
 msgstr "la transition %(tr)s n'est pas autorisée depuis l'état %(st)s"
@@ -3771,12 +3862,18 @@
 msgid "ui.time-format"
 msgstr "format de l'heure"
 
+msgid "unable to check captcha, please try again"
+msgstr "impossible de vérifier le captcha, veuillez réessayer"
+
 msgid "unaccessible"
 msgstr "inaccessible"
 
 msgid "unauthorized value"
 msgstr "valeur non autorisée"
 
+msgid "undo"
+msgstr "annuler"
+
 msgid "unique identifier used to connect to the application"
 msgstr "identifiant unique utilisé pour se connecter à l'application"
 
--- a/interfaces.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/interfaces.py	Tue Apr 06 19:46:38 2010 +0200
@@ -76,12 +76,14 @@
         """returns a dictionary describing progress/estimated cost of the
         version.
 
-        mandatory keys are (''estimated', 'done', 'todo')
-        optional keys are ('notestimated', 'notestimatedcorrected',
-                           'estimatedcorrected')
-       'noestimated' and 'notestimatedcorrected' should default to 0
-       'estimatedcorrected' should default to 'estimated'
-       """
+        - mandatory keys are (''estimated', 'done', 'todo')
+
+        - optional keys are ('notestimated', 'notestimatedcorrected',
+          'estimatedcorrected')
+
+        'noestimated' and 'notestimatedcorrected' should default to 0
+        'estimatedcorrected' should default to 'estimated'
+        """
 
     def finished(self):
         """returns True if status is finished"""
--- a/mail.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/mail.py	Tue Apr 06 19:46:38 2010 +0200
@@ -215,16 +215,9 @@
         """return a list of either 2-uple (email, language) or user entity to
         who this email should be sent
         """
-        # use super_session when available, we don't want to consider security
-        # when selecting recipients_finder
-        try:
-            req = self._cw.super_session
-        except AttributeError:
-            req = self._cw
-        finder = self._cw.vreg['components'].select('recipients_finder', req,
-                                                    rset=self.cw_rset,
-                                                    row=self.cw_row or 0,
-                                                    col=self.cw_col or 0)
+        finder = self._cw.vreg['components'].select(
+            'recipients_finder', self._cw, rset=self.cw_rset,
+            row=self.cw_row or 0, col=self.cw_col or 0)
         return finder.recipients()
 
     def send_now(self, recipients, msg):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.7.0_Any.py	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,40 @@
+typemap = repo.system_source.dbhelper.TYPE_MAPPING
+sqls = """
+CREATE TABLE transactions (
+  tx_uuid CHAR(32) PRIMARY KEY NOT NULL,
+  tx_user INTEGER NOT NULL,
+  tx_time %s NOT NULL
+);;
+CREATE INDEX transactions_tx_user_idx ON transactions(tx_user);;
+
+CREATE TABLE tx_entity_actions (
+  tx_uuid CHAR(32) REFERENCES transactions(tx_uuid) ON DELETE CASCADE,
+  txa_action CHAR(1) NOT NULL,
+  txa_public %s NOT NULL,
+  txa_order INTEGER,
+  eid INTEGER NOT NULL,
+  etype VARCHAR(64) NOT NULL,
+  changes %s
+);;
+CREATE INDEX tx_entity_actions_txa_action_idx ON tx_entity_actions(txa_action);;
+CREATE INDEX tx_entity_actions_txa_public_idx ON tx_entity_actions(txa_public);;
+CREATE INDEX tx_entity_actions_eid_idx ON tx_entity_actions(eid);;
+CREATE INDEX tx_entity_actions_etype_idx ON tx_entity_actions(etype);;
+
+CREATE TABLE tx_relation_actions (
+  tx_uuid CHAR(32) REFERENCES transactions(tx_uuid) ON DELETE CASCADE,
+  txa_action CHAR(1) NOT NULL,
+  txa_public %s NOT NULL,
+  txa_order INTEGER,
+  eid_from INTEGER NOT NULL,
+  eid_to INTEGER NOT NULL,
+  rtype VARCHAR(256) NOT NULL
+);;
+CREATE INDEX tx_relation_actions_txa_action_idx ON tx_relation_actions(txa_action);;
+CREATE INDEX tx_relation_actions_txa_public_idx ON tx_relation_actions(txa_public);;
+CREATE INDEX tx_relation_actions_eid_from_idx ON tx_relation_actions(eid_from);;
+CREATE INDEX tx_relation_actions_eid_to_idx ON tx_relation_actions(eid_to)
+""" % (typemap['Datetime'],
+       typemap['Boolean'], typemap['Bytes'], typemap['Boolean'])
+for statement in sqls.split(';;'):
+    sql(statement)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.7.2_Any.py	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,2 @@
+sql('DROP FUNCTION IF EXISTS _fsopen(bytea)')
+sql('DROP FUNCTION IF EXISTS fspath(bigint, text, text)')
--- a/misc/migration/bootstrapmigration_repository.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/misc/migration/bootstrapmigration_repository.py	Tue Apr 06 19:46:38 2010 +0200
@@ -7,89 +7,93 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
+from cubicweb.server.session import hooks_control
+from cubicweb.server import schemaserial as ss
 
 applcubicwebversion, cubicwebversion = versions_map['cubicweb']
 
-from cubicweb.server import schemaserial as ss
 def _add_relation_definition_no_perms(subjtype, rtype, objtype):
     rschema = fsschema.rschema(rtype)
-    for query, args in ss.rdef2rql(rschema, subjtype, objtype, groupmap=None):
-        rql(query, args, ask_confirm=False)
+    rdef = rschema.rdefs[(subjtype, objtype)]
+    rdef.rtype = schema.rschema(rtype)
+    rdef.subject = schema.eschema(subjtype)
+    rdef.object = schema.eschema(objtype)
+    ss.execschemarql(rql, rdef, ss.rdef2rql(rdef, CSTRMAP, groupmap=None))
     commit(ask_confirm=False)
 
 if applcubicwebversion == (3, 6, 0) and cubicwebversion >= (3, 6, 0):
+    CSTRMAP = dict(rql('Any T, X WHERE X is CWConstraintType, X name T',
+                       ask_confirm=False))
     _add_relation_definition_no_perms('CWAttribute', 'update_permission', 'CWGroup')
     _add_relation_definition_no_perms('CWAttribute', 'update_permission', 'RQLExpression')
-    session.set_pool()
-    session.unsafe_execute('SET X update_permission Y WHERE X is CWAttribute, X add_permission Y')
+    rql('SET X update_permission Y WHERE X is CWAttribute, X add_permission Y')
     drop_relation_definition('CWAttribute', 'add_permission', 'CWGroup')
     drop_relation_definition('CWAttribute', 'add_permission', 'RQLExpression')
     drop_relation_definition('CWAttribute', 'delete_permission', 'CWGroup')
     drop_relation_definition('CWAttribute', 'delete_permission', 'RQLExpression')
 
 elif applcubicwebversion < (3, 6, 0) and cubicwebversion >= (3, 6, 0):
+    CSTRMAP = dict(rql('Any T, X WHERE X is CWConstraintType, X name T',
+                       ask_confirm=False))
     session.set_pool()
-    session.execute = session.unsafe_execute
     permsdict = ss.deserialize_ertype_permissions(session)
 
-    config.disabled_hooks_categories.add('integrity')
-    for rschema in repo.schema.relations():
-        rpermsdict = permsdict.get(rschema.eid, {})
-        for rdef in rschema.rdefs.values():
-            for action in rdef.ACTIONS:
-                actperms = []
-                for something in rpermsdict.get(action == 'update' and 'add' or action, ()):
-                    if isinstance(something, tuple):
-                        actperms.append(rdef.rql_expression(*something))
-                    else: # group name
-                        actperms.append(something)
-                rdef.set_action_permissions(action, actperms)
-    for action in ('read', 'add', 'delete'):
-        _add_relation_definition_no_perms('CWRelation', '%s_permission' % action, 'CWGroup')
-        _add_relation_definition_no_perms('CWRelation', '%s_permission' % action, 'RQLExpression')
-    for action in ('read', 'update'):
-        _add_relation_definition_no_perms('CWAttribute', '%s_permission' % action, 'CWGroup')
-        _add_relation_definition_no_perms('CWAttribute', '%s_permission' % action, 'RQLExpression')
-    for action in ('read', 'add', 'delete'):
-        rql('SET X %s_permission Y WHERE X is CWRelation, '
-            'RT %s_permission Y, X relation_type RT, Y is CWGroup' % (action, action))
+    with hooks_control(session, session.HOOKS_ALLOW_ALL, 'integrity'):
+        for rschema in repo.schema.relations():
+            rpermsdict = permsdict.get(rschema.eid, {})
+            for rdef in rschema.rdefs.values():
+                for action in rdef.ACTIONS:
+                    actperms = []
+                    for something in rpermsdict.get(action == 'update' and 'add' or action, ()):
+                        if isinstance(something, tuple):
+                            actperms.append(rdef.rql_expression(*something))
+                        else: # group name
+                            actperms.append(something)
+                    rdef.set_action_permissions(action, actperms)
+        for action in ('read', 'add', 'delete'):
+            _add_relation_definition_no_perms('CWRelation', '%s_permission' % action, 'CWGroup')
+            _add_relation_definition_no_perms('CWRelation', '%s_permission' % action, 'RQLExpression')
+        for action in ('read', 'update'):
+            _add_relation_definition_no_perms('CWAttribute', '%s_permission' % action, 'CWGroup')
+            _add_relation_definition_no_perms('CWAttribute', '%s_permission' % action, 'RQLExpression')
+        for action in ('read', 'add', 'delete'):
+            rql('SET X %s_permission Y WHERE X is CWRelation, '
+                'RT %s_permission Y, X relation_type RT, Y is CWGroup' % (action, action))
+            rql('INSERT RQLExpression Y: Y exprtype YET, Y mainvars YMV, Y expression YEX, '
+                'X %s_permission Y WHERE X is CWRelation, '
+                'X relation_type RT, RT %s_permission Y2, Y2 exprtype YET, '
+                'Y2 mainvars YMV, Y2 expression YEX' % (action, action))
+        rql('SET X read_permission Y WHERE X is CWAttribute, '
+            'RT read_permission Y, X relation_type RT, Y is CWGroup')
         rql('INSERT RQLExpression Y: Y exprtype YET, Y mainvars YMV, Y expression YEX, '
-            'X %s_permission Y WHERE X is CWRelation, '
-            'X relation_type RT, RT %s_permission Y2, Y2 exprtype YET, '
-            'Y2 mainvars YMV, Y2 expression YEX' % (action, action))
-    rql('SET X read_permission Y WHERE X is CWAttribute, '
-        'RT read_permission Y, X relation_type RT, Y is CWGroup')
-    rql('INSERT RQLExpression Y: Y exprtype YET, Y mainvars YMV, Y expression YEX, '
-        'X read_permission Y WHERE X is CWAttribute, '
-        'X relation_type RT, RT read_permission Y2, Y2 exprtype YET, '
-        'Y2 mainvars YMV, Y2 expression YEX')
-    rql('SET X update_permission Y WHERE X is CWAttribute, '
-        'RT add_permission Y, X relation_type RT, Y is CWGroup')
-    rql('INSERT RQLExpression Y: Y exprtype YET, Y mainvars YMV, Y expression YEX, '
-        'X update_permission Y WHERE X is CWAttribute, '
-        'X relation_type RT, RT add_permission Y2, Y2 exprtype YET, '
-        'Y2 mainvars YMV, Y2 expression YEX')
-    for action in ('read', 'add', 'delete'):
-        drop_relation_definition('CWRType', '%s_permission' % action, 'CWGroup', commit=False)
-        drop_relation_definition('CWRType', '%s_permission' % action, 'RQLExpression')
-    config.disabled_hooks_categories.remove('integrity')
+            'X read_permission Y WHERE X is CWAttribute, '
+            'X relation_type RT, RT read_permission Y2, Y2 exprtype YET, '
+            'Y2 mainvars YMV, Y2 expression YEX')
+        rql('SET X update_permission Y WHERE X is CWAttribute, '
+            'RT add_permission Y, X relation_type RT, Y is CWGroup')
+        rql('INSERT RQLExpression Y: Y exprtype YET, Y mainvars YMV, Y expression YEX, '
+            'X update_permission Y WHERE X is CWAttribute, '
+            'X relation_type RT, RT add_permission Y2, Y2 exprtype YET, '
+            'Y2 mainvars YMV, Y2 expression YEX')
+        for action in ('read', 'add', 'delete'):
+            drop_relation_definition('CWRType', '%s_permission' % action, 'CWGroup', commit=False)
+            drop_relation_definition('CWRType', '%s_permission' % action, 'RQLExpression')
 
 if applcubicwebversion < (3, 4, 0) and cubicwebversion >= (3, 4, 0):
 
-    session.set_shared_data('do-not-insert-cwuri', True)
-    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):
-        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})
-    isession.commit()
-    reactivate_verification_hooks()
-    session.set_shared_data('do-not-insert-cwuri', False)
+    with hooks_control(session, session.HOOKS_ALLOW_ALL, 'integrity'):
+        session.set_shared_data('do-not-insert-cwuri', True)
+        add_relation_type('cwuri')
+        base_url = session.base_url()
+        for eid, in rql('Any X', ask_confirm=False):
+            type, source, extid = session.describe(eid)
+            if source == 'system':
+                rql('SET X cwuri %(u)s WHERE X eid %(x)s',
+                    {'x': eid, 'u': base_url + u'eid/%s' % eid})
+        isession.commit()
+        session.set_shared_data('do-not-insert-cwuri', False)
 
 if applcubicwebversion < (3, 5, 0) and cubicwebversion >= (3, 5, 0):
     # check that migration is not doomed
--- a/misc/migration/postcreate.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/misc/migration/postcreate.py	Tue Apr 06 19:46:38 2010 +0200
@@ -42,8 +42,8 @@
 
 # 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')
+    rql('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
+        {'x': user.eid, 's': activated.eid}, 'x')
 
 # on interactive mode, ask for level 0 persistent options
 if interactive_mode:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pytestconf.py	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,34 @@
+"""pytest configuration file: we need this to properly remove ressources
+cached on test classes, at least until we've proper support for teardown_class
+"""
+import sys
+from os.path import split, splitext
+from logilab.common.pytest import PyTester
+
+from cubicweb.etwist.server import _gc_debug
+
+class CustomPyTester(PyTester):
+    def testfile(self, filename, batchmode=False):
+        try:
+            return super(CustomPyTester, self).testfile(filename, batchmode)
+        finally:
+            modname = splitext(split(filename)[1])[0]
+            try:
+                module = sys.modules[modname]
+            except KeyError:
+                # error during test module import
+                return
+            for cls in vars(module).values():
+                if getattr(cls, '__module__', None) != modname:
+                    continue
+                clean_repo_test_cls(cls)
+            #_gc_debug()
+
+def clean_repo_test_cls(cls):
+    if 'repo' in cls.__dict__:
+        if not cls.repo._shutting_down:
+            cls.repo.shutdown()
+        del cls.repo
+    for clsattr in ('cnx', '_orig_cnx', 'config', '_config', 'vreg', 'schema'):
+        if clsattr in cls.__dict__:
+            delattr(cls, clsattr)
--- a/req.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/req.py	Tue Apr 06 19:46:38 2010 +0200
@@ -7,9 +7,11 @@
 """
 __docformat__ = "restructuredtext en"
 
+from warnings import warn
+from urlparse import urlsplit, urlunsplit
 from urllib import quote as urlquote, unquote as urlunquote
 from datetime import time, datetime, timedelta
-from cgi import parse_qsl
+from cgi import parse_qs, parse_qsl
 
 from logilab.common.decorators import cached
 from logilab.common.deprecation import deprecated
@@ -22,6 +24,12 @@
 CACHE_REGISTRY = {}
 
 
+def _check_cw_unsafe(kwargs):
+    if kwargs.pop('_cw_unsafe', False):
+        warn('[3.7] _cw_unsafe argument is deprecated, now unsafe by '
+             'default, control it using cw_[read|write]_security.',
+             DeprecationWarning, stacklevel=3)
+
 class Cache(dict):
     def __init__(self):
         super(Cache, self).__init__()
@@ -70,7 +78,8 @@
         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)
+        rset.req = self
+        return rset
 
     def eid_rset(self, eid, etype=None):
         """return a result set for the given eid without doing actual query
@@ -82,14 +91,17 @@
             etype = self.describe(eid)[0]
         rset = ResultSet([(eid,)], 'Any X WHERE X eid %(x)s', {'x': eid},
                          [(etype,)])
-        return self.decorate_rset(rset)
+        rset.req = self
+        return 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'))
+        rset = ResultSet([], 'Any X WHERE X eid -1')
+        rset.req = self
+        return rset
 
     def entity_from_eid(self, eid, etype=None):
         """return an entity instance for the given eid. No query is done"""
@@ -110,19 +122,18 @@
     # XXX move to CWEntityManager or even better as factory method (unclear
     # where yet...)
 
-    def create_entity(self, etype, _cw_unsafe=False, **kwargs):
+    def create_entity(self, etype, **kwargs):
         """add a new entity of the given type
 
         Example (in a shell session):
 
-        c = create_entity('Company', name=u'Logilab')
-        create_entity('Person', works_for=c, firstname=u'John', lastname=u'Doe')
+        >>> c = create_entity('Company', name=u'Logilab')
+        >>> create_entity('Person', firstname=u'John', lastname=u'Doe',
+        ...               works_for=c)
 
         """
-        if _cw_unsafe:
-            execute = self.unsafe_execute
-        else:
-            execute = self.execute
+        _check_cw_unsafe(kwargs)
+        execute = self.execute
         rql = 'INSERT %s X' % etype
         relations = []
         restrictions = set()
@@ -162,7 +173,7 @@
                 restr = 'X %s Y' % attr
             execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
                 restr, ','.join(str(r.eid) for r in values)),
-                         {'x': created.eid}, 'x')
+                    {'x': created.eid}, 'x', build_descr=False)
         return created
 
     def ensure_ro_rql(self, rql):
@@ -230,7 +241,7 @@
     def build_url_params(self, **kwargs):
         """return encoded params to incorporate them in an URL"""
         args = []
-        for param, values in kwargs.items():
+        for param, values in kwargs.iteritems():
             if not isinstance(values, (list, tuple)):
                 values = (values,)
             for value in values:
@@ -270,6 +281,25 @@
             except UnicodeDecodeError: # might occurs on manually typed URLs
                 yield unicode(key, 'iso-8859-1'), unicode(val, 'iso-8859-1')
 
+
+    def rebuild_url(self, url, **newparams):
+        """return the given url with newparams inserted. If any new params
+        is already specified in the url, it's overriden by the new value
+
+        newparams may only be mono-valued.
+        """
+        if isinstance(url, unicode):
+            url = url.encode(self.encoding)
+        schema, netloc, path, query, fragment = urlsplit(url)
+        query = parse_qs(query)
+        # sort for testing predictability
+        for key, val in sorted(newparams.iteritems()):
+            query[key] = (self.url_quote(val),)
+        query = '&'.join(u'%s=%s' % (param, value)
+                         for param, values in query.items()
+                         for value in values)
+        return urlunsplit((schema, netloc, path, query, fragment))
+
     # bound user related methods ###############################################
 
     @cached
@@ -281,7 +311,7 @@
             userinfo['name'] = "cubicweb"
             userinfo['email'] = ""
             return userinfo
-        user = self.actual_session().user
+        user = self.user
         userinfo['login'] = user.login
         userinfo['name'] = user.name()
         userinfo['email'] = user.get_email()
@@ -382,10 +412,6 @@
         """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
--- a/rqlrewrite.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/rqlrewrite.py	Tue Apr 06 19:46:38 2010 +0200
@@ -11,7 +11,7 @@
 __docformat__ = "restructuredtext en"
 
 from rql import nodes as n, stmts, TypeResolverException
-
+from yams import BadSchemaDefinition
 from logilab.common.graph import has_path
 
 from cubicweb import Unauthorized, typed_eid
@@ -185,7 +185,17 @@
                 vi['const'] = typed_eid(selectvar) # XXX gae
                 vi['rhs_rels'] = vi['lhs_rels'] = {}
             except ValueError:
-                vi['stinfo'] = sti = self.select.defined_vars[selectvar].stinfo
+                try:
+                    vi['stinfo'] = sti = self.select.defined_vars[selectvar].stinfo
+                except KeyError:
+                    # variable has been moved to a newly inserted subquery
+                    # we should insert snippet in that subquery
+                    subquery = self.select.aliases[selectvar].query
+                    assert len(subquery.children) == 1
+                    subselect = subquery.children[0]
+                    RQLRewriter(self.session).rewrite(subselect, [(varmap, rqlexprs)],
+                                                      subselect.solutions, self.kwargs)
+                    continue
                 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']
@@ -294,21 +304,40 @@
         """introduce the given snippet in a subquery"""
         subselect = stmts.Select()
         selectvar = varmap[0]
-        subselect.append_selected(n.VariableRef(
-            subselect.get_variable(selectvar)))
+        subselectvar = subselect.get_variable(selectvar)
+        subselect.append_selected(n.VariableRef(subselectvar))
+        snippetrqlst = n.Exists(transformedsnippet.copy(subselect))
         aliases = [selectvar]
-        subselect.add_restriction(transformedsnippet.copy(subselect))
         stinfo = self.varinfo['stinfo']
+        need_null_test = False
         for rel in stinfo['relations']:
             rschema = self.schema.rschema(rel.r_type)
             if rschema.final or (rschema.inlined and
-                                      not rel in stinfo['rhsrelations']):
-                self.select.remove_node(rel)
-                rel.children[0].name = selectvar
+                                 not rel in stinfo['rhsrelations']):
+                rel.children[0].name = selectvar # XXX explain why
                 subselect.add_restriction(rel.copy(subselect))
                 for vref in rel.children[1].iget_nodes(n.VariableRef):
+                    if isinstance(vref.variable, n.ColumnAlias):
+                        # XXX could probably be handled by generating the subquery
+                        # into the detected subquery
+                        raise BadSchemaDefinition(
+                            "cant insert security because of usage two inlined "
+                            "relations in this query. You should probably at "
+                            "least uninline %s" % rel.r_type)
                     subselect.append_selected(vref.copy(subselect))
                     aliases.append(vref.name)
+                self.select.remove_node(rel)
+                # when some inlined relation has to be copied in the subquery,
+                # we need to test that either value is NULL or that the snippet
+                # condition is satisfied
+                if rschema.inlined and rel.optional:
+                    need_null_test = True
+        if need_null_test:
+            snippetrqlst = n.Or(
+                n.make_relation(subselectvar, 'is', (None, None), n.Constant,
+                                operator='IS'),
+                snippetrqlst)
+        subselect.add_restriction(snippetrqlst)
         if self.u_varname:
             # generate an identifier for the substitution
             argname = subselect.allocate_varname()
--- a/rset.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/rset.py	Tue Apr 06 19:46:38 2010 +0200
@@ -50,7 +50,6 @@
         # .limit method
         self.limited = None
         # set by the cursor which returned this resultset
-        self.vreg = None
         self.req = None
         # actions cache
         self._rsetactions = None
@@ -83,7 +82,7 @@
         try:
             return self._rsetactions[key]
         except KeyError:
-            actions = self.vreg['actions'].poss_visible_objects(
+            actions = self.req.vreg['actions'].poss_visible_objects(
                 self.req, rset=self, **kwargs)
             self._rsetactions[key] = actions
             return actions
@@ -114,15 +113,17 @@
         # but I tend to think that since we have that, we should not need this
         # method anymore (syt)
         rset = ResultSet(self.rows+rset.rows, self.rql, self.args,
-                         self.description +rset.description)
-        return self.req.decorate_rset(rset)
+                         self.description + rset.description)
+        rset.req = self.req
+        return rset
 
     def copy(self, rows=None, descr=None):
         if rows is None:
             rows = self.rows[:]
             descr = self.description[:]
         rset = ResultSet(rows, self.rql, self.args, descr)
-        return self.req.decorate_rset(rset)
+        rset.req = self.req
+        return rset
 
     def transformed_rset(self, transformcb):
         """ the result set according to a given column types
@@ -258,8 +259,8 @@
         # try to get page boundaries from the navigation component
         # XXX we should probably not have a ref to this component here (eg in
         #     cubicweb)
-        nav = self.vreg['components'].select_or_none('navigation', self.req,
-                                                     rset=self)
+        nav = self.req.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)
@@ -286,12 +287,14 @@
             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)]
+            aliases = [nodes.VariableRef(newselect.get_variable(chr(65+i), i))
+                       for i in xrange(len(rqlst.children[0].selection))]
+            for vref in aliases:
+                newselect.append_selected(nodes.VariableRef(vref.variable))
             newselect.set_with([nodes.SubQuery(aliases, rqlst)], check=False)
             newunion = stmts.Union()
             newunion.append(newselect)
-            rql = rqlst.as_string(kwargs=self.args)
+            rql = newunion.as_string(kwargs=self.args)
             rqlst.parent = None
         return rql
 
@@ -389,7 +392,7 @@
         """
         etype = self.description[row][col]
         try:
-            eschema = self.vreg.schema.eschema(etype)
+            eschema = self.req.vreg.schema.eschema(etype)
             if eschema.final:
                 raise NotAnEntity(etype)
         except KeyError:
@@ -433,8 +436,8 @@
             return entity
         # build entity instance
         etype = self.description[row][col]
-        entity = self.vreg['etypes'].etype_class(etype)(req, rset=self,
-                                                        row=row, col=col)
+        entity = self.req.vreg['etypes'].etype_class(etype)(req, rset=self,
+                                                            row=row, col=col)
         entity.set_eid(eid)
         # cache entity
         req.set_entity_cache(entity)
@@ -470,7 +473,7 @@
                         else:
                             rql = 'Any Y WHERE Y %s X, X eid %s'
                         rrset = ResultSet([], rql % (attr, entity.eid))
-                        req.decorate_rset(rrset)
+                        rrset.req = req
                     else:
                         rrset = self._build_entity(row, outerselidx).as_rset()
                     entity.set_related_cache(attr, role, rrset)
@@ -487,10 +490,10 @@
             rqlst = self._rqlst.copy()
             # to avoid transport overhead when pyro is used, the schema has been
             # unset from the syntax tree
-            rqlst.schema = self.vreg.schema
-            self.vreg.rqlhelper.annotate(rqlst)
+            rqlst.schema = self.req.vreg.schema
+            self.req.vreg.rqlhelper.annotate(rqlst)
         else:
-            rqlst = self.vreg.parse(self.req, self.rql, self.args)
+            rqlst = self.req.vreg.parse(self.req, self.rql, self.args)
         return rqlst
 
     @cached
@@ -530,7 +533,7 @@
         etype = self.description[row][col]
         # final type, find a better one to locate the correct subquery
         # (ambiguous if possible)
-        eschema = self.vreg.schema.eschema
+        eschema = self.req.vreg.schema.eschema
         if eschema(etype).final:
             for select in rqlst.children:
                 try:
--- a/rtags.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/rtags.py	Tue Apr 06 19:46:38 2010 +0200
@@ -1,9 +1,26 @@
-"""relation tags store
+#:organization: Logilab
+#:copyright: 2001-2010 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
+
+"""
+A RelationTag object is an object which allows to link a configuration information to a relation definition. For instance, the standard primary view uses a RelationTag object (uicfg.primaryview_section) to get the section to display relations.
+
+.. sourcecode:: python
 
-:organization: Logilab
-:copyright: 2001-2010 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
+   # display ``entry_of`` relations in the ``relations`` section in the ``BlogEntry`` primary view
+   uicfg.primaryview_section.tag_subject_of(('BlogEntry', 'entry_of', '*'),
+                                             'relations')
+
+   # hide every relation ``entry_of`` in the ``Blog`` primary view
+   uicfg.primaryview_section.tag_object_of(('*', 'entry_of', 'Blog'), 'hidden')
+
+Three primitives are defined:
+   * ``tag_subject_of`` tag a relation in the subject's context
+   * ``tag_object_of`` tag a relation in the object's context
+   * ``tag_attribute`` shortcut for tag_subject_of
+
+
 """
 __docformat__ = "restructuredtext en"
 
--- a/schema.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/schema.py	Tue Apr 06 19:46:38 2010 +0200
@@ -21,7 +21,7 @@
 
 from yams import BadSchemaDefinition, buildobjs as ybo
 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema, \
-     RelationDefinitionSchema, PermissionMixIn
+     RelationDefinitionSchema, PermissionMixIn, role_name
 from yams.constraints import BaseConstraint, FormatConstraint
 from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
                          obsolete as yobsolete, cleanup_sys_modules)
@@ -34,14 +34,15 @@
 PURE_VIRTUAL_RTYPES = set(('identity', 'has_text',))
 VIRTUAL_RTYPES = set(('eid', 'identity', 'has_text',))
 
-#  set of meta-relations available for every entity types
+# set of meta-relations available for every entity types
 META_RTYPES = set((
     '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'))
+SYSTEM_RTYPES = set(('require_permission', 'custom_workflow', 'in_state',
+                     'wf_info_for'))
 
-#  set of entity and relation types used to build the schema
+# set of entity and relation types used to build the schema
 SCHEMA_TYPES = set((
     'CWEType', 'CWRType', 'CWAttribute', 'CWRelation',
     'CWConstraint', 'CWConstraintType', 'RQLExpression',
@@ -399,7 +400,9 @@
                                           __permissions__=RO_ATTR_PERMS)
             self.schema.add_relation_def(rdef)
         elif not need_has_text and has_has_text:
-            self.schema.del_relation_def(self.type, 'has_text', 'String')
+            # use rschema.del_relation_def and not schema.del_relation_def to
+            # avoid deleting the relation type accidentally...
+            self.schema['has_text'].del_relation_def(self, self.schema['String'])
 
     def schema_entity(self):
         """return True if this entity type is used to build the schema"""
@@ -681,17 +684,22 @@
             # XXX at this point if both or neither of S and O are in mainvar we
             # dunno if the validation error `occured` on eidfrom or eidto (from
             # user interface point of view)
+            #
+            # possible enhancement: check entity being created, it's probably
+            # the main eid unless this is a composite relation
             if eidto is None or 'S' in self.mainvars or not 'O' in self.mainvars:
                 maineid = eidfrom
+                qname = role_name(rtype, 'subject')
             else:
                 maineid = eidto
+                qname = role_name(rtype, 'object')
             if self.msg:
                 msg = session._(self.msg)
             else:
                 msg = '%(constraint)s %(restriction)s failed' % {
                     'constraint':  session._(self.type()),
                     'restriction': self.restriction}
-            raise ValidationError(maineid, {rtype: msg})
+            raise ValidationError(maineid, {qname: msg})
 
     def exec_query(self, session, eidfrom, eidto):
         if eidto is None:
@@ -704,7 +712,7 @@
         rql = 'Any %s WHERE %s' % (self.mainvars,  restriction)
         if self.distinct_query:
             rql = 'DISTINCT ' + rql
-        return session.unsafe_execute(rql, args, ck, build_descr=False)
+        return session.execute(rql, args, ck, build_descr=False)
 
 
 class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint):
@@ -830,13 +838,10 @@
                 return True
             return False
         if keyarg is None:
-            # on the server side, use unsafe_execute, but this is not available
-            # on the client side (session is actually a request)
-            execute = getattr(session, 'unsafe_execute', session.execute)
             kwargs.setdefault('u', session.user.eid)
             cachekey = kwargs.keys()
             try:
-                rset = execute(rql, kwargs, cachekey, build_descr=True)
+                rset = session.execute(rql, kwargs, cachekey, build_descr=True)
             except NotImplementedError:
                 self.critical('cant check rql expression, unsupported rql %s', rql)
                 if self.eid is not None:
@@ -1084,10 +1089,10 @@
     elif form is not None:
         cw = form._cw
     if cw is not None:
-        if hasattr(cw, 'is_super_session'):
+        if hasattr(cw, 'write_security'): # test it's a session and not a request
             # cw is a server session
-            hasperm = cw.is_super_session or \
-                      not cw.vreg.config.is_hook_category_activated('integrity') or \
+            hasperm = not cw.write_security or \
+                      not cw.is_hook_category_activated('integrity') or \
                       cw.user.has_permission(PERM_USE_TEMPLATE_FORMAT)
         else:
             hasperm = cw.user.has_permission(PERM_USE_TEMPLATE_FORMAT)
--- a/schemas/_regproc_bss.postgres.sql	Thu Mar 04 17:56:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
-/* -*- sql -*-
-
-   postgres specific registered procedures for the Bytes File System storage,
-   require the plpythonu language installed
-
-*/
-
-
-CREATE OR REPLACE FUNCTION _fsopen(bytea) RETURNS bytea AS $$
-    fpath = args[0]
-    if fpath:
-        try:
-            data = file(fpath, 'rb').read()
-            #/* XXX due to plpython bug we have to replace some characters... */
-            return data.replace("\\", r"\134").replace("\000", r"\000").replace("'", r"\047") #'
-        except Exception, ex:
-            plpy.warning('failed to get content for %s: %s', fpath, ex)
-    return None
-$$ LANGUAGE plpythonu
-/* WITH(ISCACHABLE) XXX does postgres handle caching of large data nicely */
-;;
-
-/* fspath(eid, entity type, attribute) */
-CREATE OR REPLACE FUNCTION fspath(bigint, text, text) RETURNS bytea AS $$
-    pkey = 'plan%s%s' % (args[1], args[2])
-    try:
-        plan = SD[pkey]
-    except KeyError:
-        #/* then prepare and cache plan to get versioned file information from a
-        # version content eid */
-        plan = plpy.prepare(
-            'SELECT X.cw_%s FROM cw_%s as X WHERE X.cw_eid=$1' % (args[2], args[1]),
-            ['bigint'])
-        SD[pkey] = plan
-    return plpy.execute(plan, [args[0]])[0]
-$$ LANGUAGE plpythonu
-/* WITH(ISCACHABLE) XXX does postgres handle caching of large data nicely */
-;;
--- a/schemas/workflow.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/schemas/workflow.py	Tue Apr 06 19:46:38 2010 +0200
@@ -19,7 +19,7 @@
 
     name = String(required=True, indexed=True, internationalizable=True,
                   maxsize=256)
-    description = RichString(fulltextindexed=True, default_format='text/rest',
+    description = RichString(default_format='text/rest',
                              description=_('semantic description of this workflow'))
 
     workflow_of = SubjectRelation('CWEType', cardinality='+*',
@@ -53,7 +53,7 @@
                   maxsize=256,
                   constraints=[RQLUniqueConstraint('S name N, S state_of WF, Y state_of WF, Y name N', 'Y',
                                                    _('workflow already have a state of that name'))])
-    description = RichString(fulltextindexed=True, default_format='text/rest',
+    description = RichString(default_format='text/rest',
                              description=_('semantic description of this state'))
 
     # XXX should be on BaseTransition w/ AND/OR selectors when we will
@@ -77,8 +77,7 @@
                   constraints=[RQLUniqueConstraint('S name N, S transition_of WF, Y transition_of WF, Y name N', 'Y',
                                                    _('workflow already have a transition of that name'))])
     type = String(vocabulary=(_('normal'), _('auto')), default='normal')
-    description = RichString(fulltextindexed=True,
-                         description=_('semantic description of this transition'))
+    description = RichString(description=_('semantic description of this transition'))
     condition = SubjectRelation('RQLExpression', cardinality='*?', composite='subject',
                                 description=_('a RQL expression which should return some results, '
                                               'else the transition won\'t be available. '
--- a/selectors.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/selectors.py	Tue Apr 06 19:46:38 2010 +0200
@@ -1,44 +1,184 @@
-"""This file contains some basic selectors required by application objects.
+# :organization: Logilab
+# :copyright: 2001-2010 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
+""".. _Selectors:
+
+Selectors
+---------
+
+Using and combining existant selectors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can combine selectors using the `&`, `|` and `~` operators.
+
+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
+means that one of them should return a positive score. On success, the first
+positive score is returned.
+
+You can also "negate" a selector by precedeing it by the `~` unary operator.
+
+Of course you can use parens to balance expressions.
+
+.. Note:
+  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)
+
+
+Example
+~~~~~~~
+
+The goal: when on a Blog, one wants the RSS link to refer to blog entries, not to
+the blog entity itself.
+
+To do that, one defines a method on entity classes that returns the RSS stream
+url for a given entity. The default implementation on
+:class:`~cubicweb.entities.AnyEntity` (the generic entity class used as base for
+all others) and a specific implementation on Blog will do what we want.
 
-A selector is responsible to score how well an object may be used with a
-given context by returning a score.
+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 generic behaviour.
+
+Hence we have two cases here, one for a single-entity rsets, the other for
+multi-entities rsets.
+
+In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
+
+.. sourcecode:: python
+
+  class RSSIconBox(ExtResourcesBoxTemplate):
+    '''just display the RSS icon on uniform result set'''
+    __select__ = ExtResourcesBoxTemplate.__select__ & non_final_entity()
+
+It takes into account:
+
+* the inherited selection criteria (one has to look them up in the class
+  hierarchy to know the details)
+
+* :class:`~cubicweb.selectors.non_final_entity`, which filters on result sets
+  containing non final entities (a 'final entity' being synonym for entity
+  attributes type, eg `String`, `Int`, etc)
 
-In CubicWeb Usually the context consists for a request object, a result set
-or None, a specific row/col in the result set, etc...
+This matches our second case. Hence we have to provide a specific component for
+the first case:
+
+.. sourcecode:: python
+
+  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 :class:`~cubicweb.selectors.one_line_rset` selector, which
+filters result sets of size 1. Thus, on a result set containing multiple
+entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
+selectable. However for a result set with one entity, the `EntityRSSIconBox`
+class will have a higher score than `RSSIconBox`, which is what we wanted.
+
+Of course, once this is done, you have to:
+
+* fill in the call method of `EntityRSSIconBox`
+
+* provide the default implementation of the method returning the RSS stream url
+  on :class:`~cubicweb.entities.AnyEntity`
+
+* redefine this method on `Blog`.
 
 
-If you have trouble with selectors, especially if the objet (typically
-a view or a component) you want to use is not selected and you want to
-know which one(s) of its selectors fail (e.g. returns 0), you can use
-`traced_selection` or even direclty `TRACED_OIDS`.
+When to use selectors?
+~~~~~~~~~~~~~~~~~~~~~~
 
-`TRACED_OIDS` is a tuple of traced object ids. The special value
-'all' may be used to log selectors for all objects.
+Selectors are to be used whenever arises the need of dispatching on the shape or
+content of a result set or whatever else context (value in request form params,
+authenticated user groups, etc...). That is, almost all the time.
 
-For instance, say that the following code yields a `NoSelectableObject`
-exception::
+Here is a quick example:
 
-    self.view('calendar', myrset)
+.. sourcecode:: python
 
-You can log the selectors involved for *calendar* by replacing the line
-above by::
+    class UserLink(component.Component):
+	'''if the user is the anonymous user, build a link to login else a link
+	to the connected user object with a loggout link
+	'''
+	__regid__ = 'loggeduserlink'
 
-    # in Python2.5
-    from cubicweb.selectors import traced_selection
-    with traced_selection():
-        self.view('calendar', myrset)
+	def call(self):
+	    if self._cw.cnx.anonymous_connection:
+		# display login link
+		...
+	    else:
+		# display a link to the connected user object with a loggout link
+		...
 
-    # in Python2.4
-    from cubicweb import selectors
-    selectors.TRACED_OIDS = ('calendar',)
-    self.view('calendar', myrset)
-    selectors.TRACED_OIDS = ()
+The proper way to implement this with |cubicweb| is two have two different
+classes sharing the same identifier but with different selectors so you'll get
+the correct one according to the context:
 
 
-:organization: Logilab
-:copyright: 2001-2010 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
+    class UserLink(component.Component):
+	'''display a link to the connected user object with a loggout link'''
+	__regid__ = 'loggeduserlink'
+	__select__ = component.Component.__select__ & authenticated_user()
+
+	def call(self):
+            # display useractions and siteactions
+	    ...
+
+    class AnonUserLink(component.Component):
+	'''build a link to login'''
+	__regid__ = 'loggeduserlink'
+	__select__ = component.Component.__select__ & anonymous_user()
+
+	def call(self):
+	    # display login link
+            ...
+
+The big advantage, aside readibily once you're familiar with the system, is that
+your cube becomes much more easily customizable by improving componentization.
+
+
+.. _CustomSelectors:
+
+Defining your own selectors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. autodocstring:: cubicweb.appobject::objectify_selector
+
+In other case, you can take a look at the following abstract base classes:
+
+.. autoclass:: cubicweb.selectors.ExpectedValueSelector
+.. autoclass:: cubicweb.selectors.EClassSelector
+.. autoclass:: cubicweb.selectors.EntitySelector
+
+Also, think to use the :func:`lltrace` decorator on your selector class' :meth:`__call__` method
+or below the :func:`objectify_selector` decorator of your selector function so it gets
+traceable when :class:`traced_selection` is activated (see :ref:`DebuggingSelectors`).
+
+.. autofunction:: cubicweb.selectors.lltrace
+
+.. Note::
+  Selectors __call__ should *always* return a positive integer, and shall never
+  return `None`.
+
+
+.. _DebuggingSelectors:
+
+Debugging selection
+~~~~~~~~~~~~~~~~~~~
+
+Once in a while, one needs to understand why a view (or any application object)
+is, or is not selected appropriately. Looking at which selectors fired (or did
+not) is the way. The :class:`cubicweb.selectors.traced_selection` context
+manager to help with that, *if you're running your instance in debug mode*.
+
+.. autoclass:: cubicweb.selectors.traced_selection
+
+
+.. |cubicweb| replace:: *CubicWeb*
 """
 __docformat__ = "restructuredtext en"
 
@@ -60,45 +200,63 @@
 
 # helpers for debugging selectors
 SELECTOR_LOGGER = logging.getLogger('cubicweb.selectors')
-TRACED_OIDS = ()
+TRACED_OIDS = None
+
+def _trace_selector(cls, ret):
+    # /!\ lltrace decorates pure function or __call__ method, this
+    #     means argument order may be different
+    if isinstance(cls, Selector):
+        selname = str(cls)
+        vobj = args[0]
+    else:
+        selname = selector.__name__
+        vobj = cls
+    if TRACED_OIDS == 'all' or class_regid(vobj) in TRACED_OIDS:
+        #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
+        print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__)
 
 def lltrace(selector):
+    """use this decorator on your selectors so the becomes traceable with
+    :class:`traced_selection`
+    """
     # don't wrap selectors if not in development mode
     if CubicWebConfiguration.mode == 'system': # XXX config.debug
         return selector
     def traced(cls, *args, **kwargs):
-        # /!\ lltrace decorates pure function or __call__ method, this
-        #     means argument order may be different
-        if isinstance(cls, Selector):
-            selname = str(cls)
-            vobj = args[0]
-        else:
-            selname = selector.__name__
-            vobj = cls
-        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)
-            print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__)
+        if TRACED_OIDS is not None:
+            _trace_selector(cls, ret)
         return ret
     traced.__name__ = selector.__name__
     traced.__doc__ = selector.__doc__
     return traced
 
 class traced_selection(object):
-    """selector debugging helper.
-
+    """
     Typical usage is :
 
-    >>> with traced_selection():
-    ...     # some code in which you want to debug selectors
-    ...     # for all objects
+    .. sourcecode:: python
+
+        >>> from cubicweb.selectors import traced_selection
+        >>> with traced_selection():
+        ...     # some code in which you want to debug selectors
+        ...     # for all objects
+
+    Don't forget the 'from __future__ import with_statement' at the module top-level
+    if you're using python prior to 2.6.
 
-    or
+    This will yield lines like this in the logs::
+
+        selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
 
-    >>> with traced_selection( ('oid1', 'oid2') ):
-    ...     # some code in which you want to debug selectors
-    ...     # for objects with id 'oid1' and 'oid2'
+    You can also give to :class:`traced_selection` the identifiers of objects on
+    which you want to debug selection ('oid1' and 'oid2' in the example above).
+
+    .. sourcecode:: python
+
+        >>> with traced_selection( ('oid1', 'oid2') ):
+        ...     # some code in which you want to debug selectors
+        ...     # for objects with id 'oid1' and 'oid2'
 
     """
     def __init__(self, traced='all'):
@@ -110,7 +268,7 @@
 
     def __exit__(self, exctype, exc, traceback):
         global TRACED_OIDS
-        TRACED_OIDS = ()
+        TRACED_OIDS = None
         return traceback is None
 
 
@@ -260,12 +418,12 @@
       - `accept_none` is False and some cell in the column has a None value
         (this may occurs with outer join)
 
-    .. note::
-       using EntitySelector or EClassSelector as base selector class impacts
-       performance, since when no entity or row is specified the later works on
-       every different *entity class* found in the result set, while the former
-       works on each *entity* (eg each row of the result set), which may be much
-       more costly.
+    .. Note::
+       using :class:`EntitySelector` or :class:`EClassSelector` as base selector
+       class impacts performance, since when no entity or row is specified the
+       later works on every different *entity class* found in the result set,
+       while the former works on each *entity* (eg each row of the result set),
+       which may be much more costly.
     """
 
     @lltrace
@@ -306,8 +464,12 @@
 
 
 class ExpectedValueSelector(Selector):
-    """Take a list of expected values as initializer argument, check
-    _get_value method return one of these expected values.
+    """Take a list of expected values as initializer argument and store them
+    into the :attr:`expected` set attribute.
+
+    You should implements the :meth:`_get_value(cls, req, **kwargs)` method
+    which should return the value for the given context. The selector will then
+    return 1 if the value is expected, else 0.
     """
     def __init__(self, *expected):
         assert expected, self
@@ -345,10 +507,12 @@
 
 
 class appobject_selectable(Selector):
-    """return 1 if another appobject is selectable using the same input context.
+    """Return 1 if another appobject is selectable using the same input context.
 
     Initializer arguments:
+
     * `registry`, a registry name
+
     * `regid`, an object identifier in this registry
     """
     def __init__(self, registry, regid):
@@ -1080,6 +1244,24 @@
             return 1
         return 0
 
+class is_in_state(score_entity):
+    """return 1 if entity is in one of the states given as argument list
+
+    you should use this instead of your own score_entity x: x.state == 'bla'
+    selector to avoid some gotchas:
+
+    * possible views gives a fake entity with no state
+    * you must use the latest tr info, not entity.state for repository side
+      checking of the current state
+    """
+    def __init__(self, *states):
+        def score(entity, states=set(states)):
+            try:
+                return entity.latest_trinfo().new_state.name in states
+            except AttributeError:
+                return None
+        super(is_in_state, self).__init__(score)
+
 
 ## deprecated stuff ############################################################
 
--- a/server/__init__.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/__init__.py	Tue Apr 06 19:46:38 2010 +0200
@@ -8,6 +8,8 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
 import sys
@@ -113,11 +115,7 @@
     from cubicweb.server.sqlutils import sqlexec, sqlschema, sqldropschema
     # configuration to avoid db schema loading and user'state checking
     # on connection
-    read_instance_schema = config.read_instance_schema
-    bootstrap_schema = config.bootstrap_schema
-    config.read_instance_schema = False
     config.creating = True
-    config.bootstrap_schema = True
     config.consider_user_state = False
     config.set_language = False
     # only enable the system source at initialization time + admin which is not
@@ -145,13 +143,10 @@
     # can't skip entities table even if system source doesn't support them,
     # they are used sometimes by generated sql. Keeping them empty is much
     # simpler than fixing this...
-    if sqlcnx.logged_user != source['db-user']:
-        schemasql = sqlschema(schema, driver, user=source['db-user'])
-    else:
-        schemasql = sqlschema(schema, driver)
-        #skip_entities=[str(e) for e in schema.entities()
-        #               if not repo.system_source.support_entity(str(e))])
-    sqlexec(schemasql, execute, pbtitle=_title)
+    schemasql = sqlschema(schema, driver)
+    #skip_entities=[str(e) for e in schema.entities()
+    #               if not repo.system_source.support_entity(str(e))])
+    sqlexec(schemasql, execute, pbtitle=_title, delimiter=';;')
     sqlcursor.close()
     sqlcnx.commit()
     sqlcnx.close()
@@ -200,10 +195,9 @@
     cnx.commit()
     cnx.close()
     session.close()
+    repo.shutdown()
     # restore initial configuration
     config.creating = False
-    config.read_instance_schema = read_instance_schema
-    config.bootstrap_schema = bootstrap_schema
     config.consider_user_state = True
     config.set_language = True
     print '-> database for instance %s initialized.' % config.appid
@@ -211,33 +205,33 @@
 
 def initialize_schema(config, schema, mhandler, event='create'):
     from cubicweb.server.schemaserial import serialize_schema
-    # deactivate every hooks but those responsible to set metadata
-    # so, NO INTEGRITY CHECKS are done, to have quicker db creation
-    oldmode = config.set_hooks_mode(config.DENY_ALL)
-    changes = config.enable_hook_category('metadata')
+    from cubicweb.server.session import hooks_control
+    session = mhandler.session
     paths = [p for p in config.cubes_path() + [config.apphome]
              if exists(join(p, 'migration'))]
-    # execute cubicweb's pre<event> script
-    mhandler.exec_event_script('pre%s' % event)
-    # execute cubes pre<event> script if any
-    for path in reversed(paths):
-        mhandler.exec_event_script('pre%s' % event, path)
-    # enter instance'schema into the database
-    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
-    for path in reversed(paths):
-        mhandler.exec_event_script('post%s' % event, path)
-    # restore hooks config
-    if changes:
-        config.disable_hook_category(changes)
-    config.set_hooks_mode(oldmode)
+    # deactivate every hooks but those responsible to set metadata
+    # so, NO INTEGRITY CHECKS are done, to have quicker db creation.
+    # Active integrity is kept else we may pb such as two default
+    # workflows for one entity type.
+    with hooks_control(session, session.HOOKS_DENY_ALL, 'metadata',
+                       'activeintegrity'):
+        # execute cubicweb's pre<event> script
+        mhandler.exec_event_script('pre%s' % event)
+        # execute cubes pre<event> script if any
+        for path in reversed(paths):
+            mhandler.exec_event_script('pre%s' % event, path)
+        # enter instance'schema into the database
+        session.set_pool()
+        serialize_schema(session, schema)
+        # execute cubicweb's post<event> script
+        mhandler.exec_event_script('post%s' % event)
+        # execute cubes'post<event> script if any
+        for path in reversed(paths):
+            mhandler.exec_event_script('post%s' % event, path)
 
 
-# sqlite'stored procedures have to be registered at connexion opening time
-SQL_CONNECT_HOOKS = {}
+# sqlite'stored procedures have to be registered at connection opening time
+from logilab.database import SQL_CONNECT_HOOKS
 
 # add to this set relations which should have their add security checking done
 # *BEFORE* adding the actual relation (done after by default)
--- a/server/checkintegrity.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/checkintegrity.py	Tue Apr 06 19:46:38 2010 +0200
@@ -6,6 +6,8 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
 import sys
@@ -15,6 +17,7 @@
 
 from cubicweb.schema import PURE_VIRTUAL_RTYPES
 from cubicweb.server.sqlutils import SQL_PREFIX
+from cubicweb.server.session import security_enabled
 
 def has_eid(sqlcursor, eid, eids):
     """return true if the eid is a valid eid"""
@@ -68,19 +71,12 @@
     """reindex all entities in the repository"""
     # deactivate modification_date hook since we don't want them
     # to be updated due to the reindexation
-    from cubicweb.server.repository import FTIndexEntityOp
     repo = session.repo
     cursor = session.pool['system']
-    if not repo.system_source.indexer.has_fti_table(cursor):
-        from indexer import get_indexer
+    if not repo.system_source.dbhelper.has_fti_table(cursor):
         print 'no text index table'
-        indexer = get_indexer(repo.system_source.dbdriver)
-        # XXX indexer.init_fti(cursor) once index 0.7 is out
-        indexer.init_extensions(cursor)
-        cursor.execute(indexer.sql_init_fti())
-    repo.config.disabled_hooks_categories.add('metadata')
-    repo.config.disabled_hooks_categories.add('integrity')
-    repo.do_fti = True  # ensure full-text indexation is activated
+        dbhelper.init_fti(cursor)
+    repo.system_source.do_fti = True  # ensure full-text indexation is activated
     etypes = set()
     for eschema in schema.entities():
         if eschema.final:
@@ -95,24 +91,18 @@
     if withpb:
         pb = ProgressBar(len(etypes) + 1)
     # first monkey patch Entity.check to disable validation
-    from cubicweb.entity import Entity
-    _check = Entity.check
-    Entity.check = lambda self, creation=False: True
     # clear fti table first
     session.system_sql('DELETE FROM %s' % session.repo.system_source.dbhelper.fti_table)
     if withpb:
         pb.update()
     # reindex entities by generating rql queries which set all indexable
     # attribute to their current value
+    source = repo.system_source
     for eschema in etypes:
         for entity in session.execute('Any X WHERE X is %s' % eschema).entities():
-            FTIndexEntityOp(session, entity=entity)
+            source.fti_index_entity(session, entity)
         if withpb:
             pb.update()
-    # restore Entity.check
-    Entity.check = _check
-    repo.config.disabled_hooks_categories.remove('metadata')
-    repo.config.disabled_hooks_categories.remove('integrity')
 
 
 def check_schema(schema, session, eids, fix=1):
@@ -290,9 +280,10 @@
     # yo, launch checks
     if checks:
         eids_cache = {}
-        for check in checks:
-            check_func = globals()['check_%s' % check]
-            check_func(repo.schema, session, eids_cache, fix=fix)
+        with security_enabled(session, read=False): # ensure no read security
+            for check in checks:
+                check_func = globals()['check_%s' % check]
+                check_func(repo.schema, session, eids_cache, fix=fix)
         if fix:
             cnx.commit()
         else:
--- a/server/hook.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/hook.py	Tue Apr 06 19:46:38 2010 +0200
@@ -19,8 +19,11 @@
 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*.
+Server start/maintenance/stop hooks (eg server_startup, server_maintenance,
+server_shutdown) have a `repo` attribute, but *their `_cw` attribute is None*.
+The `server_startup` is called on regular startup, while `server_maintenance`
+is called on cubicweb-ctl upgrade or shell commands. `server_shutdown` is
+called anyway.
 
 Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a
 `timestamp` attributes, but *their `_cw` attribute is None*.
@@ -33,6 +36,8 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
@@ -43,11 +48,12 @@
 from logilab.common.deprecation import deprecated
 from logilab.common.logging_ext import set_log_methods
 
+from cubicweb import RegistryNotFound
 from cubicweb.cwvreg import CWRegistry, VRegistry
 from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
                                 implements)
 from cubicweb.appobject import AppObject
-
+from cubicweb.server.session import security_enabled
 
 ENTITIES_HOOKS = set(('before_add_entity',    'after_add_entity',
                       'before_update_entity', 'after_update_entity',
@@ -55,47 +61,65 @@
 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',
+                    'server_startup', 'server_maintenance', 'server_shutdown',
                     'session_open', 'session_close'))
 ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
 
 
 class HooksRegistry(CWRegistry):
+    def initialization_completed(self):
+        for appobjects in self.values():
+            for cls in appobjects:
+                if not cls.enabled:
+                    warn('[3.6] %s: enabled is deprecated' % cls)
+                    self.unregister(cls)
 
     def register(self, obj, **kwargs):
-        try:
-            iter(obj.events)
-        except AttributeError:
-            raise
-        except:
-            raise Exception('bad .events attribute %s on %s.%s' % (
-                obj.events, obj.__module__, obj.__name__))
         for event in obj.events:
             if event not in ALL_HOOKS:
                 raise Exception('bad event %s on %s.%s' % (
                     event, obj.__module__, obj.__name__))
         super(HooksRegistry, self).register(obj, **kwargs)
 
-    def call_hooks(self, event, req=None, **kwargs):
+    def call_hooks(self, event, session=None, **kwargs):
         kwargs['event'] = event
-        for hook in sorted(self.possible_objects(req, **kwargs), key=lambda x: x.order):
-            if hook.enabled:
+        if session is None:
+            for hook in sorted(self.possible_objects(session, **kwargs),
+                               key=lambda x: x.order):
                 hook()
-            else:
-                warn('[3.6] %s: enabled is deprecated' % hook.__class__)
+        else:
+            # by default, hooks are executed with security turned off
+            with security_enabled(session, read=False):
+                hooks = sorted(self.possible_objects(session, **kwargs),
+                               key=lambda x: x.order)
+                with security_enabled(session, write=False):
+                    for hook in hooks:
+                        hook()
 
-VRegistry.REGISTRY_FACTORY['hooks'] = HooksRegistry
+class HooksManager(object):
+    def __init__(self, vreg):
+        self.vreg = vreg
+
+    def call_hooks(self, event, session=None, **kwargs):
+        try:
+            self.vreg['%s_hooks' % event].call_hooks(event, session, **kwargs)
+        except RegistryNotFound:
+            pass # no hooks for this event
 
 
+for event in ALL_HOOKS:
+    VRegistry.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry
+
+_MARKER = object()
 def entity_oldnewvalue(entity, attr):
     """returns the couple (old attr value, new attr value)
     NOTE: will only work in a before_update_entity hook
     """
     # get new value and remove from local dict to force a db query to
     # fetch old value
-    newvalue = entity.pop(attr, None)
+    newvalue = entity.pop(attr, _MARKER)
     oldvalue = getattr(entity, attr)
-    if newvalue is not None:
+    if newvalue is not _MARKER:
         entity[attr] = newvalue
     return oldvalue, newvalue
 
@@ -104,28 +128,17 @@
 
 @objectify_selector
 @lltrace
-def match_event(cls, req, **kwargs):
-    if kwargs.get('event') in cls.events:
-        return 1
-    return 0
+def enabled_category(cls, req, **kwargs):
+    if req is None:
+        return True # XXX how to deactivate server startup / shutdown event
+    return req.is_hook_activated(cls)
 
 @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
-    return config.is_hook_activated(cls)
-
-@objectify_selector
-@lltrace
-def regular_session(cls, req, **kwargs):
-    if req is None or req.is_super_session:
-        return 0
-    return 1
-
+def from_dbapi_query(cls, req, **kwargs):
+    if req.running_dbapi_query:
+        return 1
+    return 0
 
 class rechain(object):
     def __init__(self, *iterators):
@@ -174,11 +187,11 @@
                 return 1
         return 0
 
+
 # base class for hook ##########################################################
 
 class Hook(AppObject):
-    __registry__ = 'hooks'
-    __select__ = match_event() & enabled_category()
+    __select__ = enabled_category()
     # set this in derivated classes
     events = None
     category = None
@@ -187,6 +200,16 @@
     enabled = True
 
     @classproperty
+    def __registries__(cls):
+        try:
+            return ['%s_hooks' % ev for ev in cls.events]
+        except AttributeError:
+            raise
+        except TypeError:
+            raise Exception('bad .events attribute %s on %s.%s' % (
+                cls.events, cls.__module__, cls.__name__))
+
+    @classproperty
     def __regid__(cls):
         warn('[3.6] %s.%s: please specify an id for your hook'
              % (cls.__module__, cls.__name__), DeprecationWarning)
@@ -263,7 +286,7 @@
         else:
             assert self.rtype in self.object_relations
             meid, seid = self.eidto, self.eidfrom
-        self._cw.unsafe_execute(
+        self._cw.execute(
             'SET E %s P WHERE X %s P, X eid %%(x)s, E eid %%(e)s, NOT E %s P'\
             % (self.main_rtype, self.main_rtype, self.main_rtype),
             {'x': meid, 'e': seid}, ('x', 'e'))
@@ -281,7 +304,7 @@
 
     def __call__(self):
         eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])
-        execute = self._cw.unsafe_execute
+        execute = self._cw.execute
         for rel in self.subject_relations:
             if rel in eschema.subjrels:
                 execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
@@ -306,7 +329,7 @@
 
     def __call__(self):
         eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])
-        execute = self._cw.unsafe_execute
+        execute = self._cw.execute
         for rel in self.subject_relations:
             if rel in eschema.subjrels:
                 execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
@@ -434,6 +457,24 @@
 set_log_methods(Operation, getLogger('cubicweb.session'))
 
 
+def set_operation(session, datakey, value, opcls, **opkwargs):
+    """Search for session.transaction_data[`datakey`] (expected to be a set):
+
+    * if found, simply append `value`
+
+    * else, initialize it to set([`value`]) and instantiate the given `opcls`
+      operation class with additional keyword arguments.
+
+    You should use this instead of creating on operation for each `value`,
+    since handling operations becomes coslty on massive data import.
+    """
+    try:
+        session.transaction_data[datakey].add(value)
+    except KeyError:
+        opcls(session, *opkwargs)
+        session.transaction_data[datakey] = set((value,))
+
+
 class LateOperation(Operation):
     """special operation which should be called after all possible (ie non late)
     operations
@@ -510,6 +551,43 @@
 
 class RQLPrecommitOperation(Operation):
     def precommit_event(self):
-        execute = self.session.unsafe_execute
+        execute = self.session.execute
         for rql in self.rqls:
             execute(*rql)
+
+
+class CleanupNewEidsCacheOp(SingleLastOperation):
+    """on rollback of a insert query we have to remove from repository's
+    type/source cache eids of entities added in that transaction.
+
+    NOTE: querier's rqlst/solutions cache may have been polluted too with
+    queries such as Any X WHERE X eid 32 if 32 has been rollbacked however
+    generated queries are unpredictable and analysing all the cache probably
+    too expensive. Notice that there is no pb when using args to specify eids
+    instead of giving them into the rql string.
+    """
+
+    def rollback_event(self):
+        """the observed connections pool has been rollbacked,
+        remove inserted eid from repository type/source cache
+        """
+        try:
+            self.session.repo.clear_caches(
+                self.session.transaction_data['neweids'])
+        except KeyError:
+            pass
+
+class CleanupDeletedEidsCacheOp(SingleLastOperation):
+    """on commit of delete query, we have to remove from repository's
+    type/source cache eids of entities deleted in that transaction.
+    """
+
+    def commit_event(self):
+        """the observed connections pool has been rollbacked,
+        remove inserted eid from repository type/source cache
+        """
+        try:
+            self.session.repo.clear_caches(
+                self.session.transaction_data['pendingeids'])
+        except KeyError:
+            pass
--- a/server/hookhelper.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/hookhelper.py	Tue Apr 06 19:46:38 2010 +0200
@@ -13,7 +13,6 @@
 
 @deprecated('[3.6] entity_oldnewvalue should be imported from cw.server.hook')
 def entity_oldnewvalue(entity, attr):
-    """return the "name" attribute of the entity with the given eid"""
     return hook.entity_oldnewvalue(entity, attr)
 
 @deprecated('[3.6] entity_name is deprecated, use entity.name')
--- a/server/migractions.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/migractions.py	Tue Apr 06 19:46:38 2010 +0200
@@ -15,6 +15,8 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
 import sys
@@ -25,10 +27,12 @@
 import os.path as osp
 from datetime import datetime
 from glob import glob
+from copy import copy
 from warnings import warn
 
 from logilab.common.deprecation import deprecated
 from logilab.common.decorators import cached, clear_cache
+from logilab.common.testlib import mock_object
 
 from yams.constraints import SizeConstraint
 from yams.schema2sql import eschema2sql, rschema2sql
@@ -38,7 +42,7 @@
                              CubicWebRelationSchema, order_eschemas)
 from cubicweb.dbapi import get_repository, repo_connect
 from cubicweb.migration import MigrationHelper, yes
-
+from cubicweb.server.session import hooks_control
 try:
     from cubicweb.server import SOURCE_TYPES, schemaserial as ss
     from cubicweb.server.utils import manager_userpasswd, ask_source_config
@@ -55,7 +59,6 @@
     def __init__(self, config, schema, interactive=True,
                  repo=None, cnx=None, verbosity=1, connect=True):
         MigrationHelper.__init__(self, config, interactive, verbosity)
-        # no config on shell to a remote instance
         if not interactive:
             assert cnx
             assert repo
@@ -63,11 +66,13 @@
             assert repo
             self._cnx = cnx
             self.repo = repo
-            if config is not None:
-                self.session.data['rebuild-infered'] = False
         elif connect:
             self.repo_connect()
-        if not schema:
+        # no config on shell to a remote instance
+        if config is not None and (cnx or connect):
+            self.session.data['rebuild-infered'] = False
+            self.repo.hm.call_hooks('server_maintenance', repo=self.repo)
+        if not schema and not getattr(config, 'quick_start', False):
             schema = config.load_schema(expand_cubes=True)
         self.fs_schema = schema
         self._synchronized = set()
@@ -94,7 +99,9 @@
                 self.backup_database()
             elif options.backup_db:
                 self.backup_database(askconfirm=False)
-        super(ServerMigrationHelper, self).migrate(vcconf, toupgrade, options)
+        # disable notification during migration
+        with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, 'notification'):
+            super(ServerMigrationHelper, self).migrate(vcconf, toupgrade, options)
 
     def cmd_process_script(self, migrscript, funcname=None, *args, **kwargs):
         """execute a migration script
@@ -145,9 +152,9 @@
         try:
             for source in repo.sources:
                 try:
-                    source.backup(osp.join(tmpdir, source.uri))
-                except Exception, exc:
-                    print '-> error trying to backup %s [%s]' % (source.uri, exc)
+                    source.backup(osp.join(tmpdir, source.uri), self.confirm)
+                except Exception, ex:
+                    print '-> error trying to backup %s [%s]' % (source.uri, ex)
                     if not self.confirm('Continue anyway?', default='n'):
                         raise SystemExit(1)
                     else:
@@ -187,7 +194,6 @@
             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:
@@ -241,21 +247,30 @@
     @property
     def session(self):
         if self.config is not None:
-            return self.repo._get_session(self.cnx.sessionid)
+            session = self.repo._get_session(self.cnx.sessionid)
+            if session.pool is None:
+                session.set_read_security(False)
+                session.set_write_security(False)
+            session.set_pool()
+            return session
         # no access to session on remote instance
         return None
 
     def commit(self):
         if hasattr(self, '_cnx'):
             self._cnx.commit()
+        if self.session:
+            self.session.set_pool()
 
     def rollback(self):
         if hasattr(self, '_cnx'):
             self._cnx.rollback()
+        if self.session:
+            self.session.set_pool()
 
     def rqlexecall(self, rqliter, cachekey=None, ask_confirm=True):
         for rql, kwargs in rqliter:
-            self.rqlexec(rql, kwargs, cachekey, ask_confirm)
+            self.rqlexec(rql, kwargs, cachekey, ask_confirm=ask_confirm)
 
     @cached
     def _create_context(self):
@@ -283,6 +298,11 @@
         """cached group mapping"""
         return ss.group_mapping(self._cw)
 
+    @cached
+    def cstrtype_mapping(self):
+        """cached constraint types mapping"""
+        return ss.cstrtype_mapping(self._cw)
+
     def exec_event_script(self, event, cubepath=None, funcname=None,
                           *args, **kwargs):
         if cubepath:
@@ -306,7 +326,6 @@
                     self.cmd_reactivate_verification_hooks()
 
     def install_custom_sql_scripts(self, directory, driver):
-        self.session.set_pool() # ensure pool is set
         for fpath in glob(osp.join(directory, '*.sql.%s' % driver)):
             newname = osp.basename(fpath).replace('.sql.%s' % driver,
                                                   '.%s.sql' % driver)
@@ -400,14 +419,17 @@
             return
         self._synchronized.add(rtype)
         rschema = self.fs_schema.rschema(rtype)
+        reporschema = self.repo.schema.rschema(rtype)
         if syncprops:
-            self.rqlexecall(ss.updaterschema2rql(rschema),
+            assert reporschema.eid, reporschema
+            self.rqlexecall(ss.updaterschema2rql(rschema, reporschema.eid),
                             ask_confirm=self.verbosity>=2)
         if syncrdefs:
-            reporschema = self.repo.schema.rschema(rtype)
             for subj, obj in rschema.rdefs:
                 if (subj, obj) not in reporschema.rdefs:
                     continue
+                if rschema in VIRTUAL_RTYPES:
+                    continue
                 self._synchronize_rdef_schema(subj, rschema, obj,
                                               syncprops=syncprops,
                                               syncperms=syncperms)
@@ -440,9 +462,11 @@
                          'Y is CWEType, Y name %(y)s',
                          {'x': str(repoeschema), 'y': str(espschema)},
                          ask_confirm=False)
-        self.rqlexecall(ss.updateeschema2rql(eschema),
+        self.rqlexecall(ss.updateeschema2rql(eschema, repoeschema.eid),
                         ask_confirm=self.verbosity >= 2)
         for rschema, targettypes, role in eschema.relation_definitions(True):
+            if rschema in VIRTUAL_RTYPES:
+                continue
             if role == 'subject':
                 if not rschema in repoeschema.subject_relations():
                     continue
@@ -477,14 +501,16 @@
         self._synchronized.add((subjtype, rschema, objtype))
         if rschema.symmetric:
             self._synchronized.add((objtype, rschema, subjtype))
+        rdef = rschema.rdef(subjtype, objtype)
+        if rdef.infered:
+            return # don't try to synchronize infered relation defs
+        repordef = reporschema.rdef(subjtype, objtype)
         confirm = self.verbosity >= 2
         if syncprops:
             # properties
-            self.rqlexecall(ss.updaterdef2rql(rschema, subjtype, objtype),
+            self.rqlexecall(ss.updaterdef2rql(rdef, repordef.eid),
                             ask_confirm=confirm)
             # constraints
-            rdef = rschema.rdef(subjtype, objtype)
-            repordef = reporschema.rdef(subjtype, objtype)
             newconstraints = list(rdef.constraints)
             # 1. remove old constraints and update constraints of the same type
             # NOTE: don't use rschema.constraint_by_type because it may be
@@ -500,20 +526,18 @@
                     self.rqlexec('DELETE X constrained_by C WHERE C eid %(x)s',
                                  {'x': cstr.eid}, 'x',
                                  ask_confirm=confirm)
-                    self.rqlexec('DELETE CWConstraint C WHERE C eid %(x)s',
-                                 {'x': cstr.eid}, 'x',
-                                 ask_confirm=confirm)
                 else:
                     newconstraints.remove(newcstr)
-                    values = {'x': cstr.eid,
-                              'v': unicode(newcstr.serialize())}
-                    self.rqlexec('SET X value %(v)s WHERE X eid %(x)s',
-                                 values, 'x', ask_confirm=confirm)
+                    value = unicode(newcstr.serialize())
+                    if value != unicode(cstr.serialize()):
+                        self.rqlexec('SET X value %(v)s WHERE X eid %(x)s',
+                                     {'x': cstr.eid, 'v': value}, 'x',
+                                     ask_confirm=confirm)
             # 2. add new constraints
-            for newcstr in newconstraints:
-                self.rqlexecall(ss.constraint2rql(rschema, subjtype, objtype,
-                                                  newcstr),
-                                ask_confirm=confirm)
+            cstrtype_map = self.cstrtype_mapping()
+            self.rqlexecall(ss.constraints2rql(cstrtype_map, newconstraints,
+                                               repordef.eid),
+                            ask_confirm=confirm)
         if syncperms and not rschema in VIRTUAL_RTYPES:
             self._synchronize_permissions(rdef, repordef.eid)
 
@@ -674,18 +698,20 @@
         targeted type is known
         """
         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.final:
-                instschema.del_entity_type(etype)
-        else:
-            eschema = self.fs_schema.eschema(etype)
+        assert not etype in instschema
+        #     # XXX (syt) plz explain: if we're adding an entity type, it should
+        #     # not be there...
+        #     eschema = instschema[etype]
+        #     if eschema.final:
+        #         instschema.del_entity_type(etype)
+        # else:
+        eschema = self.fs_schema.eschema(etype)
         confirm = self.verbosity >= 2
         groupmap = self.group_mapping()
+        cstrtypemap = self.cstrtype_mapping()
         # register the entity into CWEType
-        self.rqlexecall(ss.eschema2rql(eschema, groupmap), ask_confirm=confirm)
+        execute = self._cw.execute
+        ss.execschemarql(execute, eschema, ss.eschema2rql(eschema, groupmap))
         # add specializes relation if needed
         self.rqlexecall(ss.eschemaspecialize2rql(eschema), ask_confirm=confirm)
         # register entity's attributes
@@ -698,9 +724,8 @@
                 # 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,
-                                        groupmap=groupmap),
-                            ask_confirm=confirm)
+            rdef = self._get_rdef(rschema, eschema, eschema.destination(rschema))
+            ss.execschemarql(execute, rdef, ss.rdef2rql(rdef, cstrtypemap, groupmap),)
         # 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):
@@ -760,10 +785,12 @@
                     # remember this two avoid adding twice non symmetric relation
                     # such as "Emailthread forked_from Emailthread"
                     added.append((etype, rschema.type, targettype))
-                    self.rqlexecall(ss.rdef2rql(rschema, etype, targettype,
-                                                groupmap=groupmap),
-                                    ask_confirm=confirm)
+                    rdef = self._get_rdef(rschema, eschema, targetschema)
+                    ss.execschemarql(execute, rdef,
+                                     ss.rdef2rql(rdef, cstrtypemap, groupmap))
             for rschema in eschema.object_relations():
+                if rschema.type in META_RTYPES:
+                    continue
                 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
@@ -781,9 +808,9 @@
                     elif (targettype, rschema.type, etype) in added:
                         continue
                     # register relation definition
-                    self.rqlexecall(ss.rdef2rql(rschema, targettype, etype,
-                                                groupmap=groupmap),
-                                    ask_confirm=confirm)
+                    rdef = self._get_rdef(rschema, targetschema, eschema)
+                    ss.execschemarql(execute, rdef,
+                                     ss.rdef2rql(rdef, cstrtypemap, groupmap))
         if commit:
             self.commit()
 
@@ -822,15 +849,23 @@
         committing depends on the `commit` argument value).
 
         """
+        reposchema = self.repo.schema
         rschema = self.fs_schema.rschema(rtype)
+        execute = self._cw.execute
         # register the relation into CWRType and insert necessary relation
         # definitions
-        self.rqlexecall(ss.rschema2rql(rschema, addrdef=False),
-                        ask_confirm=self.verbosity>=2)
+        ss.execschemarql(execute, rschema, ss.rschema2rql(rschema, addrdef=False))
         if addrdef:
             self.commit()
-            self.rqlexecall(ss.rdef2rql(rschema, groupmap=self.group_mapping()),
-                            ask_confirm=self.verbosity>=2)
+            gmap = self.group_mapping()
+            cmap = self.cstrtype_mapping()
+            for rdef in rschema.rdefs.itervalues():
+                if not (reposchema.has_entity(rdef.subject)
+                        and reposchema.has_entity(rdef.object)):
+                    continue
+                self._set_rdef_eid(rdef)
+                ss.execschemarql(execute, rdef,
+                                 ss.rdef2rql(rdef, cmap, gmap))
             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
@@ -839,15 +874,14 @@
                     if not etype in self.fs_schema:
                         # get sample object type and rproperties
                         objtypes = rschema.objects()
-                        assert len(objtypes) == 1
+                        assert len(objtypes) == 1, objtypes
                         objtype = objtypes[0]
-                        props = rschema.rproperties(
-                            rschema.subjects(objtype)[0], objtype)
-                        assert props
-                        self.rqlexecall(ss.rdef2rql(rschema, etype, objtype, props,
-                                                    groupmap=self.group_mapping()),
-                                        ask_confirm=self.verbosity>=2)
-
+                        rdef = copy(rschema.rdef(rschema.subjects(objtype)[0], objtype))
+                        rdef.subject = etype
+                        rdef.rtype = self.repo.schema.rschema(rschema)
+                        rdef.object = self.repo.schema.rschema(objtype)
+                        ss.execschemarql(execute, rdef,
+                                         ss.rdef2rql(rdef, cmap, gmap))
         if commit:
             self.commit()
 
@@ -877,12 +911,25 @@
         rschema = self.fs_schema.rschema(rtype)
         if not rtype in self.repo.schema:
             self.cmd_add_relation_type(rtype, addrdef=False, commit=True)
-        self.rqlexecall(ss.rdef2rql(rschema, subjtype, objtype,
-                                    groupmap=self.group_mapping()),
-                        ask_confirm=self.verbosity>=2)
+        execute = self._cw.execute
+        rdef = self._get_rdef(rschema, subjtype, objtype)
+        ss.execschemarql(execute, rdef,
+                         ss.rdef2rql(rdef, self.cstrtype_mapping(),
+                                     self.group_mapping()))
         if commit:
             self.commit()
 
+    def _get_rdef(self, rschema, subjtype, objtype):
+        return self._set_rdef_eid(rschema.rdefs[(subjtype, objtype)])
+
+    def _set_rdef_eid(self, rdef):
+        for attr in ('rtype', 'subject', 'object'):
+            schemaobj = getattr(rdef, attr)
+            if getattr(schemaobj, 'eid', None) is None:
+                schemaobj.eid =  self.repo.schema[schemaobj].eid
+                assert schemaobj.eid is not None
+        return rdef
+
     def cmd_drop_relation_definition(self, subjtype, rtype, objtype, commit=True):
         """unregister an existing relation definition"""
         rschema = self.repo.schema.rschema(rtype)
@@ -1120,9 +1167,8 @@
             return session
         return self.cnx.request()
 
-    def cmd_create_entity(self, etype, **kwargs):
+    def cmd_create_entity(self, etype, commit=False, **kwargs):
         """add a new entity of the given type"""
-        commit = kwargs.pop('commit', False)
         entity = self._cw.create_entity(etype, **kwargs)
         if commit:
             self.commit()
@@ -1140,7 +1186,6 @@
         level actions
         """
         if not ask_confirm or self.confirm('Execute sql: %s ?' % sql):
-            self.session.set_pool() # ensure pool is set
             try:
                 cu = self.session.system_sql(sql, args)
             except:
@@ -1154,15 +1199,13 @@
                 # no result to fetch
                 return
 
-    def rqlexec(self, rql, kwargs=None, cachekey=None, ask_confirm=True):
+    def rqlexec(self, rql, kwargs=None, cachekey=None, build_descr=True,
+                ask_confirm=True):
         """rql action"""
         if not isinstance(rql, (tuple, list)):
             rql = ( (rql, kwargs), )
         res = None
-        try:
-            execute = self._cw.unsafe_execute
-        except AttributeError:
-            execute = self._cw.execute
+        execute = self._cw.execute
         for rql, kwargs in rql:
             if kwargs:
                 msg = '%s (%s)' % (rql, kwargs)
@@ -1170,7 +1213,7 @@
                 msg = rql
             if not ask_confirm or self.confirm('Execute rql: %s ?' % msg):
                 try:
-                    res = execute(rql, kwargs, cachekey)
+                    res = execute(rql, kwargs, cachekey, build_descr=build_descr)
                 except Exception, ex:
                     if self.confirm('Error: %s\nabort?' % ex):
                         raise
@@ -1179,12 +1222,6 @@
     def rqliter(self, rql, kwargs=None, ask_confirm=True):
         return ForRqlIterator(self, rql, None, ask_confirm)
 
-    def cmd_deactivate_verification_hooks(self):
-        self.config.disabled_hooks_categories.add('integrity')
-
-    def cmd_reactivate_verification_hooks(self):
-        self.config.disabled_hooks_categories.remove('integrity')
-
     # broken db commands ######################################################
 
     def cmd_change_attribute_type(self, etype, attr, newtype, commit=True):
@@ -1235,6 +1272,14 @@
         if commit:
             self.commit()
 
+    @deprecated("[3.7] use session.disable_hook_categories('integrity')")
+    def cmd_deactivate_verification_hooks(self):
+        self.session.disable_hook_categories('integrity')
+
+    @deprecated("[3.7] use session.enable_hook_categories('integrity')")
+    def cmd_reactivate_verification_hooks(self):
+        self.session.enable_hook_categories('integrity')
+
 
 class ForRqlIterator:
     """specific rql iterator to make the loop skipable"""
--- a/server/msplanner.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/msplanner.py	Tue Apr 06 19:46:38 2010 +0200
@@ -313,8 +313,6 @@
             if varobj.stinfo['uidrels']:
                 vrels = varobj.stinfo['relations'] - varobj.stinfo['uidrels']
                 for rel in varobj.stinfo['uidrels']:
-                    if rel.neged(strict=True) or rel.operator() != '=':
-                        continue
                     for const in rel.children[1].get_nodes(Constant):
                         eid = const.eval(self.plan.args)
                         source = self._session.source_from_eid(eid)
@@ -1044,7 +1042,7 @@
                      for select in subquery.query.children]
             for sppi in sppis:
                 if sppi.needsplit or sppi.part_sources != ppi.part_sources:
-                    temptable = 'T%s' % make_uid(id(subquery))
+                    temptable = plan.make_temp_table_name('T%s' % make_uid(id(subquery)))
                     sstep = self._union_plan(plan, sppis, temptable)[0]
                     break
             else:
@@ -1077,7 +1075,7 @@
                 inputmap = self._ppi_subqueries(ppi)
                 aggrstep = need_aggr_step(select, sources)
                 if aggrstep:
-                    atemptable = 'T%s' % make_uid(id(select))
+                    atemptable = plan.make_temp_table_name('T%s' % make_uid(id(select)))
                     sunion = Union()
                     sunion.append(select)
                     selected = select.selection[:]
@@ -1121,7 +1119,7 @@
         subinputmap = self._ppi_subqueries(ppi)
         stepdefs = ppi.part_steps()
         if need_aggr_step(select, ppi.part_sources, stepdefs):
-            atemptable = 'T%s' % make_uid(id(select))
+            atemptable = plan.make_temp_table_name('T%s' % make_uid(id(select)))
             selection = select.selection[:]
             select_group_sort(select)
         else:
@@ -1171,6 +1169,7 @@
                 else:
                     table = '_T%s%s' % (''.join(sorted(v._ms_table_key() for v in terms)),
                                         ''.join(sorted(str(i) for i in solindices)))
+                    table = plan.make_temp_table_name(table)
                     ppi.build_non_final_part(minrqlst, solindices, sources,
                                              insertedvars, table)
         # finally: join parts, deal with aggregat/group/sorts if necessary
--- a/server/querier.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/querier.py	Tue Apr 06 19:46:38 2010 +0200
@@ -6,6 +6,8 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
 from itertools import repeat
@@ -22,9 +24,8 @@
 
 from cubicweb.server.utils import cleanup_solutions
 from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata
-from cubicweb.server.ssplanner import add_types_restriction
-
-READ_ONLY_RTYPES = set(('eid', 'has_text', 'is', 'is_instance_of', 'identity'))
+from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction
+from cubicweb.server.session import security_enabled
 
 def empty_rset(rql, args, rqlst=None):
     """build an empty result set object"""
@@ -41,17 +42,6 @@
 
 # permission utilities ########################################################
 
-def var_kwargs(restriction, args):
-    varkwargs = {}
-    for rel in restriction.iget_nodes(Relation):
-        cmp = rel.children[1]
-        if rel.r_type == 'eid' and cmp.operator == '=' and \
-               not rel.neged(strict=True) and \
-               isinstance(cmp.children[0], Constant) and \
-               cmp.children[0].type == 'Substitute':
-            varkwargs[rel.children[0].name] = typed_eid(cmp.children[0].eval(args))
-    return varkwargs
-
 def check_no_password_selected(rqlst):
     """check that Password entities are not selected"""
     for solution in rqlst.solutions:
@@ -79,32 +69,31 @@
                 rdef = rschema.rdef(solution[rel.children[0].name],
                                     solution[rel.children[1].children[0].name])
             if not user.matching_groups(rdef.get_groups('read')):
+                # XXX rqlexpr not allowed
                 raise Unauthorized('read', rel.r_type)
     localchecks = {}
     # iterate on defined_vars and not on solutions to ignore column aliases
     for varname in rqlst.defined_vars:
-        etype = solution[varname]
-        eschema = schema.eschema(etype)
+        eschema = schema.eschema(solution[varname])
         if eschema.final:
             continue
         if not user.matching_groups(eschema.get_groups('read')):
             erqlexprs = eschema.get_rqlexprs('read')
             if not erqlexprs:
-                ex = Unauthorized('read', etype)
+                ex = Unauthorized('read', solution[varname])
                 ex.var = varname
                 raise ex
-            #assert len(erqlexprs) == 1
-            localchecks[varname] = tuple(erqlexprs)
+            localchecks[varname] = erqlexprs
     return localchecks
 
-def noinvariant_vars(restricted, select, nbtrees):
+def add_noinvariant(noinvariant, restricted, select, nbtrees):
     # a variable can actually be invariant if it has not been restricted for
     # security reason or if security assertion hasn't modified the possible
     # solutions for the query
     if nbtrees != 1:
         for vname in restricted:
             try:
-                yield select.defined_vars[vname]
+                noinvariant.add(select.defined_vars[vname])
             except KeyError:
                 # this is an alias
                 continue
@@ -116,7 +105,7 @@
                 # this is an alias
                 continue
             if len(var.stinfo['possibletypes']) != 1:
-                yield var
+                noinvariant.add(var)
 
 def _expand_selection(terms, selected, aliases, select, newselect):
     for term in terms:
@@ -175,6 +164,13 @@
         finally:
             self.clean()
 
+    def make_temp_table_name(self, table):
+        """
+        return a temp table name according to db backend
+        """
+        return self.syssource.make_temp_table_name(table)
+
+
     def init_temp_table(self, table, selected, sol):
         """initialize sql schema and variable map for a temporary table which
         will be used to store result for the given rqlst
@@ -200,12 +196,35 @@
 
         return rqlst to actually execute
         """
-        noinvariant = set()
-        if security and not self.session.is_super_session:
-            self._insert_security(union, noinvariant)
-        self.rqlhelper.simplify(union)
-        self.sqlannotate(union)
-        set_qdata(self.schema.rschema, union, noinvariant)
+        cached = None
+        if security and self.session.read_security:
+            # ensure security is turned of when security is inserted,
+            # else we may loop for ever...
+            if self.session.transaction_data.get('security-rqlst-cache'):
+                key = self.cache_key
+            else:
+                key = None
+            if key is not None and key in self.session.transaction_data:
+                cachedunion, args = self.session.transaction_data[key]
+                union.children[:] = []
+                for select in cachedunion.children:
+                    union.append(select)
+                union.has_text_query = cachedunion.has_text_query
+                args.update(self.args)
+                self.args = args
+                cached = True
+            else:
+                noinvariant = set()
+                with security_enabled(self.session, read=False):
+                    self._insert_security(union, noinvariant)
+                if key is not None:
+                    self.session.transaction_data[key] = (union, self.args)
+        else:
+            noinvariant = ()
+        if cached is None:
+            self.rqlhelper.simplify(union)
+            self.sqlannotate(union)
+            set_qdata(self.schema.rschema, union, noinvariant)
         if union.has_text_query:
             self.cache_key = None
 
@@ -273,14 +292,13 @@
                     myrqlst = select.copy(solutions=lchecksolutions)
                     myunion.append(myrqlst)
                     # in-place rewrite + annotation / simplification
-                    lcheckdef = [((varmap, 'X'), rqlexprs)
-                                 for varmap, rqlexprs in lcheckdef]
+                    lcheckdef = [((var, 'X'), rqlexprs) for var, rqlexprs in lcheckdef]
                     rewrite(myrqlst, lcheckdef, lchecksolutions, self.args)
-                    noinvariant.update(noinvariant_vars(restricted, myrqlst, nbtrees))
+                    add_noinvariant(noinvariant, restricted, myrqlst, nbtrees)
                 if () in localchecks:
                     select.set_possible_types(localchecks[()])
                     add_types_restriction(self.schema, select)
-                    noinvariant.update(noinvariant_vars(restricted, select, nbtrees))
+                    add_noinvariant(noinvariant, restricted, select, nbtrees)
 
     def _check_permissions(self, rqlst):
         """return a dict defining "local checks", e.g. RQLExpression defined in
@@ -300,17 +318,26 @@
 
         note: rqlst should not have been simplified at this point
         """
-        assert not self.session.is_super_session
-        user = self.session.user
+        session = self.session
+        user = session.user
         schema = self.schema
         msgs = []
+        neweids = session.transaction_data.get('neweids', ())
+        varkwargs = {}
+        if not session.transaction_data.get('security-rqlst-cache'):
+            for var in rqlst.defined_vars.itervalues():
+                for rel in var.stinfo['uidrels']:
+                    const = rel.children[1].children[0]
+                    try:
+                        varkwargs[var.name] = typed_eid(const.eval(self.args))
+                        break
+                    except AttributeError:
+                        #from rql.nodes import Function
+                        #assert isinstance(const, Function)
+                        # X eid IN(...)
+                        pass
         # dictionnary of variables restricted for security reason
         localchecks = {}
-        if rqlst.where is not None:
-            varkwargs = var_kwargs(rqlst.where, self.args)
-            neweids = self.session.transaction_data.get('neweids', ())
-        else:
-            varkwargs = None
         restricted_vars = set()
         newsolutions = []
         for solution in rqlst.solutions:
@@ -323,21 +350,20 @@
                 LOGGER.info(msg)
             else:
                 newsolutions.append(solution)
-                if varkwargs:
-                    # try to benefit of rqlexpr.check cache for entities which
-                    # are specified by eid in query'args
-                    for varname, eid in varkwargs.iteritems():
-                        try:
-                            rqlexprs = localcheck.pop(varname)
-                        except KeyError:
-                            continue
-                        if eid in neweids:
-                            continue
-                        for rqlexpr in rqlexprs:
-                            if rqlexpr.check(self.session, eid):
-                                break
-                        else:
-                            raise Unauthorized()
+                # try to benefit of rqlexpr.check cache for entities which
+                # are specified by eid in query'args
+                for varname, eid in varkwargs.iteritems():
+                    try:
+                        rqlexprs = localcheck.pop(varname)
+                    except KeyError:
+                        continue
+                    if eid in neweids:
+                        continue
+                    for rqlexpr in rqlexprs:
+                        if rqlexpr.check(session, eid):
+                            break
+                    else:
+                        raise Unauthorized()
                 restricted_vars.update(localcheck)
                 localchecks.setdefault(tuple(localcheck.iteritems()), []).append(solution)
         # raise Unautorized exception if the user can't access to any solution
@@ -377,39 +403,6 @@
         self._r_obj_index = {}
         self._expanded_r_defs = {}
 
-    def relation_definitions(self, rqlst, to_build):
-        """add constant values to entity def, mark variables to be selected
-        """
-        to_select = {}
-        for relation in rqlst.main_relations:
-            lhs, rhs = relation.get_variable_parts()
-            rtype = relation.r_type
-            if rtype in READ_ONLY_RTYPES:
-                raise QueryError("can't assign to %s" % rtype)
-            try:
-                edef = to_build[str(lhs)]
-            except KeyError:
-                # lhs var is not to build, should be selected and added as an
-                # object relation
-                edef = to_build[str(rhs)]
-                to_select.setdefault(edef, []).append((rtype, lhs, 1))
-            else:
-                if isinstance(rhs, Constant) and not rhs.uid:
-                    # add constant values to entity def
-                    value = rhs.eval(self.args)
-                    eschema = edef.e_schema
-                    attrtype = eschema.subjrels[rtype].objects(eschema)[0]
-                    if attrtype == 'Password' and isinstance(value, unicode):
-                        value = value.encode('UTF8')
-                    edef[rtype] = value
-                elif to_build.has_key(str(rhs)):
-                    # create a relation between two newly created variables
-                    self.add_relation_def((edef, rtype, to_build[rhs.name]))
-                else:
-                    to_select.setdefault(edef, []).append( (rtype, rhs, 0) )
-        return to_select
-
-
     def add_entity_def(self, edef):
         """add an entity definition to build"""
         edef.querier_pending_relations = {}
@@ -629,20 +622,20 @@
             try:
                 self.solutions(session, rqlst, args)
             except UnknownEid:
-                # we want queries such as "Any X WHERE X eid 9999"
-                # return an empty result instead of raising UnknownEid
+                # we want queries such as "Any X WHERE X eid 9999" return an
+                # empty result instead of raising UnknownEid
                 return empty_rset(rql, args, rqlst)
             self._rql_cache[cachekey] = rqlst
         orig_rqlst = rqlst
-        if not rqlst.TYPE == 'select':
-            if not session.is_super_session:
+        if rqlst.TYPE != 'select':
+            if session.read_security:
                 check_no_password_selected(rqlst)
-            # write query, ensure session's mode is 'write' so connections
-            # won't be released until commit/rollback
+            # write query, ensure session's mode is 'write' so connections won't
+            # be released until commit/rollback
             session.mode = 'write'
             cachekey = None
         else:
-            if not session.is_super_session:
+            if session.read_security:
                 for select in rqlst.children:
                     check_no_password_selected(select)
             # on select query, always copy the cached rqlst so we don't have to
--- a/server/repository.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/repository.py	Tue Apr 06 19:46:38 2010 +0200
@@ -15,6 +15,8 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
 import sys
@@ -25,71 +27,21 @@
 
 from logilab.common.decorators import cached
 from logilab.common.compat import any
+from logilab.common import flatten
 
 from yams import BadSchemaDefinition
+from yams.schema import role_name
 from rql import RQLSyntaxError
 
 from cubicweb import (CW_SOFTWARE_ROOT, CW_MIGRATION_MAP,
                       UnknownEid, AuthenticationError, ExecutionError,
                       ETypeNotSupportedBySources, MultiSourcesError,
                       BadConnectionId, Unauthorized, ValidationError,
-                      typed_eid)
+                      RepositoryError, typed_eid, onevent)
 from cubicweb import cwvreg, schema, server
 from cubicweb.server import utils, hook, pool, querier, sources
-from cubicweb.server.session import Session, InternalSession
-
-
-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
-
-    NOTE: querier's rqlst/solutions cache may have been polluted too with
-    queries such as Any X WHERE X eid 32 if 32 has been rollbacked however
-    generated queries are unpredictable and analysing all the cache probably
-    too expensive. Notice that there is no pb when using args to specify eids
-    instead of giving them into the rql string.
-    """
-
-    def commit_event(self):
-        """the observed connections pool has been rollbacked,
-        remove inserted eid from repository type/source cache
-        """
-        try:
-            self.session.repo.clear_caches(
-                self.session.transaction_data['pendingeids'])
-        except KeyError:
-            pass
-
-    def rollback_event(self):
-        """the observed connections pool has been rollbacked,
-        remove inserted eid from repository type/source cache
-        """
-        try:
-            self.session.repo.clear_caches(
-                self.session.transaction_data['neweids'])
-        except KeyError:
-            pass
-
-
-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
-    triggered on precommit, not commit, and this should be done after other
-    precommit operation which may add relations to the entity
-    """
-
-    def precommit_event(self):
-        session = self.session
-        entity = self.entity
-        if entity.eid in session.transaction_data.get('pendingeids', ()):
-            return # entity added and deleted in the same transaction
-        session.repo.system_source.fti_unindex_entity(session, entity.eid)
-        for container in entity.fti_containers():
-            session.repo.index_entity(session, container)
-
-    def commit_event(self):
-        pass
+from cubicweb.server.session import Session, InternalSession, InternalManager, \
+     security_enabled
 
 
 def del_existing_rel_if_needed(session, eidfrom, rtype, eidto):
@@ -101,12 +53,12 @@
     this kind of behaviour has to be done in the repository so we don't have
     hooks order hazardness
     """
-    # XXX now that rql in migraction default to unsafe_execute we don't want to
-    #     skip that for super session (though we can still skip it for internal
-    #     sessions). Also we should imo rely on the orm to first fetch existing
-    #     entity if any then delete it.
+    # skip that for internal session or if integrity explicitly disabled
+    #
+    # XXX we should imo rely on the orm to first fetch existing entity if any
+    # then delete it.
     if session.is_internal_session \
-           or not session.vreg.config.is_hook_category_activated('integrity'):
+           or not session.is_hook_category_activated('activeintegrity'):
         return
     card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
     # one may be tented to check for neweids but this may cause more than one
@@ -121,22 +73,15 @@
     rschema = session.repo.schema.rschema(rtype)
     if card[0] in '1?':
         if not rschema.inlined: # inlined relations will be implicitly deleted
-            rset = session.unsafe_execute('Any X,Y WHERE X %s Y, X eid %%(x)s, '
-                                          'NOT Y eid %%(y)s' % rtype,
-                                          {'x': eidfrom, 'y': eidto}, 'x')
-            if rset:
-                safe_delete_relation(session, rschema, *rset[0])
+            with security_enabled(session, read=False):
+                session.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?':
-        rset = session.unsafe_execute('Any X,Y WHERE X %s Y, Y eid %%(y)s, '
-                                      'NOT X eid %%(x)s' % rtype,
-                                      {'x': eidfrom, 'y': eidto}, 'y')
-        if rset:
-            safe_delete_relation(session, rschema, *rset[0])
-
-def safe_delete_relation(session, rschema, subject, object):
-    if not rschema.has_perm(session, 'delete', fromeid=subject, toeid=object):
-        raise Unauthorized()
-    session.repo.glob_delete_relation(session, subject, rschema.type, object)
+        with security_enabled(session, read=False):
+            session.execute('DELETE X %sY WHERE Y eid %%(y)s, '
+                            'NOT X eid %%(x)s' % rtype,
+                            {'x': eidfrom, 'y': eidto}, 'y')
 
 
 class Repository(object):
@@ -164,8 +109,6 @@
         self.vreg.schema = self.schema # until actual schema is loaded...
         # querier helper, need to be created after sources initialization
         self.querier = querier.QuerierHelper(self, self.schema)
-        # should we reindex in changes?
-        self.do_fti = not config['delay-full-text-indexation']
         # sources
         self.sources = []
         self.sources_by_uri = {}
@@ -189,8 +132,14 @@
         # open some connections pools
         if config.open_connections_pools:
             self.open_connections_pools()
+        @onevent('after-registry-reload', self)
+        def fix_user_classes(self):
+            usercls = self.vreg['etypes'].etype_class('CWUser')
+            for session in self._sessions.values():
+                if not isinstance(session.user, InternalManager):
+                    session.user.__class__ = usercls
 
-    def _boostrap_hook_registry(self):
+    def _bootstrap_hook_registry(self):
         """called during bootstrap since we need the metadata hooks"""
         hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks')
         self.vreg.init_registration([hooksdirectory])
@@ -201,12 +150,19 @@
         config = self.config
         self._available_pools = Queue.Queue()
         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()
-        elif config.bootstrap_schema:
-            # usually during repository creation
-            self.warning("set fs instance'schema as bootstrap schema")
+        if config.quick_start:
+            # quick start, usually only to get a minimal repository to get cubes
+            # information (eg dump/restore/...)
+            config._cubes = ()
+            # only load hooks and entity classes in the registry
+            config.cube_appobject_path = set(('hooks', 'entities'))
+            config.cubicweb_appobject_path = set(('hooks', 'entities'))
+            self.set_schema(config.load_schema())
+            config['connections-pool-size'] = 1
+            # will be reinitialized later from cubes found in the database
+            config._cubes = None
+        elif config.creating:
+            # repository creation
             config.bootstrap_cubes()
             self.set_schema(config.load_schema(), resetvreg=False)
             # need to load the Any and CWUser entity types
@@ -214,8 +170,11 @@
             self.vreg.init_registration([etdirectory])
             for modname in ('__init__', 'authobjs', 'wfobjs'):
                 self.vreg.load_file(join(etdirectory, '%s.py' % modname),
-                                'cubicweb.entities.%s' % modname)
-            self._boostrap_hook_registry()
+                                    'cubicweb.entities.%s' % modname)
+            self._bootstrap_hook_registry()
+        elif config.read_instance_schema:
+            # normal start: load the instance schema from the database
+            self.fill_schema()
         else:
             # test start: use the file system schema (quicker)
             self.warning("set fs instance'schema")
@@ -244,12 +203,9 @@
             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(config['session-time']/3., self.clean_sessions)
+        if config.quick_start:
+            config.init_cubes(self.get_cubes())
+        self.hm = hook.HooksManager(self.vreg)
 
     # internals ###############################################################
 
@@ -298,6 +254,12 @@
         self.set_schema(appschema)
 
     def start_looping_tasks(self):
+        if not (self.config.creating or self.config.repairing
+                or self.config.quick_start):
+            # 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)
         assert isinstance(self._looping_tasks, list), 'already started'
         for i, (interval, func, args) in enumerate(self._looping_tasks):
             self._looping_tasks[i] = task = utils.LoopTask(interval, func, args)
@@ -349,6 +311,7 @@
         """called on server stop event to properly close opened sessions and
         connections
         """
+        assert not self._shutting_down, 'already shutting down'
         self._shutting_down = True
         if isinstance(self._looping_tasks, tuple): # if tasks have been started
             for looptask in self._looping_tasks:
@@ -360,7 +323,9 @@
             self.info('waiting thread %s...', thread.name)
             thread.join()
             self.info('thread %s finished', thread.name)
-        self.hm.call_hooks('server_shutdown', repo=self)
+        if not (self.config.creating or self.config.repairing
+                or self.config.quick_start):
+            self.hm.call_hooks('server_shutdown', repo=self)
         self.close_sessions()
         while not self._available_pools.empty():
             pool = self._available_pools.get_nowait()
@@ -387,10 +352,15 @@
     def stats(self): # XXX restrict to managers session?
         import threading
         results = {}
-        for hits, misses, title in (
-            (self.querier.cache_hit, self.querier.cache_miss, 'rqlt_st'),
-            (self.system_source.cache_hit, self.system_source.cache_miss, 'sql'),
+        querier = self.querier
+        source = self.system_source
+        for size, maxsize, hits, misses, title in (
+            (len(querier._rql_cache), self.config['rql-cache-size'],
+            querier.cache_hit, querier.cache_miss, 'rqlt_st'),
+            (len(source._cache), self.config['rql-cache-size'],
+            source.cache_hit, source.cache_miss, 'sql'),
             ):
+            results['%s_cache_size' % title] =  '%s / %s' % (size, maxsize)
             results['%s_cache_hit' % title] =  hits
             results['%s_cache_miss' % title] = misses
             results['%s_cache_hit_percent' % title] = (hits * 100) / (hits + misses)
@@ -405,7 +375,8 @@
         session = self.internal_session()
         try:
             rset = session.execute('Any L WHERE U login L, U primary_email M, '
-                                   'M address %(login)s', {'login': login})
+                                   'M address %(login)s', {'login': login},
+                                   build_descr=False)
             if rset.rowcount == 1:
                 login = rset[0][0]
         finally:
@@ -466,6 +437,7 @@
         """
         versions = self.get_versions(not (self.config.creating
                                           or self.config.repairing
+                                          or self.config.quick_start
                                           or self.config.mode == 'test'))
         cubes = list(versions)
         cubes.remove('cubicweb')
@@ -525,22 +497,25 @@
         finally:
             session.close()
 
+    # XXX protect this method: anonymous should be allowed and registration
+    # plugged
     def register_user(self, login, password, email=None, **kwargs):
         """check a user with the given login exists, if not create it with the
         given password. This method is designed to be used for anonymous
         registration on public web site.
         """
-        # XXX should not be called from web interface
         session = self.internal_session()
         # for consistency, keep same error as unique check hook (although not required)
         errmsg = session._('the value "%s" is already used, use another one')
         try:
-            if (session.execute('CWUser X WHERE X login %(login)s', {'login': login})
+            if (session.execute('CWUser X WHERE X login %(login)s', {'login': login},
+                                build_descr=False)
                 or session.execute('CWUser X WHERE X use_email C, C address %(login)s',
-                                   {'login': login})):
-                raise ValidationError(None, {'login': errmsg % login})
+                                   {'login': login}, build_descr=False)):
+                qname = role_name('login', 'subject')
+                raise ValidationError(None, {qname: errmsg % login})
             # we have to create the user
-            user = self.vreg['etypes'].etype_class('CWUser')(session, None)
+            user = self.vreg['etypes'].etype_class('CWUser')(session)
             if isinstance(password, unicode):
                 # password should *always* be utf8 encoded
                 password = password.encode('UTF8')
@@ -552,10 +527,13 @@
                             {'x': user.eid})
             if email or '@' in login:
                 d = {'login': login, 'email': email or login}
-                if session.execute('EmailAddress X WHERE X address %(email)s', d):
-                    raise ValidationError(None, {'address': errmsg % d['email']})
+                if session.execute('EmailAddress X WHERE X address %(email)s', d,
+                                   build_descr=False):
+                    qname = role_name('address', 'subject')
+                    raise ValidationError(None, {qname: errmsg % d['email']})
                 session.execute('INSERT EmailAddress X: X address %(email)s, '
-                                'U primary_email X, U use_email X WHERE U login %(login)s', d)
+                                'U primary_email X, U use_email X '
+                                'WHERE U login %(login)s', d, build_descr=False)
             session.commit()
         finally:
             session.close()
@@ -615,7 +593,7 @@
                 raise
             except:
                 # FIXME: check error to catch internal errors
-                self.exception('unexpected error')
+                self.exception('unexpected error while executing %s with %s', rqlstring, args)
                 raise
         finally:
             session.reset_pool()
@@ -629,7 +607,7 @@
             session.reset_pool()
 
     def check_session(self, sessionid):
-        """raise `BadSessionId` if the connection is no more valid"""
+        """raise `BadConnectionId` if the connection is no more valid"""
         self._get_session(sessionid, setpool=False)
 
     def get_shared_data(self, sessionid, key, default=None, pop=False):
@@ -652,7 +630,7 @@
         """commit transaction for the session with the given id"""
         self.debug('begin commit for session %s', sessionid)
         try:
-            self._get_session(sessionid).commit()
+            return self._get_session(sessionid).commit()
         except (ValidationError, Unauthorized):
             raise
         except:
@@ -701,10 +679,42 @@
           custom properties)
         """
         session = self._get_session(sessionid, setpool=False)
-        # update session properties
         for prop, value in props.items():
             session.change_property(prop, value)
 
+    def undoable_transactions(self, sessionid, ueid=None, **actionfilters):
+        """See :class:`cubicweb.dbapi.Connection.undoable_transactions`"""
+        session = self._get_session(sessionid, setpool=True)
+        try:
+            return self.system_source.undoable_transactions(session, ueid,
+                                                            **actionfilters)
+        finally:
+            session.reset_pool()
+
+    def transaction_info(self, sessionid, txuuid):
+        """See :class:`cubicweb.dbapi.Connection.transaction_info`"""
+        session = self._get_session(sessionid, setpool=True)
+        try:
+            return self.system_source.tx_info(session, txuuid)
+        finally:
+            session.reset_pool()
+
+    def transaction_actions(self, sessionid, txuuid, public=True):
+        """See :class:`cubicweb.dbapi.Connection.transaction_actions`"""
+        session = self._get_session(sessionid, setpool=True)
+        try:
+            return self.system_source.tx_actions(session, txuuid, public)
+        finally:
+            session.reset_pool()
+
+    def undo_transaction(self, sessionid, txuuid):
+        """See :class:`cubicweb.dbapi.Connection.undo_transaction`"""
+        session = self._get_session(sessionid, setpool=True)
+        try:
+            return self.system_source.undo_transaction(session, txuuid)
+        finally:
+            session.reset_pool()
+
     # public (inter-repository) interface #####################################
 
     def entities_modified_since(self, etypes, mtime):
@@ -777,7 +787,6 @@
     # data sources handling ###################################################
     # * correspondance between eid and (type, source)
     # * correspondance between eid and local id (i.e. specific to a given source)
-    # * searchable text indexes
 
     def type_and_source_from_eid(self, eid, session=None):
         """return a tuple (type, source, extid) for the entity with id <eid>"""
@@ -904,78 +913,51 @@
         and index the entity with the full text index
         """
         # begin by inserting eid/type/source/extid into the entities table
-        self.system_source.add_info(session, entity, source, extid)
-        if complete:
-            entity.complete(entity.e_schema.indexable_attributes())
-        new = session.transaction_data.setdefault('neweids', set())
-        new.add(entity.eid)
-        # now we can update the full text index
-        if self.do_fti:
-            FTIndexEntityOp(session, entity=entity)
-        CleanupEidTypeCacheOp(session)
-
-    def delete_info(self, session, eid):
-        self._prepare_delete_info(session, eid)
-        self._delete_info(session, eid)
+        hook.set_operation(session, 'neweids', entity.eid,
+                           hook.CleanupNewEidsCacheOp)
+        self.system_source.add_info(session, entity, source, extid, complete)
 
-    def _prepare_delete_info(self, session, eid):
-        """prepare the repository for deletion of an entity:
-        * update the fti
-        * mark eid as being deleted in session info
-        * setup cache update operation
-        """
-        self.system_source.fti_unindex_entity(session, eid)
-        pending = session.transaction_data.setdefault('pendingeids', set())
-        pending.add(eid)
-        CleanupEidTypeCacheOp(session)
-
-    def _delete_info(self, session, eid):
-        """delete system information on deletion of an entity:
-        * delete all relations on this entity
-        * transfer record from the entities table to the deleted_entities table
+    def delete_info(self, session, entity, sourceuri, extid):
+        """called by external source when some entity known by the system source
+        has been deleted in the external source
         """
-        etype, uri, extid = self.type_and_source_from_eid(eid, session)
-        self._clear_eid_relations(session, etype, eid)
-        self.system_source.delete_info(session, eid, etype, uri, extid)
+        # mark eid as being deleted in session info and setup cache update
+        # operation
+        hook.set_operation(session, 'pendingeids', entity.eid,
+                           hook.CleanupDeletedEidsCacheOp)
+        self._delete_info(session, entity, sourceuri, extid)
 
-    def _clear_eid_relations(self, session, etype, eid):
-        """when a entity is deleted, build and execute rql query to delete all
-        its relations
+    def _delete_info(self, session, entity, sourceuri, extid):
+                     # attributes=None, relations=None):
+        """delete system information on deletion of an entity:
+        * delete all remaining relations from/to this entity
+        * call delete info on the system source which will transfer record from
+          the entities table to the deleted_entities table
         """
-        rql = []
-        eschema = self.schema.eschema(etype)
         pendingrtypes = session.transaction_data.get('pendingrtypes', ())
-        for rschema, targetschemas, x in eschema.relation_definitions():
-            rtype = rschema.type
-            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
-                selection = 'X %s %s' % (rtype, var)
-            else:
-                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"""
-        alreadydone = session.transaction_data.setdefault('indexedeids', set())
-        if entity.eid in alreadydone:
-            self.debug('skipping reindexation of %s, already done', entity.eid)
-            return
-        alreadydone.add(entity.eid)
-        self.system_source.fti_index_entity(session, entity)
+        # delete remaining relations: if user can delete the entity, he can
+        # delete all its relations without security checking
+        with security_enabled(session, read=False, write=False):
+            eid = entity.eid
+            for rschema, _, role in entity.e_schema.relation_definitions():
+                rtype = rschema.type
+                if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
+                    continue
+                if role == 'subject':
+                    # don't skip inlined relation so they are regularly
+                    # deleted and so hooks are correctly called
+                    rql = 'DELETE X %s Y WHERE X eid %%(x)s' % rtype
+                else:
+                    rql = 'DELETE Y %s X WHERE X eid %%(x)s' % rtype
+                session.execute(rql, {'x': eid}, 'x', build_descr=False)
+        self.system_source.delete_info(session, entity, sourceuri, extid)
 
     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:
             source = self.system_source
-            if not (subjsource.may_cross_relation(rtype) 
+            if not (subjsource.may_cross_relation(rtype)
                     and objsource.may_cross_relation(rtype)):
                 raise MultiSourcesError(
                     "relation %s can't be crossed among sources"
@@ -997,6 +979,20 @@
         else:
             raise ETypeNotSupportedBySources(etype)
 
+    def init_entity_caches(self, session, entity, source):
+        """add entity to session entities cache and repo's extid cache.
+        Return entity's ext id if the source isn't the system source.
+        """
+        session.set_entity_cache(entity)
+        suri = source.uri
+        if suri == 'system':
+            extid = None
+        else:
+            extid = source.get_extid(entity)
+            self._extid_cache[(str(extid), suri)] = entity.eid
+        self._type_source_cache[entity.eid] = (entity.__regid__, suri, extid)
+        return extid
+
     def glob_add_entity(self, session, entity):
         """add an entity to the repository
 
@@ -1006,35 +1002,36 @@
         # init edited_attributes before calling before_add_entity hooks
         entity._is_saved = False # entity has an eid but is not yet saved
         entity.edited_attributes = set(entity)
-        entity = entity.pre_add_hook()
+        entity_ = entity.pre_add_hook()
+        # XXX kill that transmutation feature !
+        if not entity_ is entity:
+            entity.__class__ = entity_.__class__
+            entity.__dict__.update(entity_.__dict__)
         eschema = entity.e_schema
-        etype = str(eschema)
-        source = self.locate_etype_source(etype)
-        # attribute an eid to the entity before calling hooks
+        source = self.locate_etype_source(entity.__regid__)
+        # allocate an eid to the entity before calling hooks
         entity.set_eid(self.system_source.create_eid(session))
+        # set caches asap
+        extid = self.init_entity_caches(session, entity, source)
         if server.DEBUG & server.DBG_REPO:
-            print 'ADD entity', etype, entity.eid, dict(entity)
+            print 'ADD entity', entity.__regid__, entity.eid, dict(entity)
         relations = []
         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():
+        # inline relations XXX not true, right? (see edited_attributes
+        # affectation above)
+        for attr in entity.iterkeys():
             rschema = eschema.subjrels[attr]
             if not rschema.final: # inlined relation
                 relations.append((attr, entity[attr]))
         entity.set_defaults()
-        entity.check(creation=True)
+        if session.is_hook_category_activated('integrity'):
+            entity.check(creation=True)
         source.add_entity(session, entity)
-        if source.uri != 'system':
-            extid = source.get_extid(entity)
-            self._extid_cache[(str(extid), source.uri)] = entity.eid
-        else:
-            extid = None
         self.add_info(session, entity, source, extid, complete=False)
         entity._is_saved = True # entity has an eid and is saved
         # prefill entity relation caches
-        session.set_entity_cache(entity)
         for rschema in eschema.subject_relations():
             rtype = str(rschema)
             if rtype in schema.VIRTUAL_RTYPES:
@@ -1066,85 +1063,83 @@
         """replace an entity in the repository
         the type and the eid of an entity must not be changed
         """
-        etype = str(entity.e_schema)
         if server.DEBUG & server.DBG_REPO:
-            print 'UPDATE entity', etype, entity.eid, \
+            print 'UPDATE entity', entity.__regid__, entity.eid, \
                   dict(entity), edited_attributes
-        entity.edited_attributes = edited_attributes
-        entity.check()
+        hm = self.hm
         eschema = entity.e_schema
         session.set_entity_cache(entity)
-        only_inline_rels, need_fti_update = True, False
-        relations = []
-        for attr in edited_attributes:
-            if attr == 'eid':
-                continue
-            rschema = eschema.subjrels[attr]
-            if rschema.final:
-                if getattr(eschema.rdef(attr), 'fulltextindexed', False):
-                    need_fti_update = True
-                only_inline_rels = False
-            else:
-                # inlined relation
-                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
+        orig_edited_attributes = getattr(entity, 'edited_attributes', None)
+        entity.edited_attributes = edited_attributes
+        try:
+            if session.is_hook_category_activated('integrity'):
+                entity.check()
+            only_inline_rels, need_fti_update = True, False
+            relations = []
+            for attr in list(edited_attributes):
+                if attr == 'eid':
+                    continue
+                rschema = eschema.subjrels[attr]
+                if rschema.final:
+                    if getattr(eschema.rdef(attr), 'fulltextindexed', False):
+                        need_fti_update = True
+                    only_inline_rels = False
+                else:
+                    # inlined relation
+                    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:
+                            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:
+                        hm.call_hooks('before_add_relation', session,
+                                      eidfrom=entity.eid, rtype=attr, eidto=value)
+                    if not only_inline_rels:
+                        hm.call_hooks('before_update_entity', session, entity=entity)
+                source.update_entity(session, entity)
+            self.system_source.update_info(session, entity, need_fti_update)
+            if source.should_call_hooks:
+                if not only_inline_rels:
+                    hm.call_hooks('after_update_entity', session, entity=entity)
+                for attr, value, prevvalue in relations:
+                    # if the relation is already cached, update existant cache
+                    relcache = entity.relation_cached(attr, 'subject')
+                    if prevvalue is not None:
+                        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)
+                    if relcache is not None:
+                        session.update_rel_cache_add(entity.eid, attr, value)
                     else:
-                        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', session,
-                                    eidfrom=entity.eid, rtype=attr, eidto=value)
-            if not only_inline_rels:
-                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:
-                # reindex the entity only if this query is updating at least
-                # one indexable attribute
-                FTIndexEntityOp(session, entity=entity)
-            if source.should_call_hooks:
-                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 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)
-                if relcache is not None:
-                    session.update_rel_cache_add(entity.eid, attr, value)
-                else:
-                    entity.set_related_cache(attr, 'subject',
-                                             session.eid_rset(value))
-                self.hm.call_hooks('after_add_relation', session,
-                                    eidfrom=entity.eid, rtype=attr, eidto=value)
+                        entity.set_related_cache(attr, 'subject',
+                                                 session.eid_rset(value))
+                    hm.call_hooks('after_add_relation', session,
+                                  eidfrom=entity.eid, rtype=attr, eidto=value)
+        finally:
+            if orig_edited_attributes is not None:
+                entity.edited_attributes = orig_edited_attributes
 
     def glob_delete_entity(self, session, eid):
         """delete an entity and all related entities from the repository"""
-        # call delete_info before hooks
-        self._prepare_delete_info(session, eid)
-        etype, uri, extid = self.type_and_source_from_eid(eid, session)
+        entity = session.entity_from_eid(eid)
+        etype, sourceuri, extid = self.type_and_source_from_eid(eid, session)
         if server.DEBUG & server.DBG_REPO:
             print 'DELETE entity', etype, eid
-            if eid == 937:
-                server.DEBUG |= (server.DBG_SQL | server.DBG_RQL | server.DBG_MORE)
-        source = self.sources_by_uri[uri]
+        source = self.sources_by_uri[sourceuri]
         if source.should_call_hooks:
-            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)
+        self._delete_info(session, entity, sourceuri, extid)
+        source.delete_entity(session, entity)
         if source.should_call_hooks:
             self.hm.call_hooks('after_delete_entity', session, entity=entity)
         # don't clear cache here this is done in a hook on commit
--- a/server/schemaserial.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/schemaserial.py	Tue Apr 06 19:46:38 2010 +0200
@@ -50,6 +50,10 @@
                     continue
     return res
 
+def cstrtype_mapping(cursor):
+    """cached constraint types mapping"""
+    return dict(cursor.execute('Any T, X WHERE X is CWConstraintType, X name T'))
+
 # schema / perms deserialization ##############################################
 
 def deserialize_schema(schema, session):
@@ -214,7 +218,7 @@
     if not quiet:
         _title = '-> storing the schema in the database '
         print _title,
-    execute = cursor.unsafe_execute
+    execute = cursor.execute
     eschemas = schema.entities()
     if not quiet:
         pb_size = (len(eschemas + schema.relations())
@@ -229,14 +233,15 @@
     eschemas.remove(schema.eschema('CWEType'))
     eschemas.insert(0, schema.eschema('CWEType'))
     for eschema in eschemas:
-        for rql, kwargs in eschema2rql(eschema, groupmap):
-            execute(rql, kwargs, build_descr=False)
+        execschemarql(execute, eschema, eschema2rql(eschema, groupmap))
         if pb is not None:
             pb.update()
     # serialize constraint types
+    cstrtypemap = {}
     rql = 'INSERT CWConstraintType X: X name %(ct)s'
     for cstrtype in CONSTRAINTS:
-        execute(rql, {'ct': unicode(cstrtype)}, build_descr=False)
+        cstrtypemap[cstrtype] = execute(rql, {'ct': unicode(cstrtype)},
+                                        build_descr=False)[0][0]
         if pb is not None:
             pb.update()
     # serialize relations
@@ -246,8 +251,15 @@
             if pb is not None:
                 pb.update()
             continue
-        for rql, kwargs in rschema2rql(rschema, groupmap=groupmap):
-            execute(rql, kwargs, build_descr=False)
+        execschemarql(execute, rschema, rschema2rql(rschema, addrdef=False))
+        if rschema.symmetric:
+            rdefs = [rdef for k, rdef in rschema.rdefs.iteritems()
+                     if (rdef.subject, rdef.object) == k]
+        else:
+            rdefs = rschema.rdefs.itervalues()
+        for rdef in rdefs:
+            execschemarql(execute, rdef,
+                          rdef2rql(rdef, cstrtypemap, groupmap))
         if pb is not None:
             pb.update()
     for rql, kwargs in specialize2rql(schema):
@@ -258,6 +270,55 @@
         print
 
 
+# high level serialization functions
+
+def execschemarql(execute, schema, rqls):
+    for rql, kwargs in rqls:
+        kwargs['x'] = schema.eid
+        rset = execute(rql, kwargs, build_descr=False)
+        if schema.eid is None:
+            schema.eid = rset[0][0]
+        else:
+            assert rset
+
+def erschema2rql(erschema, groupmap):
+    if isinstance(erschema, schemamod.EntitySchema):
+        return eschema2rql(erschema, groupmap=groupmap)
+    return rschema2rql(erschema, groupmap=groupmap)
+
+def specialize2rql(schema):
+    for eschema in schema.entities():
+        if eschema.final:
+            continue
+        for rql, kwargs in eschemaspecialize2rql(eschema):
+            yield rql, kwargs
+
+# etype serialization
+
+def eschema2rql(eschema, groupmap=None):
+    """return a list of rql insert statements to enter an entity schema
+    in the database as an CWEType entity
+    """
+    relations, values = eschema_relations_values(eschema)
+    # NOTE: 'specializes' relation can't be inserted here since there's no
+    # way to make sure the parent type is inserted before the child type
+    yield 'INSERT CWEType X: %s' % ','.join(relations) , values
+    # entity permissions
+    if groupmap is not None:
+        for rql, args in _erperms2rql(eschema, groupmap):
+            yield rql, args
+
+def eschema_relations_values(eschema):
+    values = _ervalues(eschema)
+    relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
+    return relations, values
+
+def eschemaspecialize2rql(eschema):
+    specialized_type = eschema.specializes()
+    if specialized_type:
+        values = {'x': eschema.eid, 'et': specialized_type.eid}
+        yield 'SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', values
+
 def _ervalues(erschema):
     try:
         type_ = unicode(erschema.type)
@@ -273,10 +334,23 @@
         'description': desc,
         }
 
-def eschema_relations_values(eschema):
-    values = _ervalues(eschema)
-    relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
-    return relations, values
+# rtype serialization
+
+def rschema2rql(rschema, cstrtypemap=None, addrdef=True, groupmap=None):
+    """return a list of rql insert statements to enter a relation schema
+    in the database as an CWRType entity
+    """
+    if rschema.type == 'has_text':
+        return
+    relations, values = rschema_relations_values(rschema)
+    yield 'INSERT CWRType X: %s' % ','.join(relations), values
+    if addrdef:
+        assert cstrtypemap
+        # sort for testing purpose
+        for rdef in sorted(rschema.rdefs.itervalues(),
+                           key=lambda x: (x.subject, x.object)):
+            for rql, values in rdef2rql(rdef, cstrtypemap, groupmap):
+                yield rql, values
 
 def rschema_relations_values(rschema):
     values = _ervalues(rschema)
@@ -290,169 +364,58 @@
     relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
     return relations, values
 
-def _rdef_values(objtype, props):
-    amap = {'order': 'ordernum'}
+# rdef serialization
+
+def rdef2rql(rdef, cstrtypemap, groupmap=None):
+    # don't serialize infered relations
+    if rdef.infered:
+        return
+    relations, values = _rdef_values(rdef)
+    relations.append('X relation_type ER,X from_entity SE,X to_entity OE')
+    values.update({'se': rdef.subject.eid, 'rt': rdef.rtype.eid, 'oe': rdef.object.eid})
+    if rdef.final:
+        etype = 'CWAttribute'
+    else:
+        etype = 'CWRelation'
+    yield 'INSERT %s X: %s WHERE SE eid %%(se)s,ER eid %%(rt)s,OE eid %%(oe)s' % (
+        etype, ','.join(relations), ), values
+    for rql, values in constraints2rql(cstrtypemap, rdef.constraints):
+        yield rql, values
+    # no groupmap means "no security insertion"
+    if groupmap:
+        for rql, args in _erperms2rql(rdef, groupmap):
+            yield rql, args
+
+def _rdef_values(rdef):
+    amap = {'order': 'ordernum', 'default': 'defaultval'}
     values = {}
-    for prop, default in schemamod.RelationDefinitionSchema.rproperty_defs(objtype).iteritems():
+    for prop, default in rdef.rproperty_defs(rdef.object).iteritems():
         if prop in ('eid', 'constraints', 'uid', 'infered', 'permissions'):
             continue
-        value = props.get(prop, default)
+        value = getattr(rdef, prop)
+        # XXX type cast really necessary?
         if prop in ('indexed', 'fulltextindexed', 'internationalizable'):
             value = bool(value)
         elif prop == 'ordernum':
             value = int(value)
         elif isinstance(value, str):
             value = unicode(value)
+        if value is not None and prop == 'default':
+            if value is False:
+                value = u''
+            if not isinstance(value, unicode):
+                value = unicode(value)
         values[amap.get(prop, prop)] = value
-    return values
-
-def nfrdef_relations_values(objtype, props):
-    values = _rdef_values(objtype, props)
-    relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
-    return relations, values
-
-def frdef_relations_values(objtype, props):
-    values = _rdef_values(objtype, props)
-    default = values['default']
-    del values['default']
-    if default is not None:
-        if default is False:
-            default = u''
-        elif not isinstance(default, unicode):
-            default = unicode(default)
-    values['defaultval'] = default
     relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
     return relations, values
 
-
-def __rdef2rql(genmap, rschema, subjtype=None, objtype=None, props=None,
-               groupmap=None):
-    if subjtype is None:
-        assert objtype is None
-        assert props is None
-        targets = sorted(rschema.rdefs)
-    else:
-        assert not objtype is None
-        targets = [(subjtype, objtype)]
-    # relation schema
-    if rschema.final:
-        etype = 'CWAttribute'
-    else:
-        etype = 'CWRelation'
-    for subjtype, objtype in targets:
-        if props is None:
-            _props = rschema.rdef(subjtype, objtype)
-        else:
-            _props = props
-        # don't serialize infered relations
-        if _props.get('infered'):
-            continue
-        gen = genmap[rschema.final]
-        for rql, values in gen(rschema, subjtype, objtype, _props):
-            yield rql, values
-        # no groupmap means "no security insertion"
-        if groupmap:
-            for rql, args in _erperms2rql(_props, groupmap):
-                args['st'] = str(subjtype)
-                args['rt'] = str(rschema)
-                args['ot'] = str(objtype)
-                yield rql + 'X is %s, X from_entity ST, X to_entity OT, '\
-                      'X relation_type RT, RT name %%(rt)s, ST name %%(st)s, '\
-                      'OT name %%(ot)s' % etype, args
-
-
-def schema2rql(schema, skip=None, allow=None):
-    """return a list of rql insert statements to enter the schema in the
-    database as CWRType and CWEType entities
-    """
-    assert not (skip is not None and allow is not None), \
-           'can\'t use both skip and allow'
-    all = schema.entities() + schema.relations()
-    if skip is not None:
-        return chain(*[erschema2rql(schema[t]) for t in all if not t in skip])
-    elif allow is not None:
-        return chain(*[erschema2rql(schema[t]) for t in all if t in allow])
-    return chain(*[erschema2rql(schema[t]) for t in all])
-
-def erschema2rql(erschema, groupmap):
-    if isinstance(erschema, schemamod.EntitySchema):
-        return eschema2rql(erschema, groupmap=groupmap)
-    return rschema2rql(erschema, groupmap=groupmap)
-
-def eschema2rql(eschema, groupmap=None):
-    """return a list of rql insert statements to enter an entity schema
-    in the database as an CWEType entity
-    """
-    relations, values = eschema_relations_values(eschema)
-    # NOTE: 'specializes' relation can't be inserted here since there's no
-    # way to make sure the parent type is inserted before the child type
-    yield 'INSERT CWEType X: %s' % ','.join(relations) , values
-    # entity permissions
-    if groupmap is not None:
-        for rql, args in _erperms2rql(eschema, groupmap):
-            args['name'] = str(eschema)
-            yield rql + 'X is CWEType, X name %(name)s', args
-
-def specialize2rql(schema):
-    for eschema in schema.entities():
-        for rql, kwargs in eschemaspecialize2rql(eschema):
-            yield rql, kwargs
-
-def eschemaspecialize2rql(eschema):
-    specialized_type = eschema.specializes()
-    if specialized_type:
-        values = {'x': eschema.type, 'et': specialized_type.type}
-        yield 'SET X specializes ET WHERE X name %(x)s, ET name %(et)s', values
-
-def rschema2rql(rschema, addrdef=True, groupmap=None):
-    """return a list of rql insert statements to enter a relation schema
-    in the database as an CWRType entity
-    """
-    if rschema.type == 'has_text':
-        return
-    relations, values = rschema_relations_values(rschema)
-    yield 'INSERT CWRType X: %s' % ','.join(relations), values
-    if addrdef:
-        for rql, values in rdef2rql(rschema, groupmap=groupmap):
-            yield rql, values
-
-def rdef2rql(rschema, subjtype=None, objtype=None, props=None, groupmap=None):
-    genmap = {True: frdef2rql, False: nfrdef2rql}
-    return __rdef2rql(genmap, rschema, subjtype, objtype, props, groupmap)
-
-
-_LOCATE_RDEF_RQL0 = 'X relation_type ER,X from_entity SE,X to_entity OE'
-_LOCATE_RDEF_RQL1 = 'SE name %(se)s,ER name %(rt)s,OE name %(oe)s'
-
-def frdef2rql(rschema, subjtype, objtype, props):
-    relations, values = frdef_relations_values(objtype, props)
-    relations.append(_LOCATE_RDEF_RQL0)
-    values.update({'se': str(subjtype), 'rt': str(rschema), 'oe': str(objtype)})
-    yield 'INSERT CWAttribute X: %s WHERE %s' % (','.join(relations), _LOCATE_RDEF_RQL1), values
-    for rql, values in rdefrelations2rql(rschema, subjtype, objtype, props):
-        yield rql + ', EDEF is CWAttribute', values
-
-def nfrdef2rql(rschema, subjtype, objtype, props):
-    relations, values = nfrdef_relations_values(objtype, props)
-    relations.append(_LOCATE_RDEF_RQL0)
-    values.update({'se': str(subjtype), 'rt': str(rschema), 'oe': str(objtype)})
-    yield 'INSERT CWRelation X: %s WHERE %s' % (','.join(relations), _LOCATE_RDEF_RQL1), values
-    for rql, values in rdefrelations2rql(rschema, subjtype, objtype, props):
-        yield rql + ', EDEF is CWRelation', values
-
-def rdefrelations2rql(rschema, subjtype, objtype, props):
-    iterators = []
-    for constraint in props.constraints:
-        iterators.append(constraint2rql(rschema, subjtype, objtype, constraint))
-    return chain(*iterators)
-
-def constraint2rql(rschema, subjtype, objtype, constraint):
-    values = {'ctname': unicode(constraint.type()),
-              'value': unicode(constraint.serialize()),
-              'rt': str(rschema), 'se': str(subjtype), 'oe': str(objtype)}
-    yield 'INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE \
-CT name %(ctname)s, EDEF relation_type ER, EDEF from_entity SE, EDEF to_entity OE, \
-ER name %(rt)s, SE name %(se)s, OE name %(oe)s', values
+def constraints2rql(cstrtypemap, constraints, rdefeid=None):
+    for constraint in constraints:
+        values = {'ct': cstrtypemap[constraint.type()],
+                  'value': unicode(constraint.serialize()),
+                  'x': rdefeid} # when not specified, will have to be set by the caller
+        yield 'INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE \
+CT eid %(ct)s, EDEF eid %(x)s', values
 
 
 def _erperms2rql(erschema, groupmap):
@@ -471,7 +434,7 @@
             if isinstance(group_or_rqlexpr, basestring):
                 # group
                 try:
-                    yield ('SET X %s_permission Y WHERE Y eid %%(g)s, ' % action,
+                    yield ('SET X %s_permission Y WHERE Y eid %%(g)s, X eid %%(x)s' % action,
                            {'g': groupmap[group_or_rqlexpr]})
                 except KeyError:
                     continue
@@ -479,36 +442,24 @@
                 # rqlexpr
                 rqlexpr = group_or_rqlexpr
                 yield ('INSERT RQLExpression E: E expression %%(e)s, E exprtype %%(t)s, '
-                       'E mainvars %%(v)s, X %s_permission E WHERE ' % action,
+                       'E mainvars %%(v)s, X %s_permission E WHERE X eid %%(x)s' % action,
                        {'e': unicode(rqlexpr.expression),
                         'v': unicode(rqlexpr.mainvars),
                         't': unicode(rqlexpr.__class__.__name__)})
 
+# update functions
 
-def updateeschema2rql(eschema):
+def updateeschema2rql(eschema, eid):
     relations, values = eschema_relations_values(eschema)
-    values['et'] = eschema.type
-    yield 'SET %s WHERE X is CWEType, X name %%(et)s' % ','.join(relations), values
-
-def updaterschema2rql(rschema):
-    relations, values = rschema_relations_values(rschema)
-    values['rt'] = rschema.type
-    yield 'SET %s WHERE X is CWRType, X name %%(rt)s' % ','.join(relations), values
+    values['x'] = eid
+    yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values
 
-def updaterdef2rql(rschema, subjtype=None, objtype=None, props=None):
-    genmap = {True: updatefrdef2rql, False: updatenfrdef2rql}
-    return __rdef2rql(genmap, rschema, subjtype, objtype, props)
+def updaterschema2rql(rschema, eid):
+    relations, values = rschema_relations_values(rschema)
+    values['x'] = eid
+    yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values
 
-def updatefrdef2rql(rschema, subjtype, objtype, props):
-    relations, values = frdef_relations_values(objtype, props)
-    values.update({'se': subjtype, 'rt': str(rschema), 'oe': objtype})
-    yield 'SET %s WHERE %s, %s, X is CWAttribute' % (','.join(relations),
-                                                     _LOCATE_RDEF_RQL0,
-                                                     _LOCATE_RDEF_RQL1), values
-
-def updatenfrdef2rql(rschema, subjtype, objtype, props):
-    relations, values = nfrdef_relations_values(objtype, props)
-    values.update({'se': subjtype, 'rt': str(rschema), 'oe': objtype})
-    yield 'SET %s WHERE %s, %s, X is CWRelation' % (','.join(relations),
-                                                    _LOCATE_RDEF_RQL0,
-                                                    _LOCATE_RDEF_RQL1), values
+def updaterdef2rql(rdef, eid):
+    relations, values = _rdef_values(rdef)
+    values['x'] = eid
+    yield 'SET %s WHERE X eid %%(x)s' % ','.join(relations), values
--- a/server/server.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/server.py	Tue Apr 06 19:46:38 2010 +0200
@@ -90,6 +90,7 @@
 
     def run(self, req_timeout=5.0):
         """enter the service loop"""
+        self.repo.start_looping_tasks()
         while self.quiting is None:
             try:
                 self.daemon.handleRequests(req_timeout)
--- a/server/serverconfig.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/serverconfig.py	Tue Apr 06 19:46:38 2010 +0200
@@ -127,6 +127,28 @@
           'help': 'size of the parsed rql cache size.',
           'group': 'main', 'inputlevel': 1,
           }),
+        ('undo-support',
+         {'type' : 'string', 'default': '',
+          'help': 'string defining actions that will have undo support: \
+[C]reate [U]pdate [D]elete entities / [A]dd [R]emove relation. Leave it empty \
+for no undo support, set it to CUDAR for full undo support, or to DR for \
+support undoing of deletion only.',
+          'group': 'main', 'inputlevel': 1,
+          }),
+        ('keep-transaction-lifetime',
+         {'type' : 'int', 'default': 7,
+          'help': 'number of days during which transaction records should be \
+kept (hence undoable).',
+          'group': 'main', 'inputlevel': 1,
+          }),
+        ('multi-sources-etypes',
+         {'type' : 'csv', 'default': (),
+          'help': 'defines which entity types from this repository are used \
+by some other instances. You should set this properly so those instances to \
+detect updates / deletions.',
+          'group': 'main', 'inputlevel': 1,
+          }),
+
         ('delay-full-text-indexation',
          {'type' : 'yn', 'default': False,
           'help': 'When full text indexation of entity has a too important cost'
@@ -180,68 +202,14 @@
 
     # read the schema from the database
     read_instance_schema = True
-    bootstrap_schema = True
-
+    # set to true while creating an instance
+    creating = False
+    # set this to true to get a minimal repository, for instance to get cubes
+    # information on commands such as i18ninstance, db-restore, etc...
+    quick_start = False
     # check user's state at login time
     consider_user_state = True
 
-    # XXX hooks control stuff should probably be on the session, not on the config
-
-    # hooks activation configuration
-    # all hooks should be activated during normal execution
-    disabled_hooks_categories = set()
-    enabled_hooks_categories = set()
-    ALLOW_ALL = object()
-    DENY_ALL = object()
-    hooks_mode = ALLOW_ALL
-
-    @classmethod
-    def set_hooks_mode(cls, mode):
-        assert mode is cls.ALLOW_ALL or mode is cls.DENY_ALL
-        oldmode = cls.hooks_mode
-        cls.hooks_mode = mode
-        return oldmode
-
-    @classmethod
-    def disable_hook_category(cls, *categories):
-        changes = set()
-        if cls.hooks_mode is cls.DENY_ALL:
-            for category in categories:
-                if category in cls.enabled_hooks_categories:
-                    cls.enabled_hooks_categories.remove(category)
-                    changes.add(category)
-        else:
-            for category in categories:
-                if category not in cls.disabled_hooks_categories:
-                    cls.disabled_hooks_categories.add(category)
-                    changes.add(category)
-        return changes
-
-    @classmethod
-    def enable_hook_category(cls, *categories):
-        changes = set()
-        if cls.hooks_mode is cls.DENY_ALL:
-            for category in categories:
-                if category not in cls.enabled_hooks_categories:
-                    cls.enabled_hooks_categories.add(category)
-                    changes.add(category)
-        else:
-            for category in categories:
-                if category in cls.disabled_hooks_categories:
-                    cls.disabled_hooks_categories.remove(category)
-                    changes.add(category)
-        return changes
-
-    @classmethod
-    def is_hook_activated(cls, hook):
-        return cls.is_hook_category_activated(hook.category)
-
-    @classmethod
-    def is_hook_category_activated(cls, category):
-        if cls.hooks_mode is cls.DENY_ALL:
-            return category in cls.enabled_hooks_categories
-        return category not in cls.disabled_hooks_categories
-
     # should some hooks be deactivated during [pre|post]create script execution
     free_wheel = False
 
--- a/server/serverctl.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/serverctl.py	Tue Apr 06 19:46:38 2010 +0200
@@ -63,9 +63,17 @@
             password = getpass('password: ')
     extra_args = source.get('db-extra-arguments')
     extra = extra_args and {'extra_args': extra_args} or {}
-    return get_connection(driver, dbhost, dbname, user, password=password,
-                          port=source.get('db-port'),
-                          **extra)
+    cnx = get_connection(driver, dbhost, dbname, user, password=password,
+                         port=source.get('db-port'),
+                         **extra)
+    try:
+        cnx.logged_user = user
+    except AttributeError:
+        # C object, __slots__
+        from logilab.database import _SimpleConnectionWrapper
+        cnx = _SimpleConnectionWrapper(cnx)
+        cnx.logged_user = user
+    return cnx
 
 def system_source_cnx(source, dbms_system_base=False,
                       special_privs='CREATE/DROP DATABASE', verbose=True):
@@ -75,8 +83,8 @@
     create/drop the instance database)
     """
     if dbms_system_base:
-        from logilab.common.adbh import get_adv_func_helper
-        system_db = get_adv_func_helper(source['db-driver']).system_database()
+        from logilab.database import get_db_helper
+        system_db = get_db_helper(source['db-driver']).system_database()
         return source_cnx(source, system_db, special_privs=special_privs, verbose=verbose)
     return source_cnx(source, special_privs=special_privs, verbose=verbose)
 
@@ -85,11 +93,11 @@
     or a database
     """
     import logilab.common as lgp
-    from logilab.common.adbh import get_adv_func_helper
+    from logilab.database import get_db_helper
     lgp.USE_MX_DATETIME = False
     special_privs = ''
     driver = source['db-driver']
-    helper = get_adv_func_helper(driver)
+    helper = get_db_helper(driver)
     if user is not None and helper.users_support:
         special_privs += '%s USER' % what
     if db is not None:
@@ -202,10 +210,10 @@
 
     def cleanup(self):
         """remove instance's configuration and database"""
-        from logilab.common.adbh import get_adv_func_helper
+        from logilab.database import get_db_helper
         source = self.config.sources()['system']
         dbname = source['db-name']
-        helper = get_adv_func_helper(source['db-driver'])
+        helper = get_db_helper(source['db-driver'])
         if ASK.confirm('Delete database %s ?' % dbname):
             user = source['db-user'] or None
             cnx = _db_sys_cnx(source, 'DROP DATABASE', user=user)
@@ -285,8 +293,7 @@
         )
     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
+        from logilab.database import get_db_helper
         verbose = self.get('verbose')
         automatic = self.get('automatic')
         appid = pop_arg(args, msg='No instance specified !')
@@ -295,7 +302,7 @@
         dbname = source['db-name']
         driver = source['db-driver']
         create_db = self.config.create_db
-        helper = get_adv_func_helper(driver)
+        helper = get_db_helper(driver)
         if driver == 'sqlite':
             if os.path.exists(dbname) and automatic or \
                    ASK.confirm('Database %s already exists -- do you want to drop it ?' % dbname):
@@ -322,7 +329,7 @@
                                            source['db-encoding'])
                 else:
                     helper.create_database(cursor, dbname,
-                                           encoding=source['db-encoding'])
+                                           dbencoding=source['db-encoding'])
                 dbcnx.commit()
                 print '-> database %s created.' % dbname
             except:
@@ -330,8 +337,7 @@
                 raise
         cnx = system_source_cnx(source, special_privs='LANGUAGE C', verbose=verbose)
         cursor = cnx.cursor()
-        indexer = get_indexer(driver)
-        indexer.init_extensions(cursor)
+        helper.init_fti_extensions(cursor)
         # postgres specific stuff
         if driver == 'postgres':
             # install plpythonu/plpgsql language if not installed by the cube
@@ -383,7 +389,7 @@
             get_connection(
                 system['db-driver'], database=system['db-name'],
                 host=system.get('db-host'), port=system.get('db-port'),
-                user=system.get('db-user'), password=system.get('db-password'), 
+                user=system.get('db-user'), password=system.get('db-password'),
                 **extra)
         except Exception, ex:
             raise ConfigurationError(
@@ -566,17 +572,16 @@
 
 def _local_dump(appid, output):
     config = ServerConfiguration.config_for(appid)
-    # schema=1 to avoid unnecessary schema loading
-    mih = config.migration_handler(connect=False, schema=1, verbosity=1)
+    config.quick_start = True
+    mih = config.migration_handler(connect=False, verbosity=1)
     mih.backup_database(output, askconfirm=False)
     mih.shutdown()
 
 def _local_restore(appid, backupfile, drop, systemonly=True):
     config = ServerConfiguration.config_for(appid)
     config.verbosity = 1 # else we won't be asked for confirmation on problems
-    config.repairing = 1 # don't check versions
-    # schema=1 to avoid unnecessary schema loading
-    mih = config.migration_handler(connect=False, schema=1, verbosity=1)
+    config.quick_start = True
+    mih = config.migration_handler(connect=False, verbosity=1)
     mih.restore_database(backupfile, drop, systemonly, askconfirm=False)
     repo = mih.repo_connect()
     # version of the database
--- a/server/session.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/session.py	Tue Apr 06 19:46:38 2010 +0200
@@ -5,24 +5,36 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
 import sys
 import threading
 from time import time
+from uuid import uuid4
 
 from logilab.common.deprecation import deprecated
 from rql.nodes import VariableRef, Function, ETYPE_PYOBJ_MAP, etype_from_pyobj
 from yams import BASE_TYPES
 
-from cubicweb import Binary, UnknownEid
+from cubicweb import Binary, UnknownEid, schema
 from cubicweb.req import RequestSessionBase
 from cubicweb.dbapi import ConnectionProperties
-from cubicweb.utils import make_uid
+from cubicweb.utils import make_uid, RepeatList
 from cubicweb.rqlrewrite import RQLRewriter
 
 ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
 
+NO_UNDO_TYPES = schema.SCHEMA_TYPES.copy()
+NO_UNDO_TYPES.add('CWCache')
+# is / is_instance_of are usually added by sql hooks except when using
+# dataimport.NoHookRQLObjectStore, and we don't want to record them
+# anyway in the later case
+NO_UNDO_TYPES.add('is')
+NO_UNDO_TYPES.add('is_instance_of')
+# XXX rememberme,forgotpwd,apycot,vcsfile
+
 def is_final(rqlst, variable, args):
     # try to find if this is a final var or not
     for select in rqlst.children:
@@ -42,10 +54,73 @@
     return description
 
 
+class hooks_control(object):
+    """context manager to control activated hooks categories.
+
+    If mode is session.`HOOKS_DENY_ALL`, given hooks categories will
+    be enabled.
+
+    If mode is session.`HOOKS_ALLOW_ALL`, given hooks categories will
+    be disabled.
+    """
+    def __init__(self, session, mode, *categories):
+        self.session = session
+        self.mode = mode
+        self.categories = categories
+
+    def __enter__(self):
+        self.oldmode = self.session.set_hooks_mode(self.mode)
+        if self.mode is self.session.HOOKS_DENY_ALL:
+            self.changes = self.session.enable_hook_categories(*self.categories)
+        else:
+            self.changes = self.session.disable_hook_categories(*self.categories)
+
+    def __exit__(self, exctype, exc, traceback):
+        if self.changes:
+            if self.mode is self.session.HOOKS_DENY_ALL:
+                self.session.disable_hook_categories(*self.changes)
+            else:
+                self.session.enable_hook_categories(*self.changes)
+        self.session.set_hooks_mode(self.oldmode)
+
+INDENT = ''
+class security_enabled(object):
+    """context manager to control security w/ session.execute, since by
+    default security is disabled on queries executed on the repository
+    side.
+    """
+    def __init__(self, session, read=None, write=None):
+        self.session = session
+        self.read = read
+        self.write = write
+
+    def __enter__(self):
+#        global INDENT
+        if self.read is not None:
+            self.oldread = self.session.set_read_security(self.read)
+#            print INDENT + 'read', self.read, self.oldread
+        if self.write is not None:
+            self.oldwrite = self.session.set_write_security(self.write)
+#            print INDENT + 'write', self.write, self.oldwrite
+#        INDENT += '  '
+
+    def __exit__(self, exctype, exc, traceback):
+#        global INDENT
+#        INDENT = INDENT[:-2]
+        if self.read is not None:
+            self.session.set_read_security(self.oldread)
+#            print INDENT + 'reset read to', self.oldread
+        if self.write is not None:
+            self.session.set_write_security(self.oldwrite)
+#            print INDENT + 'reset write to', self.oldwrite
+
+
+
 class Session(RequestSessionBase):
     """tie session id, user, connections pool and other session data all
     together
     """
+    is_internal_session = False
 
     def __init__(self, user, repo, cnxprops=None, _id=None):
         super(Session, self).__init__(repo.vreg)
@@ -56,9 +131,14 @@
         self.cnxtype = cnxprops.cnxtype
         self.creation = time()
         self.timestamp = self.creation
-        self.is_internal_session = False
-        self.is_super_session = False
         self.default_mode = 'read'
+        # support undo for Create Update Delete entity / Add Remove relation
+        if repo.config.creating or repo.config.repairing or self.is_internal_session:
+            self.undo_actions = ()
+        else:
+            self.undo_actions = set(repo.config['undo-support'].upper())
+            if self.undo_actions - set('CUDAR'):
+                raise Exception('bad undo-support string in configuration')
         # short cut to querier .execute method
         self._execute = repo.querier.execute
         # shared data, used to communicate extra information between the client
@@ -78,19 +158,17 @@
     def hijack_user(self, user):
         """return a fake request/session using specified user"""
         session = Session(user, self.repo)
-        session._threaddata = self.actual_session()._threaddata
+        threaddata = session._threaddata
+        threaddata.pool = self.pool
+        # share pending_operations, else operation added in the hi-jacked
+        # session such as SendMailOp won't ever be processed
+        threaddata.pending_operations = self.pending_operations
+        # everything in transaction_data should be copied back but the entity
+        # type cache we don't want to avoid security pb
+        threaddata.transaction_data = self.transaction_data.copy()
+        threaddata.transaction_data.pop('ecache', None)
         return session
 
-    def _super_call(self, __cb, *args, **kwargs):
-        if self.is_super_session:
-            __cb(self, *args, **kwargs)
-            return
-        self.is_super_session = True
-        try:
-            __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.
 
@@ -102,14 +180,13 @@
         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)
+        with security_enabled(self, False, False):
+            if self.vreg.schema[rtype].inlined:
+                entity = self.entity_from_eid(fromeid)
+                entity[rtype] = toeid
+                self.repo.glob_update_entity(self, entity, set((rtype,)))
+            else:
+                self.repo.glob_add_relation(self, fromeid, rtype, toeid)
 
     def delete_relation(self, fromeid, rtype, toeid):
         """provide direct access to the repository method to delete a relation.
@@ -122,14 +199,13 @@
         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)
+        with security_enabled(self, False, False):
+            if self.vreg.schema[rtype].inlined:
+                entity = self.entity_from_eid(fromeid)
+                entity[rtype] = None
+                self.repo.glob_update_entity(self, entity, set((rtype,)))
+            else:
+                self.repo.glob_delete_relation(self, fromeid, rtype, toeid)
 
     # relations cache handling #################################################
 
@@ -198,16 +274,17 @@
 
     # resource accessors ######################################################
 
-    def actual_session(self):
-        """return the original parent session if any, else self"""
-        return self
-
     def system_sql(self, sql, args=None, rollback_on_failure=True):
         """return a sql cursor on the system database"""
         if not sql.split(None, 1)[0].upper() == 'SELECT':
             self.mode = 'write'
-        return self.pool.source('system').doexec(self, sql, args,
-                                                 rollback=rollback_on_failure)
+        source = self.pool.source('system')
+        try:
+            return source.doexec(self, sql, args, rollback=rollback_on_failure)
+        except (source.OperationalError, source.InterfaceError):
+            source.warning("trying to reconnect")
+            self.pool.reconnect(self)
+            return source.doexec(self, sql, args, rollback=rollback_on_failure)
 
     def set_language(self, language):
         """i18n configuration for translation"""
@@ -233,9 +310,15 @@
         self.set_language(value)
 
     def deleted_in_transaction(self, eid):
+        """return True if the entity of the given eid is being deleted in the
+        current transaction
+        """
         return eid in self.transaction_data.get('pendingeids', ())
 
     def added_in_transaction(self, eid):
+        """return True if the entity of the given eid is being created in the
+        current transaction
+        """
         return eid in self.transaction_data.get('neweids', ())
 
     def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
@@ -245,6 +328,165 @@
         rdef = rschema.rdef(subjtype, objtype)
         return rdef.get(rprop)
 
+    # security control #########################################################
+
+    DEFAULT_SECURITY = object() # evaluated to true by design
+
+    @property
+    def read_security(self):
+        """return a boolean telling if read security is activated or not"""
+        try:
+            return self._threaddata.read_security
+        except AttributeError:
+            self._threaddata.read_security = self.DEFAULT_SECURITY
+            return self._threaddata.read_security
+
+    def set_read_security(self, activated):
+        """[de]activate read security, returning the previous value set for
+        later restoration.
+
+        you should usually use the `security_enabled` context manager instead
+        of this to change security settings.
+        """
+        oldmode = self.read_security
+        self._threaddata.read_security = activated
+        # dbapi_query used to detect hooks triggered by a 'dbapi' query (eg not
+        # issued on the session). This is tricky since we the execution model of
+        # a (write) user query is:
+        #
+        # repository.execute (security enabled)
+        #  \-> querier.execute
+        #       \-> repo.glob_xxx (add/update/delete entity/relation)
+        #            \-> deactivate security before calling hooks
+        #                 \-> WE WANT TO CHECK QUERY NATURE HERE
+        #                      \-> potentially, other calls to querier.execute
+        #
+        # so we can't rely on simply checking session.read_security, but
+        # recalling the first transition from DEFAULT_SECURITY to something
+        # else (False actually) is not perfect but should be enough
+        #
+        # also reset dbapi_query to true when we go back to DEFAULT_SECURITY
+        self._threaddata.dbapi_query = (oldmode is self.DEFAULT_SECURITY
+                                        or activated is self.DEFAULT_SECURITY)
+        return oldmode
+
+    @property
+    def write_security(self):
+        """return a boolean telling if write security is activated or not"""
+        try:
+            return self._threaddata.write_security
+        except:
+            self._threaddata.write_security = self.DEFAULT_SECURITY
+            return self._threaddata.write_security
+
+    def set_write_security(self, activated):
+        """[de]activate write security, returning the previous value set for
+        later restoration.
+
+        you should usually use the `security_enabled` context manager instead
+        of this to change security settings.
+        """
+        oldmode = self.write_security
+        self._threaddata.write_security = activated
+        return oldmode
+
+    @property
+    def running_dbapi_query(self):
+        """return a boolean telling if it's triggered by a db-api query or by
+        a session query.
+
+        To be used in hooks, else may have a wrong value.
+        """
+        return getattr(self._threaddata, 'dbapi_query', True)
+
+    # hooks activation control #################################################
+    # all hooks should be activated during normal execution
+
+    HOOKS_ALLOW_ALL = object()
+    HOOKS_DENY_ALL = object()
+
+    @property
+    def hooks_mode(self):
+        return getattr(self._threaddata, 'hooks_mode', self.HOOKS_ALLOW_ALL)
+
+    def set_hooks_mode(self, mode):
+        assert mode is self.HOOKS_ALLOW_ALL or mode is self.HOOKS_DENY_ALL
+        oldmode = getattr(self._threaddata, 'hooks_mode', self.HOOKS_ALLOW_ALL)
+        self._threaddata.hooks_mode = mode
+        return oldmode
+
+    @property
+    def disabled_hook_categories(self):
+        try:
+            return getattr(self._threaddata, 'disabled_hook_cats')
+        except AttributeError:
+            cats = self._threaddata.disabled_hook_cats = set()
+            return cats
+
+    @property
+    def enabled_hook_categories(self):
+        try:
+            return getattr(self._threaddata, 'enabled_hook_cats')
+        except AttributeError:
+            cats = self._threaddata.enabled_hook_cats = set()
+            return cats
+
+    def disable_hook_categories(self, *categories):
+        """disable the given hook categories:
+
+        - on HOOKS_DENY_ALL mode, ensure those categories are not enabled
+        - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled
+        """
+        changes = set()
+        if self.hooks_mode is self.HOOKS_DENY_ALL:
+            enablecats = self.enabled_hook_categories
+            for category in categories:
+                if category in enablecats:
+                    enablecats.remove(category)
+                    changes.add(category)
+        else:
+            disablecats = self.disabled_hook_categories
+            for category in categories:
+                if category not in disablecats:
+                    disablecats.add(category)
+                    changes.add(category)
+        return tuple(changes)
+
+    def enable_hook_categories(self, *categories):
+        """enable the given hook categories:
+
+        - on HOOKS_DENY_ALL mode, ensure those categories are enabled
+        - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled
+        """
+        changes = set()
+        if self.hooks_mode is self.HOOKS_DENY_ALL:
+            enablecats = self.enabled_hook_categories
+            for category in categories:
+                if category not in enablecats:
+                    enablecats.add(category)
+                    changes.add(category)
+        else:
+            disablecats = self.disabled_hook_categories
+            for category in categories:
+                if category in self.disabled_hook_categories:
+                    disablecats.remove(category)
+                    changes.add(category)
+        return tuple(changes)
+
+    def is_hook_category_activated(self, category):
+        """return a boolean telling if the given category is currently activated
+        or not
+        """
+        if self.hooks_mode is self.HOOKS_DENY_ALL:
+            return category in self.enabled_hook_categories
+        return category not in self.disabled_hook_categories
+
+    def is_hook_activated(self, hook):
+        """return a boolean telling if the given hook class is currently
+        activated or not
+        """
+        return self.is_hook_category_activated(hook.category)
+
     # connection management ###################################################
 
     def keep_pool_mode(self, mode):
@@ -321,7 +563,7 @@
     def _touch(self):
         """update latest session usage timestamp and reset mode to read"""
         self.timestamp = time()
-        self.local_perm_cache.clear()
+        self.local_perm_cache.clear() # XXX simply move in transaction_data, no?
         self._threaddata.mode = self.default_mode
 
     # shared data handling ###################################################
@@ -359,10 +601,7 @@
             ecache[entity.eid] = entity
 
     def entity_cache(self, eid):
-        try:
-            return self.transaction_data['ecache'][eid]
-        except:
-            raise
+        return self.transaction_data['ecache'][eid]
 
     def cached_entities(self):
         return self.transaction_data.get('ecache', {}).values()
@@ -402,47 +641,12 @@
         """return the source where the entity with id <eid> is located"""
         return self.repo.source_from_eid(eid, self)
 
-    def decorate_rset(self, rset, propagate=False):
-        rset.vreg = self.vreg
-        rset.req = propagate and self or self.actual_session()
+    def execute(self, rql, kwargs=None, eid_key=None, build_descr=True):
+        """db-api like method directly linked to the querier execute method"""
+        rset = self._execute(self, rql, kwargs, eid_key, build_descr)
+        rset.req = self
         return rset
 
-    @property
-    def super_session(self):
-        try:
-            csession = self.childsession
-        except AttributeError:
-            if isinstance(self, (ChildSession, InternalSession)):
-                csession = self
-            else:
-                csession = ChildSession(self)
-            self.childsession = csession
-        # need shared pool set
-        self.set_pool(checkclosed=False)
-        return csession
-
-    def unsafe_execute(self, rql, kwargs=None, eid_key=None, build_descr=True,
-                       propagate=False):
-        """like .execute but with security checking disabled (this method is
-        internal to the server, it's not part of the db-api)
-
-        if `propagate` is true, the super_session will be attached to the result
-        set instead of the parent session, hence further query done through
-        entities fetched from this result set will bypass security as well
-        """
-        return self.super_session.execute(rql, kwargs, eid_key, build_descr,
-                                          propagate)
-
-    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
-
-        Becare that unlike actual cursor.execute, `build_descr` default to
-        false
-        """
-        rset = self._execute(self, rql, kwargs, eid_key, build_descr)
-        return self.decorate_rset(rset, propagate)
-
     def _clear_thread_data(self):
         """remove everything from the thread local storage, except pool
         which is explicitly removed by reset_pool, and mode which is set anyway
@@ -466,58 +670,61 @@
             return
         if self.commit_state:
             return
-        # on rollback, an operation should have the following state
-        # information:
-        # - processed by the precommit/commit event or not
-        # - if processed, is it the failed operation
-        try:
-            for trstate in ('precommit', 'commit'):
-                processed = []
-                self.commit_state = trstate
-                try:
-                    while self.pending_operations:
-                        operation = self.pending_operations.pop(0)
-                        operation.processed = trstate
-                        processed.append(operation)
+        # by default, operations are executed with security turned off
+        with security_enabled(self, False, False):
+            # on rollback, an operation should have the following state
+            # information:
+            # - processed by the precommit/commit event or not
+            # - if processed, is it the failed operation
+            try:
+                for trstate in ('precommit', 'commit'):
+                    processed = []
+                    self.commit_state = trstate
+                    try:
+                        while self.pending_operations:
+                            operation = self.pending_operations.pop(0)
+                            operation.processed = trstate
+                            processed.append(operation)
+                            operation.handle_event('%s_event' % trstate)
+                        self.pending_operations[:] = processed
+                        self.debug('%s session %s done', trstate, self.id)
+                    except:
+                        self.exception('error while %sing', trstate)
+                        # if error on [pre]commit:
+                        #
+                        # * set .failed = True on the operation causing the failure
+                        # * call revert<event>_event on processed operations
+                        # * call rollback_event on *all* operations
+                        #
+                        # that seems more natural than not calling rollback_event
+                        # for processed operations, and allow generic rollback
+                        # instead of having to implements rollback, revertprecommit
+                        # and revertcommit, that will be enough in mont case.
+                        operation.failed = True
+                        for operation in processed:
+                            operation.handle_event('revert%s_event' % trstate)
+                        # XXX use slice notation since self.pending_operations is a
+                        # read-only property.
+                        self.pending_operations[:] = processed + self.pending_operations
+                        self.rollback(reset_pool)
+                        raise
+                self.pool.commit()
+                self.commit_state = trstate = 'postcommit'
+                while self.pending_operations:
+                    operation = self.pending_operations.pop(0)
+                    operation.processed = trstate
+                    try:
                         operation.handle_event('%s_event' % trstate)
-                    self.pending_operations[:] = processed
-                    self.debug('%s session %s done', trstate, self.id)
-                except:
-                    self.exception('error while %sing', trstate)
-                    # if error on [pre]commit:
-                    #
-                    # * set .failed = True on the operation causing the failure
-                    # * call revert<event>_event on processed operations
-                    # * call rollback_event on *all* operations
-                    #
-                    # that seems more natural than not calling rollback_event
-                    # for processed operations, and allow generic rollback
-                    # instead of having to implements rollback, revertprecommit
-                    # and revertcommit, that will be enough in mont case.
-                    operation.failed = True
-                    for operation in processed:
-                        operation.handle_event('revert%s_event' % trstate)
-                    # XXX use slice notation since self.pending_operations is a
-                    # read-only property.
-                    self.pending_operations[:] = processed + self.pending_operations
-                    self.rollback(reset_pool)
-                    raise
-            self.pool.commit()
-            self.commit_state = trstate = 'postcommit'
-            while self.pending_operations:
-                operation = self.pending_operations.pop(0)
-                operation.processed = trstate
-                try:
-                    operation.handle_event('%s_event' % trstate)
-                except:
-                    self.critical('error while %sing', trstate,
-                                  exc_info=sys.exc_info())
-            self.info('%s session %s done', trstate, self.id)
-        finally:
-            self._clear_thread_data()
-            self._touch()
-            if reset_pool:
-                self.reset_pool(ignoremode=True)
+                    except:
+                        self.critical('error while %sing', trstate,
+                                      exc_info=sys.exc_info())
+                self.info('%s session %s done', trstate, self.id)
+                return self.transaction_uuid(set=False)
+            finally:
+                self._clear_thread_data()
+                self._touch()
+                if reset_pool:
+                    self.reset_pool(ignoremode=True)
 
     def rollback(self, reset_pool=True):
         """rollback the current session's transaction"""
@@ -527,21 +734,23 @@
             self._touch()
             self.debug('rollback session %s done (no db activity)', self.id)
             return
-        try:
-            while self.pending_operations:
-                try:
-                    operation = self.pending_operations.pop(0)
-                    operation.handle_event('rollback_event')
-                except:
-                    self.critical('rollback error', exc_info=sys.exc_info())
-                    continue
-            self.pool.rollback()
-            self.debug('rollback for session %s done', self.id)
-        finally:
-            self._clear_thread_data()
-            self._touch()
-            if reset_pool:
-                self.reset_pool(ignoremode=True)
+        # by default, operations are executed with security turned off
+        with security_enabled(self, False, False):
+            try:
+                while self.pending_operations:
+                    try:
+                        operation = self.pending_operations.pop(0)
+                        operation.handle_event('rollback_event')
+                    except:
+                        self.critical('rollback error', exc_info=sys.exc_info())
+                        continue
+                self.pool.rollback()
+                self.debug('rollback for session %s done', self.id)
+            finally:
+                self._clear_thread_data()
+                self._touch()
+                if reset_pool:
+                    self.reset_pool(ignoremode=True)
 
     def close(self):
         """do not close pool on session close, since they are shared now"""
@@ -563,6 +772,7 @@
                 self.error('thread %s still alive after 10 seconds, will close '
                            'session anyway', thread)
         self.rollback()
+        del self._threaddata
 
     # transaction data/operations management ##################################
 
@@ -585,10 +795,31 @@
     def add_operation(self, operation, index=None):
         """add an observer"""
         assert self.commit_state != 'commit'
-        if index is not None:
+        if index is None:
+            self.pending_operations.append(operation)
+        else:
             self.pending_operations.insert(index, operation)
-        else:
-            self.pending_operations.append(operation)
+
+    # undo support ############################################################
+
+    def undoable_action(self, action, ertype):
+        return action in self.undo_actions and not ertype in NO_UNDO_TYPES
+        # XXX elif transaction on mark it partial
+
+    def transaction_uuid(self, set=True):
+        try:
+            return self.transaction_data['tx_uuid']
+        except KeyError:
+            if not set:
+                return
+            self.transaction_data['tx_uuid'] = uuid = uuid4().hex
+            self.repo.system_source.start_undoable_transaction(self, uuid)
+            return uuid
+
+    def transaction_inc_action_counter(self):
+        num = self.transaction_data.setdefault('tx_action_count', 0) + 1
+        self.transaction_data['tx_action_count'] = num
+        return num
 
     # querier helpers #########################################################
 
@@ -608,7 +839,7 @@
             selected = rqlst.children[0].selection
             solution = rqlst.children[0].solutions[0]
             description = _make_description(selected, args, solution)
-            return [tuple(description)] * len(result)
+            return RepeatList(len(result), tuple(description))
         # hard, delegate the work :o)
         return self.manual_build_descr(rqlst, args, result)
 
@@ -637,7 +868,7 @@
                 etype = rqlst.children[0].solutions[0]
                 basedescription.append(term.get_type(etype, args))
         if not todetermine:
-            return [tuple(basedescription)] * len(result)
+            return RepeatList(len(result), tuple(basedescription))
         return self._build_descr(result, basedescription, todetermine)
 
     def _build_descr(self, result, basedescription, todetermine):
@@ -664,6 +895,28 @@
 
     # deprecated ###############################################################
 
+    @deprecated("[3.7] execute is now unsafe by default in hooks/operation. You"
+                " can also control security with the security_enabled context "
+                "manager")
+    def unsafe_execute(self, rql, kwargs=None, eid_key=None, build_descr=True,
+                       propagate=False):
+        """like .execute but with security checking disabled (this method is
+        internal to the server, it's not part of the db-api)
+        """
+        with security_enabled(self, read=False, write=False):
+            return self.execute(rql, kwargs, eid_key, build_descr)
+
+    @property
+    @deprecated("[3.7] is_super_session is deprecated, test "
+                "session.read_security and or session.write_security")
+    def is_super_session(self):
+        return not self.read_security or not self.write_security
+
+    @deprecated("[3.7] session is actual session")
+    def actual_session(self):
+        """return the original parent session if any, else self"""
+        return self
+
     @property
     @deprecated("[3.6] use session.vreg.schema")
     def schema(self):
@@ -690,98 +943,16 @@
         return self.entity_from_eid(eid)
 
 
-class ChildSession(Session):
-    """child (or internal) session are used to hijack the security system
-    """
-    cnxtype = 'inmemory'
-
-    def __init__(self, parent_session):
-        self.id = None
-        self.is_internal_session = False
-        self.is_super_session = True
-        # 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
-        self.encoding = parent_session.encoding
-        self.lang = parent_session.lang
-        self._ = self.__ = parent_session._
-        # short cut to querier .execute method
-        self._execute = self.repo.querier.execute
-
-    @property
-    def super_session(self):
-        return self
-
-    def get_mode(self):
-        return self.parent_session.mode
-    def set_mode(self, value):
-        self.parent_session.set_mode(value)
-    mode = property(get_mode, set_mode)
-
-    def get_commit_state(self):
-        return self.parent_session.commit_state
-    def set_commit_state(self, value):
-        self.parent_session.set_commit_state(value)
-    commit_state = property(get_commit_state, set_commit_state)
-
-    @property
-    def pool(self):
-        return self.parent_session.pool
-    @property
-    def pending_operations(self):
-        return self.parent_session.pending_operations
-    @property
-    def transaction_data(self):
-        return self.parent_session.transaction_data
-
-    def set_pool(self):
-        """the session need a pool to execute some queries"""
-        self.parent_session.set_pool()
-
-    def reset_pool(self):
-        """the session has no longer using its pool, at least for some time
-        """
-        self.parent_session.reset_pool()
-
-    def actual_session(self):
-        """return the original parent session if any, else self"""
-        return self.parent_session
-
-    def commit(self, reset_pool=True):
-        """commit the current session's transaction"""
-        self.parent_session.commit(reset_pool)
-
-    def rollback(self, reset_pool=True):
-        """rollback the current session's transaction"""
-        self.parent_session.rollback(reset_pool)
-
-    def close(self):
-        """do not close pool on session close, since they are shared now"""
-        self.rollback()
-
-    def user_data(self):
-        """returns a dictionnary with this user's information"""
-        return self.parent_session.user_data()
-
-
 class InternalSession(Session):
     """special session created internaly by the repository"""
+    is_internal_session = True
 
     def __init__(self, repo, cnxprops=None):
         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
-
-    @property
-    def super_session(self):
-        return self
+        self.disable_hook_categories('integrity')
 
 
 class InternalManager(object):
--- a/server/sources/__init__.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/sources/__init__.py	Tue Apr 06 19:46:38 2010 +0200
@@ -100,7 +100,7 @@
         """method called by the repository once ready to handle request"""
         pass
 
-    def backup(self, backupfile):
+    def backup(self, backupfile, confirm):
         """method called to create a backup of source's data"""
         pass
 
@@ -351,7 +351,7 @@
         """update an entity in the source"""
         raise NotImplementedError()
 
-    def delete_entity(self, session, etype, eid):
+    def delete_entity(self, session, entity):
         """delete an entity from the source"""
         raise NotImplementedError()
 
@@ -372,16 +372,36 @@
     def create_eid(self, session):
         raise NotImplementedError()
 
-    def add_info(self, session, entity, source, extid=None):
+    def add_info(self, session, entity, source, extid):
         """add type and source info for an eid into the system table"""
         raise NotImplementedError()
 
-    def delete_info(self, session, eid, etype, uri, extid):
+    def update_info(self, session, entity, need_fti_update):
+        """mark entity as being modified, fulltext reindex if needed"""
+        raise NotImplementedError()
+
+    def delete_info(self, session, entity, uri, extid, attributes, relations):
         """delete system information on deletion of an entity by transfering
         record from the entities table to the deleted_entities table
         """
         raise NotImplementedError()
 
+    def modified_entities(self, session, etypes, mtime):
+        """return a 2-uple:
+        * list of (etype, eid) of entities of the given types which have been
+          modified since the given timestamp (actually entities whose full text
+          index content has changed)
+        * list of (etype, eid) of entities of the given types which have been
+          deleted since the given timestamp
+        """
+        raise NotImplementedError()
+
+    def index_entity(self, session, entity):
+        """create an operation to [re]index textual content of the given entity
+        on commit
+        """
+        raise NotImplementedError()
+
     def fti_unindex_entity(self, session, eid):
         """remove text content for entity with the given eid from the full text
         index
@@ -393,16 +413,6 @@
         """
         raise NotImplementedError()
 
-    def modified_entities(self, session, etypes, mtime):
-        """return a 2-uple:
-        * list of (etype, eid) of entities of the given types which have been
-          modified since the given timestamp (actually entities whose full text
-          index content has changed)
-        * list of (etype, eid) of entities of the given types which have been
-          deleted since the given timestamp
-        """
-        raise NotImplementedError()
-
     # sql system source interface #############################################
 
     def sqlexec(self, session, sql, args=None):
--- a/server/sources/extlite.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/sources/extlite.py	Tue Apr 06 19:46:38 2010 +0200
@@ -20,12 +20,6 @@
         self.source = source
         self._cnx = None
 
-    @property
-    def logged_user(self):
-        if self._cnx is None:
-            self._cnx = self.source._sqlcnx
-        return self._cnx.logged_user
-
     def cursor(self):
         if self._cnx is None:
             self._cnx = self.source._sqlcnx
@@ -93,11 +87,11 @@
         AbstractSource.__init__(self, repo, appschema, source_config,
                                 *args, **kwargs)
 
-    def backup(self, backupfile):
+    def backup(self, backupfile, confirm):
         """method called to create a backup of the source's data"""
         self.close_pool_connections()
         try:
-            self.sqladapter.backup_to_file(backupfile)
+            self.sqladapter.backup_to_file(backupfile, confirm)
         finally:
             self.open_pool_connections()
 
@@ -193,9 +187,10 @@
         if self._need_sql_create:
             return []
         assert dbg_st_search(self.uri, union, varmap, args, cachekey)
-        sql, query_args = self.rqlsqlgen.generate(union, args)
-        args = self.sqladapter.merge_args(args, query_args)
-        results = self.sqladapter.process_result(self.doexec(session, sql, args))
+        sql, qargs, cbs = self.rqlsqlgen.generate(union, args)
+        args = self.sqladapter.merge_args(args, qargs)
+        cursor = self.doexec(session, sql, args)
+        results = self.sqladapter.process_result(cursor, cbs)
         assert dbg_results(results)
         return results
 
@@ -231,15 +226,15 @@
         """update an entity in the source"""
         raise NotImplementedError()
 
-    def delete_entity(self, session, etype, eid):
+    def delete_entity(self, session, entity):
         """delete an entity from the source
 
         this is not deleting a file in the svn but deleting entities from the
         source. Main usage is to delete repository content when a Repository
         entity is deleted.
         """
-        attrs = {SQL_PREFIX + 'eid': eid}
-        sql = self.sqladapter.sqlgen.delete(SQL_PREFIX + etype, attrs)
+        attrs = {'cw_eid': entity.eid}
+        sql = self.sqladapter.sqlgen.delete(SQL_PREFIX + entity.__regid__, attrs)
         self.doexec(session, sql, attrs)
 
     def local_add_relation(self, session, subject, rtype, object):
--- a/server/sources/ldapuser.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/sources/ldapuser.py	Tue Apr 06 19:46:38 2010 +0200
@@ -64,7 +64,7 @@
          {'type' : 'choice',
           'default': 'ldap',
           'choices': ('ldap', 'ldaps', 'ldapi'),
-          'help': 'ldap protocol',
+          'help': 'ldap protocol (allowed values: ldap, ldaps, ldapi)',
           'group': 'ldap-source', 'inputlevel': 1,
           }),
 
@@ -200,6 +200,7 @@
         except KeyError:
             return # no email in ldap, we're done
         session = self.repo.internal_session()
+        execute = session.execute
         try:
             cursor = session.system_sql("SELECT eid, extid FROM entities WHERE "
                                         "source='%s'" % self.uri)
@@ -210,20 +211,29 @@
                 if res:
                     ldapemailaddr = res[0].get(ldap_emailattr)
                     if ldapemailaddr:
-                        rset = session.execute('EmailAddress A WHERE '
-                                               'U use_email X, U eid %(u)s',
-                                               {'u': eid})
+                        rset = execute('Any X,A WHERE '
+                                       'X address A, U use_email X, U eid %(u)s',
+                                       {'u': eid})
                         ldapemailaddr = unicode(ldapemailaddr)
-                        for emailaddr, in rset:
+                        for emaileid, emailaddr, in rset:
                             if emailaddr == ldapemailaddr:
                                 break
                         else:
                             self.info('updating email address of user %s to %s',
                                       extid, ldapemailaddr)
-                            if rset:
-                                session.execute('SET X address %(addr)s WHERE '
-                                                'U primary_email X, U eid %(u)s',
-                                                {'addr': ldapemailaddr, 'u': eid})
+                            emailrset = execute('EmailAddress A WHERE A address %(addr)s',
+                                                {'addr': ldapemailaddr})
+                            if emailrset:
+                                execute('SET U use_email X WHERE '
+                                        'X eid %(x)s, U eid %(u)s',
+                                        {'x': emailrset[0][0], 'u': eid})
+                            elif rset:
+                                if not execute('SET X address %(addr)s WHERE '
+                                               'U primary_email X, U eid %(u)s',
+                                               {'addr': ldapemailaddr, 'u': eid}, 'u'):
+                                    execute('SET X address %(addr)s WHERE '
+                                            'X eid %(x)s',
+                                            {'addr': ldapemailaddr, 'x': rset[0][0]}, 'x')
                             else:
                                 # no email found, create it
                                 _insert_email(session, ldapemailaddr, eid)
@@ -476,7 +486,8 @@
             if eid:
                 self.warning('deleting ldap user with eid %s and dn %s',
                              eid, base)
-                self.repo.delete_info(session, eid)
+                entity = session.entity_from_eid(eid, 'CWUser')
+                self.repo.delete_info(session, entity, self.uri, base)
                 self._cache.pop(base, None)
             return []
 ##         except ldap.REFERRAL, e:
@@ -554,7 +565,7 @@
         """replace an entity in the source"""
         raise RepositoryError('this source is read only')
 
-    def delete_entity(self, session, etype, eid):
+    def delete_entity(self, session, entity):
         """delete an entity from the source"""
         raise RepositoryError('this source is read only')
 
--- a/server/sources/native.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/sources/native.py	Tue Apr 06 19:46:38 2010 +0200
@@ -11,24 +11,33 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
+from pickle import loads, dumps
 from threading import Lock
 from datetime import datetime
 from base64 import b64decode, b64encode
-
-from logilab.common.cache import Cache
-from logilab.common.configuration import Method
-from logilab.common.adbh import get_adv_func_helper
-from logilab.common.shellutils import getlogin
+from contextlib import contextmanager
 
-from indexer import get_indexer
+from logilab.common.compat import any
+from logilab.common.cache import Cache
+from logilab.common.decorators import cached, clear_cache
+from logilab.common.configuration import Method
+from logilab.common.shellutils import getlogin
+from logilab.database import get_db_helper
 
-from cubicweb import UnknownEid, AuthenticationError, Binary, server
+from cubicweb import UnknownEid, AuthenticationError, ValidationError, Binary
+from cubicweb import transaction as tx, server, neg_role
+from cubicweb.schema import VIRTUAL_RTYPES
 from cubicweb.cwconfig import CubicWebNoAppConfiguration
-from cubicweb.server.utils import crypt_password
+from cubicweb.server import hook
+from cubicweb.server.utils import crypt_password, eschema_eid
 from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
 from cubicweb.server.rqlannotation import set_qdata
+from cubicweb.server.hook import CleanupDeletedEidsCacheOp
+from cubicweb.server.session import hooks_control, security_enabled
 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
 from cubicweb.server.sources.rql2sql import SQLGenerator
 
@@ -92,6 +101,74 @@
         table, restr, attr)
 
 
+def sql_or_clauses(sql, clauses):
+    select, restr = sql.split(' WHERE ', 1)
+    restrclauses = restr.split(' AND ')
+    for clause in clauses:
+        restrclauses.remove(clause)
+    if restrclauses:
+        restr = '%s AND (%s)' % (' AND '.join(restrclauses),
+                                 ' OR '.join(clauses))
+    else:
+        restr = '(%s)' % ' OR '.join(clauses)
+    return '%s WHERE %s' % (select, restr)
+
+
+class UndoException(Exception):
+    """something went wrong during undoing"""
+
+
+def _undo_check_relation_target(tentity, rdef, role):
+    """check linked entity has not been redirected for this relation"""
+    card = rdef.role_cardinality(role)
+    if card in '?1' and tentity.related(rdef.rtype, role):
+        raise UndoException(tentity._cw._(
+            "Can't restore %(role)s relation %(rtype)s to entity %(eid)s which "
+            "is already linked using this relation.")
+                            % {'role': neg_role(role),
+                               'rtype': rdef.rtype,
+                               'eid': tentity.eid})
+
+def _undo_rel_info(session, subj, rtype, obj):
+    entities = []
+    for role, eid in (('subject', subj), ('object', obj)):
+        try:
+            entities.append(session.entity_from_eid(eid))
+        except UnknownEid:
+            raise UndoException(session._(
+                "Can't restore relation %(rtype)s, %(role)s entity %(eid)s"
+                " doesn't exist anymore.")
+                                % {'role': session._(role),
+                                   'rtype': session._(rtype),
+                                   'eid': eid})
+    sentity, oentity = entities
+    try:
+        rschema = session.vreg.schema.rschema(rtype)
+        rdef = rschema.rdefs[(sentity.__regid__, oentity.__regid__)]
+    except KeyError:
+        raise UndoException(session._(
+            "Can't restore relation %(rtype)s between %(subj)s and "
+            "%(obj)s, that relation does not exists anymore in the "
+            "schema.")
+                            % {'rtype': session._(rtype),
+                               'subj': subj,
+                               'obj': obj})
+    return sentity, oentity, rdef
+
+def _undo_has_later_transaction(session, eid):
+    return session.system_sql('''\
+SELECT T.tx_uuid FROM transactions AS TREF, transactions AS T
+WHERE TREF.tx_uuid='%(txuuid)s' AND T.tx_uuid!='%(txuuid)s'
+AND T.tx_time>=TREF.tx_time
+AND (EXISTS(SELECT 1 FROM tx_entity_actions AS TEA
+            WHERE TEA.tx_uuid=T.tx_uuid AND TEA.eid=%(eid)s)
+     OR EXISTS(SELECT 1 FROM tx_relation_actions as TRA
+               WHERE TRA.tx_uuid=T.tx_uuid AND (
+                   TRA.eid_from=%(eid)s OR TRA.eid_to=%(eid)s))
+     )''' % {'txuuid': session.transaction_data['undoing_uuid'],
+             'eid': eid}).fetchone()
+
+
 class NativeSQLSource(SQLAdapterMixIn, AbstractSource):
     """adapter for source using the native cubicweb schema (see below)
     """
@@ -148,18 +225,19 @@
                                 *args, **kwargs)
         # sql generator
         self._rql_sqlgen = self.sqlgen_class(appschema, self.dbhelper,
-                                             self.encoding, ATTR_MAP.copy())
+                                             ATTR_MAP.copy())
         # full text index helper
-        self.indexer = get_indexer(self.dbdriver, self.encoding)
-        # advanced functionality helper
-        self.dbhelper.fti_uid_attr = self.indexer.uid_attr
-        self.dbhelper.fti_table = self.indexer.table
-        self.dbhelper.fti_restriction_sql = self.indexer.restriction_sql
-        self.dbhelper.fti_need_distinct_query = self.indexer.need_distinct
+        self.do_fti = not repo.config['delay-full-text-indexation']
         # sql queries cache
         self._cache = Cache(repo.config['rql-cache-size'])
         self._temp_table_data = {}
+        # we need a lock to protect eid attribution function (XXX, really?
+        # explain)
         self._eid_creation_lock = Lock()
+        # (etype, attr) / storage mapping
+        self._storages = {}
+        # entity types that may be used by other multi-sources instances
+        self.multisources_etypes = set(repo.config['multi-sources-etypes'])
         # XXX no_sqlite_wrap trick since we've a sqlite locking pb when
         # running unittest_multisources with the wrapping below
         if self.dbdriver == 'sqlite' and \
@@ -201,17 +279,19 @@
         pool = self.repo._get_pool()
         pool.pool_set()
         # check full text index availibility
-        if not self.indexer.has_fti_table(pool['system']):
-            self.error('no text index table')
-            self.indexer = None
+        if self.do_fti:
+            if not self.dbhelper.has_fti_table(pool['system']):
+                if not self.repo.config.creating:
+                    self.critical('no text index table')
+                self.do_fti = False
         pool.pool_reset()
         self.repo._free_pool(pool)
 
-    def backup(self, backupfile):
+    def backup(self, backupfile, confirm):
         """method called to create a backup of the source's data"""
         self.close_pool_connections()
         try:
-            self.backup_to_file(backupfile)
+            self.backup_to_file(backupfile, confirm)
         finally:
             self.open_pool_connections()
 
@@ -228,12 +308,26 @@
     def init(self):
         self.init_creating()
 
-    def map_attribute(self, etype, attr, cb):
-        self._rql_sqlgen.attr_map['%s.%s' % (etype, attr)] = cb
+    # XXX deprecates [un]map_attribute ?
+    def map_attribute(self, etype, attr, cb, sourcedb=True):
+        self._rql_sqlgen.attr_map['%s.%s' % (etype, attr)] = (cb, sourcedb)
 
     def unmap_attribute(self, etype, attr):
         self._rql_sqlgen.attr_map.pop('%s.%s' % (etype, attr), None)
 
+    def set_storage(self, etype, attr, storage):
+        storage_dict = self._storages.setdefault(etype, {})
+        storage_dict[attr] = storage
+        self.map_attribute(etype, attr,
+                           storage.callback, storage.is_source_callback)
+
+    def unset_storage(self, etype, attr):
+        self._storages[etype].pop(attr)
+        # if etype has no storage left, remove the entry
+        if not self._storages[etype]:
+            del self._storages[etype]
+        self.unmap_attribute(etype, attr)
+
     # ISource interface #######################################################
 
     def compile_rql(self, rql, sols):
@@ -255,6 +349,7 @@
             pass # __init__
         for authentifier in self.authentifiers:
             authentifier.set_schema(self.schema)
+        clear_cache(self, 'need_fti_indexation')
 
     def support_entity(self, etype, write=False):
         """return true if the given entity's type is handled by this adapter
@@ -299,27 +394,26 @@
         if cachekey is None:
             self.no_cache += 1
             # generate sql query if we are able to do so (not supported types...)
-            sql, query_args = self._rql_sqlgen.generate(union, args, varmap)
+            sql, qargs, cbs = self._rql_sqlgen.generate(union, args, varmap)
         else:
             # sql may be cached
             try:
-                sql, query_args = self._cache[cachekey]
+                sql, qargs, cbs = self._cache[cachekey]
                 self.cache_hit += 1
             except KeyError:
                 self.cache_miss += 1
-                sql, query_args = self._rql_sqlgen.generate(union, args, varmap)
-                self._cache[cachekey] = sql, query_args
-        args = self.merge_args(args, query_args)
+                sql, qargs, cbs = self._rql_sqlgen.generate(union, args, varmap)
+                self._cache[cachekey] = sql, qargs, cbs
+        args = self.merge_args(args, qargs)
         assert isinstance(sql, basestring), repr(sql)
         try:
             cursor = self.doexec(session, sql, args)
-        except (self.dbapi_module.OperationalError,
-                self.dbapi_module.InterfaceError):
+        except (self.OperationalError, self.InterfaceError):
             # FIXME: better detection of deconnection pb
-            self.info("request failed '%s' ... retry with a new cursor", sql)
+            self.warning("trying to reconnect")
             session.pool.reconnect(self)
             cursor = self.doexec(session, sql, args)
-        results = self.process_result(cursor)
+        results = self.process_result(cursor, cbs)
         assert dbg_results(results)
         return results
 
@@ -333,9 +427,9 @@
             self.uri, union, varmap, args,
             prefix='ON THE FLY temp data insertion into %s from' % table)
         # generate sql queries if we are able to do so
-        sql, query_args = self._rql_sqlgen.generate(union, args, varmap)
-        query = 'INSERT INTO %s %s' % (table, sql.encode(self.encoding))
-        self.doexec(session, query, self.merge_args(args, query_args))
+        sql, qargs, cbs = self._rql_sqlgen.generate(union, args, varmap)
+        query = 'INSERT INTO %s %s' % (table, sql.encode(self._dbencoding))
+        self.doexec(session, query, self.merge_args(args, qargs))
 
     def manual_insert(self, results, table, session):
         """insert given result into a temporary table on the system source"""
@@ -351,7 +445,7 @@
             row = tuple(row)
             for index, cell in enumerate(row):
                 if isinstance(cell, Binary):
-                    cell = self.binary(cell.getvalue())
+                    cell = self._binary(cell.getvalue())
                 kwargs[str(index)] = cell
             kwargs_list.append(kwargs)
         self.doexecmany(session, query, kwargs_list)
@@ -369,34 +463,100 @@
                 except KeyError:
                     continue
 
+    @contextmanager
+    def _storage_handler(self, entity, event):
+        # 1/ memorize values as they are before the storage is called.
+        #    For instance, the BFSStorage will replace the `data`
+        #    binary value with a Binary containing the destination path
+        #    on the filesystem. To make the entity.data usage absolutely
+        #    transparent, we'll have to reset entity.data to its binary
+        #    value once the SQL query will be executed
+        restore_values = {}
+        etype = entity.__regid__
+        for attr, storage in self._storages.get(etype, {}).items():
+            try:
+                if attr in entity.edited_attributes:
+                    handler = getattr(storage, 'entity_%s' % event)
+                    real_value = handler(entity, attr)
+                    restore_values[attr] = real_value
+            except AttributeError:
+                assert event == 'deleted'
+                getattr(storage, 'entity_deleted')(entity, attr)
+        try:
+            yield # 2/ execute the source's instructions
+        finally:
+            # 3/ restore original values
+            for attr, value in restore_values.items():
+                entity[attr] = value
+
     def add_entity(self, session, entity):
         """add a new entity to the source"""
-        attrs = self.preprocess_entity(entity)
-        sql = self.sqlgen.insert(SQL_PREFIX + str(entity.e_schema), attrs)
-        self.doexec(session, sql, attrs)
+        with self._storage_handler(entity, 'added'):
+            attrs = self.preprocess_entity(entity)
+            sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs)
+            self.doexec(session, sql, attrs)
+            if session.undoable_action('C', entity.__regid__):
+                self._record_tx_action(session, 'tx_entity_actions', 'C',
+                                       etype=entity.__regid__, eid=entity.eid)
 
     def update_entity(self, session, entity):
         """replace an entity in the source"""
-        attrs = self.preprocess_entity(entity)
-        sql = self.sqlgen.update(SQL_PREFIX + str(entity.e_schema), attrs, [SQL_PREFIX + 'eid'])
-        self.doexec(session, sql, attrs)
+        with self._storage_handler(entity, 'updated'):
+            attrs = self.preprocess_entity(entity)
+            if session.undoable_action('U', entity.__regid__):
+                changes = self._save_attrs(session, entity, attrs)
+                self._record_tx_action(session, 'tx_entity_actions', 'U',
+                                       etype=entity.__regid__, eid=entity.eid,
+                                       changes=self._binary(dumps(changes)))
+            sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, attrs,
+                                     ['cw_eid'])
+            self.doexec(session, sql, attrs)
 
-    def delete_entity(self, session, etype, eid):
+    def delete_entity(self, session, entity):
         """delete an entity from the source"""
-        attrs = {SQL_PREFIX + 'eid': eid}
-        sql = self.sqlgen.delete(SQL_PREFIX + etype, attrs)
-        self.doexec(session, sql, attrs)
+        with self._storage_handler(entity, 'deleted'):
+            if session.undoable_action('D', entity.__regid__):
+                attrs = [SQL_PREFIX + r.type
+                         for r in entity.e_schema.subject_relations()
+                         if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
+                changes = self._save_attrs(session, entity, attrs)
+                self._record_tx_action(session, 'tx_entity_actions', 'D',
+                                       etype=entity.__regid__, eid=entity.eid,
+                                       changes=self._binary(dumps(changes)))
+            attrs = {'cw_eid': entity.eid}
+            sql = self.sqlgen.delete(SQL_PREFIX + entity.__regid__, attrs)
+            self.doexec(session, sql, attrs)
 
-    def add_relation(self, session, subject, rtype, object):
+    def add_relation(self, session, subject, rtype, object, inlined=False):
+        """add a relation to the source"""
+        self._add_relation(session, subject, rtype, object, inlined)
+        if session.undoable_action('A', rtype):
+            self._record_tx_action(session, 'tx_relation_actions', 'A',
+                                   eid_from=subject, rtype=rtype, eid_to=object)
+
+    def _add_relation(self, session, subject, rtype, object, inlined=False):
         """add a relation to the source"""
-        attrs = {'eid_from': subject, 'eid_to': object}
-        sql = self.sqlgen.insert('%s_relation' % rtype, attrs)
+        if inlined is False:
+            attrs = {'eid_from': subject, 'eid_to': object}
+            sql = self.sqlgen.insert('%s_relation' % rtype, attrs)
+        else: # used by data import
+            etype = session.describe(subject)[0]
+            attrs = {'cw_eid': subject, SQL_PREFIX + rtype: object}
+            sql = self.sqlgen.update(SQL_PREFIX + etype, attrs,
+                                     ['cw_eid'])
         self.doexec(session, sql, attrs)
 
     def delete_relation(self, session, subject, rtype, object):
         """delete a relation from the source"""
         rschema = self.schema.rschema(rtype)
-        if rschema.inlined:
+        self._delete_relation(session, subject, rtype, object, rschema.inlined)
+        if session.undoable_action('R', rtype):
+            self._record_tx_action(session, 'tx_relation_actions', 'R',
+                                   eid_from=subject, rtype=rtype, eid_to=object)
+
+    def _delete_relation(self, session, subject, rtype, object, inlined=False):
+        """delete a relation from the source"""
+        if inlined:
             table = SQL_PREFIX + session.describe(subject)[0]
             column = SQL_PREFIX + rtype
             sql = 'UPDATE %s SET %s=NULL WHERE %seid=%%(eid)s' % (table, column,
@@ -462,6 +622,9 @@
 
     # short cut to method requiring advanced db helper usage ##################
 
+    def binary_to_str(self, value):
+        return self.dbhelper.dbapi_module.binary_to_str(value)
+
     def create_index(self, session, table, column, unique=False):
         cursor = LogCursor(session.pool[self.uri])
         self.dbhelper.create_index(cursor, table, column, unique)
@@ -476,7 +639,7 @@
         """return a tuple (type, source, extid) for the entity with id <eid>"""
         sql = 'SELECT type, source, extid FROM entities WHERE eid=%s' % eid
         try:
-            res = session.system_sql(sql).fetchone()
+            res = self.doexec(session, sql).fetchone()
         except:
             assert session.pool, 'session has no pool set'
             raise UnknownEid(eid)
@@ -491,9 +654,10 @@
     def extid2eid(self, session, source, extid):
         """get eid from an external id. Return None if no record found."""
         assert isinstance(extid, str)
-        cursor = session.system_sql('SELECT eid FROM entities WHERE '
-                                    'extid=%(x)s AND source=%(s)s',
-                                    {'x': b64encode(extid), 's': source.uri})
+        cursor = self.doexec(session,
+                             'SELECT eid FROM entities '
+                             'WHERE extid=%(x)s AND source=%(s)s',
+                             {'x': b64encode(extid), 's': source.uri})
         # XXX testing rowcount cause strange bug with sqlite, results are there
         #     but rowcount is 0
         #if cursor.rowcount > 0:
@@ -505,6 +669,15 @@
             pass
         return None
 
+    def make_temp_table_name(self, table):
+        try: # XXX remove this once 
+            return self.dbhelper.temporary_table_name(table)
+        except AttributeError:
+            import warnings
+            warnings.warn('Please hg up logilab.database')
+            return table
+
+
     def temp_table_def(self, selected, sol, table):
         return make_schema(selected, sol, table, self.dbhelper.TYPE_MAPPING)
 
@@ -524,7 +697,7 @@
         finally:
             self._eid_creation_lock.release()
 
-    def add_info(self, session, entity, source, extid=None):
+    def add_info(self, session, entity, source, extid, complete):
         """add type and source info for an eid into the system table"""
         # begin by inserting eid/type/source/extid into the entities table
         if extid is not None:
@@ -532,44 +705,42 @@
             extid = b64encode(extid)
         attrs = {'type': entity.__regid__, 'eid': entity.eid, 'extid': extid,
                  'source': source.uri, 'mtime': datetime.now()}
-        session.system_sql(self.sqlgen.insert('entities', attrs), attrs)
+        self.doexec(session, self.sqlgen.insert('entities', attrs), attrs)
+        # now we can update the full text index
+        if self.do_fti and self.need_fti_indexation(entity.__regid__):
+            if complete:
+                entity.complete(entity.e_schema.indexable_attributes())
+            self.index_entity(session, entity=entity)
 
-    def delete_info(self, session, eid, etype, uri, extid):
-        """delete system information on deletion of an entity by transfering
-        record from the entities table to the deleted_entities table
+    def update_info(self, session, entity, need_fti_update):
+        """mark entity as being modified, fulltext reindex if needed"""
+        if self.do_fti and need_fti_update:
+            # reindex the entity only if this query is updating at least
+            # one indexable attribute
+            self.index_entity(session, entity=entity)
+        # update entities.mtime.
+        # XXX Only if entity.__regid__ in self.multisources_etypes?
+        attrs = {'eid': entity.eid, 'mtime': datetime.now()}
+        self.doexec(session, self.sqlgen.update('entities', attrs, ['eid']), attrs)
+
+    def delete_info(self, session, entity, uri, extid):
+        """delete system information on deletion of an entity:
+        * update the fti
+        * remove record from the entities table
+        * transfer it to the deleted_entities table if the entity's type is
+          multi-sources
         """
-        attrs = {'eid': eid}
-        session.system_sql(self.sqlgen.delete('entities', attrs), attrs)
+        self.fti_unindex_entity(session, entity.eid)
+        attrs = {'eid': entity.eid}
+        self.doexec(session, self.sqlgen.delete('entities', attrs), attrs)
+        if not entity.__regid__ in self.multisources_etypes:
+            return
         if extid is not None:
             assert isinstance(extid, str), type(extid)
             extid = b64encode(extid)
-        attrs = {'type': etype, 'eid': eid, 'extid': extid,
+        attrs = {'type': entity.__regid__, 'eid': entity.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
-        index
-        """
-        try:
-            self.indexer.cursor_unindex_object(eid, session.pool['system'])
-        except Exception: # let KeyboardInterrupt / SystemExit propagate
-            if self.indexer is not None:
-                self.exception('error while unindexing %s', eid)
-
-    def fti_index_entity(self, session, entity):
-        """add text content of a created/modified entity to the full text index
-        """
-        self.debug('reindexing %r', entity.eid)
-        try:
-            self.indexer.cursor_reindex_object(entity.eid, entity,
-                                               session.pool['system'])
-        except Exception: # let KeyboardInterrupt / SystemExit propagate
-            if self.indexer is not None:
-                self.exception('error while reindexing %s', entity)
-        # update entities.mtime
-        attrs = {'eid': entity.eid, 'mtime': datetime.now()}
-        session.system_sql(self.sqlgen.update('entities', attrs, ['eid']), attrs)
+        self.doexec(session, self.sqlgen.insert('deleted_entities', attrs), attrs)
 
     def modified_entities(self, session, etypes, mtime):
         """return a 2-uple:
@@ -579,18 +750,421 @@
         * list of (etype, eid) of entities of the given types which have been
           deleted since the given timestamp
         """
+        for etype in etypes:
+            if not etype in self.multisources_etypes:
+                self.critical('%s not listed as a multi-sources entity types. '
+                              'Modify your configuration' % etype)
+                self.multisources_etypes.add(etype)
         modsql = _modified_sql('entities', etypes)
-        cursor = session.system_sql(modsql, {'time': mtime})
+        cursor = self.doexec(session, modsql, {'time': mtime})
         modentities = cursor.fetchall()
         delsql = _modified_sql('deleted_entities', etypes)
-        cursor = session.system_sql(delsql, {'time': mtime})
+        cursor = self.doexec(session, delsql, {'time': mtime})
         delentities = cursor.fetchall()
         return modentities, delentities
 
+    # undo support #############################################################
+
+    def undoable_transactions(self, session, ueid=None, **actionfilters):
+        """See :class:`cubicweb.dbapi.Connection.undoable_transactions`"""
+        # force filtering to session's user if not a manager
+        if not session.user.is_in_group('managers'):
+            ueid = session.user.eid
+        restr = {}
+        if ueid is not None:
+            restr['tx_user'] = ueid
+        sql = self.sqlgen.select('transactions', restr, ('tx_uuid', 'tx_time', 'tx_user'))
+        if actionfilters:
+            # we will need subqueries to filter transactions according to
+            # actions done
+            tearestr = {} # filters on the tx_entity_actions table
+            trarestr = {} # filters on the tx_relation_actions table
+            genrestr = {} # generic filters, appliyable to both table
+            # unless public explicitly set to false, we only consider public
+            # actions
+            if actionfilters.pop('public', True):
+                genrestr['txa_public'] = True
+            # put additional filters in trarestr and/or tearestr
+            for key, val in actionfilters.iteritems():
+                if key == 'etype':
+                    # filtering on etype implies filtering on entity actions
+                    # only, and with no eid specified
+                    assert actionfilters.get('action', 'C') in 'CUD'
+                    assert not 'eid' in actionfilters
+                    tearestr['etype'] = val
+                elif key == 'eid':
+                    # eid filter may apply to 'eid' of tx_entity_actions or to
+                    # 'eid_from' OR 'eid_to' of tx_relation_actions
+                    if actionfilters.get('action', 'C') in 'CUD':
+                        tearestr['eid'] = val
+                    if actionfilters.get('action', 'A') in 'AR':
+                        trarestr['eid_from'] = val
+                        trarestr['eid_to'] = val
+                elif key == 'action':
+                    if val in 'CUD':
+                        tearestr['txa_action'] = val
+                    else:
+                        assert val in 'AR'
+                        trarestr['txa_action'] = val
+                else:
+                    raise AssertionError('unknow filter %s' % key)
+            assert trarestr or tearestr, "can't only filter on 'public'"
+            subqsqls = []
+            # append subqueries to the original query, using EXISTS()
+            if trarestr or (genrestr and not tearestr):
+                trarestr.update(genrestr)
+                trasql = self.sqlgen.select('tx_relation_actions', trarestr, ('1',))
+                if 'eid_from' in trarestr:
+                    # replace AND by OR between eid_from/eid_to restriction
+                    trasql = sql_or_clauses(trasql, ['eid_from = %(eid_from)s',
+                                                     'eid_to = %(eid_to)s'])
+                trasql += ' AND transactions.tx_uuid=tx_relation_actions.tx_uuid'
+                subqsqls.append('EXISTS(%s)' % trasql)
+            if tearestr or (genrestr and not trarestr):
+                tearestr.update(genrestr)
+                teasql = self.sqlgen.select('tx_entity_actions', tearestr, ('1',))
+                teasql += ' AND transactions.tx_uuid=tx_entity_actions.tx_uuid'
+                subqsqls.append('EXISTS(%s)' % teasql)
+            if restr:
+                sql += ' AND %s' % ' OR '.join(subqsqls)
+            else:
+                sql += ' WHERE %s' % ' OR '.join(subqsqls)
+            restr.update(trarestr)
+            restr.update(tearestr)
+        # we want results ordered by transaction's time descendant
+        sql += ' ORDER BY tx_time DESC'
+        cu = self.doexec(session, sql, restr)
+        # turn results into transaction objects
+        return [tx.Transaction(*args) for args in cu.fetchall()]
+
+    def tx_info(self, session, txuuid):
+        """See :class:`cubicweb.dbapi.Connection.transaction_info`"""
+        return tx.Transaction(txuuid, *self._tx_info(session, txuuid))
+
+    def tx_actions(self, session, txuuid, public):
+        """See :class:`cubicweb.dbapi.Connection.transaction_actions`"""
+        self._tx_info(session, txuuid)
+        restr = {'tx_uuid': txuuid}
+        if public:
+            restr['txa_public'] = True
+        # XXX use generator to avoid loading everything in memory?
+        sql = self.sqlgen.select('tx_entity_actions', restr,
+                                 ('txa_action', 'txa_public', 'txa_order',
+                                  'etype', 'eid', 'changes'))
+        cu = self.doexec(session, sql, restr)
+        actions = [tx.EntityAction(a,p,o,et,e,c and loads(self.binary_to_str(c)))
+                   for a,p,o,et,e,c in cu.fetchall()]
+        sql = self.sqlgen.select('tx_relation_actions', restr,
+                                 ('txa_action', 'txa_public', 'txa_order',
+                                  'rtype', 'eid_from', 'eid_to'))
+        cu = self.doexec(session, sql, restr)
+        actions += [tx.RelationAction(*args) for args in cu.fetchall()]
+        return sorted(actions, key=lambda x: x.order)
+
+    def undo_transaction(self, session, txuuid):
+        """See :class:`cubicweb.dbapi.Connection.undo_transaction`
+
+        important note: while undoing of a transaction, only hooks in the
+        'integrity', 'activeintegrity' and 'undo' categories are called.
+        """
+        # set mode so pool isn't released subsquently until commit/rollback
+        session.mode = 'write'
+        errors = []
+        session.transaction_data['undoing_uuid'] = txuuid
+        with hooks_control(session, session.HOOKS_DENY_ALL,
+                           'integrity', 'activeintegrity', 'undo'):
+            with security_enabled(session, read=False):
+                for action in reversed(self.tx_actions(session, txuuid, False)):
+                    undomethod = getattr(self, '_undo_%s' % action.action.lower())
+                    errors += undomethod(session, action)
+        # remove the transactions record
+        self.doexec(session,
+                    "DELETE FROM transactions WHERE tx_uuid='%s'" % txuuid)
+        return errors
+
+    def start_undoable_transaction(self, session, uuid):
+        """session callback to insert a transaction record in the transactions
+        table when some undoable transaction is started
+        """
+        ueid = session.user.eid
+        attrs = {'tx_uuid': uuid, 'tx_user': ueid, 'tx_time': datetime.now()}
+        self.doexec(session, self.sqlgen.insert('transactions', attrs), attrs)
+
+    def _save_attrs(self, session, entity, attrs):
+        """return a pickleable dictionary containing current values for given
+        attributes of the entity
+        """
+        restr = {'cw_eid': entity.eid}
+        sql = self.sqlgen.select(SQL_PREFIX + entity.__regid__, restr, attrs)
+        cu = self.doexec(session, sql, restr)
+        values = dict(zip(attrs, cu.fetchone()))
+        # ensure backend specific binary are converted back to string
+        eschema = entity.e_schema
+        for column in attrs:
+            # [3:] remove 'cw_' prefix
+            attr = column[3:]
+            if not eschema.subjrels[attr].final:
+                continue
+            if eschema.destination(attr) in ('Password', 'Bytes'):
+                value = values[column]
+                if value is not None:
+                    values[column] = self.binary_to_str(value)
+        return values
+
+    def _record_tx_action(self, session, table, action, **kwargs):
+        """record a transaction action in the given table (either
+        'tx_entity_actions' or 'tx_relation_action')
+        """
+        kwargs['tx_uuid'] = session.transaction_uuid()
+        kwargs['txa_action'] = action
+        kwargs['txa_order'] = session.transaction_inc_action_counter()
+        kwargs['txa_public'] = session.running_dbapi_query
+        self.doexec(session, self.sqlgen.insert(table, kwargs), kwargs)
+
+    def _tx_info(self, session, txuuid):
+        """return transaction's time and user of the transaction with the given uuid.
+
+        raise `NoSuchTransaction` if there is no such transaction of if the
+        session's user isn't allowed to see it.
+        """
+        restr = {'tx_uuid': txuuid}
+        sql = self.sqlgen.select('transactions', restr, ('tx_time', 'tx_user'))
+        cu = self.doexec(session, sql, restr)
+        try:
+            time, ueid = cu.fetchone()
+        except TypeError:
+            raise tx.NoSuchTransaction()
+        if not (session.user.is_in_group('managers')
+                or session.user.eid == ueid):
+            raise tx.NoSuchTransaction()
+        return time, ueid
+
+    def _undo_d(self, session, action):
+        """undo an entity deletion"""
+        errors = []
+        err = errors.append
+        eid = action.eid
+        etype = action.etype
+        _ = session._
+        # get an entity instance
+        try:
+            entity = self.repo.vreg['etypes'].etype_class(etype)(session)
+        except Exception:
+            err("can't restore entity %s of type %s, type no more supported"
+                % (eid, etype))
+            return errors
+        # check for schema changes, entities linked through inlined relation
+        # still exists, rewrap binary values
+        eschema = entity.e_schema
+        getrschema = eschema.subjrels
+        for column, value in action.changes.items():
+            rtype = column[3:] # remove cw_ prefix
+            try:
+                rschema = getrschema[rtype]
+            except KeyError:
+                err(_("Can't restore relation %(rtype)s of entity %(eid)s, "
+                      "this relation does not exists anymore in the schema.")
+                    % {'rtype': rtype, 'eid': eid})
+            if not rschema.final:
+                assert value is None
+            elif eschema.destination(rtype) in ('Bytes', 'Password'):
+                action.changes[column] = self._binary(value)
+                entity[rtype] = Binary(value)
+            elif isinstance(value, str):
+                entity[rtype] = unicode(value, session.encoding, 'replace')
+            else:
+                entity[rtype] = value
+        entity.set_eid(eid)
+        session.repo.init_entity_caches(session, entity, self)
+        entity.edited_attributes = set(entity)
+        entity.check()
+        self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
+        # restore the entity
+        action.changes['cw_eid'] = eid
+        sql = self.sqlgen.insert(SQL_PREFIX + etype, action.changes)
+        self.doexec(session, sql, action.changes)
+        # add explicitly is / is_instance_of whose deletion is not recorded for
+        # consistency with addition (done by sql in hooks)
+        self.doexec(session, 'INSERT INTO is_relation(eid_from, eid_to) '
+                    'VALUES(%s, %s)' % (eid, eschema_eid(session, eschema)))
+        for eschema in entity.e_schema.ancestors() + [entity.e_schema]:
+            self.doexec(session, 'INSERT INTO is_instance_of_relation(eid_from,'
+                        'eid_to) VALUES(%s, %s)' % (eid, eschema_eid(session, eschema)))
+        # restore record in entities (will update fti if needed)
+        self.add_info(session, entity, self, None, True)
+        # remove record from deleted_entities if entity's type is multi-sources
+        if entity.__regid__ in self.multisources_etypes:
+            self.doexec(session,
+                        'DELETE FROM deleted_entities WHERE eid=%s' % eid)
+        self.repo.hm.call_hooks('after_add_entity', session, entity=entity)
+        return errors
+
+    def _undo_r(self, session, action):
+        """undo a relation removal"""
+        errors = []
+        subj, rtype, obj = action.eid_from, action.rtype, action.eid_to
+        try:
+            sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj)
+        except UndoException, ex:
+            errors.append(unicode(ex))
+        else:
+            for role, entity in (('subject', sentity),
+                                 ('object', oentity)):
+                try:
+                    _undo_check_relation_target(entity, rdef, role)
+                except UndoException, ex:
+                    errors.append(unicode(ex))
+                    continue
+        if not errors:
+            self.repo.hm.call_hooks('before_add_relation', session,
+                                    eidfrom=subj, rtype=rtype, eidto=obj)
+            # add relation in the database
+            self._add_relation(session, subj, rtype, obj, rdef.rtype.inlined)
+            # set related cache
+            session.update_rel_cache_add(subj, rtype, obj, rdef.rtype.symmetric)
+            self.repo.hm.call_hooks('after_add_relation', session,
+                                    eidfrom=subj, rtype=rtype, eidto=obj)
+        return errors
+
+    def _undo_c(self, session, action):
+        """undo an entity creation"""
+        eid = action.eid
+        # XXX done to avoid fetching all remaining relation for the entity
+        # we should find an efficient way to do this (keeping current veolidf
+        # massive deletion performance)
+        if _undo_has_later_transaction(session, eid):
+            msg = session._('some later transaction(s) touch entity, undo them '
+                            'first')
+            raise ValidationError(eid, {None: msg})
+        etype = action.etype
+        # get an entity instance
+        try:
+            entity = self.repo.vreg['etypes'].etype_class(etype)(session)
+        except Exception:
+            return [session._(
+                "Can't undo creation of entity %(eid)s of type %(etype)s, type "
+                "no more supported" % {'eid': eid, 'etype': etype})]
+        entity.set_eid(eid)
+        # for proper eid/type cache update
+        hook.set_operation(session, 'pendingeids', eid,
+                           CleanupDeletedEidsCacheOp)
+        self.repo.hm.call_hooks('before_delete_entity', session, entity=entity)
+        # remove is / is_instance_of which are added using sql by hooks, hence
+        # unvisible as transaction action
+        self.doexec(session, 'DELETE FROM is_relation WHERE eid_from=%s' % eid)
+        self.doexec(session, 'DELETE FROM is_instance_of_relation WHERE eid_from=%s' % eid)
+        # XXX check removal of inlined relation?
+        # delete the entity
+        attrs = {'cw_eid': eid}
+        sql = self.sqlgen.delete(SQL_PREFIX + entity.__regid__, attrs)
+        self.doexec(session, sql, attrs)
+        # remove record from entities (will update fti if needed)
+        self.delete_info(session, entity, self.uri, None)
+        self.repo.hm.call_hooks('after_delete_entity', session, entity=entity)
+        return ()
+
+    def _undo_u(self, session, action):
+        """undo an entity update"""
+        return ['undoing of entity updating not yet supported.']
+
+    def _undo_a(self, session, action):
+        """undo a relation addition"""
+        errors = []
+        subj, rtype, obj = action.eid_from, action.rtype, action.eid_to
+        try:
+            sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj)
+        except UndoException, ex:
+            errors.append(unicode(ex))
+        else:
+            rschema = rdef.rtype
+            if rschema.inlined:
+                sql = 'SELECT 1 FROM cw_%s WHERE cw_eid=%s and cw_%s=%s'\
+                      % (sentity.__regid__, subj, rtype, obj)
+            else:
+                sql = 'SELECT 1 FROM %s_relation WHERE eid_from=%s and eid_to=%s'\
+                      % (rtype, subj, obj)
+            cu = self.doexec(session, sql)
+            if cu.fetchone() is None:
+                errors.append(session._(
+                    "Can't undo addition of relation %(rtype)s from %(subj)s to"
+                    " %(obj)s, doesn't exist anymore" % locals()))
+        if not errors:
+            self.repo.hm.call_hooks('before_delete_relation', session,
+                                    eidfrom=subj, rtype=rtype, eidto=obj)
+            # delete relation from the database
+            self._delete_relation(session, subj, rtype, obj, rschema.inlined)
+            # set related cache
+            session.update_rel_cache_del(subj, rtype, obj, rschema.symmetric)
+            self.repo.hm.call_hooks('after_delete_relation', session,
+                                    eidfrom=subj, rtype=rtype, eidto=obj)
+        return errors
+
+    # full text index handling #################################################
+
+    @cached
+    def need_fti_indexation(self, etype):
+        eschema = self.schema.eschema(etype)
+        if any(eschema.indexable_attributes()):
+            return True
+        if any(eschema.fulltext_containers()):
+            return True
+        return False
+
+    def index_entity(self, session, entity):
+        """create an operation to [re]index textual content of the given entity
+        on commit
+        """
+        hook.set_operation(session, 'ftindex', entity.eid, FTIndexEntityOp)
+
+    def fti_unindex_entity(self, session, eid):
+        """remove text content for entity with the given eid from the full text
+        index
+        """
+        try:
+            self.dbhelper.cursor_unindex_object(eid, session.pool['system'])
+        except Exception: # let KeyboardInterrupt / SystemExit propagate
+            self.exception('error while unindexing %s', eid)
+
+    def fti_index_entity(self, session, entity):
+        """add text content of a created/modified entity to the full text index
+        """
+        self.debug('reindexing %r', entity.eid)
+        try:
+            # use cursor_index_object, not cursor_reindex_object since
+            # unindexing done in the FTIndexEntityOp
+            self.dbhelper.cursor_index_object(entity.eid, entity,
+                                              session.pool['system'])
+        except Exception: # let KeyboardInterrupt / SystemExit propagate
+            self.exception('error while reindexing %s', entity)
+
+
+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
+    triggered on precommit, not commit, and this should be done after other
+    precommit operation which may add relations to the entity
+    """
+
+    def precommit_event(self):
+        session = self.session
+        source = session.repo.system_source
+        pendingeids = session.transaction_data.get('pendingeids', ())
+        done = session.transaction_data.setdefault('indexedeids', set())
+        for eid in session.transaction_data.pop('ftindex', ()):
+            if eid in pendingeids or eid in done:
+                # entity added and deleted in the same transaction or already
+                # processed
+                return
+            done.add(eid)
+            for container in session.entity_from_eid(eid).fti_containers():
+                source.fti_unindex_entity(session, container.eid)
+                source.fti_index_entity(session, container)
+
 
 def sql_schema(driver):
-    helper = get_adv_func_helper(driver)
-    tstamp_col_type = helper.TYPE_MAPPING['Datetime']
+    helper = get_db_helper(driver)
+    typemap = helper.TYPE_MAPPING
     schema = """
 /* Create the repository's system database */
 
@@ -602,10 +1176,10 @@
   source VARCHAR(64) NOT NULL,
   mtime %s NOT NULL,
   extid VARCHAR(256)
-);
-CREATE INDEX entities_type_idx ON entities(type);
-CREATE INDEX entities_mtime_idx ON entities(mtime);
-CREATE INDEX entities_extid_idx ON entities(extid);
+);;
+CREATE INDEX entities_type_idx ON entities(type);;
+CREATE INDEX entities_mtime_idx ON entities(mtime);;
+CREATE INDEX entities_extid_idx ON entities(extid);;
 
 CREATE TABLE deleted_entities (
   eid INTEGER PRIMARY KEY NOT NULL,
@@ -613,32 +1187,80 @@
   source VARCHAR(64) NOT NULL,
   dtime %s NOT NULL,
   extid VARCHAR(256)
-);
-CREATE INDEX deleted_entities_type_idx ON deleted_entities(type);
-CREATE INDEX deleted_entities_dtime_idx ON deleted_entities(dtime);
-CREATE INDEX deleted_entities_extid_idx ON deleted_entities(extid);
-""" % (helper.sql_create_sequence('entities_id_seq'), tstamp_col_type, tstamp_col_type)
+);;
+CREATE INDEX deleted_entities_type_idx ON deleted_entities(type);;
+CREATE INDEX deleted_entities_dtime_idx ON deleted_entities(dtime);;
+CREATE INDEX deleted_entities_extid_idx ON deleted_entities(extid);;
+
+CREATE TABLE transactions (
+  tx_uuid CHAR(32) PRIMARY KEY NOT NULL,
+  tx_user INTEGER NOT NULL,
+  tx_time %s NOT NULL
+);;
+CREATE INDEX transactions_tx_user_idx ON transactions(tx_user);;
+
+CREATE TABLE tx_entity_actions (
+  tx_uuid CHAR(32) REFERENCES transactions(tx_uuid) ON DELETE CASCADE,
+  txa_action CHAR(1) NOT NULL,
+  txa_public %s NOT NULL,
+  txa_order INTEGER,
+  eid INTEGER NOT NULL,
+  etype VARCHAR(64) NOT NULL,
+  changes %s
+);;
+CREATE INDEX tx_entity_actions_txa_action_idx ON tx_entity_actions(txa_action);;
+CREATE INDEX tx_entity_actions_txa_public_idx ON tx_entity_actions(txa_public);;
+CREATE INDEX tx_entity_actions_eid_idx ON tx_entity_actions(eid);;
+CREATE INDEX tx_entity_actions_etype_idx ON tx_entity_actions(etype);;
+
+CREATE TABLE tx_relation_actions (
+  tx_uuid CHAR(32) REFERENCES transactions(tx_uuid) ON DELETE CASCADE,
+  txa_action CHAR(1) NOT NULL,
+  txa_public %s NOT NULL,
+  txa_order INTEGER,
+  eid_from INTEGER NOT NULL,
+  eid_to INTEGER NOT NULL,
+  rtype VARCHAR(256) NOT NULL
+);;
+CREATE INDEX tx_relation_actions_txa_action_idx ON tx_relation_actions(txa_action);;
+CREATE INDEX tx_relation_actions_txa_public_idx ON tx_relation_actions(txa_public);;
+CREATE INDEX tx_relation_actions_eid_from_idx ON tx_relation_actions(eid_from);;
+CREATE INDEX tx_relation_actions_eid_to_idx ON tx_relation_actions(eid_to);;
+""" % (helper.sql_create_sequence('entities_id_seq').replace(';', ';;'),
+       typemap['Datetime'], typemap['Datetime'], typemap['Datetime'],
+       typemap['Boolean'], typemap['Bytes'], typemap['Boolean'])
+    if helper.backend_name == 'sqlite':
+        # sqlite support the ON DELETE CASCADE syntax but do nothing
+        schema += '''
+CREATE TRIGGER fkd_transactions
+BEFORE DELETE ON transactions
+FOR EACH ROW BEGIN
+    DELETE FROM tx_entity_actions WHERE tx_uuid=OLD.tx_uuid;
+    DELETE FROM tx_relation_actions WHERE tx_uuid=OLD.tx_uuid;
+END;;
+'''
     return schema
 
 
 def sql_drop_schema(driver):
-    helper = get_adv_func_helper(driver)
+    helper = get_db_helper(driver)
     return """
 %s
 DROP TABLE entities;
 DROP TABLE deleted_entities;
+DROP TABLE transactions;
+DROP TABLE tx_entity_actions;
+DROP TABLE tx_relation_actions;
 """ % helper.sql_drop_sequence('entities_id_seq')
 
 
 def grant_schema(user, set_owner=True):
     result = ''
-    if set_owner:
-        result = 'ALTER TABLE entities OWNER TO %s;\n' % user
-        result += 'ALTER TABLE deleted_entities OWNER TO %s;\n' % user
-        result += 'ALTER TABLE entities_id_seq OWNER TO %s;\n' % user
-    result += 'GRANT ALL ON entities TO %s;\n' % user
-    result += 'GRANT ALL ON deleted_entities TO %s;\n' % user
-    result += 'GRANT ALL ON entities_id_seq TO %s;\n' % user
+    for table in ('entities', 'deleted_entities', 'entities_id_seq',
+                  'transactions', 'tx_entity_actions', 'tx_relation_actions'):
+        if set_owner:
+            result = 'ALTER TABLE %s OWNER TO %s;\n' % (table, user)
+        result += 'GRANT ALL ON %s TO %s;\n' % (table, user)
     return result
 
 
--- a/server/sources/pyrorql.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/sources/pyrorql.py	Tue Apr 06 19:46:38 2010 +0200
@@ -182,6 +182,7 @@
         self._query_cache.clear()
         repo = self.repo
         session = repo.internal_session()
+        source = repo.system_source
         try:
             for etype, extid in modified:
                 try:
@@ -191,7 +192,7 @@
                         rset = session.eid_rset(eid, etype)
                         entity = rset.get_entity(0, 0)
                         entity.complete(entity.e_schema.indexable_attributes())
-                        repo.index_entity(session, entity)
+                        source.index_entity(session, entity)
                 except:
                     self.exception('while updating %s with external id %s of source %s',
                                    etype, extid, self.uri)
@@ -202,7 +203,8 @@
                                          insert=False)
                     # entity has been deleted from external repository but is not known here
                     if eid is not None:
-                        repo.delete_info(session, eid)
+                        entity = session.entity_from_eid(eid, etype)
+                        repo.delete_info(session, entity, self.uri, extid)
                 except:
                     self.exception('while updating %s with external id %s of source %s',
                                    etype, extid, self.uri)
@@ -349,11 +351,11 @@
         self._query_cache.clear()
         entity.clear_all_caches()
 
-    def delete_entity(self, session, etype, eid):
+    def delete_entity(self, session, entity):
         """delete an entity from the source"""
         cu = session.pool[self.uri]
-        cu.execute('DELETE %s X WHERE X eid %%(x)s' % etype,
-                   {'x': self.eid2extid(eid, session)}, 'x')
+        cu.execute('DELETE %s X WHERE X eid %%(x)s' % entity.__regid__,
+                   {'x': self.eid2extid(entity.eid, session)}, 'x')
         self._query_cache.clear()
 
     def add_relation(self, session, subject, rtype, object):
--- a/server/sources/rql2sql.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/sources/rql2sql.py	Tue Apr 06 19:46:38 2010 +0200
@@ -33,16 +33,30 @@
 
 import threading
 
+from logilab.database import FunctionDescr, SQL_FUNCTIONS_REGISTRY
+
 from rql import BadRQLQuery, CoercionError
 from rql.stmts import Union, Select
 from rql.nodes import (SortTerm, VariableRef, Constant, Function, Not,
                        Variable, ColumnAlias, Relation, SubQuery, Exists)
 
+from cubicweb import QueryError
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.utils import cleanup_solutions
 
 ColumnAlias._q_invariant = False # avoid to check for ColumnAlias / Variable
 
+FunctionDescr.source_execute = None
+
+def default_update_cb_stack(self, stack):
+    stack.append(self.source_execute)
+FunctionDescr.update_cb_stack = default_update_cb_stack
+
+LENGTH = SQL_FUNCTIONS_REGISTRY.get_function('LENGTH')
+def length_source_execute(source, value):
+    return len(value.getvalue())
+LENGTH.source_execute = length_source_execute
+
 def _new_var(select, varname):
     newvar = select.get_variable(varname)
     if not 'relations' in newvar.stinfo:
@@ -252,14 +266,44 @@
                         selectedidx.append(vref.name)
                         rqlst.selection.append(vref)
 
-# IGenerator implementation for RQL->SQL ######################################
+def iter_mapped_var_sels(stmt, variable):
+    # variable is a Variable or ColumnAlias node mapped to a source side
+    # callback
+    if not (len(variable.stinfo['rhsrelations']) <= 1 and # < 1 on column alias
+            variable.stinfo['selected']):
+        raise QueryError("can't use %s as a restriction variable"
+                         % variable.name)
+    for selectidx in variable.stinfo['selected']:
+        vrefs = stmt.selection[selectidx].get_nodes(VariableRef)
+        if len(vrefs) != 1:
+            raise QueryError()
+        yield selectidx, vrefs[0]
 
+def update_source_cb_stack(state, stmt, node, stack):
+    while True:
+        node = node.parent
+        if node is stmt:
+            break
+        if not isinstance(node, Function):
+            raise QueryError()
+        func = SQL_FUNCTIONS_REGISTRY.get_function(node.name)
+        if func.source_execute is None:
+            raise QueryError('%s can not be called on mapped attribute'
+                             % node.name)
+        state.source_cb_funcs.add(node)
+        func.update_cb_stack(stack)
+
+
+# IGenerator implementation for RQL->SQL #######################################
 
 class StateInfo(object):
     def __init__(self, existssols, unstablevars):
         self.existssols = existssols
         self.unstablevars = unstablevars
         self.subtables = {}
+        self.needs_source_cb = None
+        self.subquery_source_cb = None
+        self.source_cb_funcs = set()
 
     def reset(self, solution):
         """reset some visit variables"""
@@ -276,6 +320,17 @@
         self.restrictions = []
         self._restr_stack = []
         self.ignore_varmap = False
+        self._needs_source_cb = {}
+
+    def merge_source_cbs(self, needs_source_cb):
+        if self.needs_source_cb is None:
+            self.needs_source_cb = needs_source_cb
+        elif needs_source_cb != self.needs_source_cb:
+            raise QueryError('query fetch some source mapped attribute, some not')
+
+    def finalize_source_cbs(self):
+        if self.subquery_source_cb is not None:
+            self.needs_source_cb.update(self.subquery_source_cb)
 
     def add_restriction(self, restr):
         if restr:
@@ -332,15 +387,18 @@
     protected by a lock
     """
 
-    def __init__(self, schema, dbms_helper, dbencoding='UTF-8', attrmap=None):
+    def __init__(self, schema, dbhelper, attrmap=None):
         self.schema = schema
-        self.dbms_helper = dbms_helper
-        self.dbencoding = dbencoding
-        self.keyword_map = {'NOW' : self.dbms_helper.sql_current_timestamp,
-                            'TODAY': self.dbms_helper.sql_current_date,
+        self.dbhelper = dbhelper
+        self.dbencoding = dbhelper.dbencoding
+        self.keyword_map = {'NOW' : self.dbhelper.sql_current_timestamp,
+                            'TODAY': self.dbhelper.sql_current_date,
                             }
-        if not self.dbms_helper.union_parentheses_support:
+        if not self.dbhelper.union_parentheses_support:
             self.union_sql = self.noparen_union_sql
+        if self.dbhelper.fti_need_distinct:
+            self.__union_sql = self.union_sql
+            self.union_sql = self.has_text_need_distinct_union_sql
         self._lock = threading.Lock()
         if attrmap is None:
             attrmap = {}
@@ -370,10 +428,16 @@
             # union query for each rqlst / solution
             sql = self.union_sql(union)
             # we are done
-            return sql, self._query_attrs
+            return sql, self._query_attrs, self._state.needs_source_cb
         finally:
             self._lock.release()
 
+    def has_text_need_distinct_union_sql(self, union, needalias=False):
+        if getattr(union, 'has_text_query', False):
+            for select in union.children:
+                select.need_distinct = True
+        return self.__union_sql(union, needalias)
+
     def union_sql(self, union, needalias=False): # pylint: disable-msg=E0202
         if len(union.children) == 1:
             return self.select_sql(union.children[0], needalias)
@@ -382,9 +446,10 @@
         return '\nUNION ALL\n'.join(sqls)
 
     def noparen_union_sql(self, union, needalias=False):
-        # needed for sqlite backend which doesn't like parentheses around
-        # union query. This may cause bug in some condition (sort in one of
-        # the subquery) but will work in most case
+        # needed for sqlite backend which doesn't like parentheses around union
+        # query. This may cause bug in some condition (sort in one of the
+        # subquery) but will work in most case
+        #
         # see http://www.sqlite.org/cvstrac/tktview?tn=3074
         sqls = (self.select_sql(select, needalias)
                 for i, select in enumerate(union.children))
@@ -426,6 +491,9 @@
         else:
             existssols, unstable = {}, ()
         state = StateInfo(existssols, unstable)
+        if self._state is not None:
+            # state from a previous unioned select
+            state.merge_source_cbs(self._state.needs_source_cb)
         # treat subqueries
         self._subqueries_sql(select, state)
         # generate sql for this select node
@@ -481,6 +549,7 @@
                 if fneedwrap:
                     selection = ['T1.C%s' % i for i in xrange(len(origselection))]
                     sql = 'SELECT %s FROM (%s) AS T1' % (','.join(selection), sql)
+            state.finalize_source_cbs()
         finally:
             select.selection = origselection
         # limit / offset
@@ -495,13 +564,24 @@
     def _subqueries_sql(self, select, state):
         for i, subquery in enumerate(select.with_):
             sql = self.union_sql(subquery.query, needalias=True)
-            tablealias = '_T%s' % i
+            tablealias = '_T%s' % i # XXX nested subqueries
             sql = '(%s) AS %s' % (sql, tablealias)
             state.subtables[tablealias] = (0, sql)
+            latest_state = self._state
             for vref in subquery.aliases:
                 alias = vref.variable
                 alias._q_sqltable = tablealias
                 alias._q_sql = '%s.C%s' % (tablealias, alias.colnum)
+                try:
+                    stack = latest_state.needs_source_cb[alias.colnum]
+                    if state.subquery_source_cb is None:
+                        state.subquery_source_cb = {}
+                    for selectidx, vref in iter_mapped_var_sels(select, alias):
+                        stack = stack[:]
+                        update_source_cb_stack(state, select, vref, stack)
+                        state.subquery_source_cb[selectidx] = stack
+                except KeyError:
+                    continue
 
     def _solutions_sql(self, select, solutions, distinct, needalias):
         sqls = []
@@ -513,17 +593,18 @@
             sql = [self._selection_sql(select.selection, distinct, needalias)]
             if self._state.restrictions:
                 sql.append('WHERE %s' % ' AND '.join(self._state.restrictions))
+            self._state.merge_source_cbs(self._state._needs_source_cb)
             # add required tables
             assert len(self._state.actual_tables) == 1, self._state.actual_tables
             tables = self._state.actual_tables[-1]
             if tables:
                 # sort for test predictability
                 sql.insert(1, 'FROM %s' % ', '.join(sorted(tables)))
-            elif self._state.restrictions and self.dbms_helper.needs_from_clause:
+            elif self._state.restrictions and self.dbhelper.needs_from_clause:
                 sql.insert(1, 'FROM (SELECT 1) AS _T')
             sqls.append('\n'.join(sql))
         if select.need_intersect:
-            #if distinct or not self.dbms_helper.intersect_all_support:
+            #if distinct or not self.dbhelper.intersect_all_support:
             return '\nINTERSECT\n'.join(sqls)
             #else:
             #    return '\nINTERSECT ALL\n'.join(sqls)
@@ -885,7 +966,13 @@
             except KeyError:
                 mapkey = '%s.%s' % (self._state.solution[lhs.name], rel.r_type)
                 if mapkey in self.attr_map:
-                    lhssql = self.attr_map[mapkey](self, lhs.variable, rel)
+                    cb, sourcecb = self.attr_map[mapkey]
+                    if sourcecb:
+                        # callback is a source callback, we can't use this
+                        # attribute in restriction
+                        raise QueryError("can't use %s (%s) in restriction"
+                                         % (mapkey, rel.as_string()))
+                    lhssql = cb(self, lhs.variable, rel)
                 elif rel.r_type == 'eid':
                     lhssql = lhs.variable._q_sql
                 else:
@@ -934,7 +1021,7 @@
             not_ = True
         else:
             not_ = False
-        return self.dbms_helper.fti_restriction_sql(alias, const.eval(self._args),
+        return self.dbhelper.fti_restriction_sql(alias, const.eval(self._args),
                                                     jointo, not_) + restriction
 
     def visit_comparison(self, cmp):
@@ -947,7 +1034,7 @@
             rhs = cmp.children[0]
         operator = cmp.operator
         if operator in ('IS', 'LIKE', 'ILIKE'):
-            if operator == 'ILIKE' and not self.dbms_helper.ilike_support:
+            if operator == 'ILIKE' and not self.dbhelper.ilike_support:
                 operator = ' LIKE '
             else:
                 operator = ' %s ' % operator
@@ -977,10 +1064,13 @@
 
     def visit_function(self, func):
         """generate SQL name for a function"""
-        # function_description will check function is supported by the backend
-        sqlname = self.dbms_helper.func_sqlname(func.name)
-        return '%s(%s)' % (sqlname, ', '.join(c.accept(self)
-                                              for c in func.children))
+        args = [c.accept(self) for c in func.children]
+        if func in self._state.source_cb_funcs:
+            # function executed as a callback on the source
+            assert len(args) == 1
+            return args[0]
+        # func_as_sql will check function is supported by the backend
+        return self.dbhelper.func_as_sql(func.name, args)
 
     def visit_constant(self, constant):
         """generate SQL name for a constant"""
@@ -995,7 +1085,7 @@
                 rel._q_needcast = value
             return self.keyword_map[value]()
         if constant.type == 'Boolean':
-            value = self.dbms_helper.boolean_value(value)
+            value = self.dbhelper.boolean_value(value)
         if constant.type == 'Substitute':
             _id = constant.value
             if isinstance(_id, unicode):
@@ -1057,7 +1147,7 @@
                     self._state.add_restriction(restr)
             elif principal.r_type == 'has_text':
                 sql = '%s.%s' % (self._fti_table(principal),
-                                 self.dbms_helper.fti_uid_attr)
+                                 self.dbhelper.fti_uid_attr)
             elif principal in variable.stinfo['rhsrelations']:
                 if self.schema.rschema(principal.r_type).inlined:
                     sql = self._linked_var_sql(variable)
@@ -1147,12 +1237,20 @@
         if isinstance(linkedvar, ColumnAlias):
             raise BadRQLQuery('variable %s should be selected by the subquery'
                               % variable.name)
-        mapkey = '%s.%s' % (self._state.solution[linkedvar.name], rel.r_type)
-        if mapkey in self.attr_map:
-            return self.attr_map[mapkey](self, linkedvar, rel)
         try:
             sql = self._varmap['%s.%s' % (linkedvar.name, rel.r_type)]
         except KeyError:
+            mapkey = '%s.%s' % (self._state.solution[linkedvar.name], rel.r_type)
+            if mapkey in self.attr_map:
+                cb, sourcecb = self.attr_map[mapkey]
+                if not sourcecb:
+                    return cb(self, linkedvar, rel)
+                # attribute mapped at the source level (bfss for instance)
+                stmt = rel.stmt
+                for selectidx, vref in iter_mapped_var_sels(stmt, variable):
+                    stack = [cb]
+                    update_source_cb_stack(self._state, stmt, vref, stack)
+                    self._state._needs_source_cb[selectidx] = stack
             linkedvar.accept(self)
             sql = '%s.%s%s' % (linkedvar._q_sqltable, SQL_PREFIX, rel.r_type)
         return sql
@@ -1259,7 +1357,7 @@
             except AttributeError:
                 pass
         self._state.done.add(relation)
-        alias = self.alias_and_add_table(self.dbms_helper.fti_table)
+        alias = self.alias_and_add_table(self.dbhelper.fti_table)
         relation._q_sqltable = alias
         return alias
 
--- a/server/sources/storages.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/sources/storages.py	Tue Apr 06 19:46:38 2010 +0200
@@ -4,22 +4,38 @@
 from cubicweb import Binary
 from cubicweb.server.hook import Operation
 
-
-ETYPE_ATTR_STORAGE = {}
 def set_attribute_storage(repo, etype, attr, storage):
-    ETYPE_ATTR_STORAGE.setdefault(etype, {})[attr] = storage
-    repo.system_source.map_attribute(etype, attr, storage.sqlgen_callback)
+    repo.system_source.set_storage(etype, attr, storage)
 
 def unset_attribute_storage(repo, etype, attr):
-    ETYPE_ATTR_STORAGE.setdefault(etype, {}).pop(attr, None)
-    repo.system_source.unmap_attribute(etype, attr)
-
+    repo.system_source.unset_storage(etype, attr)
 
 class Storage(object):
-    """abstract storage"""
-    def sqlgen_callback(self, generator, relation, linkedvar):
-        """sql generator callback when some attribute with a custom storage is
-        accessed
+    """abstract storage
+
+    * If `source_callback` is true (by default), the callback will be run during
+      query result process of fetched attribute's valu and should have the
+      following prototype::
+
+        callback(self, source, value)
+
+      where `value` is the value actually stored in the backend. None values
+      will be skipped (eg callback won't be called).
+
+    * if `source_callback` is false, the callback will be run during sql
+      generation when some attribute with a custom storage is accessed and
+      should have the following prototype::
+
+        callback(self, generator, relation, linkedvar)
+
+      where `generator` is the sql generator, `relation` the current rql syntax
+      tree relation and linkedvar the principal syntax tree variable holding the
+      attribute.
+    """
+    is_source_callback = True
+
+    def callback(self, *args):
+        """see docstring for prototype, which vary according to is_source_callback
         """
         raise NotImplementedError()
 
@@ -43,39 +59,36 @@
     def __init__(self, defaultdir):
         self.default_directory = defaultdir
 
-    def sqlgen_callback(self, generator, linkedvar, relation):
+    def callback(self, source, value):
         """sql generator callback when some attribute with a custom storage is
         accessed
         """
-        linkedvar.accept(generator)
-        return '_fsopen(%s.cw_%s)' % (
-            linkedvar._q_sql.split('.', 1)[0], # table name
-            relation.r_type) # attribute name
+        fpath = source.binary_to_str(value)
+        try:
+            return Binary(file(fpath).read())
+        except OSError, ex:
+            source.critical("can't open %s: %s", value, ex)
+            return None
 
     def entity_added(self, entity, attr):
         """an entity using this storage for attr has been added"""
-        if not entity._cw.transaction_data.get('fs_importing'):
-            try:
-                value = entity.pop(attr)
-            except KeyError:
-                pass
-            else:
-                fpath = self.new_fs_path(entity, attr)
-                # bytes storage used to store file's path
-                entity[attr] = Binary(fpath)
-                file(fpath, 'w').write(value.getvalue())
-                AddFileOp(entity._cw, filepath=fpath)
-        # else entity[attr] is expected to be an already existant file path
+        if entity._cw.transaction_data.get('fs_importing'):
+            binary = Binary(file(entity[attr].getvalue()).read())
+        else:
+            binary = entity.pop(attr)
+            fpath = self.new_fs_path(entity, attr)
+            # bytes storage used to store file's path
+            entity[attr] = Binary(fpath)
+            file(fpath, 'w').write(binary.getvalue())
+            AddFileOp(entity._cw, filepath=fpath)
+        return binary
 
     def entity_updated(self, entity, attr):
         """an entity using this storage for attr has been updatded"""
-        try:
-            value = entity.pop(attr)
-        except KeyError:
-            pass
-        else:
-            fpath = self.current_fs_path(entity, attr)
-            UpdateFileOp(entity._cw, filepath=fpath, filedata=value.getvalue())
+        binary = entity.pop(attr)
+        fpath = self.current_fs_path(entity, attr)
+        UpdateFileOp(entity._cw, filepath=fpath, filedata=binary.getvalue())
+        return binary
 
     def entity_deleted(self, entity, attr):
         """an entity using this storage for attr has been deleted"""
@@ -92,9 +105,11 @@
         cu = sysource.doexec(entity._cw,
                              'SELECT cw_%s FROM cw_%s WHERE cw_eid=%s' % (
                                  attr, entity.__regid__, entity.eid))
-        dbmod = sysource.dbapi_module
-        return dbmod.process_value(cu.fetchone()[0], [None, dbmod.BINARY],
-                                   binarywrap=str)
+        rawvalue = cu.fetchone()[0]
+        if rawvalue is None: # no previous value
+            return self.new_fs_path(entity, attr)
+        return sysource._process_value(rawvalue, cu.description[0],
+                                       binarywrap=str)
 
 
 class AddFileOp(Operation):
--- a/server/sqlutils.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/sqlutils.py	Tue Apr 06 19:46:38 2010 +0200
@@ -11,21 +11,17 @@
 import subprocess
 from datetime import datetime, date
 
-import logilab.common as lgc
-from logilab.common import db
+from logilab import database as db, common as lgc
 from logilab.common.shellutils import ProgressBar
-from logilab.common.adbh import get_adv_func_helper
-from logilab.common.sqlgen import SQLGenerator
 from logilab.common.date import todate, todatetime
-
-from indexer import get_indexer
+from logilab.database.sqlgen import SQLGenerator
 
 from cubicweb import Binary, ConfigurationError
 from cubicweb.uilib import remove_html_tags
 from cubicweb.schema import PURE_VIRTUAL_RTYPES
 from cubicweb.server import SQL_CONNECT_HOOKS
 from cubicweb.server.utils import crypt_password
-
+from rql.utils import RQL_FUNCTIONS_REGISTRY
 
 lgc.USE_MX_DATETIME = False
 SQL_PREFIX = 'cw_'
@@ -77,8 +73,8 @@
     w(native.grant_schema(user, set_owner))
     w('')
     if text_index:
-        indexer = get_indexer(driver)
-        w(indexer.sql_grant_user(user))
+        dbhelper = db.get_db_helper(driver)
+        w(dbhelper.sql_grant_user_on_fti(user))
         w('')
     w(grant_schema(schema, user, set_owner, skip_entities=skip_entities, prefix=SQL_PREFIX))
     return '\n'.join(output)
@@ -96,17 +92,17 @@
     w = output.append
     w(native.sql_schema(driver))
     w('')
+    dbhelper = db.get_db_helper(driver)
     if text_index:
-        indexer = get_indexer(driver)
-        w(indexer.sql_init_fti())
+        w(dbhelper.sql_init_fti().replace(';', ';;'))
         w('')
-    dbhelper = get_adv_func_helper(driver)
     w(schema2sql(dbhelper, schema, prefix=SQL_PREFIX,
-                 skip_entities=skip_entities, skip_relations=skip_relations))
+                 skip_entities=skip_entities,
+                 skip_relations=skip_relations).replace(';', ';;'))
     if dbhelper.users_support and user:
         w('')
         w(sqlgrants(schema, driver, user, text_index, set_owner,
-                    skip_relations, skip_entities))
+                    skip_relations, skip_entities).replace(';', ';;'))
     return '\n'.join(output)
 
 
@@ -120,8 +116,8 @@
     w(native.sql_drop_schema(driver))
     w('')
     if text_index:
-        indexer = get_indexer(driver)
-        w(indexer.sql_drop_fti())
+        dbhelper = db.get_db_helper(driver)
+        w(dbhelper.sql_drop_fti())
         w('')
     w(dropschema2sql(schema, prefix=SQL_PREFIX,
                      skip_entities=skip_entities,
@@ -137,55 +133,42 @@
     def __init__(self, source_config):
         try:
             self.dbdriver = source_config['db-driver'].lower()
-            self.dbname = source_config['db-name']
+            dbname = source_config['db-name']
         except KeyError:
             raise ConfigurationError('missing some expected entries in sources file')
-        self.dbhost = source_config.get('db-host')
+        dbhost = source_config.get('db-host')
         port = source_config.get('db-port')
-        self.dbport = port and int(port) or None
-        self.dbuser = source_config.get('db-user')
-        self.dbpasswd = source_config.get('db-password')
-        self.encoding = source_config.get('db-encoding', 'UTF-8')
-        self.dbapi_module = db.get_dbapi_compliant_module(self.dbdriver)
-        self.dbdriver_extra_args = source_config.get('db-extra-arguments')
-        self.binary = self.dbapi_module.Binary
-        self.dbhelper = self.dbapi_module.adv_func_helper
+        dbport = port and int(port) or None
+        dbuser = source_config.get('db-user')
+        dbpassword = source_config.get('db-password')
+        dbencoding = source_config.get('db-encoding', 'UTF-8')
+        dbextraargs = source_config.get('db-extra-arguments')
+        self.dbhelper = db.get_db_helper(self.dbdriver)
+        self.dbhelper.record_connection_info(dbname, dbhost, dbport, dbuser,
+                                             dbpassword, dbextraargs,
+                                             dbencoding)
         self.sqlgen = SQLGenerator()
+        # copy back some commonly accessed attributes
+        dbapi_module = self.dbhelper.dbapi_module
+        self.OperationalError = dbapi_module.OperationalError
+        self.InterfaceError = dbapi_module.InterfaceError
+        self._binary = dbapi_module.Binary
+        self._process_value = dbapi_module.process_value
+        self._dbencoding = dbencoding
 
-    def get_connection(self, user=None, password=None):
+    def get_connection(self):
         """open and return a connection to the database"""
-        if user or self.dbuser:
-            self.info('connecting to %s@%s for user %s', self.dbname,
-                      self.dbhost or 'localhost', user or self.dbuser)
-        else:
-            self.info('connecting to %s@%s', self.dbname,
-                      self.dbhost or 'localhost')
-        extra = {}
-        if self.dbdriver_extra_args:
-            extra = {'extra_args': self.dbdriver_extra_args}
-        cnx = self.dbapi_module.connect(self.dbhost, self.dbname,
-                                        user or self.dbuser,
-                                        password or self.dbpasswd,
-                                        port=self.dbport,
-                                        **extra)
-        init_cnx(self.dbdriver, cnx)
-        #self.dbapi_module.type_code_test(cnx.cursor())
-        return cnx
+        return self.dbhelper.get_connection()
 
-    def backup_to_file(self, backupfile):
-        for cmd in self.dbhelper.backup_commands(self.dbname, self.dbhost,
-                                                 self.dbuser, backupfile,
-                                                 dbport=self.dbport,
+    def backup_to_file(self, backupfile, confirm):
+        for cmd in self.dbhelper.backup_commands(backupfile,
                                                  keepownership=False):
             if _run_command(cmd):
                 if not confirm('   [Failed] Continue anyway?', default='n'):
                     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,
-                                                  self.dbuser, backupfile,
-                                                  self.encoding,
-                                                  dbport=self.dbport,
+        for cmd in self.dbhelper.restore_commands(backupfile,
                                                   keepownership=False,
                                                   drop=drop):
             if _run_command(cmd):
@@ -198,20 +181,30 @@
             for key, val in args.iteritems():
                 # convert cubicweb binary into db binary
                 if isinstance(val, Binary):
-                    val = self.binary(val.getvalue())
+                    val = self._binary(val.getvalue())
                 newargs[key] = val
             # should not collide
             newargs.update(query_args)
             return newargs
         return query_args
 
-    def process_result(self, cursor):
+    def process_result(self, cursor, column_callbacks=None):
         """return a list of CubicWeb compliant values from data in the given cursor
         """
+        # use two different implementations to avoid paying the price of
+        # callback lookup for each *cell* in results when there is nothing to
+        # lookup
+        if not column_callbacks:
+            return self._process_result(cursor)
+        return self._cb_process_result(cursor, column_callbacks)
+
+    def _process_result(self, cursor, column_callbacks=None):
+        # begin bind to locals for optimization
         descr = cursor.description
-        encoding = self.encoding
-        process_value = self.dbapi_module.process_value
+        encoding = self._dbencoding
+        process_value = self._process_value
         binary = Binary
+        # /end
         results = cursor.fetchall()
         for i, line in enumerate(results):
             result = []
@@ -223,13 +216,38 @@
             results[i] = result
         return results
 
+    def _cb_process_result(self, cursor, column_callbacks):
+        # begin bind to locals for optimization
+        descr = cursor.description
+        encoding = self._dbencoding
+        process_value = self._process_value
+        binary = Binary
+        # /end
+        results = cursor.fetchall()
+        for i, line in enumerate(results):
+            result = []
+            for col, value in enumerate(line):
+                if value is None:
+                    result.append(value)
+                    continue
+                cbstack = column_callbacks.get(col, None)
+                if cbstack is None:
+                    value = process_value(value, descr[col], encoding, binary)
+                else:
+                    for cb in cbstack:
+                        value = cb(self, value)
+                result.append(value)
+            results[i] = result
+        return results
+
     def preprocess_entity(self, entity):
         """return a dictionary to use as extra argument to cursor.execute
         to insert/update an entity into a SQL database
         """
         attrs = {}
         eschema = entity.e_schema
-        for attr, value in entity.items():
+        for attr in entity.edited_attributes:
+            value = entity[attr]
             rschema = eschema.subjrels[attr]
             if rschema.final:
                 atype = str(entity.e_schema.destination(attr))
@@ -242,15 +260,16 @@
                         value = value.getvalue()
                     else:
                         value = crypt_password(value)
-                    value = self.binary(value)
+                    value = self._binary(value)
                 # XXX needed for sqlite but I don't think it is for other backends
                 elif atype == 'Datetime' and isinstance(value, date):
                     value = todatetime(value)
                 elif atype == 'Date' and isinstance(value, datetime):
                     value = todate(value)
                 elif isinstance(value, Binary):
-                    value = self.binary(value.getvalue())
+                    value = self._binary(value.getvalue())
             attrs[SQL_PREFIX+str(attr)] = value
+        attrs[SQL_PREFIX+'eid'] = entity.eid
         return attrs
 
 
@@ -259,12 +278,8 @@
 set_log_methods(SQLAdapterMixIn, getLogger('cubicweb.sqladapter'))
 
 def init_sqlite_connexion(cnx):
-    # XXX should not be publicly exposed
-    #def comma_join(strings):
-    #    return ', '.join(strings)
-    #cnx.create_function("COMMA_JOIN", 1, comma_join)
 
-    class concat_strings(object):
+    class group_concat(object):
         def __init__(self):
             self.values = []
         def step(self, value):
@@ -272,10 +287,7 @@
                 self.values.append(value)
         def finalize(self):
             return ', '.join(self.values)
-    # renamed to GROUP_CONCAT in cubicweb 2.45, keep old name for bw compat for
-    # some time
-    cnx.create_aggregate("CONCAT_STRINGS", 1, concat_strings)
-    cnx.create_aggregate("GROUP_CONCAT", 1, concat_strings)
+    cnx.create_aggregate("GROUP_CONCAT", 1, group_concat)
 
     def _limit_size(text, maxsize, format='text/plain'):
         if len(text) < maxsize:
@@ -293,37 +305,9 @@
     def limit_size2(text, maxsize):
         return _limit_size(text, maxsize)
     cnx.create_function("TEXT_LIMIT_SIZE", 2, limit_size2)
+
     import yams.constraints
-    if hasattr(yams.constraints, 'patch_sqlite_decimal'):
-        yams.constraints.patch_sqlite_decimal()
-
-    def fspath(eid, etype, attr):
-        try:
-            cu = cnx.cursor()
-            cu.execute('SELECT X.cw_%s FROM cw_%s as X '
-                       'WHERE X.cw_eid=%%(eid)s' % (attr, etype),
-                       {'eid': eid})
-            return cu.fetchone()[0]
-        except:
-            import traceback
-            traceback.print_exc()
-            raise
-    cnx.create_function('fspath', 3, fspath)
-
-    def _fsopen(fspath):
-        if fspath:
-            try:
-                return buffer(file(fspath).read())
-            except:
-                import traceback
-                traceback.print_exc()
-                raise
-    cnx.create_function('_fsopen', 1, _fsopen)
-
+    yams.constraints.patch_sqlite_decimal()
 
 sqlite_hooks = SQL_CONNECT_HOOKS.setdefault('sqlite', [])
 sqlite_hooks.append(init_sqlite_connexion)
-
-def init_cnx(driver, cnx):
-    for hook in SQL_CONNECT_HOOKS.get(driver, ()):
-        hook(cnx)
--- a/server/ssplanner.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/ssplanner.py	Tue Apr 06 19:46:38 2010 +0200
@@ -5,16 +5,115 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
 from copy import copy
 
 from rql.stmts import Union, Select
-from rql.nodes import Constant
+from rql.nodes import Constant, Relation
 
 from cubicweb import QueryError, typed_eid
 from cubicweb.schema import VIRTUAL_RTYPES
 from cubicweb.rqlrewrite import add_types_restriction
+from cubicweb.server.session import security_enabled
+from cubicweb.server.hook import CleanupDeletedEidsCacheOp
+
+READ_ONLY_RTYPES = set(('eid', 'has_text', 'is', 'is_instance_of', 'identity'))
+
+_CONSTANT = object()
+_FROM_SUBSTEP = object()
+
+def _extract_const_attributes(plan, rqlst, to_build):
+    """add constant values to entity def, mark variables to be selected
+    """
+    to_select = {}
+    for relation in rqlst.main_relations:
+        lhs, rhs = relation.get_variable_parts()
+        rtype = relation.r_type
+        if rtype in READ_ONLY_RTYPES:
+            raise QueryError("can't assign to %s" % rtype)
+        try:
+            edef = to_build[str(lhs)]
+        except KeyError:
+            # lhs var is not to build, should be selected and added as an
+            # object relation
+            edef = to_build[str(rhs)]
+            to_select.setdefault(edef, []).append((rtype, lhs, 1))
+        else:
+            if isinstance(rhs, Constant) and not rhs.uid:
+                # add constant values to entity def
+                value = rhs.eval(plan.args)
+                eschema = edef.e_schema
+                attrtype = eschema.subjrels[rtype].objects(eschema)[0]
+                if attrtype == 'Password' and isinstance(value, unicode):
+                    value = value.encode('UTF8')
+                edef[rtype] = value
+            elif to_build.has_key(str(rhs)):
+                # create a relation between two newly created variables
+                plan.add_relation_def((edef, rtype, to_build[rhs.name]))
+            else:
+                to_select.setdefault(edef, []).append( (rtype, rhs, 0) )
+    return to_select
+
+def _extract_eid_consts(plan, rqlst):
+    """return a dict mapping rqlst variable object to their eid if specified in
+    the syntax tree
+    """
+    session = plan.session
+    if rqlst.where is None:
+        return {}
+    eidconsts = {}
+    neweids = session.transaction_data.get('neweids', ())
+    checkread = session.read_security
+    eschema = session.vreg.schema.eschema
+    for rel in rqlst.where.get_nodes(Relation):
+        if rel.r_type == 'eid' and not rel.neged(strict=True):
+            lhs, rhs = rel.get_variable_parts()
+            if isinstance(rhs, Constant):
+                eid = typed_eid(rhs.eval(plan.args))
+                # check read permission here since it may not be done by
+                # the generated select substep if not emited (eg nothing
+                # to be selected)
+                if checkread and eid not in neweids:
+                    with security_enabled(session, read=False):
+                        eschema(session.describe(eid)[0]).check_perm(
+                            session, 'read', eid=eid)
+                eidconsts[lhs.variable] = eid
+    return eidconsts
+
+def _build_substep_query(select, origrqlst):
+    """Finalize substep select query that should be executed to get proper
+    selection of stuff to insert/update.
+
+    Return None when no query actually needed, else the given select node that
+    will be used as substep query.
+
+    When select has nothing selected, search in origrqlst for restriction that
+    should be considered.
+    """
+    if select.selection:
+        if origrqlst.where is not None:
+            select.set_where(origrqlst.where.copy(select))
+        return select
+    if origrqlst.where is None:
+        return
+    for rel in origrqlst.where.iget_nodes(Relation):
+        # search for a relation which is neither a type restriction (is) nor an
+        # eid specification (not neged eid with constant node
+        if rel.neged(strict=True) or not (
+            rel.is_types_restriction() or
+            (rel.r_type == 'eid'
+             and isinstance(rel.get_variable_parts()[1], Constant))):
+            break
+    else:
+        return
+    select.set_where(origrqlst.where.copy(select))
+    if not select.selection:
+        # no selection, append one randomly
+        select.append_selected(rel.children[0].copy(select))
+    return select
 
 
 class SSPlanner(object):
@@ -56,34 +155,37 @@
             to_build[var.name] = etype_class(etype)(session)
             plan.add_entity_def(to_build[var.name])
         # add constant values to entity def, mark variables to be selected
-        to_select = plan.relation_definitions(rqlst, to_build)
+        to_select = _extract_const_attributes(plan, rqlst, to_build)
         # add necessary steps to add relations and update attributes
         step = InsertStep(plan) # insert each entity and its relations
-        step.children += self._compute_relation_steps(plan, rqlst.solutions,
-                                                      rqlst.where, to_select)
+        step.children += self._compute_relation_steps(plan, rqlst, to_select)
         return (step,)
 
-    def _compute_relation_steps(self, plan, solutions, restriction, to_select):
+    def _compute_relation_steps(self, plan, rqlst, to_select):
         """handle the selection of relations for an insert query"""
+        eidconsts = _extract_eid_consts(plan, rqlst)
         for edef, rdefs in to_select.items():
             # create a select rql st to fetch needed data
             select = Select()
             eschema = edef.e_schema
-            for i in range(len(rdefs)):
-                rtype, term, reverse = rdefs[i]
-                select.append_selected(term.copy(select))
+            for i, (rtype, term, reverse) in enumerate(rdefs):
+                if getattr(term, 'variable', None) in eidconsts:
+                    value = eidconsts[term.variable]
+                else:
+                    select.append_selected(term.copy(select))
+                    value = _FROM_SUBSTEP
                 if reverse:
-                    rdefs[i] = rtype, RelationsStep.REVERSE_RELATION
+                    rdefs[i] = (rtype, InsertRelationsStep.REVERSE_RELATION, value)
                 else:
                     rschema = eschema.subjrels[rtype]
                     if rschema.final or rschema.inlined:
-                        rdefs[i] = rtype, RelationsStep.FINAL
+                        rdefs[i] = (rtype, InsertRelationsStep.FINAL, value)
                     else:
-                        rdefs[i] = rtype, RelationsStep.RELATION
-            if restriction is not None:
-                select.set_where(restriction.copy(select))
-            step = RelationsStep(plan, edef, rdefs)
-            step.children += self._select_plan(plan, select, solutions)
+                        rdefs[i] = (rtype, InsertRelationsStep.RELATION, value)
+            step = InsertRelationsStep(plan, edef, rdefs)
+            select = _build_substep_query(select, rqlst)
+            if select is not None:
+                step.children += self._select_plan(plan, select, rqlst.solutions)
             yield step
 
     def build_delete_plan(self, plan, rqlst):
@@ -127,37 +229,61 @@
 
     def build_set_plan(self, plan, rqlst):
         """get an execution plan from an SET RQL query"""
-        select = Select()
-        # extract variables to add to the selection
-        selected_index = {}
-        index = 0
-        relations, attrrelations = [], []
         getrschema = self.schema.rschema
-        for relation in rqlst.main_relations:
+        select = Select()   # potential substep query
+        selectedidx = {}    # local state
+        attributes = set()  # edited attributes
+        updatedefs = []     # definition of update attributes/relations
+        selidx = residx = 0 # substep selection / resulting rset indexes
+        # search for eid const in the WHERE clause
+        eidconsts = _extract_eid_consts(plan, rqlst)
+        # build `updatedefs` describing things to update and add necessary
+        # variables to the substep selection
+        for i, relation in enumerate(rqlst.main_relations):
             if relation.r_type in VIRTUAL_RTYPES:
                 raise QueryError('can not assign to %r relation'
                                  % relation.r_type)
             lhs, rhs = relation.get_variable_parts()
-            if not lhs.as_string('utf-8') in selected_index:
-                select.append_selected(lhs.copy(select))
-                selected_index[lhs.as_string('utf-8')] = index
-                index += 1
-            if not rhs.as_string('utf-8') in selected_index:
-                select.append_selected(rhs.copy(select))
-                selected_index[rhs.as_string('utf-8')] = index
-                index += 1
+            lhskey = lhs.as_string('utf-8')
+            if not lhskey in selectedidx:
+                if lhs.variable in eidconsts:
+                    eid = eidconsts[lhs.variable]
+                    lhsinfo = (_CONSTANT, eid, residx)
+                else:
+                    select.append_selected(lhs.copy(select))
+                    lhsinfo = (_FROM_SUBSTEP, selidx, residx)
+                    selidx += 1
+                residx += 1
+                selectedidx[lhskey] = lhsinfo
+            else:
+                lhsinfo = selectedidx[lhskey][:-1] + (None,)
+            rhskey = rhs.as_string('utf-8')
+            if not rhskey in selectedidx:
+                if isinstance(rhs, Constant):
+                    rhsinfo = (_CONSTANT, rhs.eval(plan.args), residx)
+                elif getattr(rhs, 'variable', None) in eidconsts:
+                    eid = eidconsts[rhs.variable]
+                    rhsinfo = (_CONSTANT, eid, residx)
+                else:
+                    select.append_selected(rhs.copy(select))
+                    rhsinfo = (_FROM_SUBSTEP, selidx, residx)
+                    selidx += 1
+                residx += 1
+                selectedidx[rhskey] = rhsinfo
+            else:
+                rhsinfo = selectedidx[rhskey][:-1] + (None,)
             rschema = getrschema(relation.r_type)
+            updatedefs.append( (lhsinfo, rhsinfo, rschema) )
             if rschema.final or rschema.inlined:
-                attrrelations.append(relation)
-            else:
-                relations.append(relation)
-        # add step necessary to fetch all selected variables values
-        if rqlst.where is not None:
-            select.set_where(rqlst.where.copy(select))
-        # set distinct to avoid potential duplicate key error
-        select.distinct = True
-        step = UpdateStep(plan, attrrelations, relations, selected_index)
-        step.children += self._select_plan(plan, select, rqlst.solutions)
+                attributes.add(relation.r_type)
+        # the update step
+        step = UpdateStep(plan, updatedefs, attributes)
+        # when necessary add substep to fetch yet unknown values
+        select = _build_substep_query(select, rqlst)
+        if select is not None:
+            # set distinct to avoid potential duplicate key error
+            select.distinct = True
+            step.children += self._select_plan(plan, select, rqlst.solutions)
         return (step,)
 
     # internal methods ########################################################
@@ -308,7 +434,7 @@
 
 # UPDATE/INSERT/DELETE steps ##################################################
 
-class RelationsStep(Step):
+class InsertRelationsStep(Step):
     """step consisting in adding attributes/relations to entity defs from a
     previous FetchStep
 
@@ -334,33 +460,38 @@
         """execute this step"""
         base_edef = self.edef
         edefs = []
-        result = self.execute_child()
+        if self.children:
+            result = self.execute_child()
+        else:
+            result = [[]]
         for row in result:
             # get a new entity definition for this row
             edef = copy(base_edef)
             # complete this entity def using row values
-            for i in range(len(self.rdefs)):
-                rtype, rorder = self.rdefs[i]
-                if rorder == RelationsStep.FINAL:
-                    edef[rtype] = row[i]
-                elif rorder == RelationsStep.RELATION:
-                    self.plan.add_relation_def( (edef, rtype, row[i]) )
-                    edef.querier_pending_relations[(rtype, 'subject')] = row[i]
+            index = 0
+            for rtype, rorder, value in self.rdefs:
+                if value is _FROM_SUBSTEP:
+                    value = row[index]
+                    index += 1
+                if rorder == InsertRelationsStep.FINAL:
+                    edef.rql_set_value(rtype, value)
+                elif rorder == InsertRelationsStep.RELATION:
+                    self.plan.add_relation_def( (edef, rtype, value) )
+                    edef.querier_pending_relations[(rtype, 'subject')] = value
                 else:
-                    self.plan.add_relation_def( (row[i], rtype, edef) )
-                    edef.querier_pending_relations[(rtype, 'object')] = row[i]
+                    self.plan.add_relation_def( (value, rtype, edef) )
+                    edef.querier_pending_relations[(rtype, 'object')] = value
             edefs.append(edef)
         self.plan.substitute_entity_def(base_edef, edefs)
         return result
 
-
 class InsertStep(Step):
     """step consisting in inserting new entities / relations"""
 
     def execute(self):
         """execute this step"""
         for step in self.children:
-            assert isinstance(step, RelationsStep)
+            assert isinstance(step, InsertRelationsStep)
             step.plan = self.plan
             step.execute()
         # insert entities first
@@ -377,11 +508,17 @@
     def execute(self):
         """execute this step"""
         results = self.execute_child()
-        todelete = frozenset(typed_eid(eid) for eid, in self.execute_child())
+        todelete = frozenset(typed_eid(eid) for eid, in results)
         session = self.plan.session
         delete = session.repo.glob_delete_entity
-        # register pending eids first to avoid multiple deletion
-        pending = session.transaction_data.setdefault('pendingeids', set())
+        # mark eids as being deleted in session info and setup cache update
+        # operation (register pending eids before actual deletion to avoid
+        # multiple call to glob_delete_entity)
+        try:
+            pending = session.transaction_data['pendingeids']
+        except KeyError:
+            pending = session.transaction_data['pendingeids'] = set()
+            CleanupDeletedEidsCacheOp(session)
         actual = todelete - pending
         pending |= actual
         for eid in actual:
@@ -408,40 +545,46 @@
     definitions and from results fetched in previous step
     """
 
-    def __init__(self, plan, attribute_relations, relations, selected_index):
+    def __init__(self, plan, updatedefs, attributes):
         Step.__init__(self, plan)
-        self.attribute_relations = attribute_relations
-        self.relations = relations
-        self.selected_index = selected_index
+        self.updatedefs = updatedefs
+        self.attributes = attributes
 
     def execute(self):
         """execute this step"""
-        plan = self.plan
         session = self.plan.session
         repo = session.repo
         edefs = {}
         # insert relations
-        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)]])
-                try:
-                    edef = edefs[eid]
-                except KeyError:
-                    edefs[eid] = edef = session.entity_from_eid(eid)
-                if isinstance(rhs, Constant):
-                    # add constant values to entity def
-                    value = rhs.eval(plan.args)
-                    edef[relation.r_type] = value
+        if self.children:
+            result = self.execute_child()
+        else:
+            result = [[]]
+        for i, row in enumerate(result):
+            newrow = []
+            for (lhsinfo, rhsinfo, rschema) in self.updatedefs:
+                lhsval = _handle_relterm(lhsinfo, row, newrow)
+                rhsval = _handle_relterm(rhsinfo, row, newrow)
+                if rschema.final or rschema.inlined:
+                    eid = typed_eid(lhsval)
+                    try:
+                        edef = edefs[eid]
+                    except KeyError:
+                        edefs[eid] = edef = session.entity_from_eid(eid)
+                    edef.rql_set_value(str(rschema), rhsval)
                 else:
-                    edef[relation.r_type] = row[self.selected_index[str(rhs)]]
-            for relation in self.relations:
-                subj = row[self.selected_index[str(relation.children[0])]]
-                obj = row[self.selected_index[str(relation.children[1])]]
-                repo.glob_add_relation(session, subj, relation.r_type, obj)
+                    repo.glob_add_relation(session, lhsval, str(rschema), rhsval)
+            result[i] = newrow
         # update entities
         for eid, edef in edefs.iteritems():
-            repo.glob_update_entity(session, edef, attributes)
+            repo.glob_update_entity(session, edef, self.attributes)
         return result
+
+def _handle_relterm(info, row, newrow):
+    if info[0] is _CONSTANT:
+        val = info[1]
+    else: # _FROM_SUBSTEP
+        val = row[info[1]]
+    if info[-1] is not None:
+        newrow.append(val)
+    return val
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/data/site_cubicweb.py	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,23 @@
+"""
+
+:organization: Logilab
+:copyright: 2001-2010 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.database import FunctionDescr
+from logilab.database.sqlite import register_sqlite_pyfunc
+from rql.utils import register_function
+
+try:
+    class DUMB_SORT(FunctionDescr):
+        supported_backends = ('sqlite',)
+
+    register_function(DUMB_SORT)
+    def dumb_sort(something):
+        return something
+    register_sqlite_pyfunc(dumb_sort)
+except:
+    # already registered
+    pass
--- a/server/test/data/site_erudi.py	Thu Mar 04 17:56:45 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-"""
-
-:organization: Logilab
-:copyright: 2001-2010 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.adbh import FunctionDescr
-from rql.utils import register_function
-
-try:
-    class DUMB_SORT(FunctionDescr):
-        supported_backends = ('sqlite',)
-
-    register_function(DUMB_SORT)
-
-
-    def init_sqlite_connexion(cnx):
-        def dumb_sort(something):
-            return something
-        cnx.create_function("DUMB_SORT", 1, dumb_sort)
-
-    from cubicweb.server import sqlutils
-    sqlutils.SQL_CONNECT_HOOKS['sqlite'].append(init_sqlite_connexion)
-except:
-    # already registered
-    pass
--- a/server/test/unittest_checkintegrity.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_checkintegrity.py	Tue Apr 06 19:46:38 2010 +0200
@@ -13,10 +13,9 @@
 
 from cubicweb.server.checkintegrity import check
 
-repo, cnx = init_test_database()
-
 class CheckIntegrityTC(TestCase):
     def test(self):
+        repo, cnx = init_test_database()
         sys.stderr = sys.stdout = StringIO()
         try:
             check(repo, cnx, ('entities', 'relations', 'text_index', 'metadata'),
@@ -24,6 +23,7 @@
         finally:
             sys.stderr = sys.__stderr__
             sys.stdout = sys.__stdout__
+        repo.shutdown()
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_hook.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_hook.py	Tue Apr 06 19:46:38 2010 +0200
@@ -69,6 +69,10 @@
 config.bootstrap_cubes()
 schema = config.load_schema()
 
+def teardown_module(*args):
+    global config, schema
+    del config, schema
+
 class AddAnyHook(hook.Hook):
     __regid__ = 'addany'
     category = 'cat1'
@@ -77,7 +81,7 @@
         raise HookCalled()
 
 
-class HooksManagerTC(TestCase):
+class HooksRegistryTC(TestCase):
 
     def setUp(self):
         """ called before each test from this class """
@@ -88,29 +92,34 @@
         class _Hook(hook.Hook):
             events = ('before_add_entiti',)
         ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEquals(str(ex), 'bad event before_add_entiti on unittest_hook._Hook')
+        self.assertEquals(str(ex), 'bad event before_add_entiti on %s._Hook' % __name__)
 
     def test_register_bad_hook2(self):
         class _Hook(hook.Hook):
             events = None
         ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEquals(str(ex), 'bad .events attribute None on unittest_hook._Hook')
+        self.assertEquals(str(ex), 'bad .events attribute None on %s._Hook' % __name__)
 
     def test_register_bad_hook3(self):
         class _Hook(hook.Hook):
             events = 'before_add_entity'
         ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEquals(str(ex), 'bad event b on unittest_hook._Hook')
+        self.assertEquals(str(ex), 'bad event b on %s._Hook' % __name__)
 
     def test_call_hook(self):
         self.o.register(AddAnyHook)
-        cw = mock_object(vreg=self.vreg)
-        self.assertRaises(HookCalled, self.o.call_hooks, 'before_add_entity', cw)
-        self.o.call_hooks('before_delete_entity', cw) # nothing to call
-        config.disabled_hooks_categories.add('cat1')
+        dis = set()
+        cw = mock_object(vreg=self.vreg,
+                         set_read_security=lambda *a,**k: None,
+                         set_write_security=lambda *a,**k: None,
+                         is_hook_activated=lambda x, cls: cls.category not in dis)
+        self.assertRaises(HookCalled,
+                          self.o.call_hooks, 'before_add_entity', cw)
+        dis.add('cat1')
         self.o.call_hooks('before_add_entity', cw) # disabled hooks category, not called
-        config.disabled_hooks_categories.remove('cat1')
-        self.assertRaises(HookCalled, self.o.call_hooks, 'before_add_entity', cw)
+        dis.remove('cat1')
+        self.assertRaises(HookCalled,
+                          self.o.call_hooks, 'before_add_entity', cw)
         self.o.unregister(AddAnyHook)
         self.o.call_hooks('before_add_entity', cw) # nothing to call
 
--- a/server/test/unittest_ldapuser.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_ldapuser.py	Tue Apr 06 19:46:38 2010 +0200
@@ -15,7 +15,7 @@
 
 from cubicweb.server.sources.ldapuser import *
 
-if socket.gethostbyname('ldap1').startswith('172'):
+if '17.1' in socket.gethostbyname('ldap1'):
     SYT = 'syt'
     ADIM = 'adim'
 else:
@@ -189,7 +189,7 @@
         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', req=self.session)
+        self.create_user('cochon')
         self.failUnless(self.sexecute('Any X,Y WHERE X login %(syt)s, Y login "cochon"', {'syt': SYT}))
 
     def test_exists1(self):
@@ -202,15 +202,15 @@
         self.assertEquals(rset.rows, [['admin', 'activated'], [SYT, 'activated']])
 
     def test_exists2(self):
-        self.create_user('comme', req=self.session)
-        self.create_user('cochon', req=self.session)
+        self.create_user('comme')
+        self.create_user('cochon')
         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', req=self.session)
-        self.create_user('cochon', req=self.session)
+        self.create_user('comme')
+        self.create_user('cochon')
         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)s, Y login "cochon"', {'syt': SYT})
@@ -219,9 +219,9 @@
         self.assertEquals(sorted(rset.rows), [['managers', 'admin'], ['users', 'comme'], ['users', SYT]])
 
     def test_exists4(self):
-        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.create_user('comme')
+        self.create_user('cochon', groups=('users', 'guests'))
+        self.create_user('billy')
         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"')
@@ -241,9 +241,9 @@
         self.assertEquals(sorted(rset.rows), sorted(all.rows))
 
     def test_exists5(self):
-        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.create_user('comme')
+        self.create_user('cochon', groups=('users', 'guests'))
+        self.create_user('billy')
         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"')
@@ -273,7 +273,7 @@
                           sorted(r[0] for r in afeids + ueids))
 
     def _init_security_test(self):
-        self.create_user('iaminguestsgrouponly', groups=('guests',), req=self.session)
+        self.create_user('iaminguestsgrouponly', groups=('guests',))
         cnx = self.login('iaminguestsgrouponly')
         return cnx.cursor()
 
@@ -370,6 +370,11 @@
 LDAPUserSourceTC._init_repo()
 repo = LDAPUserSourceTC.repo
 
+def teardown_module(*args):
+    global repo
+    del repo
+    del RQL2LDAPFilterTC.schema
+
 class RQL2LDAPFilterTC(RQLGeneratorTC):
     schema = repo.schema
 
--- a/server/test/unittest_migractions.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_migractions.py	Tue Apr 06 19:46:38 2010 +0200
@@ -14,6 +14,11 @@
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.migractions import *
 
+migrschema = None
+def teardown_module(*args):
+    global migrschema
+    del migrschema
+    del MigrationCommandsTC.origschema
 
 class MigrationCommandsTC(CubicWebTC):
 
@@ -35,6 +40,13 @@
     def _refresh_repo(cls):
         super(MigrationCommandsTC, cls)._refresh_repo()
         cls.repo.set_schema(deepcopy(cls.origschema), resetvreg=False)
+        # reset migration schema eids
+        for eschema in migrschema.entities():
+            eschema.eid = None
+        for rschema in migrschema.relations():
+            rschema.eid = None
+            for rdef in rschema.rdefs.values():
+                rdef.eid = None
 
     def setUp(self):
         CubicWebTC.setUp(self)
@@ -44,7 +56,6 @@
         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)
         self.request().create_entity('Note')
--- a/server/test/unittest_msplanner.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_msplanner.py	Tue Apr 06 19:46:38 2010 +0200
@@ -60,6 +60,11 @@
 # keep cnx so it's not garbage collected and the associated session is closed
 repo, cnx = init_test_database()
 
+def teardown_module(*args):
+    global repo, cnx
+    del repo, cnx
+
+
 class BaseMSPlannerTC(BasePlannerTC):
     """test planner related feature on a 3-sources repository:
 
@@ -87,10 +92,10 @@
         self.add_source(FakeCardSource, 'cards')
 
     def tearDown(self):
-        super(BaseMSPlannerTC, self).tearDown()
         # restore hijacked security
         self.restore_orig_affaire_security()
         self.restore_orig_cwuser_security()
+        super(BaseMSPlannerTC, self).tearDown()
 
     def restore_orig_affaire_security(self):
         affreadperms = list(self.schema['Affaire'].permissions['read'])
@@ -771,13 +776,12 @@
                          [{'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'},
+                        ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, SubDivision, Tag)',
+                         [{'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'}]),],
+                          {'X': 'SubDivision'}, {'X': 'Tag'}]),],
                        None, None, [self.system], {}, []),
                       ])
                      ])
@@ -798,24 +802,22 @@
                             [{'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'},
+                           ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, SubDivision, Tag)',
+                            [{'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'}])],
+                             {'X': 'SubDivision'}, {'X': 'Tag'}])],
                           [self.system], {}, {'X': 'table0.C0'}, []),
                          ]),
                     ('OneFetchStep',
                      [('Any X LIMIT 10 OFFSET 10',
-                       [{'X': 'Affaire'}, {'X': 'BaseTransition'}, {'X': 'Basket'},
+                       [{'X': 'Affaire'}, {'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'}])],
+                        {'X': 'SubDivision'}, {'X': 'Tag'}])],
                      10, 10, [self.system], {'X': 'table0.C0'}, [])
                      ])
 
@@ -1008,7 +1010,7 @@
         self.session = self.user_groups_session('guests')
         self._test('Any X,XT,U WHERE X is Card, X owned_by U?, X title XT, U login L',
                    [('FetchStep',
-                     [('Any U,L WHERE U identity 5, U login L, U is CWUser',
+                     [('Any U,L WHERE U login L, EXISTS(U identity 5), U is CWUser',
                        [{'L': 'String', u'U': 'CWUser'}])],
                      [self.system], {}, {'L': 'table0.C1', 'U': 'table0.C0', 'U.login': 'table0.C1'}, []),
                     ('FetchStep',
@@ -1520,15 +1522,11 @@
         repo._type_source_cache[999999] = ('Note', 'cards', 999999)
         repo._type_source_cache[999998] = ('State', 'system', None)
         self._test('INSERT Note X: X in_state S, X type T WHERE S eid %(s)s, N eid %(n)s, N type T',
-                   [('FetchStep', [('Any T WHERE N eid 999999, N type T, N is Note',
-                                    [{'N': 'Note', 'T': 'String'}])],
-                     [self.cards], None, {'N.type': 'table0.C0', 'T': 'table0.C0'}, []),
-                    ('InsertStep',
-                     [('RelationsStep',
-                       [('OneFetchStep', [('Any 999998,T WHERE N type T, N is Note',
+                   [('InsertStep',
+                     [('InsertRelationsStep',
+                       [('OneFetchStep', [('Any T WHERE N eid 999999, N type T, N is Note',
                                            [{'N': 'Note', 'T': 'String'}])],
-                        None, None, [self.system],
-                        {'N.type': 'table0.C0', 'T': 'table0.C0'}, [])])
+                        None, None, [self.cards], {}, [])])
                       ])
                     ],
                    {'n': 999999, 's': 999998})
@@ -1537,15 +1535,11 @@
         repo._type_source_cache[999999] = ('Note', 'cards', 999999)
         repo._type_source_cache[999998] = ('State', 'system', None)
         self._test('INSERT Note X: X in_state S, X type T, X migrated_from N WHERE S eid %(s)s, N eid %(n)s, N type T',
-                   [('FetchStep', [('Any T,N WHERE N eid 999999, N type T, N is Note',
-                                    [{'N': 'Note', 'T': 'String'}])],
-                     [self.cards], None, {'N': 'table0.C1', 'N.type': 'table0.C0', 'T': 'table0.C0'}, []),
-                    ('InsertStep',
-                     [('RelationsStep',
-                       [('OneFetchStep', [('Any 999998,T,N WHERE N type T, N is Note',
+                   [('InsertStep',
+                     [('InsertRelationsStep',
+                       [('OneFetchStep', [('Any T WHERE N eid 999999, N type T, N is Note',
                                            [{'N': 'Note', 'T': 'String'}])],
-                         None, None, [self.system],
-                         {'N': 'table0.C1', 'N.type': 'table0.C0', 'T': 'table0.C0'}, [])
+                         None, None, [self.cards], {}, [])
                         ])
                       ])
                     ],
@@ -1556,8 +1550,8 @@
         repo._type_source_cache[999998] = ('State', 'cards', 999998)
         self._test('INSERT Note X: X in_state S, X type T WHERE S eid %(s)s, N eid %(n)s, N type T',
                    [('InsertStep',
-                     [('RelationsStep',
-                       [('OneFetchStep', [('Any 999998,T WHERE N eid 999999, N type T, N is Note',
+                     [('InsertRelationsStep',
+                       [('OneFetchStep', [('Any T WHERE N eid 999999, N type T, N is Note',
                                            [{'N': 'Note', 'T': 'String'}])],
                          None, None, [self.cards], {}, [])]
                        )]
@@ -1569,10 +1563,7 @@
         repo._type_source_cache[999998] = ('State', 'system', None)
         self._test('INSERT Note X: X in_state S, X type "bla", X migrated_from N WHERE S eid %(s)s, N eid %(n)s',
                    [('InsertStep',
-                     [('RelationsStep',
-                       [('OneFetchStep', [('Any 999998,999999', [{}])],
-                         None, None, [self.system], {}, [])]
-                       )]
+                      [('InsertRelationsStep', [])]
                      )],
                    {'n': 999999, 's': 999998})
 
@@ -1581,12 +1572,14 @@
         repo._type_source_cache[999998] = ('State', 'system', None)
         self._test('INSERT Note X: X in_state S, X type "bla", X migrated_from N WHERE S eid %(s)s, N eid %(n)s, A concerne N',
                    [('InsertStep',
-                     [('RelationsStep',
-                       [('OneFetchStep', [('Any 999998,999999 WHERE A concerne 999999, A is Affaire',
-                                           [{'A': 'Affaire'}])],
-                         None, None, [self.system], {}, [])]
-                       )]
-                     )],
+                     [('InsertRelationsStep',
+                       [('OneFetchStep',
+                         [('Any A WHERE A concerne 999999, A is Affaire',
+                           [{'A': 'Affaire'}])],
+                         None, None, [self.system], {}, []),
+                        ]),
+                      ])
+                    ],
                    {'n': 999999, 's': 999998})
 
     def test_delete_relation1(self):
@@ -1667,7 +1660,7 @@
         # source, states should only be searched in the system source as well
         self._test('SET X in_state S WHERE X eid %(x)s, S name "deactivated"',
                    [('UpdateStep', [
-                       ('OneFetchStep', [('DISTINCT Any 5,S WHERE S name "deactivated", S is State',
+                       ('OneFetchStep', [('DISTINCT Any S WHERE S name "deactivated", S is State',
                                           [{'S': 'State'}])],
                         None, None, [self.system], {}, []),
                        ]),
@@ -1817,7 +1810,7 @@
                    [('FetchStep', [('Any Y WHERE Y multisource_rel 999998, Y is Note', [{'Y': 'Note'}])],
                      [self.cards], None, {'Y': u'table0.C0'}, []),
                     ('UpdateStep',
-                     [('OneFetchStep', [('DISTINCT Any 999999,Y WHERE Y migrated_from 999998, Y is Note',
+                     [('OneFetchStep', [('DISTINCT Any Y WHERE Y migrated_from 999998, Y is Note',
                                          [{'Y': 'Note'}])],
                        None, None, [self.system],
                        {'Y': u'table0.C0'}, [])])],
@@ -1844,14 +1837,9 @@
     def test_nonregr11(self):
         repo._type_source_cache[999999] = ('Bookmark', 'system', 999999)
         self._test('SET X bookmarked_by Y WHERE X eid %(x)s, Y login "hop"',
-                   [('FetchStep',
-                     [('Any Y WHERE Y login "hop", Y is CWUser', [{'Y': 'CWUser'}])],
-                     [self.ldap, self.system],
-                     None, {'Y': 'table0.C0'}, []),
-                    ('UpdateStep',
-                     [('OneFetchStep', [('DISTINCT Any 999999,Y WHERE Y is CWUser', [{'Y': 'CWUser'}])],
-                       None, None, [self.system], {'Y': 'table0.C0'},
-                       [])]
+                   [('UpdateStep',
+                     [('OneFetchStep', [('DISTINCT Any Y WHERE Y login "hop", Y is CWUser', [{'Y': 'CWUser'}])],
+                       None, None, [self.ldap, self.system], {}, [])]
                      )],
                    {'x': 999999})
 
--- a/server/test/unittest_multisources.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_multisources.py	Tue Apr 06 19:46:38 2010 +0200
@@ -48,7 +48,12 @@
 def teardown_module(*args):
     PyroRQLSource.get_connection = PyroRQLSource_get_connection
     Connection.close = Connection_close
-
+    global repo2, cnx2, repo3, cnx3
+    repo2.shutdown()
+    repo3.shutdown()
+    del repo2, cnx2, repo3, cnx3
+    #del TwoSourcesTC.config.vreg
+    #del TwoSourcesTC.config
 
 class TwoSourcesTC(CubicWebTC):
     config = TwoSourcesConfiguration('data')
@@ -130,7 +135,7 @@
         cu = cnx.cursor()
         rset = cu.execute('Any X WHERE X has_text "card"')
         self.assertEquals(len(rset), 5, zip(rset.rows, rset.description))
-        cnx.close()
+        Connection_close(cnx)
 
     def test_synchronization(self):
         cu = cnx2.cursor()
--- a/server/test/unittest_querier.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_querier.py	Tue Apr 06 19:46:38 2010 +0200
@@ -35,7 +35,7 @@
 SQL_CONNECT_HOOKS['sqlite'].append(init_sqlite_connexion)
 
 
-from logilab.common.adbh import _GenericAdvFuncHelper
+from logilab.database import _GenericAdvFuncHelper
 TYPEMAP = _GenericAdvFuncHelper.TYPE_MAPPING
 
 class MakeSchemaTC(TestCase):
@@ -48,6 +48,11 @@
 
 repo, cnx = init_test_database()
 
+def teardown_module(*args):
+    global repo, cnx
+    cnx.close()
+    repo.shutdown()
+    del repo, cnx
 
 
 class UtilsTC(BaseQuerierTC):
@@ -392,6 +397,18 @@
         rset = self.execute('Note X WHERE NOT Y evaluee X')
         self.assertEquals(len(rset.rows), 1, rset.rows)
 
+    def test_select_date_extraction(self):
+        self.execute("INSERT Personne X: X nom 'foo', X datenaiss %(d)s",
+                     {'d': datetime(2001, 2,3, 12,13)})
+        test_data = [('YEAR', 2001), ('MONTH', 2), ('DAY', 3),
+                     ('HOUR', 12), ('MINUTE', 13)]
+        for funcname, result in test_data:
+            rset = self.execute('Any %s(D) WHERE X is Personne, X datenaiss D'
+                                % funcname)
+            self.assertEquals(len(rset.rows), 1)
+            self.assertEquals(rset.rows[0][0], result)
+            self.assertEquals(rset.description, [('Int',)])
+
     def test_select_aggregat_count(self):
         rset = self.execute('Any COUNT(X)')
         self.assertEquals(len(rset.rows), 1)
@@ -425,7 +442,7 @@
         self.assertEquals(rset.description, [('Int',)])
 
     def test_select_custom_aggregat_concat_string(self):
-        rset = self.execute('Any CONCAT_STRINGS(N) WHERE X is CWGroup, X name N')
+        rset = self.execute('Any GROUP_CONCAT(N) WHERE X is CWGroup, X name N')
         self.failUnless(rset)
         self.failUnlessEqual(sorted(rset[0][0].split(', ')), ['guests', 'managers',
                                                              'owners', 'users'])
@@ -1023,6 +1040,10 @@
                       {'x': str(eid1), 'y': str(eid2)})
         rset = self.execute('Any X, Y WHERE X travaille Y')
         self.assertEqual(len(rset.rows), 1)
+        # test add of an existant relation but with NOT X rel Y protection
+        self.failIf(self.execute("SET X travaille Y WHERE X eid %(x)s, Y eid %(y)s,"
+                                 "NOT X travaille Y",
+                                 {'x': str(eid1), 'y': str(eid2)}))
 
     def test_update_2ter(self):
         rset = self.execute("INSERT Personne X, Societe Y: X nom 'bidule', Y nom 'toto'")
--- a/server/test/unittest_repository.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_repository.py	Tue Apr 06 19:46:38 2010 +0200
@@ -6,6 +6,9 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
+from __future__ import with_statement
 
 import os
 import sys
@@ -20,13 +23,15 @@
 
 from cubicweb import (BadConnectionId, RepositoryError, ValidationError,
                       UnknownEid, AuthenticationError)
+from cubicweb.selectors import implements
 from cubicweb.schema import CubicWebSchema, RQLConstraint
-from cubicweb.dbapi import connect, repo_connect, multiple_connections_unfix
+from cubicweb.dbapi import connect, multiple_connections_unfix
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.repotest import tuplify
 from cubicweb.server import repository, hook
 from cubicweb.server.sqlutils import SQL_PREFIX
-
+from cubicweb.server.hook import Hook
+from cubicweb.server.sources import native
 
 # start name server anyway, process will fail if already running
 os.system('pyro-ns >/dev/null 2>/dev/null &')
@@ -38,25 +43,29 @@
     """
 
     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()
-        table = SQL_PREFIX + 'CWEType'
-        namecol = SQL_PREFIX + 'name'
-        finalcol = SQL_PREFIX + 'final'
-        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',)])
+        origshema = self.repo.schema
+        try:
+            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()
+            table = SQL_PREFIX + 'CWEType'
+            namecol = SQL_PREFIX + 'name'
+            finalcol = SQL_PREFIX + 'final'
+            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',)])
+        finally:
+            self.repo.set_schema(origshema)
 
     def test_schema_has_owner(self):
         repo = self.repo
@@ -180,7 +189,9 @@
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
         # rollback state change which trigger TrInfo insertion
-        user = repo._get_session(cnxid).user
+        session = repo._get_session(cnxid)
+        session.set_pool()
+        user = session.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)
@@ -263,9 +274,13 @@
                 self.fail('something went wrong, thread still alive')
         finally:
             repository.pyro_unregister(self.repo.config)
+            from logilab.common import pyro_ext
+            pyro_ext._DAEMONS.clear()
+
 
     def _pyro_client(self, done):
-        cnx = connect(self.repo.config.appid, u'admin', password='gingkow')
+        cnx = connect(self.repo.config.appid, u'admin', password='gingkow',
+                      initlog=False) # don't reset logging configuration
         try:
             # check we can get the schema
             schema = cnx.get_schema()
@@ -275,7 +290,7 @@
             cnx.close()
             done.append(True)
         finally:
-            # connect monkey path some method by default, remove them
+            # connect monkey patch some method by default, remove them
             multiple_connections_unfix()
 
     def test_internal_api(self):
@@ -349,6 +364,43 @@
         self.assertEquals(rset.rows[0][0], p2.eid)
 
 
+    def test_set_attributes_in_before_update(self):
+        # local hook
+        class DummyBeforeHook(Hook):
+            __regid__ = 'dummy-before-hook'
+            __select__ = Hook.__select__ & implements('EmailAddress')
+            events = ('before_update_entity',)
+            def __call__(self):
+                # safety belt: avoid potential infinite recursion if the test
+                #              fails (i.e. RuntimeError not raised)
+                pendings = self._cw.transaction_data.setdefault('pending', set())
+                if self.entity.eid not in pendings:
+                    pendings.add(self.entity.eid)
+                    self.entity.set_attributes(alias=u'foo')
+        with self.temporary_appobjects(DummyBeforeHook):
+            req = self.request()
+            addr = req.create_entity('EmailAddress', address=u'a@b.fr')
+            addr.set_attributes(address=u'a@b.com')
+            rset = self.execute('Any A,AA WHERE X eid %(x)s, X address A, X alias AA',
+                                {'x': addr.eid})
+            self.assertEquals(rset.rows, [[u'a@b.com', u'foo']])
+
+    def test_set_attributes_in_before_add(self):
+        # local hook
+        class DummyBeforeHook(Hook):
+            __regid__ = 'dummy-before-hook'
+            __select__ = Hook.__select__ & implements('EmailAddress')
+            events = ('before_add_entity',)
+            def __call__(self):
+                # set_attributes is forbidden within before_add_entity()
+                self.entity.set_attributes(alias=u'foo')
+        with self.temporary_appobjects(DummyBeforeHook):
+            req = self.request()
+            # XXX will fail with python -O
+            self.assertRaises(AssertionError, req.create_entity,
+                              'EmailAddress', address=u'a@b.fr')
+
+
 class DataHelpersTC(CubicWebTC):
 
     def test_create_eid(self):
@@ -377,14 +429,14 @@
         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'])
+        self.repo.add_info(self.session, entity, self.repo.system_source)
         cu = self.session.system_sql('SELECT * FROM entities WHERE eid = -1')
         data = cu.fetchall()
         self.assertIsInstance(data[0][3], datetime)
         data[0] = list(data[0])
         data[0][3] = None
         self.assertEquals(tuplify(data), [(-1, 'Personne', 'system', None, None)])
-        self.repo.delete_info(self.session, -1)
+        self.repo.delete_info(self.session, entity, 'system', None)
         #self.repo.commit()
         cu = self.session.system_sql('SELECT * FROM entities WHERE eid = -1')
         data = cu.fetchall()
@@ -394,6 +446,7 @@
 class FTITC(CubicWebTC):
 
     def test_reindex_and_modified_since(self):
+        self.repo.system_source.multisources_etypes.add('Personne')
         eidp = self.execute('INSERT Personne X: X nom "toto", X prenom "tutu"')[0][0]
         self.commit()
         ts = datetime.now()
@@ -424,25 +477,39 @@
         self.assertEquals(modified, [])
         self.assertEquals(deleted, [('Personne', eidp)])
 
-    def test_composite_entity(self):
+    def test_fulltext_container_entity(self):
         assert self.schema.rschema('use_email').fulltext_container == 'subject'
-        eid = self.request().create_entity('EmailAddress', address=u'toto@logilab.fr').eid
+        req = self.request()
+        toto = req.create_entity('EmailAddress', address=u'toto@logilab.fr')
         self.commit()
-        rset = self.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
-        self.assertEquals(rset.rows, [[eid]])
-        self.execute('SET X use_email Y WHERE X login "admin", Y eid %(y)s', {'y': eid})
+        rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
+        self.assertEquals(rset.rows, [])
+        req.user.set_relations(use_email=toto)
+        self.commit()
+        rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
+        self.assertEquals(rset.rows, [[req.user.eid]])
+        req.execute('DELETE X use_email Y WHERE X login "admin", Y eid %(y)s',
+                    {'y': toto.eid})
         self.commit()
-        rset = self.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
-        self.assertEquals(rset.rows, [[self.session.user.eid]])
-        self.execute('DELETE X use_email Y WHERE X login "admin", Y eid %(y)s', {'y': eid})
-        self.commit()
-        rset = self.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
+        rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
         self.assertEquals(rset.rows, [])
-        eid = self.request().create_entity('EmailAddress', address=u'tutu@logilab.fr').eid
-        self.execute('SET X use_email Y WHERE X login "admin", Y eid %(y)s', {'y': eid})
+        tutu = req.create_entity('EmailAddress', address=u'tutu@logilab.fr')
+        req.user.set_relations(use_email=tutu)
+        self.commit()
+        rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'})
+        self.assertEquals(rset.rows, [[req.user.eid]])
+        tutu.set_attributes(address=u'hip@logilab.fr')
         self.commit()
-        rset = self.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'})
-        self.assertEquals(rset.rows, [[self.session.user.eid]])
+        rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'})
+        self.assertEquals(rset.rows, [])
+        rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'hip'})
+        self.assertEquals(rset.rows, [[req.user.eid]])
+
+    def test_no_uncessary_ftiindex_op(self):
+        req = self.request()
+        req.create_entity('Workflow', name=u'dummy workflow', description=u'huuuuu')
+        self.failIf(any(x for x in self.session.pending_operations
+                        if isinstance(x, native.FTIndexEntityOp)))
 
 
 class DBInitTC(CubicWebTC):
@@ -456,20 +523,12 @@
                            u'system.version.tag'])
 
 CALLED = []
-class EcritParHook(hook.Hook):
-    __regid__ = 'inlinedrelhook'
-    __select__ = hook.Hook.__select__ & hook.match_rtype('ecrit_par')
-    events = ('before_add_relation', 'after_add_relation',
-              'before_delete_relation', 'after_delete_relation')
-    def __call__(self):
-        CALLED.append((self.event, self.eidfrom, self.rtype, self.eidto))
 
 class InlineRelHooksTC(CubicWebTC):
     """test relation hooks are called for inlined relations
     """
     def setUp(self):
         CubicWebTC.setUp(self)
-        self.hm = self.repo.hm
         CALLED[:] = ()
 
     def _after_relation_hook(self, pool, fromeid, rtype, toeid):
@@ -477,20 +536,28 @@
 
     def test_inline_relation(self):
         """make sure <event>_relation hooks are called for inlined relation"""
-        self.hm.register(EcritParHook)
-        eidp = self.execute('INSERT Personne X: X nom "toto"')[0][0]
-        eidn = self.execute('INSERT Note X: X type "T"')[0][0]
-        self.execute('SET N ecrit_par Y WHERE N type "T", Y nom "toto"')
-        self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp),
-                                   ('after_add_relation', eidn, 'ecrit_par', eidp)])
-        CALLED[:] = ()
-        self.execute('DELETE N ecrit_par Y WHERE N type "T", Y nom "toto"')
-        self.assertEquals(CALLED, [('before_delete_relation', eidn, 'ecrit_par', eidp),
-                                   ('after_delete_relation', eidn, 'ecrit_par', eidp)])
-        CALLED[:] = ()
-        eidn = self.execute('INSERT Note N: N ecrit_par P WHERE P nom "toto"')[0][0]
-        self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp),
-                                   ('after_add_relation', eidn, 'ecrit_par', eidp)])
+        class EcritParHook(hook.Hook):
+            __regid__ = 'inlinedrelhook'
+            __select__ = hook.Hook.__select__ & hook.match_rtype('ecrit_par')
+            events = ('before_add_relation', 'after_add_relation',
+                      'before_delete_relation', 'after_delete_relation')
+            def __call__(self):
+                CALLED.append((self.event, self.eidfrom, self.rtype, self.eidto))
+
+        with self.temporary_appobjects(EcritParHook):
+            eidp = self.execute('INSERT Personne X: X nom "toto"')[0][0]
+            eidn = self.execute('INSERT Note X: X type "T"')[0][0]
+            self.execute('SET N ecrit_par Y WHERE N type "T", Y nom "toto"')
+            self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp),
+                                       ('after_add_relation', eidn, 'ecrit_par', eidp)])
+            CALLED[:] = ()
+            self.execute('DELETE N ecrit_par Y WHERE N type "T", Y nom "toto"')
+            self.assertEquals(CALLED, [('before_delete_relation', eidn, 'ecrit_par', eidp),
+                                       ('after_delete_relation', eidn, 'ecrit_par', eidp)])
+            CALLED[:] = ()
+            eidn = self.execute('INSERT Note N: N ecrit_par P WHERE P nom "toto"')[0][0]
+            self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp),
+                                       ('after_add_relation', eidn, 'ecrit_par', eidp)])
 
 
 if __name__ == '__main__':
--- a/server/test/unittest_rql2sql.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_rql2sql.py	Tue Apr 06 19:46:38 2010 +0200
@@ -13,7 +13,6 @@
 from logilab.common.testlib import TestCase, unittest_main, mock_object
 
 from rql import BadRQLQuery
-from indexer import get_indexer
 
 #from cubicweb.server.sources.native import remove_unused_solutions
 from cubicweb.server.sources.rql2sql import SQLGenerator, remove_unused_solutions
@@ -37,6 +36,10 @@
 schema['state_of'].inlined = False
 schema['comments'].inlined = False
 
+def teardown_module(*args):
+    global config, schema
+    del config, schema
+
 PARSER = [
     (r"Personne P WHERE P nom 'Zig\'oto';",
      '''SELECT _P.cw_eid
@@ -540,13 +543,10 @@
 ORDER BY 4 DESC'''),
 
 
-    ("Any X WHERE X eid 0, X eid 0",
-     '''SELECT 0'''),
-
-    ("Any X WHERE X eid 0, X eid 0, X test TRUE",
+    ("Any X WHERE X eid 0, X test TRUE",
      '''SELECT _X.cw_eid
 FROM cw_Personne AS _X
-WHERE _X.cw_eid=0 AND _X.cw_eid=0 AND _X.cw_test=TRUE'''),
+WHERE _X.cw_eid=0 AND _X.cw_test=TRUE'''),
 
     ("Any X,GROUP_CONCAT(TN) GROUPBY X ORDERBY XN WHERE T tags X, X name XN, T name TN, X is CWGroup",
      '''SELECT _X.cw_eid, GROUP_CONCAT(_T.cw_name)
@@ -1068,7 +1068,7 @@
 WHERE rel_is0.eid_to=2'''),
 
     ]
-from logilab.common.adbh import ADV_FUNC_HELPER_DIRECTORY
+from logilab.database import get_db_helper
 
 class CWRQLTC(RQLGeneratorTC):
     schema = schema
@@ -1102,13 +1102,8 @@
     #capture = True
     def setUp(self):
         RQLGeneratorTC.setUp(self)
-        indexer = get_indexer('postgres', 'utf8')
-        dbms_helper = ADV_FUNC_HELPER_DIRECTORY['postgres']
-        dbms_helper.fti_uid_attr = indexer.uid_attr
-        dbms_helper.fti_table = indexer.table
-        dbms_helper.fti_restriction_sql = indexer.restriction_sql
-        dbms_helper.fti_need_distinct_query = indexer.need_distinct
-        self.o = SQLGenerator(schema, dbms_helper)
+        dbhelper = get_db_helper('postgres')
+        self.o = SQLGenerator(schema, dbhelper)
 
     def _norm_sql(self, sql):
         return sql.strip()
@@ -1118,8 +1113,8 @@
             args = {'text': 'hip hop momo'}
         try:
             union = self._prepare(rql)
-            r, nargs = self.o.generate(union, args,
-                                      varmap=varmap)
+            r, nargs, cbs = self.o.generate(union, args,
+                                            varmap=varmap)
             args.update(nargs)
             self.assertLinesEquals((r % args).strip(), self._norm_sql(sql), striplines=True)
         except Exception, ex:
@@ -1140,7 +1135,7 @@
     def _checkall(self, rql, sql):
         try:
             rqlst = self._prepare(rql)
-            r, args = self.o.generate(rqlst)
+            r, args, cbs = self.o.generate(rqlst)
             self.assertEqual((r.strip(), args), sql)
         except Exception, ex:
             print rql
@@ -1202,12 +1197,19 @@
 
     def test_is_null_transform(self):
         union = self._prepare('Any X WHERE X login %(login)s')
-        r, args = self.o.generate(union, {'login': None})
+        r, args, cbs = self.o.generate(union, {'login': None})
         self.assertLinesEquals((r % args).strip(),
                                '''SELECT _X.cw_eid
 FROM cw_CWUser AS _X
 WHERE _X.cw_login IS NULL''')
 
+
+    def test_date_extraction(self):
+        self._check("Any MONTH(D) WHERE P is Personne, P creation_date D",
+                    '''SELECT CAST(EXTRACT(MONTH from _P.cw_creation_date) AS INTEGER)
+FROM cw_Personne AS _P''')
+
+
     def test_parser_parse(self):
         for t in self._parse(PARSER):
             yield t
@@ -1384,11 +1386,11 @@
                     '''SELECT COUNT(1)
 WHERE EXISTS(SELECT 1 FROM owned_by_relation AS rel_owned_by0, cw_Affaire AS _P WHERE rel_owned_by0.eid_from=_P.cw_eid AND rel_owned_by0.eid_to=1 UNION SELECT 1 FROM owned_by_relation AS rel_owned_by1, cw_Note AS _P WHERE rel_owned_by1.eid_from=_P.cw_eid AND rel_owned_by1.eid_to=1)''')
 
-    def test_attr_map(self):
+    def test_attr_map_sqlcb(self):
         def generate_ref(gen, linkedvar, rel):
             linkedvar.accept(gen)
             return 'VERSION_DATA(%s)' % linkedvar._q_sql
-        self.o.attr_map['Affaire.ref'] = generate_ref
+        self.o.attr_map['Affaire.ref'] = (generate_ref, False)
         try:
             self._check('Any R WHERE X ref R',
                         '''SELECT VERSION_DATA(_X.cw_eid)
@@ -1400,22 +1402,33 @@
         finally:
             self.o.attr_map.clear()
 
+    def test_attr_map_sourcecb(self):
+        cb = lambda x,y: None
+        self.o.attr_map['Affaire.ref'] = (cb, True)
+        try:
+            union = self._prepare('Any R WHERE X ref R')
+            r, nargs, cbs = self.o.generate(union, args={})
+            self.assertLinesEquals(r.strip(), 'SELECT _X.cw_ref\nFROM cw_Affaire AS _X')
+            self.assertEquals(cbs, {0: [cb]})
+        finally:
+            self.o.attr_map.clear()
+
 
 class SqliteSQLGeneratorTC(PostgresSQLGeneratorTC):
 
     def setUp(self):
         RQLGeneratorTC.setUp(self)
-        indexer = get_indexer('sqlite', 'utf8')
-        dbms_helper = ADV_FUNC_HELPER_DIRECTORY['sqlite']
-        dbms_helper.fti_uid_attr = indexer.uid_attr
-        dbms_helper.fti_table = indexer.table
-        dbms_helper.fti_restriction_sql = indexer.restriction_sql
-        dbms_helper.fti_need_distinct_query = indexer.need_distinct
-        self.o = SQLGenerator(schema, dbms_helper)
+        dbhelper = get_db_helper('sqlite')
+        self.o = SQLGenerator(schema, dbhelper)
 
     def _norm_sql(self, sql):
         return sql.strip().replace(' ILIKE ', ' LIKE ').replace('\nINTERSECT ALL\n', '\nINTERSECT\n')
 
+    def test_date_extraction(self):
+        self._check("Any MONTH(D) WHERE P is Personne, P creation_date D",
+                    '''SELECT MONTH(_P.cw_creation_date)
+FROM cw_Personne AS _P''')
+
     def test_union(self):
         for t in self._parse((
             ('(Any N ORDERBY 1 WHERE X name N, X is State)'
@@ -1481,26 +1494,26 @@
     def test_has_text(self):
         for t in self._parse((
             ('Any X WHERE X has_text "toto tata"',
-             """SELECT appears0.uid
+             """SELECT DISTINCT appears0.uid
 FROM appears AS appears0
 WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('toto', 'tata'))"""),
 
             ('Any X WHERE X has_text %(text)s',
-             """SELECT appears0.uid
+             """SELECT DISTINCT appears0.uid
 FROM appears AS appears0
 WHERE appears0.word_id IN (SELECT word_id FROM word WHERE word in ('hip', 'hop', 'momo'))"""),
 
             ('Personne X WHERE X has_text "toto tata"',
-             """SELECT _X.eid
+             """SELECT DISTINCT _X.eid
 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", X is IN (Basket,Folder)',
-             """SELECT _X.cw_eid
+             """SELECT DISTINCT _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
-UNION ALL
-SELECT _X.cw_eid
+UNION
+SELECT DISTINCT _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
 """),
@@ -1513,13 +1526,8 @@
 
     def setUp(self):
         RQLGeneratorTC.setUp(self)
-        indexer = get_indexer('mysql', 'utf8')
-        dbms_helper = ADV_FUNC_HELPER_DIRECTORY['mysql']
-        dbms_helper.fti_uid_attr = indexer.uid_attr
-        dbms_helper.fti_table = indexer.table
-        dbms_helper.fti_restriction_sql = indexer.restriction_sql
-        dbms_helper.fti_need_distinct_query = indexer.need_distinct
-        self.o = SQLGenerator(schema, dbms_helper)
+        dbhelper = get_db_helper('mysql')
+        self.o = SQLGenerator(schema, dbhelper)
 
     def _norm_sql(self, sql):
         sql = sql.strip().replace(' ILIKE ', ' LIKE ').replace('TRUE', '1').replace('FALSE', '0')
@@ -1533,6 +1541,11 @@
             latest = firstword
         return '\n'.join(newsql)
 
+    def test_date_extraction(self):
+        self._check("Any MONTH(D) WHERE P is Personne, P creation_date D",
+                    '''SELECT EXTRACT(MONTH from _P.cw_creation_date)
+FROM cw_Personne AS _P''')
+
     def test_from_clause_needed(self):
         queries = [("Any 1 WHERE EXISTS(T is CWGroup, T name 'managers')",
                     '''SELECT 1
--- a/server/test/unittest_rqlannotation.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_rqlannotation.py	Tue Apr 06 19:46:38 2010 +0200
@@ -8,6 +8,11 @@
 
 repo, cnx = init_test_database()
 
+def teardown_module(*args):
+    global repo, cnx
+    del repo, cnx
+
+
 class SQLGenAnnotatorTC(BaseQuerierTC):
     repo = repo
 
--- a/server/test/unittest_schemaserial.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_schemaserial.py	Tue Apr 06 19:46:38 2010 +0200
@@ -15,9 +15,19 @@
 config.bootstrap_cubes()
 schema = loader.load(config)
 
+def teardown_module(*args):
+    global schema, config, loader
+    del schema, config, loader
+
 from cubicweb.server.schemaserial import *
 from cubicweb.server.schemaserial import _erperms2rql as erperms2rql
 
+cstrtypemap = {'RQLConstraint': 'RQLConstraint_eid',
+               'SizeConstraint': 'SizeConstraint_eid',
+               'StaticVocabularyConstraint': 'StaticVocabularyConstraint_eid',
+               'FormatConstraint': 'FormatConstraint_eid',
+               }
+
 class Schema2RQLTC(TestCase):
 
     def test_eschema2rql1(self):
@@ -34,104 +44,124 @@
                  {'description': u'', 'final': True, 'name': u'String'})])
 
     def test_eschema2rql_specialization(self):
+        # x: None since eschema.eid are None
         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',
+                              [('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
+                                {'et': None, 'x': None}),
+                               ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
+                                {'et': None, 'x': None}),
+                               ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
+                                {'et': None, 'x': None}),
+                               # ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
                                #  {'et': 'File', 'x': 'Image'}),
-                               ('SET X specializes ET WHERE X name %(x)s, ET name %(et)s',
-                                {'et': 'Societe', 'x': 'Division'})])
+                               ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
+                                {'et': None, 'x': None})])
 
     def test_rschema2rql1(self):
-        self.assertListEquals(list(rschema2rql(schema.rschema('relation_type'))),
+        self.assertListEquals(list(rschema2rql(schema.rschema('relation_type'), cstrtypemap)),
                              [
             ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s',
              {'description': u'link a relation definition to its relation type', 'symmetric': False, 'name': u'relation_type', 'final' : False, 'fulltext_container': None, 'inlined': True}),
 
-            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE name %(se)s,ER name %(rt)s,OE name %(oe)s',
-             {'rt': 'relation_type', 'description': u'', 'composite': u'object', 'oe': 'CWRType',
-              'ordernum': 1, 'cardinality': u'1*', 'se': 'CWAttribute'}),
-            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT name %(ctname)s, EDEF relation_type ER, EDEF from_entity SE, EDEF to_entity OE, ER name %(rt)s, SE name %(se)s, OE name %(oe)s, EDEF is CWRelation',
-             {'rt': 'relation_type', 'oe': 'CWRType', 'ctname': u'RQLConstraint', 'se': 'CWAttribute', 'value': u';O;O final TRUE\n'}),
+            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
+             {'se': None, 'rt': None, 'oe': None,
+              'description': u'', 'composite': u'object', 'cardinality': u'1*',
+              'ordernum': 1}),
+            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
+             {'x': None, 'ct': u'RQLConstraint_eid', 'value': u';O;O final TRUE\n'}),
 
-            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE name %(se)s,ER name %(rt)s,OE name %(oe)s',
-             {'rt': 'relation_type', 'description': u'', 'composite': u'object', 'oe': 'CWRType',
-              'ordernum': 1, 'cardinality': u'1*', 'se': 'CWRelation'}),
-            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT name %(ctname)s, EDEF relation_type ER, EDEF from_entity SE, EDEF to_entity OE, ER name %(rt)s, SE name %(se)s, OE name %(oe)s, EDEF is CWRelation',
-             {'rt': 'relation_type', 'oe': 'CWRType', 'ctname': u'RQLConstraint', 'se': 'CWRelation', 'value': u';O;O final FALSE\n'}),
+            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
+             {'se': None, 'rt': None, 'oe': None,
+              'description': u'', 'composite': u'object', 
+              'ordernum': 1, 'cardinality': u'1*'}),
+            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
+             {'x': None, 'ct': u'RQLConstraint_eid', 'value': u';O;O final FALSE\n'}),
             ])
 
     def test_rschema2rql2(self):
-        self.assertListEquals(list(rschema2rql(schema.rschema('add_permission'))),
+        self.assertListEquals(list(rschema2rql(schema.rschema('add_permission'), cstrtypemap)),
                               [
             ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s', {'description': u'', 'symmetric': False, 'name': u'add_permission', 'final': False, 'fulltext_container': None, 'inlined': False}),
 
-            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE name %(se)s,ER name %(rt)s,OE name %(oe)s',
-             {'rt': 'add_permission', 'description': u'groups allowed to add entities/relations of this type', 'composite': None, 'oe': 'CWGroup', 'ordernum': 9999, 'cardinality': u'**', 'se': 'CWEType'}),
-            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE name %(se)s,ER name %(rt)s,OE name %(oe)s',
-             {'rt': 'add_permission', 'description': u'rql expression allowing to add entities/relations of this type', 'composite': 'subject', 'oe': 'RQLExpression', 'ordernum': 9999, 'cardinality': u'*?', 'se': 'CWEType'}),
+            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
+             {'se': None, 'rt': None, 'oe': None,
+              'description': u'groups allowed to add entities/relations of this type', 'composite': None, 'ordernum': 9999, 'cardinality': u'**'}),
+            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
+             {'se': None, 'rt': None, 'oe': None,
+              'description': u'rql expression allowing to add entities/relations of this type', 'composite': 'subject', 'ordernum': 9999, 'cardinality': u'*?'}),
 
-            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE name %(se)s,ER name %(rt)s,OE name %(oe)s',
-             {'rt': 'add_permission', 'description': u'groups allowed to add entities/relations of this type', 'composite': None, 'oe': 'CWGroup', 'ordernum': 9999, 'cardinality': u'**', 'se': 'CWRelation'}),
-            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE name %(se)s,ER name %(rt)s,OE name %(oe)s',
-             {'rt': 'add_permission', 'description': u'rql expression allowing to add entities/relations of this type', 'composite': 'subject', 'oe': 'RQLExpression', 'ordernum': 9999, 'cardinality': u'*?', 'se': 'CWRelation'}),
+            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
+             {'se': None, 'rt': None, 'oe': None,
+              'description': u'groups allowed to add entities/relations of this type', 'composite': None, 'ordernum': 9999, 'cardinality': u'**'}),
+            ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
+             {'se': None, 'rt': None, 'oe': None,
+              'description': u'rql expression allowing to add entities/relations of this type', 'composite': 'subject', 'ordernum': 9999, 'cardinality': u'*?'}),
             ])
 
     def test_rschema2rql3(self):
-        self.assertListEquals(list(rschema2rql(schema.rschema('cardinality'))),
+        self.assertListEquals(list(rschema2rql(schema.rschema('cardinality'), cstrtypemap)),
                              [
             ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s',
              {'description': u'', 'symmetric': False, 'name': u'cardinality', 'final': True, 'fulltext_container': None, 'inlined': False}),
 
-            ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,X description %(description)s,X fulltextindexed %(fulltextindexed)s,X indexed %(indexed)s,X internationalizable %(internationalizable)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE name %(se)s,ER name %(rt)s,OE name %(oe)s',
-             {'rt': 'cardinality', 'description': u'subject/object cardinality', 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 5, 'defaultval': None, 'indexed': False, 'cardinality': u'?1', 'oe': 'String', 'se': 'CWAttribute'}),
-            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT name %(ctname)s, EDEF relation_type ER, EDEF from_entity SE, EDEF to_entity OE, ER name %(rt)s, SE name %(se)s, OE name %(oe)s, EDEF is CWAttribute',
-             {'rt': 'cardinality', 'oe': 'String', 'ctname': u'SizeConstraint', 'se': 'CWAttribute', 'value': u'max=2'}),
-            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT name %(ctname)s, EDEF relation_type ER, EDEF from_entity SE, EDEF to_entity OE, ER name %(rt)s, SE name %(se)s, OE name %(oe)s, EDEF is CWAttribute',
-             {'rt': 'cardinality', 'oe': 'String', 'ctname': u'StaticVocabularyConstraint', 'se': 'CWAttribute', 'value': u"u'?1', u'11'"}),
+            ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,X description %(description)s,X fulltextindexed %(fulltextindexed)s,X indexed %(indexed)s,X internationalizable %(internationalizable)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
+             {'se': None, 'rt': None, 'oe': None,
+              'description': u'subject/object cardinality', 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 5, 'defaultval': None, 'indexed': False, 'cardinality': u'?1'}),
+            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
+             {'x': None, 'ct': u'SizeConstraint_eid', 'value': u'max=2'}),
+            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
+             {'x': None, 'ct': u'StaticVocabularyConstraint_eid', 'value': u"u'?1', u'11'"}),
 
-            ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,X description %(description)s,X fulltextindexed %(fulltextindexed)s,X indexed %(indexed)s,X internationalizable %(internationalizable)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE name %(se)s,ER name %(rt)s,OE name %(oe)s',
-             {'rt': 'cardinality', 'description': u'subject/object cardinality', 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 5, 'defaultval': None, 'indexed': False, 'cardinality': u'?1', 'oe': 'String', 'se': 'CWRelation'}),
-            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT name %(ctname)s, EDEF relation_type ER, EDEF from_entity SE, EDEF to_entity OE, ER name %(rt)s, SE name %(se)s, OE name %(oe)s, EDEF is CWAttribute',
-             {'rt': 'cardinality', 'oe': 'String', 'ctname': u'SizeConstraint', 'se': 'CWRelation', 'value': u'max=2'}),
-            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT name %(ctname)s, EDEF relation_type ER, EDEF from_entity SE, EDEF to_entity OE, ER name %(rt)s, SE name %(se)s, OE name %(oe)s, EDEF is CWAttribute',
-             {'rt': 'cardinality', 'oe': 'String', 'ctname': u'StaticVocabularyConstraint', 'se': 'CWRelation', 'value': u"u'?*', u'1*', u'+*', u'**', u'?+', u'1+', u'++', u'*+', u'?1', u'11', u'+1', u'*1', u'??', u'1?', u'+?', u'*?'"}),
+            ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,X description %(description)s,X fulltextindexed %(fulltextindexed)s,X indexed %(indexed)s,X internationalizable %(internationalizable)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
+             {'se': None, 'rt': None, 'oe': None,
+              'description': u'subject/object cardinality', 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 5, 'defaultval': None, 'indexed': False, 'cardinality': u'?1'}),
+            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
+             {'x': None, 'ct': u'SizeConstraint_eid', 'value': u'max=2'}),
+            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
+             {'x': None, 'ct': u'StaticVocabularyConstraint_eid', 'value': u"u'?*', u'1*', u'+*', u'**', u'?+', u'1+', u'++', u'*+', u'?1', u'11', u'+1', u'*1', u'??', u'1?', u'+?', u'*?'"}),
             ])
 
+    def test_rdef2rql(self):
+        self.assertListEquals(list(rdef2rql(schema['description_format'].rdefs[('CWRType', 'String')], cstrtypemap)),
+                              [
+            ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,X description %(description)s,X fulltextindexed %(fulltextindexed)s,X indexed %(indexed)s,X internationalizable %(internationalizable)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
+             {'se': None, 'rt': None, 'oe': None,
+              'description': u'', 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 7, 'defaultval': u'text/plain', 'indexed': False, 'cardinality': u'?1'}),
+            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
+             {'x': None, 'value': u'None', 'ct': 'FormatConstraint_eid'}),
+            ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
+             {'x': None, 'value': u'max=50', 'ct': 'SizeConstraint_eid'})])
+
 
     def test_updateeschema2rql1(self):
-        self.assertListEquals(list(updateeschema2rql(schema.eschema('CWAttribute'))),
-                              [('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X is CWEType, X name %(et)s',
-                                {'description': u'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', 'et': 'CWAttribute', 'final': False, 'name': u'CWAttribute'}),
+        self.assertListEquals(list(updateeschema2rql(schema.eschema('CWAttribute'), 1)),
+                              [('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s',
+                                {'description': u'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', 'x': 1, 'final': False, 'name': u'CWAttribute'}),
                                ])
 
     def test_updateeschema2rql2(self):
-        self.assertListEquals(list(updateeschema2rql(schema.eschema('String'))),
-                              [('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X is CWEType, X name %(et)s',
-                                {'description': u'', 'et': 'String', 'final': True, 'name': u'String'})
+        self.assertListEquals(list(updateeschema2rql(schema.eschema('String'), 1)),
+                              [('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s',
+                                {'description': u'', 'x': 1, 'final': True, 'name': u'String'})
                                ])
 
     def test_updaterschema2rql1(self):
-        self.assertListEquals(list(updaterschema2rql(schema.rschema('relation_type'))),
+        self.assertListEquals(list(updaterschema2rql(schema.rschema('relation_type'), 1)),
                              [
-            ('SET X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s WHERE X is CWRType, X name %(rt)s',
-             {'rt': 'relation_type', 'symmetric': False,
+            ('SET X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s WHERE X eid %(x)s',
+             {'x': 1, 'symmetric': False,
               'description': u'link a relation definition to its relation type',
               'final': False, 'fulltext_container': None, 'inlined': True, 'name': u'relation_type'})
             ])
 
     def test_updaterschema2rql2(self):
         expected = [
-            ('SET X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s WHERE X is CWRType, X name %(rt)s',
-             {'rt': 'add_permission', 'symmetric': False,
+            ('SET X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s WHERE X eid %(x)s',
+             {'x': 1, 'symmetric': False,
               'description': u'', 'final': False, 'fulltext_container': None,
               'inlined': False, 'name': u'add_permission'})
             ]
-        for i, (rql, args) in enumerate(updaterschema2rql(schema.rschema('add_permission'))):
+        for i, (rql, args) in enumerate(updaterschema2rql(schema.rschema('add_permission'), 1)):
             yield self.assertEquals, (rql, args), expected[i]
 
 class Perms2RQLTC(TestCase):
@@ -144,29 +174,29 @@
 
     def test_eperms2rql1(self):
         self.assertListEquals([(rql, kwargs) for rql, kwargs in erperms2rql(schema.eschema('CWEType'), self.GROUP_MAPPING)],
-                              [('SET X read_permission Y WHERE Y eid %(g)s, ', {'g': 0}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, ', {'g': 1}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, ', {'g': 2}),
-                               ('SET X add_permission Y WHERE Y eid %(g)s, ', {'g': 0}),
-                               ('SET X update_permission Y WHERE Y eid %(g)s, ', {'g': 0}),
-                               ('SET X delete_permission Y WHERE Y eid %(g)s, ', {'g': 0}),
+                              [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
+                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
+                               ('SET X add_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                               ('SET X update_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                               ('SET X delete_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
                                ])
 
     def test_rperms2rql2(self):
         self.assertListEquals([(rql, kwargs) for rql, kwargs in erperms2rql(schema.rschema('read_permission').rdef('CWEType', 'CWGroup'), self.GROUP_MAPPING)],
-                              [('SET X read_permission Y WHERE Y eid %(g)s, ', {'g': 0}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, ', {'g': 1}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, ', {'g': 2}),
-                               ('SET X add_permission Y WHERE Y eid %(g)s, ', {'g': 0}),
-                               ('SET X delete_permission Y WHERE Y eid %(g)s, ', {'g': 0}),
+                              [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
+                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
+                               ('SET X add_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                               ('SET X delete_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
                                ])
 
     def test_rperms2rql3(self):
         self.assertListEquals([(rql, kwargs) for rql, kwargs in erperms2rql(schema.rschema('name').rdef('CWEType', 'String'), self.GROUP_MAPPING)],
-                              [('SET X read_permission Y WHERE Y eid %(g)s, ', {'g': 0}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, ', {'g': 1}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, ', {'g': 2}),
-                               ('SET X update_permission Y WHERE Y eid %(g)s, ', {'g': 0}),
+                              [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
+                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
+                               ('SET X update_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
                                ])
 
     #def test_perms2rql(self):
--- a/server/test/unittest_sqlutils.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_sqlutils.py	Tue Apr 06 19:46:38 2010 +0200
@@ -20,13 +20,13 @@
 
     def test_init(self):
         o = SQLAdapterMixIn(BASE_CONFIG)
-        self.assertEquals(o.encoding, 'UTF-8')
+        self.assertEquals(o.dbhelper.dbencoding, 'UTF-8')
 
     def test_init_encoding(self):
         config = BASE_CONFIG.copy()
         config['db-encoding'] = 'ISO-8859-1'
         o = SQLAdapterMixIn(config)
-        self.assertEquals(o.encoding, 'ISO-8859-1')
+        self.assertEquals(o.dbhelper.dbencoding, 'ISO-8859-1')
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_ssplanner.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/test/unittest_ssplanner.py	Tue Apr 06 19:46:38 2010 +0200
@@ -12,6 +12,10 @@
 # keep cnx so it's not garbage collected and the associated session closed
 repo, cnx = init_test_database()
 
+def teardown_module(*args):
+    global repo, cnx
+    del repo, cnx
+
 class SSPlannerTC(BasePlannerTC):
     repo = repo
     _test = test_plan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/unittest_storage.py	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,184 @@
+"""unit tests for module cubicweb.server.sources.storages
+
+:organization: Logilab
+:copyright: 2010 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 __future__ import with_statement
+
+from logilab.common.testlib import unittest_main
+from cubicweb.devtools.testlib import CubicWebTC
+
+import os.path as osp
+import shutil
+import tempfile
+
+from cubicweb import Binary, QueryError
+from cubicweb.selectors import implements
+from cubicweb.server.sources import storages
+from cubicweb.server.hook import Hook, Operation
+
+class DummyBeforeHook(Hook):
+    __regid__ = 'dummy-before-hook'
+    __select__ = Hook.__select__ & implements('File')
+    events = ('before_add_entity',)
+
+    def __call__(self):
+        self._cw.transaction_data['orig_file_value'] = self.entity.data.getvalue()
+
+
+class DummyAfterHook(Hook):
+    __regid__ = 'dummy-after-hook'
+    __select__ = Hook.__select__ & implements('File')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        # new value of entity.data should be the same as before
+        oldvalue = self._cw.transaction_data['orig_file_value']
+        assert oldvalue == self.entity.data.getvalue()
+
+class StorageTC(CubicWebTC):
+
+    def setup_database(self):
+        self.tempdir = tempfile.mkdtemp()
+        bfs_storage = storages.BytesFileSystemStorage(self.tempdir)
+        storages.set_attribute_storage(self.repo, 'File', 'data', bfs_storage)
+
+    def tearDown(self):
+        super(CubicWebTC, self).tearDown()
+        storages.unset_attribute_storage(self.repo, 'File', 'data')
+        shutil.rmtree(self.tempdir)
+
+
+    def create_file(self, content='the-data'):
+        req = self.request()
+        return req.create_entity('File', data=Binary(content),
+                                 data_format=u'text/plain', data_name=u'foo')
+
+    def test_bfss_storage(self):
+        f1 = self.create_file()
+        expected_filepath = osp.join(self.tempdir, '%s_data' % f1.eid)
+        self.failUnless(osp.isfile(expected_filepath))
+        self.assertEquals(file(expected_filepath).read(), 'the-data')
+        self.rollback()
+        self.failIf(osp.isfile(expected_filepath))
+        f1 = self.create_file()
+        self.commit()
+        self.assertEquals(file(expected_filepath).read(), 'the-data')
+        f1.set_attributes(data=Binary('the new data'))
+        self.rollback()
+        self.assertEquals(file(expected_filepath).read(), 'the-data')
+        f1.delete()
+        self.failUnless(osp.isfile(expected_filepath))
+        self.rollback()
+        self.failUnless(osp.isfile(expected_filepath))
+        f1.delete()
+        self.commit()
+        self.failIf(osp.isfile(expected_filepath))
+
+    def test_bfss_sqlite_fspath(self):
+        f1 = self.create_file()
+        expected_filepath = osp.join(self.tempdir, '%s_data' % f1.eid)
+        fspath = self.execute('Any fspath(D) WHERE F eid %(f)s, F data D',
+                              {'f': f1.eid})[0][0]
+        self.assertEquals(fspath.getvalue(), expected_filepath)
+
+    def test_bfss_fs_importing_doesnt_touch_path(self):
+        self.session.transaction_data['fs_importing'] = True
+        filepath = osp.abspath(__file__)
+        f1 = self.session.create_entity('File', data=Binary(filepath),
+                                        data_format=u'text/plain', data_name=u'foo')
+        fspath = self.execute('Any fspath(D) WHERE F eid %(f)s, F data D',
+                              {'f': f1.eid})[0][0]
+        self.assertEquals(fspath.getvalue(), filepath)
+
+    def test_source_storage_transparency(self):
+        with self.temporary_appobjects(DummyBeforeHook, DummyAfterHook):
+            self.create_file()
+
+    def test_source_mapped_attribute_error_cases(self):
+        ex = self.assertRaises(QueryError, self.execute,
+                               'Any X WHERE X data ~= "hop", X is File')
+        self.assertEquals(str(ex), 'can\'t use File.data (X data ILIKE "hop") in restriction')
+        ex = self.assertRaises(QueryError, self.execute,
+                               'Any X, Y WHERE X data D, Y data D, '
+                               'NOT X identity Y, X is File, Y is File')
+        self.assertEquals(str(ex), "can't use D as a restriction variable")
+        # query returning mix of mapped / regular attributes (only file.data
+        # mapped, not image.data for instance)
+        ex = self.assertRaises(QueryError, self.execute,
+                               'Any X WITH X BEING ('
+                               ' (Any NULL)'
+                               '  UNION '
+                               ' (Any D WHERE X data D, X is File)'
+                               ')')
+        self.assertEquals(str(ex), 'query fetch some source mapped attribute, some not')
+        ex = self.assertRaises(QueryError, self.execute,
+                               '(Any D WHERE X data D, X is File)'
+                               ' UNION '
+                               '(Any D WHERE X data D, X is Image)')
+        self.assertEquals(str(ex), 'query fetch some source mapped attribute, some not')
+        ex = self.assertRaises(QueryError,
+                               self.execute, 'Any D WHERE X data D')
+        self.assertEquals(str(ex), 'query fetch some source mapped attribute, some not')
+
+    def test_source_mapped_attribute_advanced(self):
+        f1 = self.create_file()
+        rset = self.execute('Any X,D WITH D,X BEING ('
+                            ' (Any D, X WHERE X eid %(x)s, X data D)'
+                            '  UNION '
+                            ' (Any D, X WHERE X eid %(x)s, X data D)'
+                            ')', {'x': f1.eid}, 'x')
+        self.assertEquals(len(rset), 2)
+        self.assertEquals(rset[0][0], f1.eid)
+        self.assertEquals(rset[1][0], f1.eid)
+        self.assertEquals(rset[0][1].getvalue(), 'the-data')
+        self.assertEquals(rset[1][1].getvalue(), 'the-data')
+        rset = self.execute('Any X,LENGTH(D) WHERE X eid %(x)s, X data D',
+                            {'x': f1.eid}, 'x')
+        self.assertEquals(len(rset), 1)
+        self.assertEquals(rset[0][0], f1.eid)
+        self.assertEquals(rset[0][1], len('the-data'))
+        rset = self.execute('Any X,LENGTH(D) WITH D,X BEING ('
+                            ' (Any D, X WHERE X eid %(x)s, X data D)'
+                            '  UNION '
+                            ' (Any D, X WHERE X eid %(x)s, X data D)'
+                            ')', {'x': f1.eid}, 'x')
+        self.assertEquals(len(rset), 2)
+        self.assertEquals(rset[0][0], f1.eid)
+        self.assertEquals(rset[1][0], f1.eid)
+        self.assertEquals(rset[0][1], len('the-data'))
+        self.assertEquals(rset[1][1], len('the-data'))
+        ex = self.assertRaises(QueryError, self.execute,
+                               'Any X,UPPER(D) WHERE X eid %(x)s, X data D',
+                               {'x': f1.eid}, 'x')
+        self.assertEquals(str(ex), 'UPPER can not be called on mapped attribute')
+
+
+    def test_bfss_fs_importing_transparency(self):
+        self.session.transaction_data['fs_importing'] = True
+        filepath = osp.abspath(__file__)
+        f1 = self.session.create_entity('File', data=Binary(filepath),
+                                        data_format=u'text/plain', data_name=u'foo')
+        self.assertEquals(f1.data.getvalue(), file(filepath).read(),
+                          'files content differ')
+
+
+    def test_bfss_update_with_existing_data(self):
+        # use self.session to use server-side cache
+        f1 = self.session.create_entity('File', data=Binary('some data'),
+                                        data_format=u'text/plain', data_name=u'foo')
+        # NOTE: do not use set_attributes() which would automatically
+        #       update f1's local dict. We want the pure rql version to work
+        self.execute('SET F data %(d)s WHERE F eid %(f)s',
+                     {'d': Binary('some other data'), 'f': f1.eid})
+        self.assertEquals(f1.data.getvalue(), 'some other data')
+        self.commit()
+        f2 = self.entity('Any F WHERE F eid %(f)s, F is File', {'f': f1.eid})
+        self.assertEquals(f2.data.getvalue(), 'some other data')
+
+
+if __name__ == '__main__':
+    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/unittest_undo.py	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,272 @@
+"""
+
+:organization: Logilab
+:copyright: 2001-2010 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 __future__ import with_statement
+
+from cubicweb import ValidationError
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.transaction import *
+
+class UndoableTransactionTC(CubicWebTC):
+
+    def setup_database(self):
+        self.session.undo_actions = set('CUDAR')
+        self.toto = self.create_user('toto', password='toto', groups=('users',),
+                                     commit=False)
+        self.txuuid = self.commit()
+
+    def tearDown(self):
+        self.restore_connection()
+        self.session.undo_support = set()
+        super(UndoableTransactionTC, self).tearDown()
+
+    def check_transaction_deleted(self, txuuid):
+        # also check transaction actions have been properly deleted
+        cu = self.session.system_sql(
+            "SELECT * from tx_entity_actions WHERE tx_uuid='%s'" % txuuid)
+        self.failIf(cu.fetchall())
+        cu = self.session.system_sql(
+            "SELECT * from tx_relation_actions WHERE tx_uuid='%s'" % txuuid)
+        self.failIf(cu.fetchall())
+
+    def test_undo_api(self):
+        self.failUnless(self.txuuid)
+        # test transaction api
+        self.assertRaises(NoSuchTransaction,
+                          self.cnx.transaction_info, 'hop')
+        self.assertRaises(NoSuchTransaction,
+                          self.cnx.transaction_actions, 'hop')
+        self.assertRaises(NoSuchTransaction,
+                          self.cnx.undo_transaction, 'hop')
+        txinfo = self.cnx.transaction_info(self.txuuid)
+        self.failUnless(txinfo.datetime)
+        self.assertEquals(txinfo.user_eid, self.session.user.eid)
+        self.assertEquals(txinfo.user().login, 'admin')
+        actions = txinfo.actions_list()
+        self.assertEquals(len(actions), 2)
+        actions = txinfo.actions_list(public=False)
+        self.assertEquals(len(actions), 6)
+        a1 = actions[0]
+        self.assertEquals(a1.action, 'C')
+        self.assertEquals(a1.eid, self.toto.eid)
+        self.assertEquals(a1.etype,'CWUser')
+        self.assertEquals(a1.changes, None)
+        self.assertEquals(a1.public, True)
+        self.assertEquals(a1.order, 1)
+        a4 = actions[3]
+        self.assertEquals(a4.action, 'A')
+        self.assertEquals(a4.rtype, 'in_group')
+        self.assertEquals(a4.eid_from, self.toto.eid)
+        self.assertEquals(a4.eid_to, self.toto.in_group[0].eid)
+        self.assertEquals(a4.order, 4)
+        for i, rtype in ((1, 'owned_by'), (2, 'owned_by'),
+                         (4, 'in_state'), (5, 'created_by')):
+            a = actions[i]
+            self.assertEquals(a.action, 'A')
+            self.assertEquals(a.eid_from, self.toto.eid)
+            self.assertEquals(a.rtype, rtype)
+            self.assertEquals(a.order, i+1)
+        # test undoable_transactions
+        txs = self.cnx.undoable_transactions()
+        self.assertEquals(len(txs), 1)
+        self.assertEquals(txs[0].uuid, self.txuuid)
+        # test transaction_info / undoable_transactions security
+        cnx = self.login('anon')
+        self.assertRaises(NoSuchTransaction,
+                          cnx.transaction_info, self.txuuid)
+        self.assertRaises(NoSuchTransaction,
+                          cnx.transaction_actions, self.txuuid)
+        self.assertRaises(NoSuchTransaction,
+                          cnx.undo_transaction, self.txuuid)
+        txs = cnx.undoable_transactions()
+        self.assertEquals(len(txs), 0)
+
+    def test_undoable_transactions(self):
+        toto = self.toto
+        e = self.session.create_entity('EmailAddress',
+                                       address=u'toto@logilab.org',
+                                       reverse_use_email=toto)
+        txuuid1 = self.commit()
+        toto.delete()
+        txuuid2 = self.commit()
+        undoable_transactions = self.cnx.undoable_transactions
+        txs = undoable_transactions(action='D')
+        self.assertEquals(len(txs), 1, txs)
+        self.assertEquals(txs[0].uuid, txuuid2)
+        txs = undoable_transactions(action='C')
+        self.assertEquals(len(txs), 2, txs)
+        self.assertEquals(txs[0].uuid, txuuid1)
+        self.assertEquals(txs[1].uuid, self.txuuid)
+        txs = undoable_transactions(eid=toto.eid)
+        self.assertEquals(len(txs), 3)
+        self.assertEquals(txs[0].uuid, txuuid2)
+        self.assertEquals(txs[1].uuid, txuuid1)
+        self.assertEquals(txs[2].uuid, self.txuuid)
+        txs = undoable_transactions(etype='CWUser')
+        self.assertEquals(len(txs), 2)
+        txs = undoable_transactions(etype='CWUser', action='C')
+        self.assertEquals(len(txs), 1)
+        self.assertEquals(txs[0].uuid, self.txuuid)
+        txs = undoable_transactions(etype='EmailAddress', action='D')
+        self.assertEquals(len(txs), 0)
+        txs = undoable_transactions(etype='EmailAddress', action='D',
+                                    public=False)
+        self.assertEquals(len(txs), 1)
+        self.assertEquals(txs[0].uuid, txuuid2)
+        txs = undoable_transactions(eid=toto.eid, action='R', public=False)
+        self.assertEquals(len(txs), 1)
+        self.assertEquals(txs[0].uuid, txuuid2)
+
+    def test_undo_deletion_base(self):
+        toto = self.toto
+        e = self.session.create_entity('EmailAddress',
+                                       address=u'toto@logilab.org',
+                                       reverse_use_email=toto)
+        # entity with inlined relation
+        p = self.session.create_entity('CWProperty',
+                                       pkey=u'ui.default-text-format',
+                                       value=u'text/rest',
+                                       for_user=toto)
+        self.commit()
+        txs = self.cnx.undoable_transactions()
+        self.assertEquals(len(txs), 2)
+        toto.delete()
+        txuuid = self.commit()
+        actions = self.cnx.transaction_info(txuuid).actions_list()
+        self.assertEquals(len(actions), 1)
+        toto.clear_all_caches()
+        e.clear_all_caches()
+        errors = self.cnx.undo_transaction(txuuid)
+        undotxuuid = self.commit()
+        self.assertEquals(undotxuuid, None) # undo not undoable
+        self.assertEquals(errors, [])
+        self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': toto.eid}, 'x'))
+        self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': e.eid}, 'x'))
+        self.failUnless(self.execute('Any X WHERE X has_text "toto@logilab"'))
+        self.assertEquals(toto.state, 'activated')
+        self.assertEquals(toto.get_email(), 'toto@logilab.org')
+        self.assertEquals([(p.pkey, p.value) for p in toto.reverse_for_user],
+                          [('ui.default-text-format', 'text/rest')])
+        self.assertEquals([g.name for g in toto.in_group],
+                          ['users'])
+        self.assertEquals([et.name for et in toto.related('is', entities=True)],
+                          ['CWUser'])
+        self.assertEquals([et.name for et in toto.is_instance_of],
+                          ['CWUser'])
+        # undoing shouldn't be visble in undoable transaction, and the undoed
+        # transaction should be removed
+        txs = self.cnx.undoable_transactions()
+        self.assertEquals(len(txs), 2)
+        self.assertRaises(NoSuchTransaction,
+                          self.cnx.transaction_info, txuuid)
+        self.check_transaction_deleted(txuuid)
+        # the final test: check we can login with the previously deleted user
+        self.login('toto')
+
+    def test_undo_deletion_integrity_1(self):
+        session = self.session
+        # 'Personne fiche Card with' '??' cardinality
+        c = session.create_entity('Card', title=u'hop', content=u'hop')
+        p = session.create_entity('Personne', nom=u'louis', fiche=c)
+        self.commit()
+        c.delete()
+        txuuid = self.commit()
+        c2 = session.create_entity('Card', title=u'hip', content=u'hip')
+        p.set_relations(fiche=c2)
+        self.commit()
+        errors = self.cnx.undo_transaction(txuuid)
+        self.commit()
+        p.clear_all_caches()
+        self.assertEquals(p.fiche[0].eid, c2.eid)
+        self.assertEquals(len(errors), 1)
+        self.assertEquals(errors[0],
+                          "Can't restore object relation fiche to entity "
+                          "%s which is already linked using this relation." % p.eid)
+
+    def test_undo_deletion_integrity_2(self):
+        # test validation error raised if we can't restore a required relation
+        session = self.session
+        g = session.create_entity('CWGroup', name=u'staff')
+        session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid})
+        self.toto.set_relations(in_group=g)
+        self.commit()
+        self.toto.delete()
+        txuuid = self.commit()
+        g.delete()
+        self.commit()
+        errors = self.cnx.undo_transaction(txuuid)
+        self.assertEquals(errors,
+                          [u"Can't restore relation in_group, object entity "
+                          "%s doesn't exist anymore." % g.eid])
+        ex = self.assertRaises(ValidationError, self.commit)
+        self.assertEquals(ex.entity, self.toto.eid)
+        self.assertEquals(ex.errors,
+                          {'in_group-subject': u'at least one relation in_group is '
+                           'required on CWUser (%s)' % self.toto.eid})
+
+    def test_undo_creation_1(self):
+        session = self.session
+        c = session.create_entity('Card', title=u'hop', content=u'hop')
+        p = session.create_entity('Personne', nom=u'louis', fiche=c)
+        txuuid = self.commit()
+        errors = self.cnx.undo_transaction(txuuid)
+        self.commit()
+        self.failIf(errors)
+        self.failIf(self.execute('Any X WHERE X eid %(x)s', {'x': c.eid}, 'x'))
+        self.failIf(self.execute('Any X WHERE X eid %(x)s', {'x': p.eid}, 'x'))
+        self.failIf(self.execute('Any X,Y WHERE X fiche Y'))
+        self.session.set_pool()
+        for eid in (p.eid, c.eid):
+            self.failIf(session.system_sql(
+                'SELECT * FROM entities WHERE eid=%s' % eid).fetchall())
+            self.failIf(session.system_sql(
+                'SELECT 1 FROM owned_by_relation WHERE eid_from=%s' % eid).fetchall())
+            # added by sql in hooks (except when using dataimport)
+            self.failIf(session.system_sql(
+                'SELECT 1 FROM is_relation WHERE eid_from=%s' % eid).fetchall())
+            self.failIf(session.system_sql(
+                'SELECT 1 FROM is_instance_of_relation WHERE eid_from=%s' % eid).fetchall())
+        self.check_transaction_deleted(txuuid)
+
+
+    def test_undo_creation_integrity_1(self):
+        session = self.session
+        tutu = self.create_user('tutu', commit=False)
+        txuuid = self.commit()
+        email = self.request().create_entity('EmailAddress', address=u'tutu@cubicweb.org')
+        prop = self.request().create_entity('CWProperty', pkey=u'ui.default-text-format',
+                                            value=u'text/html')
+        tutu.set_relations(use_email=email, reverse_for_user=prop)
+        self.commit()
+        ex = self.assertRaises(ValidationError,
+                               self.cnx.undo_transaction, txuuid)
+        self.assertEquals(ex.entity, tutu.eid)
+        self.assertEquals(ex.errors,
+                          {None: 'some later transaction(s) touch entity, undo them first'})
+
+    def test_undo_creation_integrity_2(self):
+        session = self.session
+        g = session.create_entity('CWGroup', name=u'staff')
+        txuuid = self.commit()
+        session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid})
+        self.toto.set_relations(in_group=g)
+        self.commit()
+        ex = self.assertRaises(ValidationError,
+                               self.cnx.undo_transaction, txuuid)
+        self.assertEquals(ex.entity, g.eid)
+        self.assertEquals(ex.errors,
+                          {None: 'some later transaction(s) touch entity, undo them first'})
+        # self.assertEquals(errors,
+        #                   [u"Can't restore relation in_group, object entity "
+        #                   "%s doesn't exist anymore." % g.eid])
+        # ex = self.assertRaises(ValidationError, self.commit)
+        # self.assertEquals(ex.entity, self.toto.eid)
+        # self.assertEquals(ex.errors,
+        #                   {'in_group-subject': u'at least one relation in_group is '
+        #                    'required on CWUser (%s)' % self.toto.eid})
+
+    # test implicit 'replacement' of an inlined relation
--- a/server/utils.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/server/utils.py	Tue Apr 06 19:46:38 2010 +0200
@@ -65,6 +65,18 @@
                 del sol[vname]
 
 
+def eschema_eid(session, eschema):
+    """get eid of the CWEType entity for the given yams type. You should use
+    this because when schema has been loaded from the file-system, not from the
+    database, (e.g. during tests), eschema.eid is not set.
+    """
+    if eschema.eid is None:
+        eschema.eid = session.execute(
+            'Any X WHERE X is CWEType, X name %(name)s',
+            {'name': str(eschema)})[0][0]
+    return eschema.eid
+
+
 DEFAULT_MSG = 'we need a manager connection on the repository \
 (the server doesn\'t have to run, even should better not)'
 
--- a/skeleton/__pkginfo__.py.tmpl	Thu Mar 04 17:56:45 2010 +0100
+++ b/skeleton/__pkginfo__.py.tmpl	Tue Apr 06 19:46:38 2010 +0200
@@ -44,7 +44,7 @@
 # them to be included in the debian package
 
 __depends_cubes__ = {}
-__depends__ = {'cubicweb': '>= 3.5.0'}
+__depends__ = {'cubicweb': '>= 3.6.0'}
 __use__ = (%(dependancies)s)
 __recommend__ = ()
 
--- a/sobjects/notification.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/sobjects/notification.py	Tue Apr 06 19:46:38 2010 +0200
@@ -33,11 +33,9 @@
     def recipients(self):
         mode = self._cw.vreg.config['default-recipients-mode']
         if mode == 'users':
-            # use unsafe execute else we may don't have the right to see users
-            # to notify...
-            execute = self._cw.unsafe_execute
+            execute = self._cw.execute
             dests = [(u.get_email(), u.property_value('ui.language'))
-                     for u in execute(self.user_rql, build_descr=True, propagate=True).entities()]
+                     for u in execute(self.user_rql, build_descr=True).entities()]
         elif mode == 'default-dest-addrs':
             lang = self._cw.vreg.property_value('ui.language')
             dests = zip(self._cw.vreg.config['default-dest-addrs'], repeat(lang))
@@ -132,7 +130,7 @@
       override call)
     """
     __abstract__ = True
-    id = 'notif_entity_updated'
+    __regid__ = 'notif_entity_updated'
     msgid_timestamp = False
     message = _('updated')
     no_detailed_change_attrs = ()
@@ -158,7 +156,8 @@
                 if not rdef.has_perm(self._cw, 'read', eid=self.cw_rset[0][0]):
                     continue
             # XXX suppose it's a subject relation...
-            elif not rschema.has_perm(self._cw, 'read', fromeid=self.cw_rset[0][0]): # XXX toeid
+            elif not rschema.has_perm(self._cw, 'read',
+                                      fromeid=self.cw_rset[0][0]):
                 continue
             if attr in self.no_detailed_change_attrs:
                 msg = _('%s updated') % _(attr)
--- a/sobjects/supervising.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/sobjects/supervising.py	Tue Apr 06 19:46:38 2010 +0200
@@ -92,7 +92,7 @@
         return self._cw._('[%s supervision] changes summary') % self._cw.vreg.config.appid
 
     def call(self, changes):
-        user = self._cw.actual_session().user
+        user = self._cw.user
         self.w(self._cw._('user %s has made the following change(s):\n\n')
                % user.login)
         for event, changedescr in filter_changes(changes):
@@ -129,17 +129,16 @@
         self.w(u'  %s' % entity.absolute_url())
 
     def _relation_context(self, changedescr):
-        _ = self._cw._
-        session = self._cw.actual_session()
+        session = self._cw
         def describe(eid):
             try:
-                return _(session.describe(eid)[0]).lower()
+                return session._(session.describe(eid)[0]).lower()
             except UnknownEid:
                 # may occurs when an entity has been deleted from an external
                 # source and we're cleaning its relation
-                return _('unknown external entity')
+                return session._('unknown external entity')
         eidfrom, rtype, eidto = changedescr.eidfrom, changedescr.rtype, changedescr.eidto
-        return {'rtype': _(rtype),
+        return {'rtype': session._(rtype),
                 'eidfrom': eidfrom,
                 'frometype': describe(eidfrom),
                 'eidto': eidto,
--- a/sobjects/test/unittest_supervising.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/sobjects/test/unittest_supervising.py	Tue Apr 06 19:46:38 2010 +0200
@@ -27,7 +27,6 @@
 
 
     def test_supervision(self):
-        session = self.session
         # do some modification
         user = self.execute('INSERT CWUser X: X login "toto", X upassword "sosafe", X in_group G '
                             'WHERE G name "users"').get_entity(0, 0)
@@ -37,6 +36,7 @@
         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
+        session = self.session
         sentops = [op for op in session.pending_operations
                    if isinstance(op, SupervisionMailOp)]
         self.assertEquals(len(sentops), 1)
--- a/test/data/rewrite/schema.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/test/data/rewrite/schema.py	Tue Apr 06 19:46:38 2010 +0200
@@ -39,3 +39,10 @@
 class require_state(RelationDefinition):
     subject = 'CWPermission'
     object = 'State'
+
+
+class inlined_card(RelationDefinition):
+    subject = 'Affaire'
+    object = 'Card'
+    inlined = True
+    cardinality = '?*'
--- a/test/unittest_dbapi.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/test/unittest_dbapi.py	Tue Apr 06 19:46:38 2010 +0200
@@ -5,11 +5,13 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+from copy import copy
+
 from cubicweb import ConnectionError
 from cubicweb.dbapi import ProgrammingError
 from cubicweb.devtools.testlib import CubicWebTC
 
-
 class DBAPITC(CubicWebTC):
 
     def test_public_repo_api(self):
@@ -35,8 +37,8 @@
         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
-        self.assertRaises(ConnectionError, cnx.user, None)
-        self.assertRaises(ConnectionError, cnx.describe, 1)
+        self.assertRaises(ProgrammingError, cnx.user, None)
+        self.assertRaises(ProgrammingError, cnx.describe, 1)
 
     def test_session_data_api(self):
         cnx = self.login('anon')
@@ -64,9 +66,10 @@
         cnx.set_shared_data('data', 4)
         self.assertEquals(cnx.get_shared_data('data'), 4)
         self.restore_connection() # proper way to close cnx
-        self.assertRaises(ConnectionError, cnx.check)
-        self.assertRaises(ConnectionError, cnx.set_shared_data, 'data', 0)
-        self.assertRaises(ConnectionError, cnx.get_shared_data, 'data')
+        self.assertRaises(ProgrammingError, cnx.check)
+        self.assertRaises(ProgrammingError, cnx.set_shared_data, 'data', 0)
+        self.assertRaises(ProgrammingError, cnx.get_shared_data, 'data')
+
 
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
--- a/test/unittest_entity.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/test/unittest_entity.py	Tue Apr 06 19:46:38 2010 +0200
@@ -436,7 +436,7 @@
 
     def test_complete_relation(self):
         session = self.session
-        eid = session.unsafe_execute(
+        eid = session.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')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_req.py	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,16 @@
+from logilab.common.testlib import TestCase, unittest_main
+from cubicweb.req import RequestSessionBase
+
+class RebuildURLTC(TestCase):
+    def test(self):
+        rebuild_url = RequestSessionBase(None).rebuild_url
+        self.assertEquals(rebuild_url('http://logilab.fr?__message=pouet', __message='hop'),
+                          'http://logilab.fr?__message=hop')
+        self.assertEquals(rebuild_url('http://logilab.fr', __message='hop'),
+                          'http://logilab.fr?__message=hop')
+        self.assertEquals(rebuild_url('http://logilab.fr?vid=index', __message='hop'),
+                          'http://logilab.fr?__message=hop&vid=index')
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/test/unittest_rqlrewrite.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/test/unittest_rqlrewrite.py	Tue Apr 06 19:46:38 2010 +0200
@@ -7,7 +7,7 @@
 """
 from logilab.common.testlib import unittest_main, TestCase
 from logilab.common.testlib import mock_object
-
+from yams import BadSchemaDefinition
 from rql import parse, nodes, RQLHelper
 
 from cubicweb import Unauthorized
@@ -123,7 +123,7 @@
                              "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):
+    def test_optional_var_base(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?')
@@ -131,15 +131,51 @@
         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)")
+                             "(Any C WHERE EXISTS(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)")
+                             "(Any C,T WHERE C title T, EXISTS(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)")
+
+    def test_optional_var_inlined(self):
+        c1 = ('X require_permission P')
+        c2 = ('X inlined_card O, O require_permission P')
+        rqlst = parse('Any C,A,R WHERE A? inlined_card C, A ref R')
+        rewrite(rqlst, {('C', 'X'): (c1,),
+                        ('A', 'X'): (c2,),
+                        }, {})
+        # XXX suboptimal
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any C,A,R WITH A,R,C BEING "
+                             "(Any A,R,C WHERE A ref R, A? inlined_card C, "
+                             "(A is NULL) OR (EXISTS(A inlined_card B, B require_permission D, "
+                             "B is Card, D is CWPermission)), "
+                             "A is Affaire, C is Card, EXISTS(C require_permission E, E is CWPermission))")
+
+    # def test_optional_var_inlined_has_perm(self):
+    #     c1 = ('X require_permission P')
+    #     c2 = ('X inlined_card O, U has_read_permission O')
+    #     rqlst = parse('Any C,A,R WHERE A? inlined_card C, A ref R')
+    #     rewrite(rqlst, {('C', 'X'): (c1,),
+    #                     ('A', 'X'): (c2,),
+    #                     }, {})
+    #     self.failUnlessEqual(rqlst.as_string(),
+    #                          "")
+
+    def test_optional_var_inlined_imbricated_error(self):
+        c1 = ('X require_permission P')
+        c2 = ('X inlined_card O, O require_permission P')
+        rqlst = parse('Any C,A,R,A2,R2 WHERE A? inlined_card C, A ref R,A2? inlined_card C, A2 ref R2')
+        self.assertRaises(BadSchemaDefinition,
+                          rewrite, rqlst, {('C', 'X'): (c1,),
+                                           ('A', 'X'): (c2,),
+                                           ('A2', 'X'): (c2,),
+                                           }, {})
 
     def test_relation_optimization_1_lhs(self):
         # since Card in_state State as monovalued cardinality, the in_state
@@ -243,7 +279,7 @@
         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)")
+                             u"Any X,C WHERE X? documented_by C, C is Card WITH X BEING (Any X WHERE EXISTS(X concerne A), X is Affaire)")
 
 
     def test_rrqlexpr_nonexistant_subject_1(self):
--- a/test/unittest_rset.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/test/unittest_rset.py	Tue Apr 06 19:46:38 2010 +0200
@@ -11,7 +11,7 @@
 
 from rql import parse
 
-from logilab.common.testlib import TestCase, unittest_main
+from logilab.common.testlib import TestCase, unittest_main, mock_object
 
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.rset import NotAnEntity, ResultSet, attr_desc_iterator
@@ -60,7 +60,7 @@
         self.rset = ResultSet([[12, 'adim'], [13, 'syt']],
                               'Any U,L where U is CWUser, U login L',
                               description=[['CWUser', 'String'], ['Bar', 'String']])
-        self.rset.vreg = self.vreg
+        self.rset.req = mock_object(vreg=self.vreg)
 
     def compare_urls(self, url1, url2):
         info1 = urlsplit(url1)
@@ -371,6 +371,18 @@
         rset = self.execute(u'Any X WHERE X has_text %(text)s', {'text' : 'foo'})
         self.assertEquals(rset.searched_text(), 'foo')
 
+    def test_union_limited_rql(self):
+        rset = self.execute('(Any X,N WHERE X is Bookmark, X title N)'
+                            ' UNION '
+                            '(Any X,N WHERE X is CWGroup, X name N)')
+        rset.limit(2, 10, inplace=True)
+        self.assertEquals(rset.limited_rql(),
+                          'Any A,B LIMIT 2 OFFSET 10 '
+                          'WITH A,B BEING ('
+                          '(Any X,N WHERE X is Bookmark, X title N) '
+                          'UNION '
+                          '(Any X,N WHERE X is CWGroup, X name N)'
+                          ')')
 
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_utils.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/test/unittest_utils.py	Tue Apr 06 19:46:38 2010 +0200
@@ -11,7 +11,7 @@
 import datetime
 
 from logilab.common.testlib import TestCase, unittest_main
-from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList
+from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList, RepeatList
 
 try:
     import simplejson
@@ -41,6 +41,52 @@
         self.assert_(UStringIO())
 
 
+class RepeatListTC(TestCase):
+
+    def test_base(self):
+        l = RepeatList(3, (1, 3))
+        self.assertEquals(l[0], (1, 3))
+        self.assertEquals(l[2], (1, 3))
+        self.assertEquals(l[-1], (1, 3))
+        self.assertEquals(len(l), 3)
+        # XXX
+        self.assertEquals(l[4], (1, 3))
+
+        self.failIf(RepeatList(0, None))
+
+    def test_slice(self):
+        l = RepeatList(3, (1, 3))
+        self.assertEquals(l[0:1], [(1, 3)])
+        self.assertEquals(l[0:4], [(1, 3)]*3)
+        self.assertEquals(l[:], [(1, 3)]*3)
+
+    def test_iter(self):
+        self.assertEquals(list(RepeatList(3, (1, 3))),
+                          [(1, 3)]*3)
+
+    def test_add(self):
+        l = RepeatList(3, (1, 3))
+        self.assertEquals(l + [(1, 4)], [(1, 3)]*3  + [(1, 4)])
+        self.assertEquals([(1, 4)] + l, [(1, 4)] + [(1, 3)]*3)
+        self.assertEquals(l + RepeatList(2, (2, 3)), [(1, 3)]*3 + [(2, 3)]*2)
+
+        x = l + RepeatList(2, (1, 3))
+        self.assertIsInstance(x, RepeatList)
+        self.assertEquals(len(x), 5)
+        self.assertEquals(x[0], (1, 3))
+
+        x = l + [(1, 3)] * 2
+        self.assertEquals(x, [(1, 3)] * 5)
+
+    def test_eq(self):
+        self.assertEquals(RepeatList(3, (1, 3)),
+                          [(1, 3)]*3)
+
+    def test_pop(self):
+        l = RepeatList(3, (1, 3))
+        l.pop(2)
+        self.assertEquals(l, [(1, 3)]*2)
+
 class SizeConstrainedListTC(TestCase):
 
     def test_append(self):
@@ -59,7 +105,7 @@
             l.extend(extension)
             yield self.assertEquals, l, expected
 
-class JSONEncoerTests(TestCase):
+class JSONEncoderTC(TestCase):
     def setUp(self):
         if simplejson is None:
             self.skip('simplejson not available')
@@ -81,5 +127,6 @@
     def test_encoding_unknown_stuff(self):
         self.assertEquals(self.encode(TestCase), 'null')
 
+
 if __name__ == '__main__':
     unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transaction.py	Tue Apr 06 19:46:38 2010 +0200
@@ -0,0 +1,96 @@
+"""undoable transaction objects.
+
+
+This module is in the cubicweb package and not in cubicweb.server because those
+objects should be accessible to client through pyro, where the cubicweb.server
+package may not be installed.
+
+:organization: Logilab
+:copyright: 2010 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"
+_ = unicode
+
+from cubicweb import RepositoryError
+
+
+ACTION_LABELS = {
+    'C': _('entity creation'),
+    'U': _('entity update'),
+    'D': _('entity deletion'),
+    'A': _('relation add'),
+    'R': _('relation removal'),
+    }
+
+
+class NoSuchTransaction(RepositoryError):
+    pass
+
+
+class Transaction(object):
+    """an undoable transaction"""
+
+    def __init__(self, uuid, time, ueid):
+        self.uuid = uuid
+        self.datetime = time
+        self.user_eid = ueid
+        # should be set by the dbapi connection
+        self.req = None
+
+    def __repr__(self):
+        return '<Transaction %s by %s on %s>' % (
+            self.uuid, self.user_eid, self.datetime)
+
+    def user(self):
+        """return the user entity which has done the transaction,
+        none if not found.
+        """
+        return self.req.execute('Any X WHERE X eid %(x)s',
+                                {'x': self.user_eid}, 'x').get_entity(0, 0)
+
+    def actions_list(self, public=True):
+        """return an ordered list of action effectued during that transaction
+
+        if public is true, return only 'public' action, eg not ones triggered
+        under the cover by hooks.
+        """
+        return self.req.cnx.transaction_actions(self.uuid, public)
+
+
+class AbstractAction(object):
+    def __init__(self, action, public, order):
+        self.action = action
+        self.public = public
+        self.order = order
+
+    @property
+    def label(self):
+        return ACTION_LABELS[self.action]
+
+
+class EntityAction(AbstractAction):
+    def __init__(self, action, public, order, etype, eid, changes):
+        AbstractAction.__init__(self, action, public, order)
+        self.etype = etype
+        self.eid = eid
+        self.changes = changes
+
+    def __repr__(self):
+        return '<%s: %s %s (%s)>' % (
+            self.label, self.eid, self.changes,
+            self.public and 'dbapi' or 'hook')
+
+
+class RelationAction(AbstractAction):
+    def __init__(self, action, public, order, rtype, eidfrom, eidto):
+        AbstractAction.__init__(self, action, public, order)
+        self.rtype = rtype
+        self.eid_from = eidfrom
+        self.eid_to = eidto
+
+    def __repr__(self):
+        return '<%s: %s %s %s (%s)>' % (
+            self.label, self.eid_from, self.rtype, self.eid_to,
+            self.public and 'dbapi' or 'hook')
--- a/utils.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/utils.py	Tue Apr 06 19:46:38 2010 +0200
@@ -7,39 +7,35 @@
 """
 __docformat__ = "restructuredtext en"
 
+import os
 import sys
 import decimal
 import datetime
 import random
+from itertools import repeat
+from uuid import uuid4
+from warnings import warn
 
 from logilab.mtconverter import xml_escape
 from logilab.common.deprecation import deprecated
 
+_MARKER = object()
+
 # initialize random seed from current time
 random.seed()
 
-if sys.version_info[:2] < (2, 5):
+def make_uid(key=None):
+    """Return a unique identifier string.
 
-    from time import time
-    from md5 import md5
-    from random import randint
+    if specified, `key` is used to prefix the generated uid so it can be used
+    for instance as a DOM id or as sql table names.
 
-    def make_uid(key):
-        """forge a unique identifier
-        XXX not that unique on win32
-        """
-        key = str(key)
-        msg = key + "%.10f" % time() + str(randint(0, 1000000))
-        return key + 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('-', '')
+    See uuid.uuid4 documentation for the shape of the generated identifier, but
+    this is basicallly a 32 bits hexadecimal string.
+    """
+    if key is None:
+        return uuid4().hex
+    return str(key) + uuid4().hex
 
 
 def dump_class(cls, clsname):
@@ -52,14 +48,9 @@
     # type doesn't accept unicode name
     # return type.__new__(type, str(clsname), (cls,), {})
     # __autogenerated__ attribute is just a marker
-    return type(str(clsname), (cls,), {'__autogenerated__': True})
-
-
-def merge_dicts(dict1, dict2):
-    """update a copy of `dict1` with `dict2`"""
-    dict1 = dict(dict1)
-    dict1.update(dict2)
-    return dict1
+    return type(str(clsname), (cls,), {'__autogenerated__': True,
+                                       '__doc__': cls.__doc__,
+                                       '__module__': cls.__module__})
 
 
 # use networkX instead ?
@@ -111,6 +102,42 @@
     __iadd__ = extend
 
 
+class RepeatList(object):
+    """fake a list with the same element in each row"""
+    __slots__ = ('_size', '_item')
+    def __init__(self, size, item):
+        self._size = size
+        self._item = item
+    def __len__(self):
+        return self._size
+    def __nonzero__(self):
+        return self._size
+    def __iter__(self):
+        return repeat(self._item, self._size)
+    def __getitem__(self, index):
+        return self._item
+    def __getslice__(self, i, j):
+        # XXX could be more efficient, but do we bother?
+        return ([self._item] * self._size)[i:j]
+    def __add__(self, other):
+        if isinstance(other, RepeatList):
+            if other._item == self._item:
+                return RepeatList(self._size + other._size, self._item)
+            return ([self._item] * self._size) + other[:]
+        return ([self._item] * self._size) + other
+    def __radd__(self, other):
+        if isinstance(other, RepeatList):
+            if other._item == self._item:
+                return RepeatList(self._size + other._size, self._item)
+            return other[:] + ([self._item] * self._size)
+        return other[:] + ([self._item] * self._size)
+    def __eq__(self, other):
+        if isinstance(other, RepeatList):
+            return other._size == self.size and other._item == self.item
+        return self[:] == other
+    def pop(self, i):
+        self._size -= 1
+
 class UStringIO(list):
     """a file wrapper which automatically encode unicode string to an encoding
     specifed in the constructor
@@ -166,15 +193,12 @@
     def add_post_inline_script(self, content):
         self.post_inlined_scripts.append(content)
 
-    def add_onload(self, jscode, jsoncall=False):
-        if jsoncall:
-            self.add_post_inline_script(u"""jQuery(CubicWeb).bind('ajax-loaded', function(event) {
-%s
-});""" % jscode)
-        else:
-            self.add_post_inline_script(u"""jQuery(document).ready(function () {
- %s
- });""" % jscode)
+    def add_onload(self, jscode, jsoncall=_MARKER):
+        if jsoncall is not _MARKER:
+            warn('[3.7] specifying jsoncall is not needed anymore',
+                 DeprecationWarning, stacklevel=2)
+        self.add_post_inline_script(u"""jQuery(CubicWeb).one('server-response', function(event) {
+%s});""" % jscode)
 
 
     def add_js(self, jsfile):
@@ -195,10 +219,10 @@
         if (cssfile, media) not in self.cssfiles:
             self.cssfiles.append( (cssfile, media) )
 
-    def add_ie_css(self, cssfile, media='all'):
+    def add_ie_css(self, cssfile, media='all', iespec=u'[if lt IE 8]'):
         """registers some IE specific CSS"""
-        if (cssfile, media) not in self.ie_cssfiles:
-            self.ie_cssfiles.append( (cssfile, media) )
+        if (cssfile, media, iespec) not in self.ie_cssfiles:
+            self.ie_cssfiles.append( (cssfile, media, iespec) )
 
     def add_unload_pagedata(self):
         """registers onunload callback to clean page data on server"""
@@ -228,8 +252,8 @@
               (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:
+            for cssfile, media, iespec in self.ie_cssfiles:
+                w(u'<!--%s>\n' % iespec)
                 w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
                   (media, xml_escape(cssfile)))
             w(u'<![endif]--> \n')
@@ -285,31 +309,35 @@
                                                  self.body.getvalue())
 
 
-def can_do_pdf_conversion(__answer=[None]):
-    """pdf conversion depends on
-    * pysixt (python package)
-    * fop 0.9x
-    """
-    if __answer[0] is not None:
-        return __answer[0]
+def _pdf_conversion_availability():
     try:
         import pysixt
     except ImportError:
-        __answer[0] = False
         return False
     from subprocess import Popen, STDOUT
-    import os
+    if not os.path.isfile('/usr/bin/fop'):
+        return False
     try:
         Popen(['/usr/bin/fop', '-q'],
               stdout=open(os.devnull, 'w'),
               stderr=STDOUT)
     except OSError, e:
-        print e
-        __answer[0] = False
+        getLogger('cubicweb').info('fop not usable (%s)', e)
         return False
-    __answer[0] = True
     return True
 
+def can_do_pdf_conversion(__answer_cache=[]):
+    """pdf conversion depends on
+    * pysixt (python package)
+    * fop 0.9x
+
+    NOTE: actual check is done by _pdf_conversion_availability and
+    result is cached
+    """
+    if not __answer_cache: # first time, not in cache
+        __answer_cache.append(_pdf_conversion_availability())
+    return __answer_cache[0]
+
 try:
     # may not be there if cubicweb-web not installed
     from simplejson import dumps, JSONEncoder
@@ -337,6 +365,14 @@
                 # just return None in those cases.
                 return None
 
+
+@deprecated('[3.7] merge_dicts is deprecated')
+def merge_dicts(dict1, dict2):
+    """update a copy of `dict1` with `dict2`"""
+    dict1 = dict(dict1)
+    dict1.update(dict2)
+    return dict1
+
 from logilab.common import date
 _THIS_MOD_NS = globals()
 for funcname in ('date_range', 'todate', 'todatetime', 'datetime2ticks',
--- a/vregistry.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/vregistry.py	Tue Apr 06 19:46:38 2010 +0200
@@ -48,7 +48,21 @@
             subfiles = [join(fileordir, fname) for fname in listdir(fileordir)]
             _toload_info(subfiles, extrapath, _toload)
         elif fileordir[-3:] == '.py':
-            modname = '.'.join(modpath_from_file(fileordir, extrapath))
+            modpath = modpath_from_file(fileordir, extrapath)
+            # omit '__init__' from package's name to avoid loading that module
+            # once for each name when it is imported by some other appobject
+            # module. This supposes import in modules are done as::
+            #
+            #   from package import something
+            #
+            # not::
+            #
+            #  from package.__init__ import something
+            #
+            # which seems quite correct.
+            if modpath[-1] == '__init__':
+                modpath.pop()
+            modname = '.'.join(modpath)
             _toload[0][modname] = fileordir
             _toload[1].append((fileordir, modname))
     return _toload
@@ -68,6 +82,11 @@
         return cls.id
     return cls.__regid__
 
+def class_registries(cls, registryname):
+    if registryname:
+        return (registryname,)
+    return cls.__registries__
+
 
 class Registry(dict):
 
@@ -142,11 +161,12 @@
     # dynamic selection methods ################################################
 
     def object_by_id(self, oid, *args, **kwargs):
-        """return object with the given oid. Only one object is expected to be
-        found.
+        """return object with the `oid` identifier. Only one object is expected
+        to be found.
 
-        raise `ObjectNotFound` if not object with id <oid> in <registry>
-        raise `AssertionError` if there is more than one object there
+        raise :exc:`ObjectNotFound` if not object with id <oid> in <registry>
+
+        raise :exc:`AssertionError` if there is more than one object there
         """
         objects = self[oid]
         assert len(objects) == 1, objects
@@ -156,8 +176,9 @@
         """return the most specific object among those with the given oid
         according to the given context.
 
-        raise `ObjectNotFound` if not object with id <oid> in <registry>
-        raise `NoSelectableObject` if not object apply
+        raise :exc:`ObjectNotFound` if not object with id <oid> in <registry>
+
+        raise :exc:`NoSelectableObject` if not object apply
         """
         return self._select_best(self[oid], *args, **kwargs)
 
@@ -191,25 +212,25 @@
         if len(args) > 1:
             warn('[3.5] only the request param can not be named when calling select*',
                  DeprecationWarning, stacklevel=3)
-        score, winners = 0, []
+        score, winners = 0, None
         for appobject in appobjects:
             appobjectscore = appobject.__select__(appobject, *args, **kwargs)
             if appobjectscore > score:
                 score, winners = appobjectscore, [appobject]
             elif appobjectscore > 0 and appobjectscore == score:
                 winners.append(appobject)
-        if not winners:
+        if winners is None:
             raise NoSelectableObject('args: %s\nkwargs: %s %s'
                                      % (args, kwargs.keys(),
                                         [repr(v) for v in appobjects]))
         if len(winners) > 1:
+            # log in production environement, error while debugging
             if self.config.debugmode:
-                self.error('select ambiguity, args: %s\nkwargs: %s %s',
-                           args, kwargs.keys(), [repr(v) for v in winners])
-            else:
                 raise Exception('select ambiguity, args: %s\nkwargs: %s %s'
                                 % (args, kwargs.keys(),
                                    [repr(v) for v in winners]))
+            self.error('select ambiguity, args: %s\nkwargs: %s %s',
+                       args, kwargs.keys(), [repr(v) for v in winners])
         # return the result of calling the appobject
         return winners[0](*args, **kwargs)
 
@@ -299,36 +320,66 @@
 #         self[regname].pop(oid, None)
 
     def register_all(self, objects, modname, butclasses=()):
+        """register all `objects` given. Objects which are not from the module
+        `modname` or which are in `butclasses` won't be registered.
+
+        Typical usage is:
+
+        .. sourcecode:: python
+
+            vreg.register_all(globals().values(), __name__, (ClassIWantToRegisterExplicitly,))
+
+        So you get partially automatic registration, keeping manual registration
+        for some object (to use
+        :meth:`~cubicweb.cwvreg.CubicWebRegistry.register_and_replace` for
+        instance)
+        """
         for obj in objects:
             try:
                 if obj.__module__ != modname or obj in butclasses:
                     continue
                 oid = class_regid(obj)
-                registryname = obj.__registry__
             except AttributeError:
                 continue
             if oid and not '__abstract__' in obj.__dict__:
-                self.register(obj, registryname)
+                self.register(obj, oid=oid)
 
     def register(self, obj, registryname=None, oid=None, clear=False):
-        """base method to add an object in the registry"""
+        """register `obj` application object into `registryname` or
+        `obj.__registry__` if not specified, with identifier `oid` or
+        `obj.__regid__` if not specified.
+
+        If `clear` is true, all objects with the same identifier will be
+        previously unregistered.
+        """
         assert not '__abstract__' in obj.__dict__
-        registryname = registryname or obj.__registry__
-        registry = self.setdefault(registryname)
-        registry.register(obj, oid=oid, clear=clear)
         try:
             vname = obj.__name__
         except AttributeError:
+            # XXX may occurs?
             vname = obj.__class__.__name__
-        self.debug('registered appobject %s in registry %s with id %s',
-                   vname, registryname, oid or class_regid(obj))
+        for registryname in class_registries(obj, registryname):
+            registry = self.setdefault(registryname)
+            registry.register(obj, oid=oid, clear=clear)
+            self.debug('registered appobject %s in registry %s with id %s',
+                       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)
+        """unregister `obj` application object from the registry `registryname` or
+        `obj.__registry__` if not specified.
+        """
+        for registryname in class_registries(obj, registryname):
+            self[registryname].unregister(obj)
 
     def register_and_replace(self, obj, replaced, registryname=None):
-        self[registryname or obj.__registry__].register_and_replace(obj, replaced)
+        """register `obj` application object into `registryname` or
+        `obj.__registry__` if not specified. If found, the `replaced` object
+        will be unregistered first (else a warning will be issued as it's
+        generally unexpected).
+        """
+        for registryname in class_registries(obj, registryname):
+            self[registryname].register_and_replace(obj, replaced)
 
     # initialization methods ###################################################
 
@@ -437,7 +488,7 @@
             self._load_ancestors_then_object(modname, parent)
         if (appobjectcls.__dict__.get('__abstract__')
             or appobjectcls.__name__[0] == '_'
-            or not appobjectcls.__registry__
+            or not appobjectcls.__registries__
             or not class_regid(appobjectcls)):
             return
         try:
--- a/web/application.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/application.py	Tue Apr 06 19:46:38 2010 +0200
@@ -123,7 +123,11 @@
         SESSION_MANAGER = self.session_manager
         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)
+        if self.vreg.config.mode != 'test':
+            # don't try to reset session manager during test, this leads to
+            # weird failures when running multiple tests
+            CW_EVENT_MANAGER.bind('after-registry-reload',
+                                  self.reset_session_manager)
 
     def reset_session_manager(self):
         data = self.session_manager.dump_data()
@@ -217,13 +221,13 @@
             path = 'view'
         raise Redirect(req.build_url(path, **args))
 
-    def logout(self, req):
+    def logout(self, req, goto_url):
         """logout from the instance by cleaning the session and raising
         `AuthenticationError`
         """
         self.session_manager.close_session(req.cnx)
         req.remove_cookie(req.get_cookie(), self.SESSION_VAR)
-        raise AuthenticationError()
+        raise AuthenticationError(url=goto_url)
 
 
 class CubicWebPublisher(object):
@@ -342,7 +346,11 @@
                 # redirect is raised by edit controller when everything went fine,
                 # so try to commit
                 try:
-                    req.cnx.commit()
+                    txuuid = req.cnx.commit()
+                    if txuuid is not None:
+                        msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
+                            req.build_url('undo', txuuid=txuuid), req._('undo'))
+                        req.append_to_redirect_message(msg)
                 except ValidationError, ex:
                     self.validation_error_handler(req, ex)
                 except Unauthorized, ex:
@@ -393,7 +401,7 @@
         self.exception(repr(ex))
         req.set_header('Cache-Control', 'no-cache')
         req.remove_header('Etag')
-        req.message = None
+        req.reset_message()
         req.reset_headers()
         if req.json_request:
             raise RemoteCallFailed(unicode(ex))
--- a/web/captcha.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/captcha.py	Tue Apr 06 19:46:38 2010 +0200
@@ -17,7 +17,7 @@
 from time import time
 
 from cubicweb import tags
-from cubicweb.web import formwidgets as fw
+from cubicweb.web import ProcessFormError, formwidgets as fw
 
 
 def pil_captcha(text, fontfile, fontsize):
@@ -63,7 +63,22 @@
 class CaptchaWidget(fw.TextInput):
     def render(self, form, field, renderer=None):
         # t=int(time()*100) to make sure img is not cached
-        src = form._cw.build_url('view', vid='captcha', t=int(time()*100))
+        src = form._cw.build_url('view', vid='captcha', t=int(time()*100),
+                                 captchakey=field.input_name(form))
         img = tags.img(src=src, alt=u'captcha')
         img = u'<div class="captcha">%s</div>' % img
         return img + super(CaptchaWidget, self).render(form, field, renderer)
+
+    def process_field_data(self, form, field):
+        captcha = form._cw.get_session_data(field.input_name(form), None,
+                                            pop=True)
+        val = super(CaptchaWidget, self).process_field_data(form, field)
+        if val is None:
+            return val # required will be checked by field
+        if captcha is None:
+            msg = form._cw._('unable to check captcha, please try again')
+            raise ProcessFormError(msg)
+        elif val.lower() != captcha.lower():
+            msg = form._cw._('incorrect captcha value')
+            raise ProcessFormError(msg)
+        return val
--- a/web/component.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/component.py	Tue Apr 06 19:46:38 2010 +0200
@@ -14,7 +14,6 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb import role
-from cubicweb.utils import merge_dicts
 from cubicweb.view import Component
 from cubicweb.selectors import (
     paginated_rset, one_line_rset, primary_view, match_context_prop,
@@ -116,8 +115,9 @@
             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,})
+        params = dict(params)
+        params.update({self.start_param : start,
+                       self.stop_param : stop,})
         if path == 'json':
             rql = params.pop('rql', self.cw_rset.printable_rql())
             # latest 'true' used for 'swap' mode
--- a/web/controller.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/controller.py	Tue Apr 06 19:46:38 2010 +0200
@@ -8,6 +8,8 @@
 """
 __docformat__ = "restructuredtext en"
 
+from logilab.mtconverter import xml_escape
+
 from cubicweb.selectors import yes
 from cubicweb.appobject import AppObject
 from cubicweb.web import LOGGER, Redirect, RequestError
@@ -79,19 +81,6 @@
                 self.cw_rset = pp.process_query(rql)
         return self.cw_rset
 
-    def check_expected_params(self, params):
-        """check that the given list of parameters are specified in the form
-        dictionary
-        """
-        missing = []
-        for param in params:
-            if not self._cw.form.get(param):
-                missing.append(param)
-        if missing:
-            raise RequestError('missing required parameter(s): %s'
-                               % ','.join(missing))
-
-
     def notify_edited(self, entity):
         """called by edit_entity() to notify which entity is edited"""
         # NOTE: we can't use entity.rest_path() at this point because
@@ -100,31 +89,10 @@
         if not self._edited_entity:
             self._edited_entity = entity
 
-    # XXX move to EditController (only customer)
-    def delete_entities(self, eidtypes):
-        """delete entities from the repository"""
-        redirect_info = set()
-        eidtypes = tuple(eidtypes)
-        for eid, etype in eidtypes:
-            entity = self._cw.entity_from_eid(eid, etype)
-            path, params = entity.after_deletion_path()
-            redirect_info.add( (path, tuple(params.iteritems())) )
-            entity.delete()
-        if len(redirect_info) > 1:
-            # In the face of ambiguity, refuse the temptation to guess.
-            self._after_deletion_path = 'view', ()
-        else:
-            self._after_deletion_path = iter(redirect_info).next()
-        if len(eidtypes) > 1:
-            self._cw.set_message(self._cw._('entities deleted'))
-        else:
-            self._cw.set_message(self._cw._('entity deleted'))
-
     def validate_cache(self, view):
         view.set_http_cache_headers()
         self._cw.validate_cache()
 
-    # XXX is that used AT ALL ?
     def reset(self):
         """reset form parameters and redirect to a view determinated by given
         parameters
@@ -132,7 +100,7 @@
         newparams = {}
         # sets message if needed
         if self._cw.message:
-            newparams['__message'] = self._cw.message
+            newparams['_cwmsgid'] = self._cw.set_redirect_message(self._cw.message)
         if self._cw.form.has_key('__action_apply'):
             self._return_to_edition_view(newparams)
         if self._cw.form.has_key('__action_cancel'):
@@ -140,8 +108,6 @@
         else:
             self._return_to_original_view(newparams)
 
-
-    # XXX is that used AT ALL ?
     def _return_to_original_view(self, newparams):
         """validate-button case"""
         # transforms __redirect[*] parameters into regular form parameters
@@ -156,10 +122,13 @@
         elif '__redirectpath' in self._cw.form:
             # if redirect path was explicitly specified in the form, use it
             path = self._cw.form['__redirectpath']
-            if self._edited_entity and path != self._edited_entity.rest_path():
-                # XXX may be here on modification? if yes the message should be
-                # modified where __createdpath is detected (cw.web.request)
-                newparams['__createdpath'] = self._edited_entity.rest_path()
+            if (self._edited_entity and path != self._edited_entity.rest_path()
+                and '_cwmsgid' in newparams):
+                # XXX may be here on modification?
+                msg = u'(<a href="%s">%s</a>)' % (
+                    xml_escape(self._edited_entity.absolute_url()),
+                    self._cw._('click here to see created entity'))
+                self._cw.append_to_redirect_message(msg)
         elif self._after_deletion_path:
             # else it should have been set during form processing
             path, params = self._after_deletion_path
@@ -167,6 +136,9 @@
             params.update(newparams)
             newparams = params
         elif self._edited_entity:
+            # clear caches in case some attribute participating to the rest path
+            # has been modified
+            self._edited_entity.clear_all_caches()
             path = self._edited_entity.rest_path()
         else:
             path = 'view'
@@ -174,7 +146,6 @@
         url = append_url_params(url, self._cw.form.get('__redirectparams'))
         raise Redirect(url)
 
-    # XXX is that used AT ALL ?
     def _return_to_edition_view(self, newparams):
         """apply-button case"""
         form = self._cw.form
@@ -186,7 +157,7 @@
             path = 'view'
             newparams['rql'] = form['rql']
         else:
-            self.warning("the edited data seems inconsistent")
+            self.warning('the edited data seems inconsistent')
             path = 'view'
         # pick up the correction edition view
         if form.get('__form_id'):
@@ -198,7 +169,6 @@
         raise Redirect(self._cw.build_url(path, **newparams))
 
 
-    # XXX is that used AT ALL ?
     def _return_to_lastpage(self, newparams):
         """cancel-button case: in this case we are always expecting to go back
         where we came from, and this is not easy. Currently we suppose that
--- a/web/data/cubicweb.ajax.js	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/data/cubicweb.ajax.js	Tue Apr 06 19:46:38 2010 +0200
@@ -92,12 +92,9 @@
        setFormsTarget(node);
     }
     loadDynamicFragments(node);
-    // 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');
+    // XXX [3.7] jQuery.one is now used instead jQuery.bind,
+    // jquery.treeview.js can be unpatched accordingly.
+    jQuery(CubicWeb).trigger('server-response', [true, node]);
 }
 
 /* cubicweb loadxhtml plugin to make jquery handle xhtml response
--- a/web/data/cubicweb.edition.js	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/data/cubicweb.edition.js	Tue Apr 06 19:46:38 2010 +0200
@@ -35,8 +35,8 @@
  * this function is called when an AJAX form was generated to
  * make sure tabindex remains consistent
  */
-function reorderTabindex(start) {
-    var form = getNode('entityForm');
+function reorderTabindex(start, formid) {
+    var form = getNode(formid || 'entityForm');
     var inputTypes = ['INPUT', 'SELECT', 'TEXTAREA'];
     var tabindex = (start==null)?15:start;
     nodeWalkDepthFirst(form, function(elem) {
@@ -254,7 +254,7 @@
         form.css('display', 'none');
         form.insertBefore(insertBefore).slideDown('fast');
         updateInlinedEntitiesCounters(rtype, role);
-        reorderTabindex();
+        reorderTabindex(null, $(insertBefore).closest('form')[0]);
         jQuery(CubicWeb).trigger('inlinedform-added', form);
         // if the inlined form contains a file input, we must force
         // the form enctype to multipart/form-data
@@ -322,7 +322,7 @@
 
 function _clearPreviousErrors(formid) {
     jQuery('#' + formid + 'ErrorMessage').remove();
-    jQuery('#' + formid + ' span.error').remove();
+    jQuery('#' + formid + ' span.errorMsg').remove();
     jQuery('#' + formid + ' .error').removeClass('error');
 }
 
@@ -331,25 +331,30 @@
     var firsterrfield = null;
     for (fieldname in errors) {
 	var errmsg = errors[fieldname];
-	var fieldid = fieldname + ':' + eid;
-	var suffixes = ['', '-subject', '-object'];
-	var found = false;
-	for (var i=0, length=suffixes.length; i<length;i++) {
-	    var field = jqNode(fieldname + suffixes[i] + ':' + eid);
-	    if (field && getNodeAttribute(field, 'type') != 'hidden') {
-		if ( !firsterrfield ) {
-		    firsterrfield = 'err-' + fieldid;
+	if (!fieldname) {
+	    globalerrors.push(errmsg);
+	} else {
+	    var fieldid = fieldname + ':' + eid;
+	    var suffixes = ['', '-subject', '-object'];
+	    var found = false;
+	    // XXX remove suffixes at some point
+	    for (var i=0, length=suffixes.length; i<length;i++) {
+		var field = jqNode(fieldname + suffixes[i] + ':' + eid);
+		if (field && getNodeAttribute(field, 'type') != 'hidden') {
+		    if ( !firsterrfield ) {
+			firsterrfield = 'err-' + fieldid;
+		    }
+		    addElementClass(field, 'error');
+		    var span = SPAN({'id': 'err-' + fieldid, 'class': "errorMsg"}, errmsg);
+		    field.before(span);
+		    found = true;
+		    break;
 		}
-		addElementClass(field, 'error');
-		var span = SPAN({'id': 'err-' + fieldid, 'class': "error"}, errmsg);
-		field.before(span);
-		found = true;
-		break;
 	    }
-	}
-	if (!found) {
-	    firsterrfield = formid;
-	    globalerrors.push(_(fieldname) + ' : ' + errmsg);
+	    if (!found) {
+		firsterrfield = formid;
+		globalerrors.push(_(fieldname) + ' : ' + errmsg);
+	    }
 	}
     }
     if (globalerrors.length) {
--- a/web/data/cubicweb.facets.js	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/data/cubicweb.facets.js	Tue Apr 06 19:46:38 2010 +0200
@@ -47,10 +47,11 @@
 	var rql = result[0];
 	var $bkLink = jQuery('#facetBkLink');
 	if ($bkLink.length) {
-	    var bkUrl = $bkLink.attr('cubicweb:target') + '&path=view?rql=' + rql;
+	    var bkPath = 'view?rql=' + escape(rql);
 	    if (vid) {
-		bkUrl += '&vid=' + vid;
+		bkPath += '&vid=' + escape(vid);
 	    }
+	    var bkUrl = $bkLink.attr('cubicweb:target') + '&path=' + escape(bkPath);
 	    $bkLink.attr('href', bkUrl);
 	}
 	var toupdate = result[1];
--- a/web/data/cubicweb.form.css	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/data/cubicweb.form.css	Tue Apr 06 19:46:38 2010 +0200
@@ -192,11 +192,14 @@
   background-color: #eeedd9;
 }
 
-input.error {
+.error input { /* error added by the form renderer */
+  background: transparent url("error.png") 100% 50% no-repeat;
+}
+input.error { /* error added by javascript */
   background: transparent url("error.png") 100% 50% no-repeat;
 }
 
-span.error {
+span.errorMsg {
   display: block;
   font-weight: bold;
   color: #ed0d0d;
--- a/web/data/cubicweb.python.js	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/data/cubicweb.python.js	Tue Apr 06 19:46:38 2010 +0200
@@ -394,4 +394,13 @@
     }
 };
 
+jQuery(document).ready(function() {
+    jQuery(CubicWeb).trigger('server-response', [false, document]);
+});
+
+jQuery(CubicWeb).bind('ajax-loaded', function() {
+    log('[3.7] "ajax-loaded" event is deprecated, use "server-response" instead');
+    jQuery(CubicWeb).trigger('server-response', [false, document]);
+});
+
 CubicWeb.provide('python.js');
--- a/web/data/cubicweb.widgets.js	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/data/cubicweb.widgets.js	Tue Apr 06 19:46:38 2010 +0200
@@ -113,8 +113,63 @@
     }
 
 });
+//remote version of RestrictedSuggestField
+Widgets.LazySuggestField = defclass('LazySuggestField', [Widgets.SuggestField], {
+    __init__: function(node, options) {
+	var self = this;
+	var multi = "no";
+	options = options || {};
+	options.max = 50;
+	options.delay = 50;
+	options.cacheLength=0;
+	options.mustMatch = true;
+        // multiple selection not supported yet (still need to formalize correctly
+        // initial values / display values)
+        var initialvalue = evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null');
+        if (!initialvalue) {
+            initialvalue = node.value;
+        }
+        options = jQuery.extend({dataType: 'json',
+                                 multiple: (multi == "yes") ? true : false,
+                                 parse: this.parseResult
+                                }, options);
+        var dataurl = node.getAttribute('cubicweb:dataurl');
+        // remove 'name' from original input and add the hidden one that will
+        // store the actual value
+        var hidden = INPUT({'type': "hidden", 'name': node.name, 'value': initialvalue});
+        node.parentNode.appendChild(hidden);
+        jQuery(node).bind('result', {hinput: hidden, input:node}, self.hideRealValue)
+            .removeAttr('name').autocomplete(dataurl, options);
+    },
 
 
+    hideRealValue: function(evt, data, value) {
+	if (!value){
+	    value="";
+	}
+        evt.data.hinput.value = value;
+    },
+
+    /*
+     * @param data: a list of couple (value, label) to fill the suggestion list,
+     *              (returned by CW through AJAX)
+     */
+    parseResult: function(data) {
+        var parsed = [];
+        for (var i=0; i < data.length; i++) {
+                var value = ''+data[i][0]; // a string is required later by jquery.autocomplete.js
+                var label = data[i][1];
+                parsed[parsed.length] = {
+                    data: [label],
+                    value: value,
+                    result: label
+                };
+        };
+        return parsed;
+    }
+
+});
+
 /*
  * suggestform displays a suggest field and associated validate / cancel buttons
  * constructor's argumemts are the same that BaseSuggestField widget
--- a/web/data/jquery.autocomplete.js	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/data/jquery.autocomplete.js	Tue Apr 06 19:46:38 2010 +0200
@@ -1,15 +1,13 @@
 /*
- * Autocomplete - jQuery plugin 1.0.2
+ * jQuery Autocomplete plugin 1.1
  *
- * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
+ * Copyright (c) 2009 Jörn Zaefferer
  *
  * Dual licensed under the MIT and GPL licenses:
  *   http://www.opensource.org/licenses/mit-license.php
  *   http://www.gnu.org/licenses/gpl.html
  *
- * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
- *
- */;(function($){$.fn.extend({autocomplete:function(urlOrData,options){var isUrl=typeof urlOrData=="string";options=$.extend({},$.Autocompleter.defaults,{url:isUrl?urlOrData:null,data:isUrl?null:urlOrData,delay:isUrl?$.Autocompleter.defaults.delay:10,max:options&&!options.scroll?10:150},options);options.highlight=options.highlight||function(value){return value;};options.formatMatch=options.formatMatch||options.formatItem;return this.each(function(){new $.Autocompleter(this,options);});},result:function(handler){return this.bind("result",handler);},search:function(handler){return this.trigger("search",[handler]);},flushCache:function(){return this.trigger("flushCache");},setOptions:function(options){return this.trigger("setOptions",[options]);},unautocomplete:function(){return this.trigger("unautocomplete");}});$.Autocompleter=function(input,options){var KEY={UP:38,DOWN:40,DEL:46,TAB:9,RETURN:13,ESC:27,COMMA:188,PAGEUP:33,PAGEDOWN:34,BACKSPACE:8};var $input=$(input).attr("autocomplete","off").addClass(options.inputClass);var timeout;var previousValue="";var cache=$.Autocompleter.Cache(options);var hasFocus=0;var lastKeyPressCode;var config={mouseDownOnSelect:false};var select=$.Autocompleter.Select(options,input,selectCurrent,config);var blockSubmit;$.browser.opera&&$(input.form).bind("submit.autocomplete",function(){if(blockSubmit){blockSubmit=false;return false;}});$input.bind(($.browser.opera?"keypress":"keydown")+".autocomplete",function(event){lastKeyPressCode=event.keyCode;switch(event.keyCode){case KEY.UP:event.preventDefault();if(select.visible()){select.prev();}else{onChange(0,true);}break;case KEY.DOWN:event.preventDefault();if(select.visible()){select.next();}else{onChange(0,true);}break;case KEY.PAGEUP:event.preventDefault();if(select.visible()){select.pageUp();}else{onChange(0,true);}break;case KEY.PAGEDOWN:event.preventDefault();if(select.visible()){select.pageDown();}else{onChange(0,true);}break;case options.multiple&&$.trim(options.multipleSeparator)==","&&KEY.COMMA:case KEY.TAB:case KEY.RETURN:if(selectCurrent()){event.preventDefault();blockSubmit=true;return false;}break;case KEY.ESC:select.hide();break;default:clearTimeout(timeout);timeout=setTimeout(onChange,options.delay);break;}}).focus(function(){hasFocus++;}).blur(function(){hasFocus=0;if(!config.mouseDownOnSelect){hideResults();}}).click(function(){if(hasFocus++>1&&!select.visible()){onChange(0,true);}}).bind("search",function(){var fn=(arguments.length>1)?arguments[1]:null;function findValueCallback(q,data){var result;if(data&&data.length){for(var i=0;i<data.length;i++){if(data[i].result.toLowerCase()==q.toLowerCase()){result=data[i];break;}}}if(typeof fn=="function")fn(result);else $input.trigger("result",result&&[result.data,result.value]);}$.each(trimWords($input.val()),function(i,value){request(value,findValueCallback,findValueCallback);});}).bind("flushCache",function(){cache.flush();}).bind("setOptions",function(){$.extend(options,arguments[1]);if("data"in arguments[1])cache.populate();}).bind("unautocomplete",function(){select.unbind();$input.unbind();$(input.form).unbind(".autocomplete");});function selectCurrent(){var selected=select.selected();if(!selected)return false;var v=selected.result;previousValue=v;if(options.multiple){var words=trimWords($input.val());if(words.length>1){v=words.slice(0,words.length-1).join(options.multipleSeparator)+options.multipleSeparator+v;}v+=options.multipleSeparator;}$input.val(v);hideResultsNow();$input.trigger("result",[selected.data,selected.value]);return true;}function onChange(crap,skipPrevCheck){if(lastKeyPressCode==KEY.DEL){select.hide();return;}var currentValue=$input.val();if(!skipPrevCheck&&currentValue==previousValue)return;previousValue=currentValue;currentValue=lastWord(currentValue);if(currentValue.length>=options.minChars){$input.addClass(options.loadingClass);if(!options.matchCase)currentValue=currentValue.toLowerCase();request(currentValue,receiveData,hideResultsNow);}else{stopLoading();select.hide();}};function trimWords(value){if(!value){return[""];}var words=value.split(options.multipleSeparator);var result=[];$.each(words,function(i,value){if($.trim(value))result[i]=$.trim(value);});return result;}function lastWord(value){if(!options.multiple)return value;var words=trimWords(value);return words[words.length-1];}function autoFill(q,sValue){if(options.autoFill&&(lastWord($input.val()).toLowerCase()==q.toLowerCase())&&lastKeyPressCode!=KEY.BACKSPACE){$input.val($input.val()+sValue.substring(lastWord(previousValue).length));$.Autocompleter.Selection(input,previousValue.length,previousValue.length+sValue.length);}};function hideResults(){clearTimeout(timeout);timeout=setTimeout(hideResultsNow,200);};function hideResultsNow(){var wasVisible=select.visible();select.hide();clearTimeout(timeout);stopLoading();if(options.mustMatch){$input.search(function(result){if(!result){if(options.multiple){var words=trimWords($input.val()).slice(0,-1);$input.val(words.join(options.multipleSeparator)+(words.length?options.multipleSeparator:""));}else
-$input.val("");}});}if(wasVisible)$.Autocompleter.Selection(input,input.value.length,input.value.length);};function receiveData(q,data){if(data&&data.length&&hasFocus){stopLoading();select.display(data,q);autoFill(q,data[0].value);select.show();}else{hideResultsNow();}};function request(term,success,failure){if(!options.matchCase)term=term.toLowerCase();var data=cache.load(term);if(data&&data.length){success(term,data);}else if((typeof options.url=="string")&&(options.url.length>0)){var extraParams={timestamp:+new Date()};$.each(options.extraParams,function(key,param){extraParams[key]=typeof param=="function"?param():param;});$.ajax({mode:"abort",port:"autocomplete"+input.name,dataType:options.dataType,url:options.url,data:$.extend({q:lastWord(term),limit:options.max},extraParams),success:function(data){var parsed=options.parse&&options.parse(data)||parse(data);cache.add(term,parsed);success(term,parsed);}});}else{select.emptyList();failure(term);}};function parse(data){var parsed=[];var rows=data.split("\n");for(var i=0;i<rows.length;i++){var row=$.trim(rows[i]);if(row){row=row.split("|");parsed[parsed.length]={data:row,value:row[0],result:options.formatResult&&options.formatResult(row,row[0])||row[0]};}}return parsed;};function stopLoading(){$input.removeClass(options.loadingClass);};};$.Autocompleter.defaults={inputClass:"ac_input",resultsClass:"ac_results",loadingClass:"ac_loading",minChars:1,delay:400,matchCase:false,matchSubset:true,matchContains:false,cacheLength:10,max:100,mustMatch:false,extraParams:{},selectFirst:true,formatItem:function(row){return row[0];},formatMatch:null,autoFill:false,width:0,multiple:false,multipleSeparator:", ",highlight:function(value,term){return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)("+term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi,"\\$1")+")(?![^<>]*>)(?![^&;]+;)","gi"),"<strong>$1</strong>");},scroll:true,scrollHeight:180};$.Autocompleter.Cache=function(options){var data={};var length=0;function matchSubset(s,sub){if(!options.matchCase)s=s.toLowerCase();var i=s.indexOf(sub);if(i==-1)return false;return i==0||options.matchContains;};function add(q,value){if(length>options.cacheLength){flush();}if(!data[q]){length++;}data[q]=value;}function populate(){if(!options.data)return false;var stMatchSets={},nullData=0;if(!options.url)options.cacheLength=1;stMatchSets[""]=[];for(var i=0,ol=options.data.length;i<ol;i++){var rawValue=options.data[i];rawValue=(typeof rawValue=="string")?[rawValue]:rawValue;var value=options.formatMatch(rawValue,i+1,options.data.length);if(value===false)continue;var firstChar=value.charAt(0).toLowerCase();if(!stMatchSets[firstChar])stMatchSets[firstChar]=[];var row={value:value,data:rawValue,result:options.formatResult&&options.formatResult(rawValue)||value};stMatchSets[firstChar].push(row);if(nullData++<options.max){stMatchSets[""].push(row);}};$.each(stMatchSets,function(i,value){options.cacheLength++;add(i,value);});}setTimeout(populate,25);function flush(){data={};length=0;}return{flush:flush,add:add,populate:populate,load:function(q){if(!options.cacheLength||!length)return null;if(!options.url&&options.matchContains){var csub=[];for(var k in data){if(k.length>0){var c=data[k];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub.push(x);}});}}return csub;}else
+ * Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $
+ */;(function($){$.fn.extend({autocomplete:function(urlOrData,options){var isUrl=typeof urlOrData=="string";options=$.extend({},$.Autocompleter.defaults,{url:isUrl?urlOrData:null,data:isUrl?null:urlOrData,delay:isUrl?$.Autocompleter.defaults.delay:10,max:options&&!options.scroll?10:150},options);options.highlight=options.highlight||function(value){return value;};options.formatMatch=options.formatMatch||options.formatItem;return this.each(function(){new $.Autocompleter(this,options);});},result:function(handler){return this.bind("result",handler);},search:function(handler){return this.trigger("search",[handler]);},flushCache:function(){return this.trigger("flushCache");},setOptions:function(options){return this.trigger("setOptions",[options]);},unautocomplete:function(){return this.trigger("unautocomplete");}});$.Autocompleter=function(input,options){var KEY={UP:38,DOWN:40,DEL:46,TAB:9,RETURN:13,ESC:27,COMMA:188,PAGEUP:33,PAGEDOWN:34,BACKSPACE:8};var $input=$(input).attr("autocomplete","off").addClass(options.inputClass);var timeout;var previousValue="";var cache=$.Autocompleter.Cache(options);var hasFocus=0;var lastKeyPressCode;var config={mouseDownOnSelect:false};var select=$.Autocompleter.Select(options,input,selectCurrent,config);var blockSubmit;$.browser.opera&&$(input.form).bind("submit.autocomplete",function(){if(blockSubmit){blockSubmit=false;return false;}});$input.bind(($.browser.opera?"keypress":"keydown")+".autocomplete",function(event){hasFocus=1;lastKeyPressCode=event.keyCode;switch(event.keyCode){case KEY.UP:event.preventDefault();if(select.visible()){select.prev();}else{onChange(0,true);}break;case KEY.DOWN:event.preventDefault();if(select.visible()){select.next();}else{onChange(0,true);}break;case KEY.PAGEUP:event.preventDefault();if(select.visible()){select.pageUp();}else{onChange(0,true);}break;case KEY.PAGEDOWN:event.preventDefault();if(select.visible()){select.pageDown();}else{onChange(0,true);}break;case options.multiple&&$.trim(options.multipleSeparator)==","&&KEY.COMMA:case KEY.TAB:case KEY.RETURN:if(selectCurrent()){event.preventDefault();blockSubmit=true;return false;}break;case KEY.ESC:select.hide();break;default:clearTimeout(timeout);timeout=setTimeout(onChange,options.delay);break;}}).focus(function(){hasFocus++;}).blur(function(){hasFocus=0;if(!config.mouseDownOnSelect){hideResults();}}).click(function(){if(hasFocus++>1&&!select.visible()){onChange(0,true);}}).bind("search",function(){var fn=(arguments.length>1)?arguments[1]:null;function findValueCallback(q,data){var result;if(data&&data.length){for(var i=0;i<data.length;i++){if(data[i].result.toLowerCase()==q.toLowerCase()){result=data[i];break;}}}if(typeof fn=="function")fn(result);else $input.trigger("result",result&&[result.data,result.value]);}$.each(trimWords($input.val()),function(i,value){request(value,findValueCallback,findValueCallback);});}).bind("flushCache",function(){cache.flush();}).bind("setOptions",function(){$.extend(options,arguments[1]);if("data"in arguments[1])cache.populate();}).bind("unautocomplete",function(){select.unbind();$input.unbind();$(input.form).unbind(".autocomplete");});function selectCurrent(){var selected=select.selected();if(!selected)return false;var v=selected.result;previousValue=v;if(options.multiple){var words=trimWords($input.val());if(words.length>1){var seperator=options.multipleSeparator.length;var cursorAt=$(input).selection().start;var wordAt,progress=0;$.each(words,function(i,word){progress+=word.length;if(cursorAt<=progress){wordAt=i;return false;}progress+=seperator;});words[wordAt]=v;v=words.join(options.multipleSeparator);}v+=options.multipleSeparator;}$input.val(v);hideResultsNow();$input.trigger("result",[selected.data,selected.value]);return true;}function onChange(crap,skipPrevCheck){if(lastKeyPressCode==KEY.DEL){select.hide();return;}var currentValue=$input.val();if(!skipPrevCheck&&currentValue==previousValue)return;previousValue=currentValue;currentValue=lastWord(currentValue);if(currentValue.length>=options.minChars){$input.addClass(options.loadingClass);if(!options.matchCase)currentValue=currentValue.toLowerCase();request(currentValue,receiveData,hideResultsNow);}else{stopLoading();select.hide();}};function trimWords(value){if(!value)return[""];if(!options.multiple)return[$.trim(value)];return $.map(value.split(options.multipleSeparator),function(word){return $.trim(value).length?$.trim(word):null;});}function lastWord(value){if(!options.multiple)return value;var words=trimWords(value);if(words.length==1)return words[0];var cursorAt=$(input).selection().start;if(cursorAt==value.length){words=trimWords(value)}else{words=trimWords(value.replace(value.substring(cursorAt),""));}return words[words.length-1];}function autoFill(q,sValue){if(options.autoFill&&(lastWord($input.val()).toLowerCase()==q.toLowerCase())&&lastKeyPressCode!=KEY.BACKSPACE){$input.val($input.val()+sValue.substring(lastWord(previousValue).length));$(input).selection(previousValue.length,previousValue.length+sValue.length);}};function hideResults(){clearTimeout(timeout);timeout=setTimeout(hideResultsNow,200);};function hideResultsNow(){var wasVisible=select.visible();select.hide();clearTimeout(timeout);stopLoading();if(options.mustMatch){$input.search(function(result){if(!result){if(options.multiple){var words=trimWords($input.val()).slice(0,-1);$input.val(words.join(options.multipleSeparator)+(words.length?options.multipleSeparator:""));}else{$input.val("");$input.trigger("result",null);}}});}};function receiveData(q,data){if(data&&data.length&&hasFocus){stopLoading();select.display(data,q);autoFill(q,data[0].value);select.show();}else{hideResultsNow();}};function request(term,success,failure){if(!options.matchCase)term=term.toLowerCase();var data=cache.load(term);if(data&&data.length){success(term,data);}else if((typeof options.url=="string")&&(options.url.length>0)){var extraParams={timestamp:+new Date()};$.each(options.extraParams,function(key,param){extraParams[key]=typeof param=="function"?param():param;});$.ajax({mode:"abort",port:"autocomplete"+input.name,dataType:options.dataType,url:options.url,data:$.extend({q:lastWord(term),limit:options.max},extraParams),success:function(data){var parsed=options.parse&&options.parse(data)||parse(data);cache.add(term,parsed);success(term,parsed);}});}else{select.emptyList();failure(term);}};function parse(data){var parsed=[];var rows=data.split("\n");for(var i=0;i<rows.length;i++){var row=$.trim(rows[i]);if(row){row=row.split("|");parsed[parsed.length]={data:row,value:row[0],result:options.formatResult&&options.formatResult(row,row[0])||row[0]};}}return parsed;};function stopLoading(){$input.removeClass(options.loadingClass);};};$.Autocompleter.defaults={inputClass:"ac_input",resultsClass:"ac_results",loadingClass:"ac_loading",minChars:1,delay:400,matchCase:false,matchSubset:true,matchContains:false,cacheLength:10,max:100,mustMatch:false,extraParams:{},selectFirst:true,formatItem:function(row){return row[0];},formatMatch:null,autoFill:false,width:0,multiple:false,multipleSeparator:", ",highlight:function(value,term){return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)("+term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi,"\\$1")+")(?![^<>]*>)(?![^&;]+;)","gi"),"<strong>$1</strong>");},scroll:true,scrollHeight:180};$.Autocompleter.Cache=function(options){var data={};var length=0;function matchSubset(s,sub){if(!options.matchCase)s=s.toLowerCase();var i=s.indexOf(sub);if(options.matchContains=="word"){i=s.toLowerCase().search("\\b"+sub.toLowerCase());}if(i==-1)return false;return i==0||options.matchContains;};function add(q,value){if(length>options.cacheLength){flush();}if(!data[q]){length++;}data[q]=value;}function populate(){if(!options.data)return false;var stMatchSets={},nullData=0;if(!options.url)options.cacheLength=1;stMatchSets[""]=[];for(var i=0,ol=options.data.length;i<ol;i++){var rawValue=options.data[i];rawValue=(typeof rawValue=="string")?[rawValue]:rawValue;var value=options.formatMatch(rawValue,i+1,options.data.length);if(value===false)continue;var firstChar=value.charAt(0).toLowerCase();if(!stMatchSets[firstChar])stMatchSets[firstChar]=[];var row={value:value,data:rawValue,result:options.formatResult&&options.formatResult(rawValue)||value};stMatchSets[firstChar].push(row);if(nullData++<options.max){stMatchSets[""].push(row);}};$.each(stMatchSets,function(i,value){options.cacheLength++;add(i,value);});}setTimeout(populate,25);function flush(){data={};length=0;}return{flush:flush,add:add,populate:populate,load:function(q){if(!options.cacheLength||!length)return null;if(!options.url&&options.matchContains){var csub=[];for(var k in data){if(k.length>0){var c=data[k];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub.push(x);}});}}return csub;}else
 if(data[q]){return data[q];}else
-if(options.matchSubset){for(var i=q.length-1;i>=options.minChars;i--){var c=data[q.substr(0,i)];if(c){var csub=[];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub[csub.length]=x;}});return csub;}}}return null;}};};$.Autocompleter.Select=function(options,input,select,config){var CLASSES={ACTIVE:"ac_over"};var listItems,active=-1,data,term="",needsInit=true,element,list;function init(){if(!needsInit)return;element=$("<div/>").hide().addClass(options.resultsClass).css("position","absolute").appendTo(document.body);list=$("<ul/>").appendTo(element).mouseover(function(event){if(target(event).nodeName&&target(event).nodeName.toUpperCase()=='LI'){active=$("li",list).removeClass(CLASSES.ACTIVE).index(target(event));$(target(event)).addClass(CLASSES.ACTIVE);}}).click(function(event){$(target(event)).addClass(CLASSES.ACTIVE);select();input.focus();return false;}).mousedown(function(){config.mouseDownOnSelect=true;}).mouseup(function(){config.mouseDownOnSelect=false;});if(options.width>0)element.css("width",options.width);needsInit=false;}function target(event){var element=event.target;while(element&&element.tagName!="LI")element=element.parentNode;if(!element)return[];return element;}function moveSelect(step){listItems.slice(active,active+1).removeClass(CLASSES.ACTIVE);movePosition(step);var activeItem=listItems.slice(active,active+1).addClass(CLASSES.ACTIVE);if(options.scroll){var offset=0;listItems.slice(0,active).each(function(){offset+=this.offsetHeight;});if((offset+activeItem[0].offsetHeight-list.scrollTop())>list[0].clientHeight){list.scrollTop(offset+activeItem[0].offsetHeight-list.innerHeight());}else if(offset<list.scrollTop()){list.scrollTop(offset);}}};function movePosition(step){active+=step;if(active<0){active=listItems.size()-1;}else if(active>=listItems.size()){active=0;}}function limitNumberOfItems(available){return options.max&&options.max<available?options.max:available;}function fillList(){list.empty();var max=limitNumberOfItems(data.length);for(var i=0;i<max;i++){if(!data[i])continue;var formatted=options.formatItem(data[i].data,i+1,max,data[i].value,term);if(formatted===false)continue;var li=$("<li/>").html(options.highlight(formatted,term)).addClass(i%2==0?"ac_even":"ac_odd").appendTo(list)[0];$.data(li,"ac_data",data[i]);}listItems=list.find("li");if(options.selectFirst){listItems.slice(0,1).addClass(CLASSES.ACTIVE);active=0;}if($.fn.bgiframe)list.bgiframe();}return{display:function(d,q){init();data=d;term=q;fillList();},next:function(){moveSelect(1);},prev:function(){moveSelect(-1);},pageUp:function(){if(active!=0&&active-8<0){moveSelect(-active);}else{moveSelect(-8);}},pageDown:function(){if(active!=listItems.size()-1&&active+8>listItems.size()){moveSelect(listItems.size()-1-active);}else{moveSelect(8);}},hide:function(){element&&element.hide();listItems&&listItems.removeClass(CLASSES.ACTIVE);active=-1;},visible:function(){return element&&element.is(":visible");},current:function(){return this.visible()&&(listItems.filter("."+CLASSES.ACTIVE)[0]||options.selectFirst&&listItems[0]);},show:function(){var offset=$(input).offset();element.css({width:typeof options.width=="string"||options.width>0?options.width:$(input).width(),top:offset.top+input.offsetHeight,left:offset.left}).show();if(options.scroll){list.scrollTop(0);list.css({maxHeight:options.scrollHeight,overflow:'auto'});if($.browser.msie&&typeof document.body.style.maxHeight==="undefined"){var listHeight=0;listItems.each(function(){listHeight+=this.offsetHeight;});var scrollbarsVisible=listHeight>options.scrollHeight;list.css('height',scrollbarsVisible?options.scrollHeight:listHeight);if(!scrollbarsVisible){listItems.width(list.width()-parseInt(listItems.css("padding-left"))-parseInt(listItems.css("padding-right")));}}}},selected:function(){var selected=listItems&&listItems.filter("."+CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);return selected&&selected.length&&$.data(selected[0],"ac_data");},emptyList:function(){list&&list.empty();},unbind:function(){element&&element.remove();}};};$.Autocompleter.Selection=function(field,start,end){if(field.createTextRange){var selRange=field.createTextRange();selRange.collapse(true);selRange.moveStart("character",start);selRange.moveEnd("character",end);selRange.select();}else if(field.setSelectionRange){field.setSelectionRange(start,end);}else{if(field.selectionStart){field.selectionStart=start;field.selectionEnd=end;}}field.focus();};})(jQuery);
\ No newline at end of file
+if(options.matchSubset){for(var i=q.length-1;i>=options.minChars;i--){var c=data[q.substr(0,i)];if(c){var csub=[];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub[csub.length]=x;}});return csub;}}}return null;}};};$.Autocompleter.Select=function(options,input,select,config){var CLASSES={ACTIVE:"ac_over"};var listItems,active=-1,data,term="",needsInit=true,element,list;function init(){if(!needsInit)return;element=$("<div/>").hide().addClass(options.resultsClass).css("position","absolute").appendTo(document.body);list=$("<ul/>").appendTo(element).mouseover(function(event){if(target(event).nodeName&&target(event).nodeName.toUpperCase()=='LI'){active=$("li",list).removeClass(CLASSES.ACTIVE).index(target(event));$(target(event)).addClass(CLASSES.ACTIVE);}}).click(function(event){$(target(event)).addClass(CLASSES.ACTIVE);select();input.focus();return false;}).mousedown(function(){config.mouseDownOnSelect=true;}).mouseup(function(){config.mouseDownOnSelect=false;});if(options.width>0)element.css("width",options.width);needsInit=false;}function target(event){var element=event.target;while(element&&element.tagName.toUpperCase()!="LI")element=element.parentNode;if(!element)return[];return element;}function moveSelect(step){listItems.slice(active,active+1).removeClass(CLASSES.ACTIVE);movePosition(step);var activeItem=listItems.slice(active,active+1).addClass(CLASSES.ACTIVE);if(options.scroll){var offset=0;listItems.slice(0,active).each(function(){offset+=this.offsetHeight;});if((offset+activeItem[0].offsetHeight-list.scrollTop())>list[0].clientHeight){list.scrollTop(offset+activeItem[0].offsetHeight-list.innerHeight());}else if(offset<list.scrollTop()){list.scrollTop(offset);}}};function movePosition(step){active+=step;if(active<0){active=listItems.size()-1;}else if(active>=listItems.size()){active=0;}}function limitNumberOfItems(available){return options.max&&options.max<available?options.max:available;}function fillList(){list.empty();var max=limitNumberOfItems(data.length);for(var i=0;i<max;i++){if(!data[i])continue;var formatted=options.formatItem(data[i].data,i+1,max,data[i].value,term);if(formatted===false)continue;var li=$("<li/>").html(options.highlight(formatted,term)).addClass(i%2==0?"ac_even":"ac_odd").appendTo(list)[0];$.data(li,"ac_data",data[i]);}listItems=list.find("li");if(options.selectFirst){listItems.slice(0,1).addClass(CLASSES.ACTIVE);active=0;}if($.fn.bgiframe)list.bgiframe();}return{display:function(d,q){init();data=d;term=q;fillList();},next:function(){moveSelect(1);},prev:function(){moveSelect(-1);},pageUp:function(){if(active!=0&&active-8<0){moveSelect(-active);}else{moveSelect(-8);}},pageDown:function(){if(active!=listItems.size()-1&&active+8>listItems.size()){moveSelect(listItems.size()-1-active);}else{moveSelect(8);}},hide:function(){element&&element.hide();listItems&&listItems.removeClass(CLASSES.ACTIVE);active=-1;},visible:function(){return element&&element.is(":visible");},current:function(){return this.visible()&&(listItems.filter("."+CLASSES.ACTIVE)[0]||options.selectFirst&&listItems[0]);},show:function(){var offset=$(input).offset();element.css({width:typeof options.width=="string"||options.width>0?options.width:$(input).width(),top:offset.top+input.offsetHeight,left:offset.left}).show();if(options.scroll){list.scrollTop(0);list.css({maxHeight:options.scrollHeight,overflow:'auto'});if($.browser.msie&&typeof document.body.style.maxHeight==="undefined"){var listHeight=0;listItems.each(function(){listHeight+=this.offsetHeight;});var scrollbarsVisible=listHeight>options.scrollHeight;list.css('height',scrollbarsVisible?options.scrollHeight:listHeight);if(!scrollbarsVisible){listItems.width(list.width()-parseInt(listItems.css("padding-left"))-parseInt(listItems.css("padding-right")));}}}},selected:function(){var selected=listItems&&listItems.filter("."+CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);return selected&&selected.length&&$.data(selected[0],"ac_data");},emptyList:function(){list&&list.empty();},unbind:function(){element&&element.remove();}};};$.fn.selection=function(start,end){if(start!==undefined){return this.each(function(){if(this.createTextRange){var selRange=this.createTextRange();if(end===undefined||start==end){selRange.move("character",start);selRange.select();}else{selRange.collapse(true);selRange.moveStart("character",start);selRange.moveEnd("character",end);selRange.select();}}else if(this.setSelectionRange){this.setSelectionRange(start,end);}else if(this.selectionStart){this.selectionStart=start;this.selectionEnd=end;}});}var field=this[0];if(field.createTextRange){var range=document.selection.createRange(),orig=field.value,teststring="<->",textLength=range.text.length;range.text=teststring;var caretAt=field.value.indexOf(teststring);field.value=orig;this.selection(caretAt,caretAt+textLength);return{start:caretAt,end:caretAt+textLength}}else if(field.selectionStart!==undefined){return{start:field.selectionStart,end:field.selectionEnd}}};})(jQuery);
\ No newline at end of file
--- a/web/facet.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/facet.py	Tue Apr 06 19:46:38 2010 +0200
@@ -338,10 +338,11 @@
 
 class RelationFacet(VocabularyFacet):
     __select__ = partial_relation_possible() & match_context_prop()
-    # class attributes to configure the rel ation facet
+    # class attributes to configure the relation facet
     rtype = None
     role = 'subject'
     target_attr = 'eid'
+    target_type = None
     # set this to a stored procedure name if you want to sort on the result of
     # this function's result instead of direct value
     sortfunc = None
@@ -365,8 +366,11 @@
             sort = self.sortasc
         try:
             mainvar = self.filtered_variable
-            insert_attr_select_relation(rqlst, mainvar, self.rtype, self.role,
-                                        self.target_attr, self.sortfunc, sort)
+            var = insert_attr_select_relation(
+                rqlst, mainvar, self.rtype, self.role, self.target_attr,
+                self.sortfunc, sort)
+            if self.target_type is not None:
+                rqlst.add_type_restriction(var, self.target_type)
             try:
                 rset = self.rqlexec(rqlst.as_string(), self.cw_rset.args, self.cw_rset.cachekey)
             except:
--- a/web/formfields.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/formfields.py	Tue Apr 06 19:46:38 2010 +0200
@@ -14,15 +14,14 @@
 
 from logilab.mtconverter import xml_escape
 from logilab.common.date import ustrftime
-from logilab.common.decorators import cached
 
-from yams.schema import KNOWN_METAATTRIBUTES
+from yams.schema import KNOWN_METAATTRIBUTES, role_name
 from yams.constraints import (SizeConstraint, StaticVocabularyConstraint,
                               FormatConstraint)
 
 from cubicweb import Binary, tags, uilib
 from cubicweb.web import INTERNAL_FIELD_VALUE, ProcessFormError, eid_param, \
-     formwidgets as fw
+     formwidgets as fw, uicfg
 
 
 class UnmodifiedField(Exception):
@@ -189,24 +188,33 @@
         """return the widget instance associated to this field"""
         return self.widget
 
-    # cached is necessary else we get some pb on entity creation : entity.eid is
-    # modified from creation mark (eg 'X') to its actual eid (eg 123), and then
-    # `field.input_name()` won't return the right key anymore if not cached
-    # (first call to input_name done *before* eventual eid affectation).
-    @cached
     def input_name(self, form, suffix=None):
         """return 'qualified name' for this field"""
-        name = self.role_name()
-        if suffix is not None:
-            name += suffix
-        if self.eidparam:
-            return eid_param(name, form.edited_entity.eid)
-        return name
+        # caching is necessary else we get some pb on entity creation :
+        # entity.eid is modified from creation mark (eg 'X') to its actual eid
+        # (eg 123), and then `field.input_name()` won't return the right key
+        # anymore if not cached (first call to input_name done *before* eventual
+        # eid affectation).
+        #
+        # note that you should NOT use @cached else it will create a memory leak
+        # on persistent fields (eg created once for all on a form class) because
+        # of the 'form' appobject argument: the cache will keep growing as new
+        # form are created...
+        try:
+            return form.formvalues[(self, 'input_name', suffix)]
+        except KeyError:
+            name = self.role_name()
+            if suffix is not None:
+                name += suffix
+            if self.eidparam:
+                name = eid_param(name, form.edited_entity.eid)
+            form.formvalues[(self, 'input_name', suffix)] = name
+            return name
 
     def role_name(self):
         """return <field.name>-<field.role> if role is specified, else field.name"""
         if self.role is not None:
-            return '%s-%s' % (self.name, self.role)
+            return role_name(self.name, self.role)
         return self.name
 
     def dom_id(self, form, suffix=None):
@@ -373,7 +381,10 @@
         for field in self.actual_fields(form):
             if field is self:
                 try:
-                    yield field, field.process_form_value(form)
+                    value = field.process_form_value(form)
+                    if value is None and field.required:
+                        raise ProcessFormError(form._cw._("required field"))
+                    yield field, value
                 except UnmodifiedField:
                     continue
             else:
@@ -856,7 +867,7 @@
             eids.add(typed_eid)
         return eids
 
-
+# XXX use cases where we don't actually want a better widget?
 class CompoundField(Field):
     def __init__(self, fields, *args, **kwargs):
         super(CompoundField, self).__init__(*args, **kwargs)
@@ -866,9 +877,15 @@
         return self.fields
 
     def actual_fields(self, form):
-        return [self] + list(self.fields)
+        # don't add [self] to actual fields, compound field is usually kinda
+        # virtual, all interesting values are in subfield. Skipping it may avoid
+        # error when processed by the editcontroller : it may be marked as required
+        # while it has no value, hence generating a false error.
+        return list(self.fields)
 
 
+_AFF_KWARGS = uicfg.autoform_field_kwargs
+
 def guess_field(eschema, rschema, role='subject', skip_meta_attr=True, **kwargs):
     """return the most adapated widget to edit the relation
     'subjschema rschema objschema' according to information found in the schema
@@ -883,14 +900,14 @@
     else:
         targetschema = rdef.subject
     card = rdef.role_cardinality(role)
-    kwargs['required'] = card in '1+'
     kwargs['name'] = rschema.type
     kwargs['role'] = role
+    kwargs['eidparam'] = True
+    kwargs.setdefault('required', card in '1+')
     if role == 'object':
         kwargs.setdefault('label', (eschema.type, rschema.type + '_object'))
     else:
         kwargs.setdefault('label', (eschema.type, rschema.type))
-    kwargs['eidparam'] = True
     kwargs.setdefault('help', rdef.description)
     if rschema.final:
         if skip_meta_attr and rschema in eschema.meta_attributes():
@@ -918,8 +935,10 @@
             for metadata in KNOWN_METAATTRIBUTES:
                 metaschema = eschema.has_metadata(rschema, metadata)
                 if metaschema is not None:
+                    metakwargs = _AFF_KWARGS.etype_get(eschema, metaschema, 'subject')
                     kwargs['%s_field' % metadata] = guess_field(eschema, metaschema,
-                                                                skip_meta_attr=False)
+                                                                skip_meta_attr=False,
+                                                                **metakwargs)
         return fieldclass(**kwargs)
     return RelationField.fromcardinality(card, **kwargs)
 
--- a/web/formwidgets.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/formwidgets.py	Tue Apr 06 19:46:38 2010 +0200
@@ -263,8 +263,8 @@
     """<select>, for field having a specific vocabulary"""
     vocabulary_widget = True
 
-    def __init__(self, attrs=None, multiple=False):
-        super(Select, self).__init__(attrs)
+    def __init__(self, attrs=None, multiple=False, **kwargs):
+        super(Select, self).__init__(attrs, **kwargs)
         self._multiple = multiple
 
     def render(self, form, field, renderer):
@@ -456,16 +456,17 @@
     needs_js = ('jquery.timePicker.js',)
     needs_css = ('jquery.timepicker.css',)
 
-    def __init__(self, timestr=None, timesteps=30, **kwargs):
+    def __init__(self, timestr=None, timesteps=30, separator=u':', **kwargs):
         super(JQueryTimePicker, self).__init__(**kwargs)
         self.timestr = timestr
         self.timesteps = timesteps
+        self.separator = separator
 
     def _render(self, form, field, renderer):
         req = form._cw
         domid = field.dom_id(form, self.suffix)
-        req.add_onload(u'jqNode("%s").timePicker({selectedTime: "%s", step: %s})' % (
-            domid, self.timestr, self.timesteps))
+        req.add_onload(u'jqNode("%s").timePicker({selectedTime: "%s", step: %s, separator: "%s"})' % (
+            domid, self.timestr, self.timesteps, self.separator))
         if self.timestr is None:
             value = self.values(form, field)[0]
         else:
@@ -603,6 +604,34 @@
     wdgtype = 'RestrictedSuggestField'
 
 
+class LazyRestrictedAutoCompletionWidget(RestrictedAutoCompletionWidget):
+    """remote autocomplete """
+    wdgtype = 'LazySuggestField'
+
+    def values_and_attributes(self, form, field):
+        self.add_media(form)
+
+        """override values_and_attributes to handle initial displayed values"""
+        values, attrs = super(LazyRestrictedAutoCompletionWidget, self).values_and_attributes(form, field)
+        assert len(values) == 1, "multiple selection is not supported yet by LazyWidget"
+        if not values[0]:
+            values = form.cw_extra_kwargs.get(field.name,'')
+            if not isinstance(values, (tuple, list)):
+                values = (values,)
+        try:
+            values = list(values)
+            values[0] = int(values[0])
+            attrs['cubicweb:initialvalue'] = values[0]
+            values = (self.display_value_for(form, values[0]),)
+        except (TypeError, ValueError):
+            pass
+        return values, attrs
+
+    def display_value_for(self, form, value):
+        entity =form._cw.entity_from_eid(value)
+        return entity.view('combobox')
+
+
 class AddComboBoxWidget(Select):
     def values_and_attributes(self, form, field):
         values, attrs = super(AddComboBoxWidget, self).values_and_attributes(form, field)
@@ -754,6 +783,7 @@
             attrs['id'] = field.dom_id(form, 'fqs')
         if self.settabindex:
             attrs['tabindex'] = req.next_tabindex()
+        attrs.setdefault('cols', 60)
         attrs.setdefault('onkeyup', 'autogrow(this)')
         inputs += [tags.textarea(fqs, name=fqsqname, **attrs),
                    u'</td></tr></table>']
--- a/web/request.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/request.py	Tue Apr 06 19:46:38 2010 +0200
@@ -68,7 +68,6 @@
 
     def __init__(self, vreg, https, form=None):
         super(CubicWebRequestBase, self).__init__(vreg)
-        self.message = None
         self.authmode = vreg.config['auth-mode']
         self.https = https
         # raw html headers that can be added from any view
@@ -126,35 +125,24 @@
         """
         super(CubicWebRequestBase, self).set_connection(cnx, user)
         # set request language
-        try:
-            vreg = self.vreg
-            if self.user:
-                try:
-                    # 1. user specified language
-                    lang = vreg.typed_value('ui.language',
-                                            self.user.properties['ui.language'])
+        vreg = self.vreg
+        if self.user:
+            try:
+                # 1. user specified language
+                lang = vreg.typed_value('ui.language',
+                                        self.user.properties['ui.language'])
+                self.set_language(lang)
+                return
+            except KeyError:
+                pass
+        if vreg.config['language-negociation']:
+            # 2. http negociated language
+            for lang in self.header_accept_language():
+                if lang in self.translations:
                     self.set_language(lang)
                     return
-                except KeyError:
-                    pass
-            if vreg.config['language-negociation']:
-                # 2. http negociated language
-                for lang in self.header_accept_language():
-                    if lang in self.translations:
-                        self.set_language(lang)
-                        return
-            # 3. default language
-            self.set_default_language(vreg)
-        finally:
-            # XXX code smell
-            # have to be done here because language is not yet set in setup_params
-            #
-            # special key for created entity, added in controller's reset method
-            # if no message set, we don't want this neither
-            if '__createdpath' in self.form and self.message:
-                self.message += ' (<a href="%s">%s</a>)' % (
-                    self.build_url(self.form.pop('__createdpath')),
-                    self._('click here to see created entity'))
+        # 3. default language
+        self.set_default_language(vreg)
 
     def set_language(self, lang):
         gettext, self.pgettext = self.translations[lang]
@@ -179,26 +167,27 @@
 
         subclasses should overrides to
         """
+        self.form = {}
         if params is None:
-            params = {}
-        self.form = params
+            return
         encoding = self.encoding
-        for k, v in params.items():
-            if isinstance(v, (tuple, list)):
-                v = [unicode(x, encoding) for x in v]
-                if len(v) == 1:
-                    v = v[0]
-            if k in self.no_script_form_params:
-                v = self.no_script_form_param(k, value=v)
-            if isinstance(v, str):
-                v = unicode(v, encoding)
-            if k == '__message':
-                self.set_message(v)
-                del self.form[k]
+        for param, val in params.iteritems():
+            if isinstance(val, (tuple, list)):
+                val = [unicode(x, encoding) for x in val]
+                if len(val) == 1:
+                    val = val[0]
+            elif isinstance(val, str):
+                val = unicode(val, encoding)
+            if param in self.no_script_form_params and val:
+                val = self.no_script_form_param(param, val)
+            if param == '_cwmsgid':
+                self.set_message_id(val)
+            elif param == '__message':
+                self.set_message(val)
             else:
-                self.form[k] = v
+                self.form[param] = val
 
-    def no_script_form_param(self, param, default=None, value=None):
+    def no_script_form_param(self, param, value):
         """ensure there is no script in a user form param
 
         by default return a cleaned string instead of raising a security
@@ -208,16 +197,12 @@
         that are at some point inserted in a generated html page to protect
         against script kiddies
         """
-        if value is None:
-            value = self.form.get(param, default)
-        if not value is default and value:
-            # safety belt for strange urls like http://...?vtitle=yo&vtitle=yo
-            if isinstance(value, (list, tuple)):
-                self.error('no_script_form_param got a list (%s). Who generated the URL ?',
-                           repr(value))
-                value = value[0]
-            return remove_html_tags(value)
-        return value
+        # safety belt for strange urls like http://...?vtitle=yo&vtitle=yo
+        if isinstance(value, (list, tuple)):
+            self.error('no_script_form_param got a list (%s). Who generated the URL ?',
+                       repr(value))
+            value = value[0]
+        return remove_html_tags(value)
 
     def list_form_param(self, param, form=None, pop=False):
         """get param from form parameters and return its value as a list,
@@ -245,9 +230,48 @@
 
     # web state helpers #######################################################
 
+    @property
+    def message(self):
+        try:
+            return self.get_session_data(self._msgid, default=u'', pop=True)
+        except AttributeError:
+            try:
+                return self._msg
+            except AttributeError:
+                return None
+
     def set_message(self, msg):
         assert isinstance(msg, unicode)
-        self.message = msg
+        self._msg = msg
+
+    def set_message_id(self, msgid):
+        self._msgid = msgid
+
+    @cached
+    def redirect_message_id(self):
+        return make_uid()
+
+    def set_redirect_message(self, msg):
+        assert isinstance(msg, unicode)
+        msgid = self.redirect_message_id()
+        self.set_session_data(msgid, msg)
+        return msgid
+
+    def append_to_redirect_message(self, msg):
+        msgid = self.redirect_message_id()
+        currentmsg = self.get_session_data(msgid)
+        if currentmsg is not None:
+            currentmsg = '%s %s' % (currentmsg, msg)
+        else:
+            currentmsg = msg
+        self.set_session_data(msgid, currentmsg)
+        return msgid
+
+    def reset_message(self):
+        if hasattr(self, '_msg'):
+            del self._msg
+        if hasattr(self, '_msgid'):
+            del self._msgid
 
     def update_search_state(self):
         """update the current search state"""
@@ -287,7 +311,11 @@
             if breadcrumbs is None:
                 breadcrumbs = SizeConstrainedList(10)
                 self.set_session_data('breadcrumbs', breadcrumbs)
-            breadcrumbs.append(self.url())
+                breadcrumbs.append(self.url())
+            else:
+                url = self.url()
+                if breadcrumbs and breadcrumbs[-1] != url:
+                    breadcrumbs.append(url)
 
     def last_visited_page(self):
         breadcrumbs = self.get_session_data('breadcrumbs', None)
@@ -477,7 +505,7 @@
     # high level methods for HTML headers management ##########################
 
     def add_onload(self, jscode):
-        self.html_headers.add_onload(jscode, self.json_request)
+        self.html_headers.add_onload(jscode)
 
     def add_js(self, jsfiles, localfile=True):
         """specify a list of JS files to include in the HTML headers
@@ -492,26 +520,33 @@
                 jsfile = self.datadir_url + jsfile
             self.html_headers.add_js(jsfile)
 
-    def add_css(self, cssfiles, media=u'all', localfile=True, ieonly=False):
+    def add_css(self, cssfiles, media=u'all', localfile=True, ieonly=False,
+                iespec=u'[if lt IE 8]'):
         """specify a CSS file to include in the HTML headers
         :param cssfiles: a CSS filename or a list of CSS filenames
         :param media: the CSS's media if necessary
         :param localfile: if True, the default data dir prefix is added to the
                           CSS filename
+        :param ieonly: True if this css is specific to IE
+        :param iespec: conditional expression that will be used around
+                       the css inclusion. cf:
+                       http://msdn.microsoft.com/en-us/library/ms537512(VS.85).aspx
         """
         if isinstance(cssfiles, basestring):
             cssfiles = (cssfiles,)
         if ieonly:
             if self.ie_browser():
+                extraargs = [iespec]
                 add_css = self.html_headers.add_ie_css
             else:
                 return # no need to do anything on non IE browsers
         else:
+            extraargs = []
             add_css = self.html_headers.add_css
         for cssfile in cssfiles:
             if localfile:
                 cssfile = self.datadir_url + cssfile
-            add_css(cssfile, media)
+            add_css(cssfile, media, *extraargs)
 
     def build_ajax_replace_url(self, nodeid, rql, vid, replacemode='replace',
                                **extraparams):
--- a/web/test/unittest_application.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/test/unittest_application.py	Tue Apr 06 19:46:38 2010 +0200
@@ -186,36 +186,66 @@
         self.assertEquals(values['eid'], eid)
         error = forminfo['error']
         self.assertEquals(error.entity, user.eid)
-        self.assertEquals(error.errors['login'], 'required attribute')
+        self.assertEquals(error.errors['login-subject'], 'required field')
 
 
-    def test_validation_error_dont_loose_subentity_data(self):
+    def test_validation_error_dont_loose_subentity_data_ctrl(self):
         """test creation of two linked entities
+
+        error occurs on the web controller
         """
         req = self.request()
-        form = {'eid': ['X', 'Y'], '__maineid': 'X',
-                '__type:X': 'CWUser', '_cw_edited_fields:X': 'login-subject,surname-subject',
-                # missing required field
-                'login-subject:X': u'',
-                'surname-subject:X': u'Mr Ouaoua',
-                # but email address is set
-                '__type:Y': 'EmailAddress', '_cw_edited_fields:Y': 'address-subject,alias-subject,use_email-object',
-                'address-subject:Y': u'bougloup@logilab.fr',
-                'alias-subject:Y': u'',
-                'use_email-object:Y': 'X',
-                # necessary to get validation error handling
-                '__errorurl': 'view?vid=edition...',
-                }
-        req.form = form
-        # monkey patch edited_eid to ensure both entities are edited, not only X
-        req.edited_eids = lambda : ('Y', 'X')
+        # set Y before X to ensure both entities are edited, not only X
+        req.form = {'eid': ['Y', 'X'], '__maineid': 'X',
+                    '__type:X': 'CWUser', '_cw_edited_fields:X': 'login-subject',
+                    # missing required field
+                    'login-subject:X': u'',
+                    # but email address is set
+                    '__type:Y': 'EmailAddress', '_cw_edited_fields:Y': 'address-subject',
+                    'address-subject:Y': u'bougloup@logilab.fr',
+                    'use_email-object:Y': 'X',
+                    # necessary to get validation error handling
+                    '__errorurl': 'view?vid=edition...',
+                    }
         path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
         forminfo = req.get_session_data('view?vid=edition...')
         self.assertEquals(set(forminfo['eidmap']), set('XY'))
+        self.assertEquals(forminfo['eidmap']['X'], None)
+        self.assertIsInstance(forminfo['eidmap']['Y'], int)
+        self.assertEquals(forminfo['error'].entity, 'X')
+        self.assertEquals(forminfo['error'].errors,
+                          {'login-subject': 'required field'})
+        self.assertEquals(forminfo['values'], req.form)
+
+
+    def test_validation_error_dont_loose_subentity_data_repo(self):
+        """test creation of two linked entities
+
+        error occurs on the repository
+        """
+        req = self.request()
+        # set Y before X to ensure both entities are edited, not only X
+        req.form = {'eid': ['Y', 'X'], '__maineid': 'X',
+                    '__type:X': 'CWUser', '_cw_edited_fields:X': 'login-subject,upassword-subject',
+                    # already existent user
+                    'login-subject:X': u'admin',
+                    'upassword-subject:X': u'admin', 'upassword-subject-confirm:X': u'admin',
+                    '__type:Y': 'EmailAddress', '_cw_edited_fields:Y': 'address-subject',
+                    'address-subject:Y': u'bougloup@logilab.fr',
+                    'use_email-object:Y': 'X',
+                    # necessary to get validation error handling
+                    '__errorurl': 'view?vid=edition...',
+                    }
+        path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+        forminfo = req.get_session_data('view?vid=edition...')
+        self.assertEquals(set(forminfo['eidmap']), set('XY'))
+        self.assertIsInstance(forminfo['eidmap']['X'], int)
+        self.assertIsInstance(forminfo['eidmap']['Y'], int)
         self.assertEquals(forminfo['error'].entity, forminfo['eidmap']['X'])
-        self.assertEquals(forminfo['error'].errors, {'login': 'required attribute',
-                                                     'upassword': 'required attribute'})
-        self.assertEquals(forminfo['values'], form)
+        self.assertEquals(forminfo['error'].errors,
+                          {'login-subject': u'the value "admin" is already used, use another one'})
+        self.assertEquals(forminfo['values'], req.form)
+
 
     def _test_cleaned(self, kwargs, injected, cleaned):
         req = self.request(**kwargs)
--- a/web/test/unittest_pdf.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/test/unittest_pdf.py	Tue Apr 06 19:46:38 2010 +0200
@@ -1,12 +1,11 @@
-from unittest import TestCase
 import os.path as osp
+from tempfile import NamedTemporaryFile
+from subprocess import Popen as sub
 from xml.etree.cElementTree import ElementTree, fromstring, tostring, dump
 
-from tempfile import NamedTemporaryFile
-from subprocess import Popen as sub
+from logilab.common.testlib import TestCase, unittest_main
 
 from cubicweb.utils import can_do_pdf_conversion
-
 from cubicweb.ext.xhtml2fo import ReportTransformer
 
 DATADIR = osp.join(osp.dirname(__file__), 'data')
@@ -36,3 +35,7 @@
         self.assertEquals( len(output), len(reference) )
         # cut begin & end 'cause they contain variyng data
         self.assertTextEquals(output[150:1500], reference[150:1500])
+
+if __name__ == '__main__':
+    unittest_main()
+
--- a/web/test/unittest_uicfg.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/test/unittest_uicfg.py	Tue Apr 06 19:46:38 2010 +0200
@@ -1,10 +1,12 @@
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web import uicfg
 
+abaa = uicfg.actionbox_appearsin_addmenu
+
 class UICFGTC(CubicWebTC):
 
-    def test(self):
-        self.skip('write some tests')
+    def test_default_actionbox_appearsin_addmenu_config(self):
+        self.failIf(abaa.etype_get('TrInfo', 'wf_info_for', 'object', 'CWUser'))
 
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
--- a/web/test/unittest_views_basecontrollers.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/test/unittest_views_basecontrollers.py	Tue Apr 06 19:46:38 2010 +0200
@@ -51,7 +51,7 @@
                     'upassword-subject-confirm:X': u'toto',
                     }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'login': 'the value "admin" is already used, use another one'})
+        self.assertEquals(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
 
     def test_user_editing_itself(self):
         """checking that a manager user can edit itself
@@ -219,7 +219,7 @@
                     'described_by_test-subject:X': u(feid),
                 }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'amount': 'value [0;100] constraint failed for value -10'})
+        self.assertEquals(ex.errors, {'amount-subject': 'value [0;100] constraint failed for value -10'})
         req = self.request()
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
@@ -228,7 +228,7 @@
                     'described_by_test-subject:X': u(feid),
                     }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'amount': 'value [0;100] constraint failed for value 110'})
+        self.assertEquals(ex.errors, {'amount-subject': 'value [0;100] constraint failed for value 110'})
         req = self.request()
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
@@ -343,7 +343,7 @@
                     '__action_delete': ''}
         path, params = self.expect_redirect_publish(req, 'edit')
         self.assertEquals(path, 'blogentry')
-        self.assertEquals(params, {u'__message': u'entity deleted'})
+        self.assertIn('_cwmsgid', params)
         eid = req.create_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')
@@ -353,7 +353,7 @@
                     '__action_delete': ''}
         path, params = self.expect_redirect_publish(req, 'edit')
         self.assertEquals(path, 'cwuser/admin')
-        self.assertEquals(params, {u'__message': u'entity deleted'})
+        self.assertIn('_cwmsgid', params)
         eid1 = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
         eid2 = req.create_entity('EmailAddress', address=u'hop@logilab.fr').eid
         req = self.request()
@@ -363,7 +363,7 @@
                     '__action_delete': ''}
         path, params = self.expect_redirect_publish(req, 'edit')
         self.assertEquals(path, 'view')
-        self.assertEquals(params, {u'__message': u'entities deleted'})
+        self.assertIn('_cwmsgid', params)
 
     def test_nonregr_eetype_etype_editing(self):
         """non-regression test checking that a manager user can edit a CWEType entity
@@ -430,7 +430,7 @@
                     'use_email-object:Y': 'X',
                     }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'address': u'required attribute'})
+        self.assertEquals(ex.errors, {'address-subject': u'required field'})
 
     def test_nonregr_copy(self):
         user = self.user()
@@ -489,7 +489,7 @@
         # fail if the controller API changes and if EmbedController is not
         # updated (which is what happened before this test)
         req = self.request()
-        req.form['url'] = 'http://intranet.logilab.fr/'
+        req.form['url'] = 'http://www.logilab.fr/'
         controller = self.vreg['controllers'].select('embed', req)
         result = controller.publish(rset=None)
 
@@ -498,12 +498,20 @@
 
     def test_usable_by_guets(self):
         self.login('anon')
-        self.vreg['controllers'].select('reportbug', self.request())
+        self.assertRaises(NoSelectableObject,
+                          self.vreg['controllers'].select, 'reportbug', self.request())
+        self.vreg['controllers'].select('reportbug', self.request(description='hop'))
 
 
 class SendMailControllerTC(CubicWebTC):
 
     def test_not_usable_by_guets(self):
+        self.assertRaises(NoSelectableObject,
+                          self.vreg['controllers'].select, 'sendmail', self.request())
+        self.vreg['controllers'].select('sendmail',
+                                        self.request(subject='toto',
+                                                     recipient='toto@logilab.fr',
+                                                     mailbody='hop'))
         self.login('anon')
         self.assertRaises(NoSelectableObject,
                           self.vreg['controllers'].select, 'sendmail', self.request())
--- a/web/test/unittest_views_pyviews.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/test/unittest_views_pyviews.py	Tue Apr 06 19:46:38 2010 +0200
@@ -9,10 +9,10 @@
         content = view.render(pyvalue=[[1, 'a'], [2, 'b']],
                               headers=['num', 'char'])
         self.assertEquals(content.strip(), '''<table class="listing">
-<tr><th>num</th><th>char</th></tr>
-<tr><td>1</td><td>a</td></tr>
+<thead><tr><th>num</th><th>char</th></tr>
+</thead><tbody><tr><td>1</td><td>a</td></tr>
 <tr><td>2</td><td>b</td></tr>
-</table>''')
+</tbody></table>''')
 
     def test_pyvallist(self):
         view = self.vreg['views'].select('pyvallist', self.request(),
--- a/web/test/unittest_viewselector.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/test/unittest_viewselector.py	Tue Apr 06 19:46:38 2010 +0200
@@ -67,6 +67,7 @@
         req = self.request()
         self.assertListEqual(self.pviews(req, None),
                              [('changelog', wdoc.ChangeLogView),
+                              ('gc', debug.GCView),
                               ('index', startup.IndexView),
                               ('info', debug.ProcessInformationView),
                               ('manage', startup.ManageView),
--- a/web/uicfg.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/uicfg.py	Tue Apr 06 19:46:38 2010 +0200
@@ -1,54 +1,121 @@
-"""This module regroups a set of structures that may be used to configure
-various places of the generated web interface.
+#:organization: Logilab
+#:copyright: 2009-2010 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
+
+"""This module (``cubicweb.web.uicfg``) regroups a set of structures that may be used to configure
+various options of the generated web interface.
+
+To configure the interface generation, we use ``RelationTag`` objects.
 
 Primary view configuration
 ``````````````````````````
-:primaryview_section:
-   where to display a relation in primary view. Value may be one of:
-   * 'attributes', display in the attributes section
-   * 'relations', display in the relations section (below attributes)
-   * 'sideboxes', display in the side boxes (beside attributes)
-   * 'hidden', don't display
+
+If you want to customize the primary view of an entity, overriding the primary
+view class may not be necessary. For simple adjustments (attributes or relations
+display locations and styles), a much simpler way is to use uicfg.
+
+Attributes/relations display location
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In the primary view, there are 3 sections where attributes and relations can be
+displayed (represented in pink in the image below):
+
+* attributes
+* relations
+* sideboxes
+
+.. image:: ../../images/primaryview_template.png
+
+
+**Attributes** can only be displayed in the attributes section (default
+  behavior). They can also be hidden.
+
+For instance, to hide the ``title`` attribute of the ``Blog`` entity:
 
-:primaryview_display_ctrl:
+.. sourcecode:: python
+
+   from cubicweb.web import uicfg
+   uicfg.primaryview_section.tag_attribute(('Blog', 'title'), 'hidden')
+
+
+**Relations** can be either displayed in one of the three sections or hidden.
+
+For relations, there are two methods:
 
-   how to display a relation in primary view. Values are dict with some of the
-   following keys:
+* ``tag_object_of`` for modifying the primary view of the object
+* ``tag_subject_of`` for modifying the primary view of the subject
+
+These two methods take two arguments:
+
+* a triplet ``(subject, relation_name, object)``, where subject or object can be replaced with ``'*'``
+* the section name or ``hidden``
+
+.. sourcecode:: python
+
+   # hide every relation ``entry_of`` in the ``Blog`` primary view
+   uicfg.primaryview_section.tag_object_of(('*', 'entry_of', 'Blog'), 'hidden')
 
-   :vid:
-      identifier of a view to use to display the result set. Defaults depends on
-      the section:
-      * 'attributes' section: 'reledit' view
-      * 'relations' section: 'autolimited' view
-      * 'sideboxes' section: 'sidebox' view
+   # display ``entry_of`` relations in the ``relations`` section in the ``BlogEntry`` primary view
+   uicfg.primaryview_section.tag_subject_of(('BlogEntry', 'entry_of', '*'),
+                                             'relations')
+
+
+Display content
+^^^^^^^^^^^^^^^
+
+You can use ``primaryview_display_ctrl`` to customize the display of attributes
+or relations. Values of ``primaryview_display_ctrl`` are dictionaries.
+
 
-   :label:
-     label for the relations section or side box
+Common keys for attributes and relations are:
+* ``vid``: specifies the regid of the view for displaying the attribute or the relation.
+
+  If ``vid`` is not specified, the default value depends on the section:
+  * ``attributes`` section: 'reledit' view
+  * ``relations`` section: 'autolimited' view
+  * ``sideboxes`` section: 'sidebox' view
+
+* ``order``: int used to control order within a section. When not specified,
+  automatically set according to order in which tags are added.
+
 
-   :limit:
-      boolean telling if the results should be limited according to the
-      configuration
+Keys for relations only:
+
+* ``label``: label for the relations section or side box
+
+* ``showlabel``: boolean telling whether the label is displayed
+
+* ``limit``: boolean telling if the results should be limited. If so, a link to all results is displayed
+
+* ``filter``: callback taking the related result set as argument and returning it filtered
+
+.. sourcecode:: python
 
-   :filter:
-      callback taking the related result set as argument and returning it
-      filtered
+   # in ``CWUser`` primary view, display ``created_by`` relations in relations section
+   uicfg.primaryview_section.tag_object_of(('*', 'created_by', 'CWUser'), 'relations')
 
-   :order:
-      int used to control order within a section. When not specified,
-      automatically set according to order in which tags are added.
+   # displays this relation as a list, sets the label, limits the number of results and filters on comments
+   uicfg.primaryview_display_ctrl.tag_object_of(
+       ('*', 'created_by', 'CWUser'),
+       {'vid': 'list', 'label': _('latest comment(s):'), 'limit': True,
+        'filter': lambda rset: rset.filtered_rset(lambda x: x.e_schema == 'Comment')})
 
-   Notice those values are only considered if the relation is in a displayed
-   section (controlled by :attr:`primaryview_section`)
+.. Warning:: with the ``primaryview_display_ctrl`` rtag, the subject or the
+   object of the relation is ignored for respectively ``tag_object_of`` or
+   ``tag_subject_of``. To avoid warnings during execution, they should be set to
+   ``'*'``.
 
 
 Index view configuration
 ````````````````````````
 :indexview_etype_section:
-   entity type category in the index/manage page. May be one of
-   * 'application'
-   * 'system'
-   * 'schema'
-   * 'subobject' (not displayed by default)
+   entity type category in the index/manage page. May be one of:
+
+      * ``application``
+      * ``system``
+      * ``schema``
+      * ``subobject`` (not displayed by default)
 
 
 Actions box configuration
@@ -57,50 +124,83 @@
   simple boolean relation tags used to control the "add entity" submenu.
   Relations whose rtag is True will appears, other won't.
 
+.. sourcecode:: python
+
+   # Adds all subjects of the entry_of relation in the add menu of the ``Blog`` primary view
+   uicfg.actionbox_appearsin_addmenu.tag_object_of(('*', 'entry_of', 'Blog'), True)
+
+
 
 Automatic form configuration
 ````````````````````````````
-:autoform_section:
-   where to display a relation in entity form, according to form type.
-   `tag_attribute`, `tag_subject_of` and `tag_object_of` methods for this
-    relation tags expect two arguments additionaly to the relation key: a
-   `formtype` and a `section`.
+
+Attributes/relations display location
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``uicfg.autoform_section`` specifies where to display a relation in
+creation/edition entity form for a given form type.  ``tag_attribute``,
+``tag_subject_of`` and ``tag_object_of`` methods for this relation tag expect
+two arguments additionally to the relation key: a ``formtype`` and a
+``section``.
 
-   formtype may be one of:
-   * 'main', the main entity form
-   * 'inlined', the form for an entity inlined into another's one
-   * 'muledit', the multiple entity (table) form
+formtype may be one of:
+
+* ``main``, the main entity form (via the modify action)
+* ``inlined``, the form for an entity inlined into another form
+* ``muledit``, the table form to edit multiple entities
+
+section may be one of:
+
+* ``hidden``, don't display
+
+* ``attributes``, display in the attributes section
 
-   section may be one of:
-   * 'hidden', don't display
-   * 'attributes', display in the attributes section
-   * 'relations', display in the relations section, using the generic relation
-     selector combobox (available in main form only, and not for attribute
-     relation)
-   * 'inlined', display target entity of the relation in an inlined form
-     (available in main form only, and not for attribute relation)
-   * 'metadata', display in a special metadata form (NOT YET IMPLEMENTED,
-     subject to changes)
+* ``relations``, display in the relations section, using the generic relation
+  selector combobox (available in main form only, and not for attribute
+  relation)
+
+* ``inlined``, display target entity of the relation in an inlined form
+  (available in main form only, and not for attribute relation)
+
+* ``metadata``, display in a special metadata form (NOT YET IMPLEMENTED, subject
+  to changes)
 
-:autoform_field:
-  specify a custom field instance to use for a relation
+By default, mandatory relations are displayed in the ``attributes`` section,
+others in ``relations`` section.
+
+Change default fields
+^^^^^^^^^^^^^^^^^^^^^
 
-:autoform_field_kwargs:
-  specify a dictionnary of arguments to give to the field constructor for a
-  relation. You usually want to use either `autoform_field` or
-  `autoform_field_kwargs`, not both. The later won't have any effect if the
-  former is specified for a relation.
+Use ``autoform_field`` to replace the default field type of an attribute.
 
-:autoform_permissions_overrides:
-
-  provide a way to by-pass security checking for dark-corner case where it can't
-  be verified properly. XXX documents.
+.. Warning:
+   ``autoform_field_kwargs`` should usually be used instead of
+   ``autoform_field``. Do not use both methods for the same relation!
 
 
-:organization: Logilab
-:copyright: 2009-2010 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
+Customize field options
+^^^^^^^^^^^^^^^^^^^^^^^
+
+In order to customize field options (see :class:`cubicweb.web.formfields.Field`
+for a detailed list of options), use ``autoform_field_kwargs``. This rtag takes
+a relation triplet and a dictionary as arguments.
+
+.. sourcecode:: python
+
+   # Change the content of the combobox
+   # here ``ticket_done_in_choices`` is a function which returns a list of
+   # elements to populate the combobox
+   uicfg.autoform_field_kwargs.tag_subject_of(('Ticket', 'done_in', '*'), {'sort': False,
+                                                  'choices': ticket_done_in_choices})
+
+
+
+Overriding permissions
+^^^^^^^^^^^^^^^^^^^^^^
+
+``autoform_permissions_overrides`` provides a way to by-pass security checking
+for dark-corner case where it can't be verified properly. XXX documents.
+
 """
 __docformat__ = "restructuredtext en"
 
--- a/web/views/actions.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/actions.py	Tue Apr 06 19:46:38 2010 +0200
@@ -8,6 +8,8 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from warnings import warn
+
 from cubicweb.schema import display_name
 from cubicweb.appobject import objectify_selector
 from cubicweb.selectors import (EntitySelector, yes,
@@ -402,16 +404,8 @@
 
 addmenu = uicfg.actionbox_appearsin_addmenu
 addmenu.tag_subject_of(('*', 'require_permission', '*'), False)
-addmenu.tag_subject_of(('*', 'wf_info_for', '*'), False)
-addmenu.tag_object_of(('*', 'wf_info_for', '*'), False)
-addmenu.tag_object_of(('*', 'state_of', 'CWEType'), True)
-addmenu.tag_object_of(('*', 'transition_of', 'CWEType'), True)
 addmenu.tag_object_of(('*', 'relation_type', 'CWRType'), True)
 addmenu.tag_object_of(('*', 'from_entity', 'CWEType'), False)
 addmenu.tag_object_of(('*', 'to_entity', 'CWEType'), False)
 addmenu.tag_object_of(('*', 'in_group', 'CWGroup'), True)
 addmenu.tag_object_of(('*', 'bookmarked_by', 'CWUser'), True)
-addmenu.tag_subject_of(('Transition', 'destination_state', '*'), True)
-addmenu.tag_object_of(('*', 'allowed_transition', 'Transition'), True)
-addmenu.tag_object_of(('*', 'destination_state', 'State'), True)
-addmenu.tag_subject_of(('State', 'allowed_transition', '*'), True)
--- a/web/views/authentication.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/authentication.py	Tue Apr 06 19:46:38 2010 +0200
@@ -39,19 +39,13 @@
     __regid__ = 'loginpwdauth'
     order = 10
 
-    def __init__(self, vreg):
-        self.anoninfo = vreg.config.anonymous_user()
-
     def authentication_information(self, req):
         """retreive authentication information from the given request, raise
         NoAuthInfo if expected information is not found.
         """
         login, password = req.get_authorization()
         if not login:
-            # No session and no login -> try anonymous
-            login, password = self.anoninfo
-            if not login: # anonymous not authorized
-                raise NoAuthInfo()
+            raise NoAuthInfo()
         return login, {'password': password}
 
 
@@ -65,6 +59,7 @@
         self.authinforetreivers = sorted(vreg['webauth'].possible_objects(vreg),
                                     key=lambda x: x.order)
         assert self.authinforetreivers
+        self.anoninfo = vreg.config.anonymous_user()
 
     def validate_session(self, req, session):
         """check session validity, and return eventually hijacked session
@@ -114,41 +109,44 @@
                 login, authinfo = retreiver.authentication_information(req)
             except NoAuthInfo:
                 continue
-            cnx = self._authenticate(req, login, authinfo)
+            try:
+                cnx = self._authenticate(req, login, authinfo)
+            except ExplicitLogin:
+                continue # the next one may succeed
+            for retreiver_ in self.authinforetreivers:
+                retreiver_.authenticated(req, cnx, retreiver)
             break
         else:
-            raise ExplicitLogin()
-        for retreiver_ in self.authinforetreivers:
-            retreiver_.authenticated(req, cnx, retreiver)
+            # false if no authentication info found, eg this is not an
+            # authentication failure
+            if 'login' in locals():
+                req.set_message(req._('authentication failure'))
+            cnx = self._open_anonymous_connection(req)
         return cnx
 
     def _authenticate(self, req, login, authinfo):
-        # remove possibly cached cursor coming from closed connection
-        clear_cache(req, 'cursor')
         cnxprops = ConnectionProperties(self.vreg.config.repo_method,
                                         close=False, log=self.log_queries)
         try:
             cnx = repo_connect(self.repo, login, cnxprops=cnxprops, **authinfo)
         except AuthenticationError:
-            req.set_message(req._('authentication failure'))
-            # restore an anonymous connection if possible
-            anonlogin, anonpassword = self.vreg.config.anonymous_user()
-            if anonlogin and anonlogin != login:
-                cnx = repo_connect(self.repo, anonlogin, password=anonpassword,
-                                   cnxprops=cnxprops)
-                self._init_cnx(cnx, anonlogin, {'password': anonpassword})
-            else:
-                raise ExplicitLogin()
-        else:
-            self._init_cnx(cnx, login, authinfo)
+            raise ExplicitLogin()
+        self._init_cnx(cnx, login, authinfo)
         # associate the connection to the current request
         req.set_connection(cnx)
         return cnx
 
+    def _open_anonymous_connection(self, req):
+        # restore an anonymous connection if possible
+        login, password = self.anoninfo
+        if login:
+            cnx = self._authenticate(req, login, {'password': password})
+            cnx.anonymous_connection = True
+            return cnx
+        raise ExplicitLogin()
+
     def _init_cnx(self, cnx, login, authinfo):
         # decorate connection
-        if login == self.vreg.config.anonymous_user()[0]:
-            cnx.anonymous_connection = True
         cnx.vreg = self.vreg
         cnx.login = login
         cnx.authinfo = authinfo
--- a/web/views/autoform.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/autoform.py	Tue Apr 06 19:46:38 2010 +0200
@@ -9,6 +9,8 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from warnings import warn
+
 from simplejson import dumps
 
 from logilab.mtconverter import xml_escape
--- a/web/views/basecomponents.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/basecomponents.py	Tue Apr 06 19:46:38 2010 +0200
@@ -15,7 +15,8 @@
 from logilab.mtconverter import xml_escape
 from rql import parse
 
-from cubicweb.selectors import yes, multi_etypes_rset, match_form_params
+from cubicweb.selectors import (yes, multi_etypes_rset, match_form_params,
+                                anonymous_user, authenticated_user)
 from cubicweb.schema import display_name
 from cubicweb.uilib import toggle_action
 from cubicweb.web import component
@@ -79,35 +80,19 @@
                   self._cw._(u'help'),))
 
 
-class UserLink(component.Component):
-    """if the user is the anonymous user, build a link to login
-    else a link to the connected user object with a loggout link
+class _UserLink(component.Component):
+    """if the user is the anonymous user, build a link to login else display a menu
+    with user'action (preference, logout, etc...)
     """
     cw_property_defs = VISIBLE_PROP_DEF
     # don't want user to hide this component using an cwproperty
     site_wide = True
     __regid__ = 'loggeduserlink'
 
+
+class AnonUserLink(_UserLink):
+    __select__ = _UserLink.__select__ & anonymous_user()
     def call(self):
-        if not self._cw.cnx.anonymous_connection:
-            # display useractions and siteactions
-            actions = self._cw.vreg['actions'].possible_actions(self._cw, rset=self.cw_rset)
-            box = MenuWidget('', 'userActionsBox', _class='', islist=False)
-            menu = PopupBoxMenu(self._cw.user.login, isitem=False)
-            box.append(menu)
-            for action in actions.get('useractions', ()):
-                menu.append(BoxLink(action.url(), self._cw._(action.title),
-                                    action.html_class()))
-            if actions.get('useractions') and actions.get('siteactions'):
-                menu.append(BoxSeparator())
-            for action in actions.get('siteactions', ()):
-                menu.append(BoxLink(action.url(), self._cw._(action.title),
-                                    action.html_class()))
-            box.render(w=self.w)
-        else:
-            self.anon_user_link()
-
-    def anon_user_link(self):
         if self._cw.vreg.config['auth-mode'] == 'cookie':
             self.w(self._cw._('anonymous'))
             self.w(u'''&#160;[<a class="logout" href="javascript: popupLoginBox();">%s</a>]'''
@@ -118,6 +103,26 @@
                    % (self._cw.build_url('login'), self._cw._('login')))
 
 
+class UserLink(_UserLink):
+    __select__ = _UserLink.__select__ & authenticated_user()
+
+    def call(self):
+        # display useractions and siteactions
+        actions = self._cw.vreg['actions'].possible_actions(self._cw, rset=self.cw_rset)
+        box = MenuWidget('', 'userActionsBox', _class='', islist=False)
+        menu = PopupBoxMenu(self._cw.user.login, isitem=False)
+        box.append(menu)
+        for action in actions.get('useractions', ()):
+            menu.append(BoxLink(action.url(), self._cw._(action.title),
+                                action.html_class()))
+        if actions.get('useractions') and actions.get('siteactions'):
+            menu.append(BoxSeparator())
+        for action in actions.get('siteactions', ()):
+            menu.append(BoxLink(action.url(), self._cw._(action.title),
+                                action.html_class()))
+        box.render(w=self.w)
+
+
 class ApplicationMessage(component.Component):
     """display messages given using the __message parameter into a special div
     section
@@ -216,20 +221,6 @@
         self.w(u'</div>')
 
 
-class PdfViewComponent(component.EntityVComponent):
-    __regid__ = 'pdfview'
-
-    context = 'ctxtoolbar'
-
-    def cell_call(self, row, col, view):
-        entity = self.cw_rset.get_entity(row, col)
-        url = entity.absolute_url(vid=view.__regid__, __template='pdf-main-template')
-        iconurl = self._cw.build_url('data/pdf_icon.gif')
-        label = self._cw._('Download page as pdf')
-        self.w(u'<a href="%s" title="%s" class="toolbarButton"><img src="%s" alt="%s"/></a>' %
-               (xml_escape(url), label, iconurl, label))
-
-
 class MetaDataComponent(component.EntityVComponent):
     __regid__ = 'metadata'
     context = 'navbottom'
--- a/web/views/basecontrollers.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/basecontrollers.py	Tue Apr 06 19:46:38 2010 +0200
@@ -17,9 +17,10 @@
 from logilab.common.decorators import cached
 from logilab.common.date import strptime
 
-from cubicweb import NoSelectableObject, ValidationError, ObjectNotFound, typed_eid
+from cubicweb import (NoSelectableObject, ValidationError, ObjectNotFound,
+                      typed_eid)
 from cubicweb.utils import CubicWebJsonEncoder
-from cubicweb.selectors import yes, match_user_groups
+from cubicweb.selectors import authenticated_user, match_form_params
 from cubicweb.mail import format_mail
 from cubicweb.web import ExplicitLogin, Redirect, RemoteCallFailed, json_dumps
 from cubicweb.web.controller import Controller
@@ -82,8 +83,19 @@
 
     def publish(self, rset=None):
         """logout from the instance"""
-        return self.appli.session_handler.logout(self._cw)
+        return self.appli.session_handler.logout(self._cw, self.goto_url())
 
+    def goto_url(self):
+        # * in http auth mode, url will be ignored
+        # * in cookie mode redirecting to the index view is enough : either
+        #   anonymous connection is allowed and the page will be displayed or
+        #   we'll be redirected to the login form
+        msg = self._cw._('you have been logged out')
+        if self._cw.https:
+            # XXX hack to generate an url on the http version of the site
+            self._cw._base_url =  self._cw.vreg.config['base-url']
+            self._cw.https = False
+        return self._cw.build_url('view', vid='index', __message=msg)
 
 class ViewController(Controller):
     """standard entry point :
@@ -264,14 +276,16 @@
         args = self._cw.form.get('arg', ())
         if not isinstance(args, (list, tuple)):
             args = (args,)
-        args = [simplejson.loads(arg) for arg in args]
+        try:
+            args = [simplejson.loads(arg) for arg in args]
+        except ValueError, exc:
+            self.exception('error while decoding json arguments for js_%s: %s', fname, args, exc)
+            raise RemoteCallFailed(repr(exc))
         try:
             result = func(*args)
         except RemoteCallFailed:
             raise
         except Exception, ex:
-            import traceback
-            traceback.print_exc()
             self.exception('an exception occured while calling js_%s(%s): %s',
                            fname, args, ex)
             raise RemoteCallFailed(repr(ex))
@@ -380,9 +394,27 @@
         else: # we receive unicode keys which is not supported by the **syntax
             extraargs = dict((str(key), value)
                              for key, value in extraargs.items())
-        comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset, **extraargs)
+        # XXX while it sounds good, addition of the try/except below cause pb:
+        # when filtering using facets return an empty rset, the edition box
+        # isn't anymore selectable, as expected. The pb is that with the
+        # try/except below, we see a "an error occured" message in the ui, while
+        # we don't see it without it. Proper fix would probably be to deal with
+        # this by allowing facet handling code to tell to js_component that such
+        # error is expected and should'nt be reported.
+        #try:
+        comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset,
+                                              **extraargs)
+        #except NoSelectableObject:
+        #    raise RemoteCallFailed('unselectable')
         extraargs = extraargs or {}
-        return comp.render(**extraargs)
+        stream = comp.set_stream()
+        comp.render(**extraargs)
+        extresources = self._cw.html_headers.getvalue(skiphead=True)
+        if extresources:
+            stream.write(u'<div class="ajaxHtmlHead">\n')
+            stream.write(extresources)
+            stream.write(u'</div>\n')
+        return stream.getvalue()
 
     @check_pageid
     @xhtmlize
@@ -413,6 +445,7 @@
         view = req.vreg['views'].select('doreledit', req, rset=rset, rtype=args['rtype'])
         stream = view.set_stream()
         view.render(**args)
+        # XXX why not _call_view ?
         extresources = req.html_headers.getvalue(skiphead=True)
         if extresources:
             stream.write(u'<div class="ajaxHtmlHead">\n')
@@ -548,7 +581,7 @@
 
 class SendMailController(Controller):
     __regid__ = 'sendmail'
-    __select__ = match_user_groups('managers', 'users')
+    __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject')
 
     def recipients(self):
         """returns an iterator on email's recipients as entities"""
@@ -596,7 +629,7 @@
 
 class MailBugReportController(SendMailController):
     __regid__ = 'reportbug'
-    __select__ = yes()
+    __select__ = match_form_params('description')
 
     def publish(self, rset=None):
         body = self._cw.form['description']
@@ -604,3 +637,27 @@
         url = self._cw.build_url(__message=self._cw._('bug report sent'))
         raise Redirect(url)
 
+
+class UndoController(SendMailController):
+    __regid__ = 'undo'
+    __select__ = authenticated_user() & match_form_params('txuuid')
+
+    def publish(self, rset=None):
+        txuuid = self._cw.form['txuuid']
+        errors = self._cw.cnx.undo_transaction(txuuid)
+        if errors:
+            self.w(self._cw._('some errors occured:'))
+            self.wview('pyvalist', pyvalue=errors)
+        else:
+            self.redirect()
+
+    def redirect(self):
+        req = self._cw
+        breadcrumbs = req.get_session_data('breadcrumbs', None)
+        if breadcrumbs is not None and len(breadcrumbs) > 1:
+            url = req.rebuild_url(breadcrumbs[-2],
+                                  __message=req._('transaction undoed'))
+        else:
+            url = req.build_url(__message=req._('transaction undoed'))
+        raise Redirect(url)
+
--- a/web/views/basetemplates.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/basetemplates.py	Tue Apr 06 19:46:38 2010 +0200
@@ -16,7 +16,7 @@
 from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW
 from cubicweb.utils import UStringIO, can_do_pdf_conversion
 from cubicweb.schema import display_name
-from cubicweb.web import formfields as ff, formwidgets as fw
+from cubicweb.web import component, formfields as ff, formwidgets as fw
 from cubicweb.web.views import forms
 
 # main templates ##############################################################
@@ -270,7 +270,7 @@
 
 if can_do_pdf_conversion():
     try:
-      from xml.etree.cElementTree import ElementTree
+        from xml.etree.cElementTree import ElementTree
     except ImportError: #python2.4
         from elementtree import ElementTree
     from subprocess import Popen as sub
@@ -278,6 +278,20 @@
     from tempfile import NamedTemporaryFile
     from cubicweb.ext.xhtml2fo import ReportTransformer
 
+
+    class PdfViewComponent(component.EntityVComponent):
+        __regid__ = 'pdfview'
+
+        context = 'ctxtoolbar'
+
+        def cell_call(self, row, col, view):
+            entity = self.cw_rset.get_entity(row, col)
+            url = entity.absolute_url(vid=view.__regid__, __template='pdf-main-template')
+            iconurl = self._cw.build_url('data/pdf_icon.gif')
+            label = self._cw._('Download page as pdf')
+            self.w(u'<a href="%s" title="%s" class="toolbarButton"><img src="%s" alt="%s"/></a>' %
+                   (xml_escape(url), label, xml_escape(iconurl), label))
+
     class PdfMainTemplate(TheMainTemplate):
         __regid__ = 'pdf-main-template'
 
--- a/web/views/bookmark.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/bookmark.py	Tue Apr 06 19:46:38 2010 +0200
@@ -113,7 +113,7 @@
                     # we can't edit shared bookmarks we don't own
                     bookmarksrql = 'Bookmark B WHERE B bookmarked_by U, B owned_by U, U eid %(x)s'
                     erset = req.execute(bookmarksrql, {'x': ueid}, 'x',
-                                                build_descr=False)
+                                        build_descr=False)
                     bookmarksrql %= {'x': ueid}
                 if erset:
                     url = self._cw.build_url(vid='muledit', rql=bookmarksrql)
--- a/web/views/debug.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/debug.py	Tue Apr 06 19:46:38 2010 +0200
@@ -27,13 +27,13 @@
 
 
 class ProcessInformationView(StartupView):
+    """display various web server /repository information"""
     __regid__ = 'info'
     __select__ = none_rset() & match_user_groups('managers')
 
     title = _('server information')
 
     def call(self, **kwargs):
-        """display server information"""
         req = self._cw
         dtformat = req.property_value('ui.datetime-format')
         _ = req._
@@ -111,24 +111,59 @@
 
 
 class RegistryView(StartupView):
+    """display vregistry content"""
     __regid__ = 'registry'
     __select__ = StartupView.__select__ & match_user_groups('managers')
     title = _('registry')
 
     def call(self, **kwargs):
-        """The default view representing the instance's management"""
         self.w(u'<h1>%s</h1>' % _("Registry's content"))
         keys = sorted(self._cw.vreg)
-        self.w(u'<p>%s</p>\n' % ' - '.join('<a href="/_registry#%s">%s</a>'
-                                           % (key, key) for key in keys))
+        url = xml_escape(self._cw.url())
+        self.w(u'<p>%s</p>\n' % ' - '.join('<a href="%s#%s">%s</a>'
+                                           % (url, key, key) for key in keys))
         for key in keys:
-            self.w(u'<h2><a name="%s">%s</a></h2>' % (key, key))
-            items = self._cw.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')
+            self.w(u'<h2 id="%s">%s</h2>' % (key, key))
+            if self._cw.vreg[key]:
+                values = sorted(self._cw.vreg[key].iteritems())
+                self.wview('pyvaltable', pyvalue=[(key, xml_escape(repr(val)))
+                                                  for key, val in values])
             else:
                 self.w(u'<p>Empty</p>\n')
+
+
+class GCView(StartupView):
+    """display garbage collector information"""
+    __regid__ = 'gc'
+    __select__ = StartupView.__select__ & match_user_groups('managers')
+    title = _('memory leak debugging')
+
+    def call(self, **kwargs):
+        from cubicweb._gcdebug import gc_info
+        from rql.stmts import Union
+        from cubicweb.appobject import AppObject
+        from cubicweb.rset import ResultSet
+        from cubicweb.dbapi import Connection, Cursor
+        from cubicweb.web.request import CubicWebRequestBase
+        lookupclasses = (AppObject,
+                         Union, ResultSet,
+                         Connection, Cursor,
+                         CubicWebRequestBase)
+        try:
+            from cubicweb.server.session import Session, ChildSession, InternalSession
+            lookupclasses += (InternalSession, ChildSession, Session)
+        except ImportError:
+            pass # no server part installed
+        self.w(u'<h1>%s</h1>' % _('Garbage collection information'))
+        counters, ocounters, garbage = gc_info(lookupclasses,
+                                               viewreferrersclasses=())
+        self.w(u'<h3>%s</h3>' % _('Looked up classes'))
+        values = sorted(counters.iteritems(), key=lambda x: x[1], reverse=True)
+        self.wview('pyvaltable', pyvalue=values)
+        self.w(u'<h3>%s</h3>' % _('Most referenced classes'))
+        values = sorted(ocounters.iteritems(), key=lambda x: x[1], reverse=True)
+        self.wview('pyvaltable', pyvalue=values[:self._cw.form.get('nb', 20)])
+        if garbage:
+            self.w(u'<h3>%s</h3>' % _('Unreachable objects'))
+            values = sorted(xml_escape(repr(o) for o in garbage))
+            self.wview('pyvallist', pyvalue=values)
--- a/web/views/editcontroller.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/editcontroller.py	Tue Apr 06 19:46:38 2010 +0200
@@ -7,6 +7,8 @@
 """
 __docformat__ = "restructuredtext en"
 
+from warnings import warn
+
 from rql.utils import rqlvar_maker
 
 from logilab.common.textutils import splitstrip
@@ -15,6 +17,11 @@
 from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, ProcessFormError
 from cubicweb.web.views import basecontrollers, autoform
 
+def valerror_eid(eid):
+    try:
+        return typed_eid(eid)
+    except (ValueError, TypeError):
+        return eid
 
 class RqlQuery(object):
     def __init__(self):
@@ -108,7 +115,7 @@
         self._cw.remove_pending_operations()
         if self.errors:
             errors = dict((f.name, unicode(ex)) for f, ex in self.errors)
-            raise ValidationError(form.get('__maineid'), errors)
+            raise ValidationError(valerror_eid(form.get('__maineid')), errors)
 
     def _insert_entity(self, etype, eid, rqlquery):
         rql = rqlquery.insert_query(etype)
@@ -164,7 +171,7 @@
                     self.handle_formfield(form, field, rqlquery)
         if self.errors:
             errors = dict((f.role_name(), unicode(ex)) for f, ex in self.errors)
-            raise ValidationError(entity.eid, errors)
+            raise ValidationError(valerror_eid(entity.eid), errors)
         if eid is None: # creation or copy
             entity.eid = self._insert_entity(etype, formparams['eid'], rqlquery)
         elif rqlquery.edited: # edition of an existant entity
@@ -250,6 +257,25 @@
             for reid in seteids:
                 self.relations_rql.append((rql, {'x': eid, 'y': reid}, ('x', 'y')))
 
+    def delete_entities(self, eidtypes):
+        """delete entities from the repository"""
+        redirect_info = set()
+        eidtypes = tuple(eidtypes)
+        for eid, etype in eidtypes:
+            entity = self._cw.entity_from_eid(eid, etype)
+            path, params = entity.after_deletion_path()
+            redirect_info.add( (path, tuple(params.iteritems())) )
+            entity.delete()
+        if len(redirect_info) > 1:
+            # In the face of ambiguity, refuse the temptation to guess.
+            self._after_deletion_path = 'view', ()
+        else:
+            self._after_deletion_path = iter(redirect_info).next()
+        if len(eidtypes) > 1:
+            self._cw.set_message(self._cw._('entities deleted'))
+        else:
+            self._cw.set_message(self._cw._('entity deleted'))
+
     def _action_apply(self):
         self._default_publish()
         self.reset()
@@ -258,11 +284,9 @@
         errorurl = self._cw.form.get('__errorurl')
         if errorurl:
             self._cw.cancel_edition(errorurl)
-        self._cw.message = self._cw._('edit canceled')
+        self._cw.set_message(self._cw._('edit canceled'))
         return self.reset()
 
     def _action_delete(self):
         self.delete_entities(self._cw.edited_eids(withtype=True))
         return self.reset()
-
-
--- a/web/views/editviews.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/editviews.py	Tue Apr 06 19:46:38 2010 +0200
@@ -15,7 +15,7 @@
 from cubicweb.view import EntityView, StartupView
 from cubicweb.selectors import (one_line_rset, non_final_entity,
                                 match_search_state)
-from cubicweb.web import httpcache, captcha
+from cubicweb.web import httpcache
 from cubicweb.web.views import baseviews, linksearch_select_url
 
 
@@ -95,17 +95,23 @@
         else:
             super(EditableFinalView, self).cell_call(row, col, props)
 
-
-class CaptchaView(StartupView):
-    __regid__ = 'captcha'
+try:
+    from cubicweb.web import captcha
+except ImportError:
+    # PIL not installed
+    pass
+else:
+    class CaptchaView(StartupView):
+        __regid__ = 'captcha'
 
-    http_cache_manager = httpcache.NoHTTPCacheManager
-    binary = True
-    templatable = False
-    content_type = 'image/jpg'
+        http_cache_manager = httpcache.NoHTTPCacheManager
+        binary = True
+        templatable = False
+        content_type = 'image/jpg'
 
-    def call(self):
-        text, data = captcha.captcha(self._cw.vreg.config['captcha-font-file'],
-                                     self._cw.vreg.config['captcha-font-size'])
-        self._cw.set_session_data('captcha', text)
-        self.w(data.read())
+        def call(self):
+            text, data = captcha.captcha(self._cw.vreg.config['captcha-font-file'],
+                                         self._cw.vreg.config['captcha-font-size'])
+            key = self._cw.form.get('captchakey', 'captcha')
+            self._cw.set_session_data(key, text)
+            self.w(data.read())
--- a/web/views/facets.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/facets.py	Tue Apr 06 19:46:38 2010 +0200
@@ -105,7 +105,10 @@
     def display_bookmark_link(self, rset):
         eschema = self._cw.vreg.schema.eschema('Bookmark')
         if eschema.has_perm(self._cw, 'add'):
-            bk_path = 'view?rql=%s' % rset.printable_rql()
+            bk_path = 'rql=%s' % self._cw.url_quote(rset.printable_rql())
+            if self._cw.form.get('vid'):
+                bk_path += '&vid=%s' % self._cw.url_quote(self._cw.form['vid'])
+            bk_path = 'view?' + bk_path
             bk_title = self._cw._('my custom search')
             linkto = 'bookmarked_by:%s:subject' % self._cw.user.eid
             bk_add_url = self._cw.build_url('add/Bookmark', path=bk_path, title=bk_title, __linkto=linkto)
--- a/web/views/formrenderers.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/formrenderers.py	Tue Apr 06 19:46:38 2010 +0200
@@ -224,6 +224,8 @@
                     w(u' class="error"')
                 w(u'>')
                 w(field.render(form, self))
+                if error:
+                    self.render_error(w, error)
                 if self.display_help:
                     w(self.render_help(form, field))
                 w(u'</td></tr>')
@@ -241,7 +243,7 @@
 
     def render_error(self, w, err):
         """return validation error for widget's field, if any"""
-        w(u'<span class="error">%s</span>' % err)
+        w(u'<span class="errorMsg">%s</span>' % err)
 
 
 
@@ -259,7 +261,7 @@
     +--------------+--------------+---------+
     | field1 label | field2 label |         |
     +--------------+--------------+---------+
-    | field1 input | field2 input | buttons
+    | field1 input | field2 input | buttons |
     +--------------+--------------+---------+
     """
     __regid__ = 'htable'
--- a/web/views/iprogress.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/iprogress.py	Tue Apr 06 19:46:38 2010 +0200
@@ -261,8 +261,7 @@
         self._cw.html_headers.add_onload('draw_progressbar("canvas%s", %i, %i, %i, "%s");' %
                                          (cid,
                                           int(100.*done/maxi), int(100.*(done+todo)/maxi),
-                                          int(100.*budget/maxi), color),
-                                         jsoncall=self._cw.json_request)
+                                          int(100.*budget/maxi), color))
         self.w(u'%s<br/>'
                u'<canvas class="progressbar" id="canvas%s" width="100" height="10"></canvas>'
                % (short_title.replace(' ','&nbsp;'), cid))
--- a/web/views/plots.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/plots.py	Tue Apr 06 19:46:38 2010 +0200
@@ -115,8 +115,7 @@
                                     {'plotdefs': '\n'.join(plotdefs),
                                      'figid': figid,
                                      'plotdata': ','.join(plotdata),
-                                     'mode': self.timemode and "'time'" or 'null'},
-                                    jsoncall=req.json_request)
+                                     'mode': self.timemode and "'time'" or 'null'})
 
 
 class PlotView(baseviews.AnyRsetView):
--- a/web/views/primary.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/primary.py	Tue Apr 06 19:46:38 2010 +0200
@@ -15,7 +15,7 @@
 from cubicweb import Unauthorized
 from cubicweb.selectors import match_kwargs
 from cubicweb.view import EntityView
-from cubicweb.schema import display_name
+from cubicweb.schema import VIRTUAL_RTYPES, display_name
 from cubicweb.web import uicfg
 
 
@@ -202,6 +202,8 @@
         rdefs = []
         eschema = entity.e_schema
         for rschema, tschemas, role in eschema.relation_definitions(True):
+            if rschema in VIRTUAL_RTYPES:
+                continue
             matchtschemas = []
             for tschema in tschemas:
                 section = self.rsection.etype_get(eschema, rschema, role,
--- a/web/views/pyviews.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/pyviews.py	Tue Apr 06 19:46:38 2010 +0200
@@ -17,18 +17,23 @@
     def call(self, pyvalue, headers=None):
         if headers is None:
             headers = self._cw.form.get('headers')
-        self.w(u'<table class="listing">\n')
+        w = self.w
+        w(u'<table class="listing">\n')
         if headers:
-            self.w(u'<tr>')
+            w(u'<thead>')
+            w(u'<tr>')
             for header in headers:
-                self.w(u'<th>%s</th>' % header)
-            self.w(u'</tr>\n')
+                w(u'<th>%s</th>' % header)
+            w(u'</tr>\n')
+            w(u'</thead>')
+        w(u'<tbody>')
         for row in pyvalue:
-            self.w(u'<tr>')
+            w(u'<tr>')
             for cell in row:
-                self.w(u'<td>%s</td>' % cell)
-            self.w(u'</tr>\n')
-        self.w(u'</table>\n')
+                w(u'<td>%s</td>' % cell)
+            w(u'</tr>\n')
+        w(u'</tbody>')
+        w(u'</table>\n')
 
 
 class PyValListView(View):
--- a/web/views/schema.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/schema.py	Tue Apr 06 19:46:38 2010 +0200
@@ -247,7 +247,7 @@
         self.w(u'<h2>%s</h2>' % _('Relations'))
         rset = self._cw.execute(
             'Any R,C,TT,K,D,A,RN,TTN ORDERBY RN '
-            'WHERE A is CWRelation, A description D, A composite K?, '
+            'WHERE A is CWRelation, A description D, A composite K, '
             'A relation_type R, R name RN, A to_entity TT, TT name TTN, '
             'A cardinality C, A from_entity S, S eid %(x)s',
             {'x': entity.eid})
@@ -255,7 +255,7 @@
                    displaycols=range(6), mainindex=5)
         rset = self._cw.execute(
             'Any R,C,TT,K,D,A,RN,TTN ORDERBY RN '
-            'WHERE A is CWRelation, A description D, A composite K?, '
+            'WHERE A is CWRelation, A description D, A composite K, '
             'A relation_type R, R name RN, A from_entity TT, TT name TTN, '
             'A cardinality C, A to_entity O, O eid %(x)s',
             {'x': entity.eid})
--- a/web/views/sessions.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/sessions.py	Tue Apr 06 19:46:38 2010 +0200
@@ -22,6 +22,8 @@
         #assert isinstance(self.authmanager, RepositoryAuthenticationManager)
         self._sessions = {}
 
+    # dump_data / restore_data to avoid loosing open sessions on registry
+    # reloading
     def dump_data(self):
         return self._sessions
     def restore_data(self, data):
@@ -38,9 +40,9 @@
         if self.has_expired(session):
             self.close_session(session)
             raise InvalidSession()
-        # give an opportunity to auth manager to hijack the session
-        # (necessary with the RepositoryAuthenticationManager in case
-        #  the connection to the repository has expired)
+        # give an opportunity to auth manager to hijack the session (necessary
+        # with the RepositoryAuthenticationManager in case the connection to the
+        # repository has expired)
         try:
             session = self.authmanager.validate_session(req, session)
             # necessary in case session has been hijacked
--- a/web/views/tabs.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/tabs.py	Tue Apr 06 19:46:38 2010 +0200
@@ -32,9 +32,7 @@
 
     def lazyview(self, vid, rql=None, eid=None, rset=None, tabid=None,
                  reloadable=False, show_spinbox=True, w=None):
-        """a lazy version of wview
-        first version only support lazy viewing for an entity at a time
-        """
+        """ a lazy version of wview """
         w = w or self.w
         self._cw.add_js('cubicweb.lazy.js')
         urlparams = {'vid' : vid, 'fname' : 'view'}
--- a/web/views/treeview.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/treeview.py	Tue Apr 06 19:46:38 2010 +0200
@@ -46,8 +46,7 @@
         self._cw.add_css('jquery.treeview.css')
         self._cw.add_js(('cubicweb.ajax.js', 'cubicweb.widgets.js', 'jquery.treeview.js'))
         self._cw.html_headers.add_onload(u"""
-jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid,
-                                         jsoncall=toplevel_thru_ajax)
+jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid)
 
     def call(self, subvid=None, treeid=None,
              initial_load=True, initial_thru_ajax=False, **morekwargs):
--- a/web/views/workflow.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/workflow.py	Tue Apr 06 19:46:38 2010 +0200
@@ -35,6 +35,15 @@
 _abaa.tag_subject_of(('State', 'allowed_transition', 'BaseTransition'), False)
 _abaa.tag_object_of(('SubWorkflowExitPoint', 'destination_state', 'State'),
                     False)
+_abaa.tag_subject_of(('*', 'wf_info_for', '*'), False)
+_abaa.tag_object_of(('*', 'wf_info_for', '*'), False)
+
+_abaa.tag_object_of(('*', 'state_of', 'CWEType'), True)
+_abaa.tag_object_of(('*', 'transition_of', 'CWEType'), True)
+_abaa.tag_subject_of(('Transition', 'destination_state', '*'), True)
+_abaa.tag_object_of(('*', 'allowed_transition', 'Transition'), True)
+_abaa.tag_object_of(('*', 'destination_state', 'State'), True)
+_abaa.tag_subject_of(('State', 'allowed_transition', '*'), True)
 _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)
--- a/web/views/xmlrss.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/views/xmlrss.py	Tue Apr 06 19:46:38 2010 +0200
@@ -50,11 +50,14 @@
         self.w(u'<%s>\n' % (entity.e_schema))
         for rschema, attrschema in entity.e_schema.attribute_definitions():
             attr = rschema.type
-            try:
-                value = entity[attr]
-            except KeyError:
-                # Bytes
-                continue
+            if attr == 'eid':
+                value = entity.eid
+            else:
+                try:
+                    value = entity[attr]
+                except KeyError:
+                    # Bytes
+                    continue
             if value is not None:
                 if attrschema == 'Bytes':
                     from base64 import b64encode
@@ -148,6 +151,7 @@
     content_type = 'text/xml'
     http_cache_manager = httpcache.MaxAgeHTTPCacheManager
     cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
+    item_vid = 'rssitem'
 
     def _open(self):
         req = self._cw
@@ -174,7 +178,7 @@
         self._close()
 
     def cell_call(self, row, col):
-        self.wview('rssitem', self.cw_rset, row=row, col=col)
+        self.wview(self.item_vid, self.cw_rset, row=row, col=col)
 
 
 class RSSItemView(EntityView):
--- a/web/webconfig.py	Thu Mar 04 17:56:45 2010 +0100
+++ b/web/webconfig.py	Tue Apr 06 19:46:38 2010 +0200
@@ -300,14 +300,16 @@
 
     def _init_base_url(self):
         # normalize base url(s)
-        baseurl = self['base-url']
+        baseurl = self['base-url'] or self.default_base_url()
         if baseurl and baseurl[-1] != '/':
             baseurl += '/'
+        if not self.repairing:
             self.global_set_option('base-url', baseurl)
         httpsurl = self['https-url']
         if httpsurl and httpsurl[-1] != '/':
             httpsurl += '/'
-            self.global_set_option('https-url', httpsurl)
+            if not self.repairing:
+                self.global_set_option('https-url', httpsurl)
 
     def _build_ext_resources(self):
         libresourcesfile = join(self.shared_dir(), 'data', 'external_resources')