[merge] reintegrate that black sheep
authorAurelien Campeas <aurelien.campeas@logilab.fr>
Tue, 23 Oct 2012 15:00:53 +0200
changeset 8576 c9c72ac10db3
parent 8573 ae0a567dff30 (diff)
parent 8574 268bc595271b (current diff)
child 8577 3f082de68802
[merge] reintegrate that black sheep
--- a/.hgtags	Wed Feb 22 11:57:42 2012 +0100
+++ b/.hgtags	Tue Oct 23 15:00:53 2012 +0200
@@ -245,3 +245,24 @@
 4d0f5d18e8a07ab218efe90d758af723ea4a1b2b cubicweb-debian-version-3.14.3-1
 508645a542870cb0def9c43056e5084ff8def5ca cubicweb-version-3.14.4
 bc40991b7f13642d457f5ca80ac1486c29e25a6e cubicweb-debian-version-3.14.4-1
+4c8cb2e9d0ee13af1d584e2920d1ae76f47380e9 cubicweb-debian-version-3.14.4-2
+f559ab9602e7eeb4996ac0f83d544a6e0374e204 cubicweb-version-3.14.5
+55fc796ed5d5f31245ae60bd148c9e42657a1af6 cubicweb-debian-version-3.14.5-1
+db021578232b885dc5e55dfca045332ce01e7f35 cubicweb-version-3.14.6
+75364c0994907764715bd5011f6a59d934dbeb7d cubicweb-debian-version-3.14.6-1
+0642b2d03acaa5e065cae7590e82b388a280ca22 cubicweb-version-3.15.0
+925db25a3250c5090cf640fc2b02bde5818b9798 cubicweb-debian-version-3.15.0-1
+3ba3ee5b3a89a54d1dc12ed41d5c12232eda1952 cubicweb-version-3.14.7
+20ee573bd2379a00f29ff27bb88a8a3344d4cdfe cubicweb-debian-version-3.14.7-1
+15fe07ff687238f8cc09d8e563a72981484085b3 cubicweb-version-3.14.8
+81394043ad226942ac0019b8e1d4f7058d67a49f cubicweb-debian-version-3.14.8-1
+9337812cef6b949eee89161190e0c3d68d7f32ea cubicweb-version-3.14.9
+68c762adf2d5a2c338910ef1091df554370586f0 cubicweb-debian-version-3.14.9-1
+783a5df54dc742e63c8a720b1582ff08366733bd cubicweb-version-3.15.1
+fe5e60862b64f1beed2ccdf3a9c96502dfcd811b cubicweb-debian-version-3.15.1-1
+2afc157ea9b2b92eccb0f2d704094e22ce8b5a05 cubicweb-version-3.15.2
+9aa5553b26520ceb68539e7a32721b5cd5393e16 cubicweb-debian-version-3.15.2-1
+0e012eb80990ca6f91aa9a8ad3324fbcf51435b1 cubicweb-version-3.15.3
+7ad423a5b6a883dbdf00e6c87a5f8ab121041640 cubicweb-debian-version-3.15.3-1
+63260486de89a9dc32128cd0eacef891a668977b cubicweb-version-3.15.4
+70cb36c826df86de465f9b69647cef7096dcf12c cubicweb-debian-version-3.15.4-1
--- a/MANIFEST.in	Wed Feb 22 11:57:42 2012 +0100
+++ b/MANIFEST.in	Tue Oct 23 15:00:53 2012 +0200
@@ -12,7 +12,7 @@
 include web/views/*.pt
 recursive-include web/data external_resources *.js *.css *.py *.png *.gif *.ico *.ttf
 recursive-include web/wdoc *.rst *.png *.xml ChangeLog*
-recursive-include devtools/data *.js *.css
+recursive-include devtools/data *.js *.css *.sh
 
 recursive-include i18n *.pot *.po
 recursive-include schemas *.py *.sql
--- a/__init__.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/__init__.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -41,6 +41,7 @@
 from StringIO import StringIO
 
 from logilab.common.logging_ext import set_log_methods
+from yams.constraints import BASE_CONVERTERS
 
 
 if os.environ.get('APYCOT_ROOT'):
@@ -120,6 +121,13 @@
         binary.seek(0)
         return binary
 
+def str_or_binary(value):
+    if isinstance(value, Binary):
+        return value
+    return str(value)
+BASE_CONVERTERS['Password'] = str_or_binary
+
+
 
 # use this dictionary to rename entity types while keeping bw compat
 ETYPE_NAME_MAP = {}
@@ -191,3 +199,26 @@
         CW_EVENT_MANAGER.bind(event, func, *args, **kwargs)
         return func
     return _decorator
+
+
+from yams.schema import role_name as rname
+
+def validation_error(entity, errors, substitutions=None, i18nvalues=None):
+    """easy way to retrieve a :class:`cubicweb.ValidationError` for an entity or eid.
+
+    You may also have 2-tuple as error keys, :func:`yams.role_name` will be
+    called automatically for them.
+
+    Messages in errors **should not be translated yet**, though marked for
+    internationalization. You may give an additional substition dictionary that
+    will be used for interpolation after the translation.
+    """
+    if substitutions is None:
+        # set empty dict else translation won't be done for backward
+        # compatibility reason (see ValidationError.tr method)
+        substitutions = {}
+    for key in errors.keys():
+        if isinstance(key, tuple):
+            errors[rname(*key)] = errors.pop(key)
+    return ValidationError(getattr(entity, 'eid', entity), errors,
+                           substitutions, i18nvalues)
--- a/__pkginfo__.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/__pkginfo__.py	Tue Oct 23 15:00:53 2012 +0200
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 15, 0)
+numversion = (3, 15, 4)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
@@ -42,9 +42,8 @@
 __depends__ = {
     'logilab-common': '>= 0.58.0',
     'logilab-mtconverter': '>= 0.8.0',
-    'rql': '>= 0.28.0',
-    'yams': '>= 0.34.0',
-    'docutils': '>= 0.6',
+    'rql': '>= 0.31.2',
+    'yams': '>= 0.36.0',
     #gettext                    # for xgettext, msgcat, etc...
     # web dependancies
     'simplejson': '>= 2.0.9',
@@ -54,9 +53,11 @@
     # server dependencies
     'logilab-database': '>= 1.8.2',
     'pysqlite': '>= 2.5.5', # XXX install pysqlite2
+    'passlib': '',
     }
 
 __recommends__ = {
+    'docutils': '>= 0.6',
     'Pyro': '>= 3.9.1, < 4.0.0',
     'PIL': '',                  # for captcha
     'pycrypto': '',             # for crypto extensions
--- a/_exceptions.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/_exceptions.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -19,7 +19,7 @@
 
 __docformat__ = "restructuredtext en"
 
-from yams import ValidationError
+from yams import ValidationError as ValidationError
 
 # abstract exceptions #########################################################
 
@@ -30,9 +30,10 @@
         if self.msg:
             if self.args:
                 return self.msg % tuple(self.args)
-            return self.msg
-        return ' '.join(unicode(arg) for arg in self.args)
-
+            else:
+                return self.msg
+        else:
+            return u' '.join(unicode(arg) for arg in self.args)
 
 class ConfigurationError(CubicWebException):
     """a misconfiguration error"""
@@ -81,6 +82,7 @@
 class UniqueTogetherError(RepositoryError):
     """raised when a unique_together constraint caused an IntegrityError"""
 
+
 # security exceptions #########################################################
 
 class Unauthorized(SecurityError):
@@ -128,6 +130,35 @@
     a non final entity
     """
 
+class UndoTransactionException(QueryError):
+    """Raised when undoing a transaction could not be performed completely.
+
+    Note that :
+      1) the partial undo operation might be acceptable
+         depending upon the final application
+
+      2) the undo operation can also fail with a `ValidationError` in
+         cases where the undoing breaks integrity constraints checked
+         immediately.
+
+      3) It might be that neither of those exception is raised but a
+         subsequent `commit` might raise a `ValidationError` in cases
+         where the undoing breaks integrity constraints checked at
+         commit time.
+
+    :type txuuix: int
+    :param txuuid: Unique identifier of the partialy undone transaction
+
+    :type errors: list
+    :param errors: List of errors occured during undoing
+    """
+    msg = u"The following error(s) occured while undoing transaction #%d : %s"
+
+    def __init__(self, txuuid, errors):
+        super(UndoTransactionException, self).__init__(txuuid, errors)
+        self.txuuid = txuuid
+        self.errors = errors
+
 # tools exceptions ############################################################
 
 class ExecutionError(Exception):
--- a/bin/clone_deps.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/bin/clone_deps.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,24 +1,25 @@
 #!/usr/bin/python
-import os
 import sys
-from subprocess import call, Popen, PIPE
-try:
-    from mercurial.dispatch import dispatch as hg_call
-except ImportError:
+
+from subprocess import call as sbp_call, Popen, PIPE
+from urllib import urlopen
+import os
+from os import path as osp, pardir, chdir
+
+
+def find_mercurial():
+    print "trying to find mercurial from the command line ..."
     print '-' * 20
-    print "mercurial module is not reachable from this Python interpreter"
-    print "trying from command line ..."
-    tryhg = os.system('hg --version')
+    tryhg = sbp_call(['hg', '--version'])
     if tryhg:
-        print 'mercurial seems to unavailable, please install it'
+        print 'mercurial seems to be unavailable, please install it'
         raise
-    print 'found it, ok'
     print '-' * 20
     def hg_call(args):
-        call(['hg'] + args)
-from urllib import urlopen
-from os import path as osp, pardir
-from os.path import normpath, join, dirname
+        return sbp_call(['hg'] + args)
+
+    return hg_call
+
 
 BASE_URL = 'http://www.logilab.org/hg/'
 
@@ -27,7 +28,7 @@
             'logilab/devtools', 'logilab/mtconverter',
             'cubes/blog', 'cubes/calendar', 'cubes/card', 'cubes/comment',
             'cubes/datafeed', 'cubes/email', 'cubes/file', 'cubes/folder',
-            'cubes/forgotpwd', 'cubes/keyword', 'cubes/link',
+            'cubes/forgotpwd', 'cubes/keyword', 'cubes/link', 'cubes/localperms',
             'cubes/mailinglist', 'cubes/nosylist', 'cubes/person',
             'cubes/preview', 'cubes/registration', 'cubes/rememberme',
             'cubes/tag', 'cubes/vcsfile', 'cubes/zone']
@@ -65,9 +66,10 @@
     else:
         sys.stderr.write('usage %s [base_url]\n' %  sys.argv[0])
         sys.exit(1)
+    hg_call = find_mercurial()
     print len(to_clone), 'repositories will be cloned'
-    base_dir = normpath(join(dirname(__file__), pardir, pardir))
-    os.chdir(base_dir)
+    base_dir = osp.normpath(osp.join(osp.dirname(__file__), pardir, pardir))
+    chdir(base_dir)
     not_updated = []
     for repo in to_clone:
         url = base_url + repo
@@ -78,7 +80,7 @@
             directory, repo = repo.split('/')
             if not osp.isdir(directory):
                 os.mkdir(directory)
-                open(join(directory, '__init__.py'), 'w').close()
+                open(osp.join(directory, '__init__.py'), 'w').close()
             target_path = osp.join(directory, repo)
         if osp.exists(target_path):
             print target_path, 'seems already cloned. Skipping it.'
--- a/cwconfig.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/cwconfig.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -171,6 +171,7 @@
 
 import sys
 import os
+import stat
 import logging
 import logging.config
 from smtplib import SMTP
@@ -306,7 +307,10 @@
 _forced_mode = os.environ.get('CW_MODE')
 assert _forced_mode in (None, 'system', 'user')
 
-CWDEV = exists(join(CW_SOFTWARE_ROOT, '.hg'))
+# CWDEV tells whether directories such as i18n/, web/data/, etc. (ie containing
+# some other resources than python libraries) are located with the python code
+# or as a 'shared' cube
+CWDEV = exists(join(CW_SOFTWARE_ROOT, 'i18n'))
 
 try:
     _INSTALL_PREFIX = os.environ['CW_INSTALL_PREFIX']
@@ -386,14 +390,6 @@
           'help': 'allow users to login with their primary email if set',
           'group': 'main', 'level': 2,
           }),
-        ('use-request-subdomain',
-         {'type' : 'yn',
-          'default': None,
-          'help': ('if set, base-url subdomain is replaced by the request\'s '
-                   'host, to help managing sites with several subdomains in a '
-                   'single cubicweb instance'),
-          'group': 'main', 'level': 1,
-          }),
         ('mangle-emails',
          {'type' : 'yn',
           'default': False,
@@ -675,51 +671,6 @@
     cubicweb_appobject_path = set(['entities'])
     cube_appobject_path = set(['entities'])
 
-    @classmethod
-    def build_vregistry_path(cls, templpath, evobjpath=None, tvobjpath=None):
-        """given a list of directories, return a list of sub files and
-        directories that should be loaded by the instance objects registry.
-
-        :param evobjpath:
-          optional list of sub-directories (or files without the .py ext) of
-          the cubicweb library that should be tested and added to the output list
-          if they exists. If not give, default to `cubicweb_appobject_path` class
-          attribute.
-        :param tvobjpath:
-          optional list of sub-directories (or files without the .py ext) of
-          directories given in `templpath` that should be tested and added to
-          the output list if they exists. If not give, default to
-          `cube_appobject_path` class attribute.
-        """
-        vregpath = cls.build_vregistry_cubicweb_path(evobjpath)
-        vregpath += cls.build_vregistry_cube_path(templpath, tvobjpath)
-        return vregpath
-
-    @classmethod
-    def build_vregistry_cubicweb_path(cls, evobjpath=None):
-        vregpath = []
-        if evobjpath is None:
-            evobjpath = cls.cubicweb_appobject_path
-        for subdir in evobjpath:
-            path = join(CW_SOFTWARE_ROOT, subdir)
-            if exists(path):
-                vregpath.append(path)
-        return vregpath
-
-    @classmethod
-    def build_vregistry_cube_path(cls, templpath, tvobjpath=None):
-        vregpath = []
-        if tvobjpath is None:
-            tvobjpath = cls.cube_appobject_path
-        for directory in templpath:
-            for subdir in tvobjpath:
-                path = join(directory, subdir)
-                if exists(path):
-                    vregpath.append(path)
-                elif exists(path + '.py'):
-                    vregpath.append(path + '.py')
-        return vregpath
-
     def __init__(self, debugmode=False):
         if debugmode:
             # in python 2.7, DeprecationWarning are not shown anymore by default
@@ -767,12 +718,57 @@
         # configure simpleTal logger
         logging.getLogger('simpleTAL').setLevel(logging.ERROR)
 
-    def vregistry_path(self):
+    def appobjects_path(self):
         """return a list of files or directories where the registry will look
         for application objects. By default return nothing in NoApp config.
         """
         return []
 
+    def build_appobjects_path(self, templpath, evobjpath=None, tvobjpath=None):
+        """given a list of directories, return a list of sub files and
+        directories that should be loaded by the instance objects registry.
+
+        :param evobjpath:
+          optional list of sub-directories (or files without the .py ext) of
+          the cubicweb library that should be tested and added to the output list
+          if they exists. If not give, default to `cubicweb_appobject_path` class
+          attribute.
+        :param tvobjpath:
+          optional list of sub-directories (or files without the .py ext) of
+          directories given in `templpath` that should be tested and added to
+          the output list if they exists. If not give, default to
+          `cube_appobject_path` class attribute.
+        """
+        vregpath = self.build_appobjects_cubicweb_path(evobjpath)
+        vregpath += self.build_appobjects_cube_path(templpath, tvobjpath)
+        return vregpath
+
+    def build_appobjects_cubicweb_path(self, evobjpath=None):
+        vregpath = []
+        if evobjpath is None:
+            evobjpath = self.cubicweb_appobject_path
+        # NOTE: for the order, see http://www.cubicweb.org/ticket/2330799
+        #       it is clearly a workaround
+        for subdir in sorted(evobjpath, key=lambda x:x != 'entities'):
+            path = join(CW_SOFTWARE_ROOT, subdir)
+            if exists(path):
+                vregpath.append(path)
+        return vregpath
+
+    def build_appobjects_cube_path(self, templpath, tvobjpath=None):
+        vregpath = []
+        if tvobjpath is None:
+            tvobjpath = self.cube_appobject_path
+        for directory in templpath:
+            # NOTE: for the order, see http://www.cubicweb.org/ticket/2330799
+            for subdir in sorted(tvobjpath, key=lambda x:x != 'entities'):
+                path = join(directory, subdir)
+                if exists(path):
+                    vregpath.append(path)
+                elif exists(path + '.py'):
+                    vregpath.append(path + '.py')
+        return vregpath
+
     apphome = None
 
     def load_site_cubicweb(self, paths=None):
@@ -825,7 +821,7 @@
     _cubes = None
 
     def init_cubes(self, cubes):
-        assert self._cubes is None, self._cubes
+        assert self._cubes is None, repr(self._cubes)
         self._cubes = self.reorder_cubes(cubes)
         # load cubes'__init__.py file first
         for cube in cubes:
@@ -1078,7 +1074,12 @@
         If not, try to fix this, letting exception propagate when not possible.
         """
         if not exists(path):
-            os.makedirs(path)
+            self.info('creating %s directory', path)
+            try:
+                os.makedirs(path)
+            except OSError, ex:
+                self.warning('error while creating %s directory: %s', path, ex)
+                return
         if self['uid']:
             try:
                 uid = int(self['uid'])
@@ -1092,10 +1093,20 @@
                 return
         fstat = os.stat(path)
         if fstat.st_uid != uid:
-            os.chown(path, uid, os.getgid())
-        import stat
+            self.info('giving ownership of %s directory to %s', path, self['uid'])
+            try:
+                os.chown(path, uid, os.getgid())
+            except OSError, ex:
+                self.warning('error while giving ownership of %s directory to %s: %s',
+                             path, self['uid'], ex)
         if not (fstat.st_mode & stat.S_IWUSR):
-            os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+            self.info('forcing write permission on directory %s', path)
+            try:
+                os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+            except OSError, ex:
+                self.warning('error while forcing write permission on directory %s: %s',
+                             path, ex)
+                return
 
     @cached
     def instance_md5_version(self):
@@ -1157,17 +1168,20 @@
                 tr = translation('cubicweb', path, languages=[language])
                 self.translations[language] = (tr.ugettext, tr.upgettext)
             except (ImportError, AttributeError, IOError):
-                self.exception('localisation support error for language %s',
-                               language)
+                if self.mode != 'test':
+                    # in test contexts, data/i18n does not exist, hence
+                    # logging will only pollute the logs
+                    self.exception('localisation support error for language %s',
+                                   language)
 
-    def vregistry_path(self):
+    def appobjects_path(self):
         """return a list of files or directories where the registry will look
         for application objects
         """
         templpath = list(reversed(self.cubes_path()))
         if self.apphome: # may be unset in tests
             templpath.append(self.apphome)
-        return self.build_vregistry_path(templpath)
+        return self.build_appobjects_path(templpath)
 
     def set_sources_mode(self, sources):
         if not 'all' in sources:
--- a/cwctl.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/cwctl.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -38,6 +38,8 @@
 
 from os.path import exists, join, isfile, isdir, dirname, abspath
 
+from urlparse import urlparse
+
 from logilab.common.clcommands import CommandLine
 from logilab.common.shellutils import ASK
 
@@ -867,31 +869,45 @@
           'group': 'local'
           }),
 
-        ('pyro',
-         {'short': 'P', 'action' : 'store_true',
-          'help': 'connect to a running instance through Pyro.',
-          'group': 'remote',
-          }),
-        ('pyro-ns-host',
-         {'short': 'H', 'type' : 'string', 'metavar': '<host[:port]>',
-          'help': 'Pyro name server host. If not set, will be detected by '
-          'using a broadcast query.',
+        ('repo-uri',
+         {'short': 'H', 'type' : 'string', 'metavar': '<protocol>://<[host][:port]>',
+          'help': 'URI of the CubicWeb repository to connect to. URI can be \
+pyro://[host:port] the Pyro name server host; if the pyro nameserver is not set, \
+it will be detected by using a broadcast query, a ZMQ URL or \
+inmemory:// (default) use an in-memory repository.',
           'group': 'remote'
           }),
         )
 
     def run(self, args):
         appid = args.pop(0)
-        if self.config.pyro:
+        if self.config.repo_uri:
+            uri = urlparse(self.config.repo_uri)
+            if uri.scheme == 'pyro':
+                cnxtype = uri.scheme
+                hostport = uri.netloc
+            elif uri.scheme == 'inmemory':
+                cnxtype = ''
+                hostport = ''
+            else:
+                cnxtype = 'zmq'
+                hostport = self.config.repo_uri
+        else:
+            cnxtype = ''
+
+        if cnxtype:
             from cubicweb import AuthenticationError
-            from cubicweb.dbapi import connect
+            from cubicweb.dbapi import connect, ConnectionProperties
             from cubicweb.server.utils import manager_userpasswd
             from cubicweb.server.migractions import ServerMigrationHelper
+            cnxprops = ConnectionProperties(cnxtype=cnxtype)
+
             while True:
                 try:
                     login, pwd = manager_userpasswd(msg=None)
                     cnx = connect(appid, login=login, password=pwd,
-                                  host=self.config.pyro_ns_host, mulcnx=False)
+                                  host=hostport, mulcnx=False,
+                                  cnxprops=cnxprops)
                 except AuthenticationError, ex:
                     print ex
                 except (KeyboardInterrupt, EOFError):
@@ -901,7 +917,7 @@
                     break
             cnx.load_appobjects()
             repo = cnx._repo
-            mih = ServerMigrationHelper(None, repo=repo, cnx=cnx,
+            mih = ServerMigrationHelper(None, repo=repo, cnx=cnx, verbosity=0,
                                          # hack so it don't try to load fs schema
                                         schema=1)
         else:
@@ -927,7 +943,7 @@
             else:
                 mih.interactive_shell()
         finally:
-            if not self.config.pyro:
+            if not cnxtype: # shutdown in-memory repo
                 mih.shutdown()
             else:
                 cnx.close()
--- a/cwvreg.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/cwvreg.py	Tue Oct 23 15:00:53 2012 +0200
@@ -256,6 +256,12 @@
                       key=lambda x: x.cw_propval('order'))
 
 
+def related_appobject(obj, appobjectattr='__appobject__'):
+    """ adapts any object to a potential appobject bound to it
+    through the __appobject__ attribute
+    """
+    return getattr(obj, appobjectattr, obj)
+
 
 class ETypeRegistry(CWRegistry):
 
@@ -272,6 +278,7 @@
         self.clear_caches()
 
     def register(self, obj, **kwargs):
+        obj = related_appobject(obj)
         oid = kwargs.get('oid') or obj.__regid__
         if oid != 'Any' and not oid in self.schema:
             self.error('don\'t register %s, %s type not defined in the '
@@ -537,6 +544,20 @@
     def itervalues(self):
         return (value for key, value in self.items())
 
+    def load_module(self, module):
+        """ variation from the base implementation:
+        apply related_appobject to the automatically registered objects
+        """
+        self.info('loading %s from %s', module.__name__, module.__file__)
+        if hasattr(module, 'registration_callback'):
+            module.registration_callback(self)
+            return
+        for objname, obj in vars(module).iteritems():
+            if objname.startswith('_'):
+                continue
+            self._load_ancestors_then_object(module.__name__,
+                                             related_appobject(obj))
+
     def reset(self):
         CW_EVENT_MANAGER.emit('before-registry-reset', self)
         super(CWRegistryStore, self).reset()
@@ -552,11 +573,22 @@
                 self.register_property(key, **propdef)
         CW_EVENT_MANAGER.emit('after-registry-reset', self)
 
+    def register_all(self, objects, modname, butclasses=()):
+        butclasses = set(related_appobject(obj)
+                         for obj in butclasses)
+        objects = [related_appobject(obj) for obj in objects]
+        super(CWRegistryStore, self).register_all(objects, modname, butclasses)
+
+    def register_and_replace(self, obj, replaced):
+        obj = related_appobject(obj)
+        replaced = related_appobject(replaced)
+        super(CWRegistryStore, self).register_and_replace(obj, replaced)
+
     def set_schema(self, schema):
         """set instance'schema and load application objects"""
         self._set_schema(schema)
         # now we can load application's web objects
-        self.reload(self.config.vregistry_path(), force_reload=False)
+        self.reload(self.config.appobjects_path(), force_reload=False)
         # map lowered entity type names to their actual name
         self.case_insensitive_etypes = {}
         for eschema in self.schema.entities():
@@ -566,7 +598,7 @@
             clear_cache(eschema, 'meta_attributes')
 
     def reload_if_needed(self):
-        path = self.config.vregistry_path()
+        path = self.config.appobjects_path()
         if self.is_reload_needed(path):
             self.reload(path)
 
@@ -582,7 +614,7 @@
             cfg = self.config
             for cube in cfg.expand_cubes(cubes, with_recommends=True):
                 if not cube in cubes:
-                    cpath = cfg.build_vregistry_cube_path([cfg.cube_dir(cube)])
+                    cpath = cfg.build_appobjects_cube_path([cfg.cube_dir(cube)])
                     cleanup_sys_modules(cpath)
         self.register_objects(path)
         CW_EVENT_MANAGER.emit('after-registry-reload')
@@ -624,6 +656,7 @@
         If `clear` is true, all objects with the same identifier will be
         previously unregistered.
         """
+        obj = related_appobject(obj)
         super(CWRegistryStore, self).register(obj, *args, **kwargs)
         # XXX bw compat
         ifaces = use_interfaces(obj)
--- a/dataimport.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/dataimport.py	Tue Oct 23 15:00:53 2012 +0200
@@ -182,7 +182,10 @@
     assert isinstance(row, dict)
     assert isinstance(map, list)
     for src, dest, funcs in map:
-        res[dest] = row[src]
+        try:
+            res[dest] = row[src]
+        except KeyError:
+            continue
         try:
             for func in funcs:
                 res[dest] = func(res[dest])
@@ -446,9 +449,12 @@
         if session is None:
             sys.exit('please provide a session of run this script with cubicweb-ctl shell and pass cnx as session')
         if not hasattr(session, 'set_cnxset'):
-            # connection
-            cnx = session
-            session = session.request()
+            if hasattr(session, 'request'):
+                # connection object
+                cnx = session
+                session = session.request()
+            else: # object is already a request
+                cnx = session.cnx
             session.set_cnxset = lambda : None
             commit = commit or cnx.commit
         else:
--- a/dbapi.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/dbapi.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -80,9 +80,8 @@
 
 
 class ConnectionProperties(object):
-    def __init__(self, cnxtype=None, lang=None, close=True, log=False):
+    def __init__(self, cnxtype=None, close=True, log=False):
         self.cnxtype = cnxtype or 'pyro'
-        self.lang = lang
         self.log_queries = log
         self.close_on_del = close
 
@@ -93,14 +92,18 @@
     Only 'in-memory' and 'pyro' are supported for now. Either vreg or config
     argument should be given
     """
-    assert method in ('pyro', 'inmemory')
+    assert method in ('pyro', 'inmemory', 'zmq')
     assert vreg or config
     if vreg and not config:
         config = vreg.config
     if method == 'inmemory':
         # get local access to the repository
         from cubicweb.server.repository import Repository
-        return Repository(config, vreg=vreg)
+        from cubicweb.server.utils import TasksManager
+        return Repository(config, TasksManager(), vreg=vreg)
+    elif method == 'zmq':
+        from cubicweb.zmqclient import ZMQRepositoryClient
+        return ZMQRepositoryClient(database)
     else: # method == 'pyro'
         # resolve the Pyro object
         from logilab.common.pyro_ext import ns_get_proxy, get_proxy
@@ -145,8 +148,8 @@
       the user login to use to authenticate.
 
     :host:
-      the pyro nameserver host. Will be detected using broadcast query if
-      unspecified.
+      - pyro: nameserver host. Will be detected using broadcast query if unspecified
+      - zmq: repository host socket address
 
     :group:
       the instance's pyro nameserver group. You don't have to specify it unless
@@ -183,6 +186,8 @@
             config.global_set_option('pyro-ns-host', host)
         if group:
             config.global_set_option('pyro-ns-group', group)
+    elif method == 'zmq':
+        config = cwconfig.CubicWebNoAppConfiguration()
     else:
         assert database
         config = cwconfig.instance_configuration(database)
@@ -277,16 +282,18 @@
         return '<DBAPISession %r>' % self.sessionid
 
 class DBAPIRequest(RequestSessionBase):
+    #: Request language identifier eg: 'en'
+    lang = None
 
     def __init__(self, vreg, session=None):
         super(DBAPIRequest, self).__init__(vreg)
+        #: 'language' => translation_function() mapping
         try:
             # no vreg or config which doesn't handle translations
             self.translations = vreg.config.translations
         except AttributeError:
             self.translations = {}
-        self.set_default_language(vreg)
-        # cache entities built during the request
+        #: cache entities built during the request
         self._eid_cache = {}
         if session is not None:
             self.set_session(session)
@@ -295,6 +302,7 @@
             # established
             self.session = None
             self.cnx = self.user = _NeedAuthAccessMock()
+        self.set_default_language(vreg)
 
     def from_controller(self):
         return 'view'
@@ -308,7 +316,7 @@
             self.cnx = session.cnx
             self.execute = session.cnx.cursor(self).execute
             if user is None:
-                user = self.cnx.user(self, {'lang': self.lang})
+                user = self.cnx.user(self)
         if user is not None:
             self.user = user
             self.set_entity_cache(user)
@@ -321,19 +329,20 @@
 
     def set_default_language(self, vreg):
         try:
-            self.lang = vreg.property_value('ui.language')
+            lang = vreg.property_value('ui.language')
         except Exception: # property may not be registered
-            self.lang = 'en'
-        # use req.__ to translate a message without registering it to the catalog
+            lang = 'en'
         try:
-            gettext, pgettext = self.translations[self.lang]
-            self._ = self.__ = gettext
-            self.pgettext = pgettext
+            self.set_language(lang)
         except KeyError:
             # this occurs usually during test execution
             self._ = self.__ = unicode
             self.pgettext = lambda x, y: unicode(y)
-        self.debug('request default language: %s', self.lang)
+
+    # server-side service call #################################################
+
+    def call_service(self, regid, async=False, **kwargs):
+        return self.cnx.call_service(regid, async, **kwargs)
 
     # entities cache management ###############################################
 
@@ -556,6 +565,12 @@
             except Exception:
                 pass
 
+    # server-side service call #################################################
+
+    @check_not_closed
+    def call_service(self, regid, async=False, **kwargs):
+        return self._repo.call_service(self.sessionid, regid, async, **kwargs)
+
     # connection initialization methods ########################################
 
     def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True):
@@ -577,10 +592,15 @@
             esubpath = list(subpath)
             esubpath.remove('views')
             esubpath.append(join('web', 'views'))
+        # first load available configs, necessary for proper persistent
+        # properties initialization
+        config.load_available_configs()
+        # then init cubes
         config.init_cubes(cubes)
-        vpath = config.build_vregistry_path(reversed(config.cubes_path()),
-                                            evobjpath=esubpath,
-                                            tvobjpath=subpath)
+        # then load appobjects into the registry
+        vpath = config.build_appobjects_path(reversed(config.cubes_path()),
+                                             evobjpath=esubpath,
+                                             tvobjpath=subpath)
         self.vreg.register_objects(vpath)
 
     def use_web_compatible_requests(self, baseurl, sitetitle=None):
@@ -650,11 +670,6 @@
     # session data methods #####################################################
 
     @check_not_closed
-    def set_session_props(self, **props):
-        """raise `BadConnectionId` if the connection is no more valid"""
-        self._repo.set_session_props(self.sessionid, props)
-
-    @check_not_closed
     def get_shared_data(self, key, default=None, pop=False, txdata=False):
         """return value associated to key in the session's data dictionary or
         session's transaction's data if `txdata` is true.
--- a/debian/changelog	Wed Feb 22 11:57:42 2012 +0100
+++ b/debian/changelog	Tue Oct 23 15:00:53 2012 +0200
@@ -1,3 +1,69 @@
+cubicweb (3.15.4-1) unstable; urgency=low
+
+  * New upstream release
+
+ -- Julien Cristau <jcristau@debian.org>  Fri, 31 Aug 2012 16:43:11 +0200
+
+cubicweb (3.15.3-1) unstable; urgency=low
+
+  * New upstream release
+
+ -- Pierre-Yves David <pierre-yves.david@logilab.fr>  Tue, 21 Aug 2012 14:19:31 +0200
+
+cubicweb (3.15.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 20 Jul 2012 15:17:17 +0200
+
+cubicweb (3.15.1-1) quantal; urgency=low
+
+  * new upstream release
+
+ -- David Douard <david.douard@logilab.fr>  Mon, 11 Jun 2012 09:45:24 +0200
+
+cubicweb (3.15.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 12 Apr 2012 13:52:05 +0200
+
+cubicweb (3.14.9-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Pierre-Yves David <pierre-yves.david@logilab.fr>  Tue, 31 Jul 2012 16:16:28 +0200
+
+cubicweb (3.14.8-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 23 May 2012 11:42:54 +0200
+
+cubicweb (3.14.7-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- David Douard <david.douard@logilab.fr>  Wed, 11 Apr 2012 09:28:46 +0200
+
+cubicweb (3.14.6-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Tue, 13 Mar 2012 14:21:04 +0100
+
+cubicweb (3.14.5-1) unstable; urgency=low
+
+  * New upstream release
+
+ -- David Douard <david.douard@logilab.fr>  Thu, 01 Mar 2012 15:29:29 +0100
+
+cubicweb (3.14.4-2) unstable; urgency=low
+
+  * add missing build-deps to generate the documentation
+
+ -- David Douard <david.douard@logilab.fr>  Wed, 29 Feb 2012 17:00:52 +0100
+
 cubicweb (3.14.4-1) unstable; urgency=low
 
   * New upstream release
--- a/debian/control	Wed Feb 22 11:57:42 2012 +0100
+++ b/debian/control	Tue Oct 23 15:00:53 2012 +0200
@@ -7,18 +7,25 @@
            Adrien Di Mascio <Adrien.DiMascio@logilab.fr>,
            Aurélien Campéas <aurelien.campeas@logilab.fr>,
            Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 7), python (>= 2.5), python-central (>= 0.5), python-sphinx
-# for the documentation:
-# python-sphinx, python-logilab-common, python-unittest2, logilab-doctools, logilab-xml
+Build-Depends:
+ debhelper (>= 7),
+ python (>= 2.5),
+ python-central (>= 0.5),
+ python-sphinx,
+ python-logilab-common,
+ python-unittest2,
+ python-logilab-mtconverter,
+ python-rql,
+ python-yams,
+ python-lxml,
 Standards-Version: 3.9.1
 Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.5, << 3.0
+XS-Python-Version: >= 2.5
 
 Package: cubicweb
 Architecture: all
 XB-Python-Version: ${python:Versions}
 Depends: ${misc:Depends}, ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-twisted (= ${source:Version})
-XB-Recommends: (postgresql, postgresql-plpython) | mysql | sqlite3
 Recommends: postgresql | mysql | sqlite3
 Description: the complete CubicWeb framework
  CubicWeb is a semantic web application framework.
@@ -35,7 +42,7 @@
 Conflicts: cubicweb-multisources
 Replaces: cubicweb-multisources
 Provides: cubicweb-multisources
-Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.2), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.2), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2, python-passlib
 Recommends: pyro (<< 4.0.0), cubicweb-documentation (= ${source:Version})
 Suggests: python-zmq
 Description: server part of the CubicWeb framework
@@ -85,8 +92,8 @@
 Package: cubicweb-web
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3)
-Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-imaging, python-rdflib
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 2.0.9)
+Recommends: python-docutils (>= 0.6), python-vobject, fckeditor, python-fyzz, python-imaging, python-rdflib
 Description: web interface library for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -100,7 +107,7 @@
 Package: cubicweb-common
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.58.0), python-yams (>= 0.34.0), python-rql (>= 0.28.0), python-lxml
+Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.58.0), python-yams (>= 0.36.0), python-rql (>= 0.31.2), python-lxml
 Recommends: python-simpletal (>= 4.0), python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/debian/rules	Wed Feb 22 11:57:42 2012 +0100
+++ b/debian/rules	Tue Oct 23 15:00:53 2012 +0200
@@ -11,10 +11,14 @@
 build-stamp:
 	dh_testdir
 	NO_SETUPTOOLS=1 python setup.py build
+	# cubicweb.foo needs to be importable by sphinx, so create a cubicweb symlink to the source dir
+	mkdir -p debian/pythonpath
+	ln -sf $(CURDIR) debian/pythonpath/cubicweb
 	# documentation build is now made optional since it can break for old
 	# distributions and we don't want to block a new release of Cubicweb
 	# because of documentation issues.
-	-PYTHONPATH=$(CURDIR)/.. $(MAKE) -C doc/book/en all
+	-PYTHONPATH=$${PYTHONPATH:+$${PYTHONPATH}:}$(CURDIR)/debian/pythonpath $(MAKE) -C doc/book/en all
+	rm -rf debian/pythonpath
 	touch build-stamp
 
 clean:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/watch	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,2 @@
+version=3
+http://download.logilab.org/pub/cubicweb cubicweb-(.*)\.tar\.gz
--- a/devtools/__init__.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/__init__.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -168,7 +168,7 @@
     def load_configuration(self):
         super(TestServerConfiguration, self).load_configuration()
         # no undo support in tests
-        self.global_set_option('undo-support', '')
+        self.global_set_option('undo-enabled', 'n')
 
     def main_config_file(self):
         """return instance's control configuration file"""
@@ -355,7 +355,7 @@
     def _restore_database(self, backup_coordinates, config):
         """Actual restore of the current database.
 
-        Use the value tostored in db_cache as input """
+        Use the value stored in db_cache as input """
         raise NotImplementedError()
 
     def get_repo(self, startup=False):
@@ -466,7 +466,6 @@
         ``pre_setup_func`` to setup the database.
 
         This function backup any database it build"""
-
         if self.has_cache(test_db_id):
             return #test_db_id, 'already in cache'
         if test_db_id is DEFAULT_EMPTY_DB_ID:
@@ -723,7 +722,7 @@
         dbfile = self.absolute_dbfile()
         self._cleanup_database(dbfile)
         shutil.copy(backup_coordinates, dbfile)
-        repo = self.get_repo()
+        self.get_repo()
 
     def init_test_database(self):
         """initialize a fresh sqlite databse used for testing purpose"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/data/xvfb-run.sh	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,190 @@
+#!/bin/sh
+
+# This script starts an instance of Xvfb, the "fake" X server, runs a command
+# with that server available, and kills the X server when done.  The return
+# value of the command becomes the return value of this script.
+#
+# If anyone is using this to build a Debian package, make sure the package
+# Build-Depends on xvfb and xauth.
+
+set -e
+
+PROGNAME=xvfb-run
+SERVERNUM=99
+AUTHFILE=
+ERRORFILE=/dev/null
+XVFBARGS="-screen 0 640x480x8"
+LISTENTCP="-nolisten tcp"
+XAUTHPROTO=.
+
+# Query the terminal to establish a default number of columns to use for
+# displaying messages to the user.  This is used only as a fallback in the event
+# the COLUMNS variable is not set.  ($COLUMNS can react to SIGWINCH while the
+# script is running, and this cannot, only being calculated once.)
+DEFCOLUMNS=$(stty size 2>/dev/null | awk '{print $2}') || true
+if ! expr "$DEFCOLUMNS" : "[[:digit:]]\+$" >/dev/null 2>&1; then
+    DEFCOLUMNS=80
+fi
+
+# Display a message, wrapping lines at the terminal width.
+message () {
+    echo "$PROGNAME: $*" | fmt -t -w ${COLUMNS:-$DEFCOLUMNS}
+}
+
+# Display an error message.
+error () {
+    message "error: $*" >&2
+}
+
+# Display a usage message.
+usage () {
+    if [ -n "$*" ]; then
+        message "usage error: $*"
+    fi
+    cat <<EOF
+Usage: $PROGNAME [OPTION ...] COMMAND
+Run COMMAND (usually an X client) in a virtual X server environment.
+Options:
+-a        --auto-servernum          try to get a free server number, starting at
+                                    --server-num
+-e FILE   --error-file=FILE         file used to store xauth errors and Xvfb
+                                    output (default: $ERRORFILE)
+-f FILE   --auth-file=FILE          file used to store auth cookie
+                                    (default: ./.Xauthority)
+-h        --help                    display this usage message and exit
+-n NUM    --server-num=NUM          server number to use (default: $SERVERNUM)
+-l        --listen-tcp              enable TCP port listening in the X server
+-p PROTO  --xauth-protocol=PROTO    X authority protocol name to use
+                                    (default: xauth command's default)
+-s ARGS   --server-args=ARGS        arguments (other than server number and
+                                    "-nolisten tcp") to pass to the Xvfb server
+                                    (default: "$XVFBARGS")
+EOF
+}
+
+# Find a free server number by looking at .X*-lock files in /tmp.
+find_free_servernum() {
+    # Sadly, the "local" keyword is not POSIX.  Leave the next line commented in
+    # the hope Debian Policy eventually changes to allow it in /bin/sh scripts
+    # anyway.
+    #local i
+
+    i=$SERVERNUM
+    while [ -f /tmp/.X$i-lock ]; do
+        i=$(($i + 1))
+    done
+    echo $i
+}
+
+# Clean up files
+clean_up() {
+    if [ -e "$AUTHFILE" ]; then
+        XAUTHORITY=$AUTHFILE xauth remove ":$SERVERNUM" >>"$ERRORFILE" 2>&1
+    fi
+    if [ -n "$XVFB_RUN_TMPDIR" ]; then
+        if ! rm -r "$XVFB_RUN_TMPDIR"; then
+            error "problem while cleaning up temporary directory"
+            exit 5
+        fi
+    fi
+    if [ -n "$XVFBPID" ]; then
+        kill "$XVFBPID"
+    fi
+}
+
+# Parse the command line.
+ARGS=$(getopt --options +ae:f:hn:lp:s:w: \
+       --long auto-servernum,error-file:,auth-file:,help,server-num:,listen-tcp,xauth-protocol:,server-args:,wait: \
+       --name "$PROGNAME" -- "$@")
+GETOPT_STATUS=$?
+
+if [ $GETOPT_STATUS -ne 0 ]; then
+    error "internal error; getopt exited with status $GETOPT_STATUS"
+    exit 6
+fi
+
+eval set -- "$ARGS"
+
+while :; do
+    case "$1" in
+        -a|--auto-servernum) SERVERNUM=$(find_free_servernum); AUTONUM="yes" ;;
+        -e|--error-file) ERRORFILE="$2"; shift ;;
+        -f|--auth-file) AUTHFILE="$2"; shift ;;
+        -h|--help) SHOWHELP="yes" ;;
+        -n|--server-num) SERVERNUM="$2"; shift ;;
+        -l|--listen-tcp) LISTENTCP="" ;;
+        -p|--xauth-protocol) XAUTHPROTO="$2"; shift ;;
+        -s|--server-args) XVFBARGS="$2"; shift ;;
+        -w|--wait) shift ;;
+        --) shift; break ;;
+        *) error "internal error; getopt permitted \"$1\" unexpectedly"
+           exit 6
+           ;;
+    esac
+    shift
+done
+
+if [ "$SHOWHELP" ]; then
+    usage
+    exit 0
+fi
+
+if [ -z "$*" ]; then
+    usage "need a command to run" >&2
+    exit 2
+fi
+
+if ! which xauth >/dev/null; then
+    error "xauth command not found"
+    exit 3
+fi
+
+# tidy up after ourselves
+trap clean_up EXIT
+
+# If the user did not specify an X authorization file to use, set up a temporary
+# directory to house one.
+if [ -z "$AUTHFILE" ]; then
+    XVFB_RUN_TMPDIR="$(mktemp -d -t $PROGNAME.XXXXXX)"
+    # Create empty file to avoid xauth warning
+    AUTHFILE=$(tempfile -n "$XVFB_RUN_TMPDIR/Xauthority")
+fi
+
+# Start Xvfb.
+MCOOKIE=$(mcookie)
+tries=10
+while [ $tries -gt 0 ]; do
+    tries=$(( $tries - 1 ))
+    XAUTHORITY=$AUTHFILE xauth source - << EOF >>"$ERRORFILE" 2>&1
+add :$SERVERNUM $XAUTHPROTO $MCOOKIE
+EOF
+    # handle SIGUSR1 so Xvfb knows to send a signal when it's ready to accept
+    # connections
+    trap : USR1
+    (trap '' USR1; XAUTHORITY=$AUTHFILE exec Xvfb ":$SERVERNUM" $XVFBARGS $LISTENTCP >>"$ERRORFILE" 2>&1) &
+    XVFBPID=$!
+
+    wait || :
+    if kill -0 $XVFBPID 2>/dev/null; then
+        break
+    elif [ -n "$AUTONUM" ]; then
+        # The display is in use so try another one (if '-a' was specified).
+        SERVERNUM=$((SERVERNUM + 1))
+        SERVERNUM=$(find_free_servernum)
+        continue
+    fi
+    error "Xvfb failed to start" >&2
+    XVFBPID=
+    exit 1
+done
+
+# Start the command and save its exit status.
+set +e
+DISPLAY=:$SERVERNUM XAUTHORITY=$AUTHFILE "$@" 2>&1
+RETVAL=$?
+set -e
+
+# Return the executed command's exit status.
+exit $RETVAL
+
+# vim:set ai et sts=4 sw=4 tw=80:
--- a/devtools/devctl.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/devctl.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -91,7 +91,7 @@
         if mod.__file__ is None:
             # odd/rare but real
             continue
-        for path in config.vregistry_path():
+        for path in config.appobjects_path():
             if mod.__file__.startswith(path):
                 del sys.modules[name]
                 break
@@ -303,7 +303,7 @@
         from logilab.common.shellutils import globfind, find, rm
         from logilab.common.modutils import get_module_files
         from cubicweb.i18n import extract_from_tal, execute
-        tempdir = tempfile.mkdtemp()
+        tempdir = tempfile.mkdtemp(prefix='cw-')
         cwi18ndir = WebConfiguration.i18n_lib_dir()
         print '-> extract schema messages.'
         schemapot = osp.join(tempdir, 'schema.pot')
@@ -726,7 +726,7 @@
     min_args = max_args = 1
     options = [
         ('output-file',
-         {'type':'file', 'default': None,
+         {'type':'string', 'default': None,
           'metavar': '<file>', 'short':'o', 'help':'output image file',
           'input':False,
           }),
--- a/devtools/fake.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/fake.py	Tue Oct 23 15:00:53 2012 +0200
@@ -33,6 +33,7 @@
 class FakeConfig(dict, BaseApptestConfiguration):
     translations = {}
     uiprops = {}
+    https_uiprops = {}
     apphome = None
     debugmode = False
     def __init__(self, appid='data', apphome=None, cubes=()):
@@ -44,6 +45,7 @@
         self['base-url'] = BASE_URL
         self['rql-cache-size'] = 3000
         self.datadir_url = BASE_URL + 'data/'
+        self.https_datadir_url = (BASE_URL + 'data/').replace('http://', 'https://')
 
     def cubes(self, expand=False):
         return self._cubes
@@ -59,10 +61,10 @@
         if not (args or 'vreg' in kwargs):
             kwargs['vreg'] = CWRegistryStore(FakeConfig(), initlog=False)
         kwargs['https'] = False
+        self._http_method = kwargs.pop('method', 'GET')
         self._url = kwargs.pop('url', None) or 'view?rql=Blop&vid=blop'
         super(FakeRequest, self).__init__(*args, **kwargs)
         self._session_data = {}
-        self._headers_in = Headers()
 
     def set_cookie(self, name, value, maxage=300, expires=None, secure=False):
         super(FakeRequest, self).set_cookie(name, value, maxage, expires, secure)
@@ -74,8 +76,8 @@
         """returns an ordered list of preferred languages"""
         return ('en',)
 
-    def header_if_modified_since(self):
-        return None
+    def http_method(self):
+        return self._http_method
 
     def relative_path(self, includeparams=True):
         """return the normalized path of the request (ie at least relative
@@ -90,35 +92,23 @@
             return url
         return url.split('?', 1)[0]
 
-    def get_header(self, header, default=None, raw=True):
-        """return the value associated with the given input header, raise
-        KeyError if the header is not set
-        """
-        if raw:
-            return self._headers_in.getRawHeaders(header, [default])[0]
-        return self._headers_in.getHeader(header, default)
-
-    ## extend request API to control headers in / out values
     def set_request_header(self, header, value, raw=False):
-        """set an input HTTP header"""
+        """set an incoming HTTP header (For test purpose only)"""
         if isinstance(value, basestring):
             value = [value]
-        if raw:
+        if raw: #
+            # adding encoded header is important, else page content
+            # will be reconverted back to unicode and apart unefficiency, this
+            # may cause decoding problem (e.g. when downloading a file)
             self._headers_in.setRawHeaders(header, value)
-        else:
-            self._headers_in.setHeader(header, value)
+        else: #
+            self._headers_in.setHeader(header, value) #
 
     def get_response_header(self, header, default=None, raw=False):
-        """return the value associated with the given input header,
-        raise KeyError if the header is not set
-        """
-        if raw:
-            return self.headers_out.getRawHeaders(header, default)[0]
-        else:
-            return self.headers_out.getHeader(header, default)
-
-    def validate_cache(self):
-        pass
+        """return output header (For test purpose only"""
+        if raw: #
+            return self.headers_out.getRawHeaders(header, [default])[0]
+        return self.headers_out.getHeader(header, default)
 
     def build_url_params(self, **kwargs):
         # overriden to get predictable resultts
@@ -165,6 +155,12 @@
     def set_entity_cache(self, entity):
         pass
 
+    def security_enabled(self, read=False, write=False):
+        class FakeCM(object):
+            def __enter__(self): pass
+            def __exit__(self, exctype, exc, traceback): pass
+        return FakeCM()
+
     # for use with enabled_security context manager
     read_security = write_security = True
     def init_security(self, *args):
--- a/devtools/qunit.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/qunit.py	Tue Oct 23 15:00:53 2012 +0200
@@ -64,7 +64,7 @@
 
     def __init__(self, url=None):
         self._process = None
-        self._tmp_dir = mkdtemp()
+        self._tmp_dir = mkdtemp(prefix='cwtest-ffxprof-')
         self._profile_data = {'uid': uuid4()}
         self._profile_name = self.profile_name_mask % self._profile_data
         fnull = open(os.devnull, 'w')
@@ -72,7 +72,7 @@
         stderr = TemporaryFile()
         self.firefox_cmd = ['firefox', '-no-remote']
         if os.name == 'posix':
-            self.firefox_cmd = ['xvfb-run', '-a'] + self.firefox_cmd
+            self.firefox_cmd = [osp.join(osp.dirname(__file__), 'data', 'xvfb-run.sh'), '-a'] + self.firefox_cmd
         try:
             home = osp.expanduser('~')
             user = getlogin()
--- a/devtools/repotest.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/repotest.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/devtools/test/unittest_testlib.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/test/unittest_testlib.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -16,6 +16,7 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unittests for cw.devtools.testlib module"""
+from __future__ import with_statement
 
 from cStringIO import StringIO
 
@@ -155,5 +156,20 @@
         self.assertEqual(self.page_info.has_link_regexp('L[ai]gilab'), False)
 
 
+class CWUtilitiesTC(CubicWebTC):
+    def test_temporary_permissions_eschema(self):
+        eschema = self.schema['CWUser']
+        with self.temporary_permissions(CWUser={'read': ()}):
+            self.assertEqual(eschema.permissions['read'], ())
+            self.assertTrue(eschema.permissions['add'])
+        self.assertTrue(eschema.permissions['read'], ())
+
+    def test_temporary_permissions_rdef(self):
+        rdef = self.schema['CWUser'].rdef('in_group')
+        with self.temporary_permissions((rdef, {'read': ()})):
+            self.assertEqual(rdef.permissions['read'], ())
+            self.assertTrue(rdef.permissions['add'])
+        self.assertTrue(rdef.permissions['read'], ())
+
 if __name__ == '__main__':
     unittest_main()
--- a/devtools/testlib.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/testlib.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -31,6 +31,7 @@
 from contextlib import contextmanager
 from warnings import warn
 from types import NoneType
+from itertools import chain
 
 import yams.schema
 
@@ -46,7 +47,7 @@
 from cubicweb import cwconfig, dbapi, devtools, web, server
 from cubicweb.sobjects import notification
 from cubicweb.web import Redirect, application
-from cubicweb.server.session import Session, security_enabled
+from cubicweb.server.session import Session
 from cubicweb.server.hook import SendMailOp
 from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS
 from cubicweb.devtools import BASE_URL, fake, htmlparser, DEFAULT_EMPTY_DB_ID
@@ -444,7 +445,7 @@
         finally:
             self.session.set_cnxset() # ensure cnxset still set after commit
 
-    # # server side db api #######################################################
+    # server side db api #######################################################
 
     def sexecute(self, rql, args=None, eid_key=None):
         if eid_key is not None:
@@ -466,6 +467,51 @@
             for obj in appobjects:
                 self.vreg.unregister(obj)
 
+    @contextmanager
+    def temporary_permissions(self, *perm_overrides, **perm_kwoverrides):
+        """Set custom schema permissions within context.
+
+        There are two ways to call this method, which may be used together :
+
+        * using positional argument(s):
+
+          .. sourcecode:: python
+
+                rdef = self.schema['CWUser'].rdef('login')
+                with self.temporary_permissions((rdef, {'read': ()})):
+                    ...
+
+
+        * using named argument(s):
+
+          .. sourcecode:: python
+
+                rdef = self.schema['CWUser'].rdef('login')
+                with self.temporary_permissions(CWUser={'read': ()}):
+                    ...
+
+        Usually the former will be prefered to override permissions on a
+        relation definition, while the latter is well suited for entity types.
+
+        The allowed keys in the permission dictionary depends on the schema type
+        (entity type / relation definition). Resulting permissions will be
+        similar to `orig_permissions.update(partial_perms)`.
+        """
+        torestore = []
+        for erschema, etypeperms in chain(perm_overrides, perm_kwoverrides.iteritems()):
+            if isinstance(erschema, basestring):
+                erschema = self.schema[erschema]
+            for action, actionperms in etypeperms.iteritems():
+                origperms = erschema.permissions[action]
+                erschema.set_action_permissions(action, actionperms)
+                torestore.append([erschema, action, origperms])
+        yield
+        for erschema, action, permissions in torestore:
+            if action is None:
+                erschema.permissions = permissions
+            else:
+                erschema.set_action_permissions(action, permissions)
+
     def assertModificationDateGreater(self, entity, olddate):
         entity.cw_attr_cache.pop('modification_date', None)
         self.assertTrue(entity.modification_date > olddate)
@@ -592,9 +638,9 @@
         return publisher
 
     requestcls = fake.FakeRequest
-    def request(self, rollbackfirst=False, url=None, **kwargs):
+    def request(self, rollbackfirst=False, url=None, headers={}, **kwargs):
         """return a web ui request"""
-        req = self.requestcls(self.vreg, url=url, form=kwargs)
+        req = self.requestcls(self.vreg, url=url, headers=headers, form=kwargs)
         if rollbackfirst:
             self.websession.cnx.rollback()
         req.set_session(self.websession)
@@ -608,8 +654,13 @@
         ctrl = self.vreg['controllers'].select('ajax', req)
         return ctrl.publish(), req
 
-    def app_publish(self, req, path='view'):
-        return self.app.publish(path, req)
+    def app_handle_request(self, req, path='view'):
+        return self.app.core_handle(req, path)
+
+    @deprecated("[3.15] app_handle_request is the new and better way"
+                " (beware of small semantic changes)")
+    def app_publish(self, *args, **kwargs):
+        return self.app_handle_request(*args, **kwargs)
 
     def ctrl_publish(self, req, ctrl='edit'):
         """call the publish method of the edit controller"""
@@ -646,6 +697,20 @@
         ctrlid, rset = self.app.url_resolver.process(req, req.relative_path(False))
         return self.ctrl_publish(req, ctrlid)
 
+    @staticmethod
+    def _parse_location(req, location):
+        try:
+            path, params = location.split('?', 1)
+        except ValueError:
+            path = location
+            params = {}
+        else:
+            cleanup = lambda p: (p[0], unquote(p[1]))
+            params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
+        if path.startswith(req.base_url()): # may be relative
+            path = path[len(req.base_url()):]
+        return path, params
+
     def expect_redirect(self, callback, req):
         """call the given callback with req as argument, expecting to get a
         Redirect exception
@@ -653,25 +718,24 @@
         try:
             callback(req)
         except Redirect, ex:
-            try:
-                path, params = ex.location.split('?', 1)
-            except ValueError:
-                path = ex.location
-                params = {}
-            else:
-                cleanup = lambda p: (p[0], unquote(p[1]))
-                params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
-            if path.startswith(req.base_url()): # may be relative
-                path = path[len(req.base_url()):]
-            return path, params
+            return self._parse_location(req, ex.location)
         else:
             self.fail('expected a Redirect exception')
 
-    def expect_redirect_publish(self, req, path='edit'):
+    def expect_redirect_handle_request(self, req, path='edit'):
         """call the publish method of the application publisher, expecting to
         get a Redirect exception
         """
-        return self.expect_redirect(lambda x: self.app_publish(x, path), req)
+        result = self.app_handle_request(req, path)
+        self.assertTrue(300 <= req.status_out <400, req.status_out)
+        location = req.get_response_header('location')
+        return self._parse_location(req, location)
+
+    @deprecated("[3.15] expect_redirect_handle_request is the new and better way"
+                " (beware of small semantic changes)")
+    def expect_redirect_publish(self, *args, **kwargs):
+        return self.expect_redirect_handle_request(*args, **kwargs)
+
 
     def set_auth_mode(self, authmode, anonuser=None):
         self.set_option('auth-mode', authmode)
@@ -697,13 +761,11 @@
 
     def assertAuthSuccess(self, req, origsession, nbsessions=1):
         sh = self.app.session_handler
-        path, params = self.expect_redirect(lambda x: self.app.connect(x), req)
+        self.app.connect(req)
         session = req.session
         self.assertEqual(len(self.open_sessions), nbsessions, self.open_sessions)
         self.assertEqual(session.login, origsession.login)
         self.assertEqual(session.anonymous_session, False)
-        self.assertEqual(path, 'view')
-        self.assertMessageEqual(req, params, 'welcome %s !' % req.user.login)
 
     def assertAuthFailure(self, req, nbsessions=0):
         self.app.connect(req)
@@ -755,9 +817,8 @@
         """
         req = req or rset and rset.req or self.request()
         req.form['vid'] = vid
-        kwargs['rset'] = rset
         viewsreg = self.vreg['views']
-        view = viewsreg.select(vid, req, **kwargs)
+        view = viewsreg.select(vid, req, rset=rset, **kwargs)
         # set explicit test description
         if rset is not None:
             self.set_description("testing vid=%s defined in %s with (%s)" % (
@@ -769,10 +830,8 @@
             viewfunc = view.render
         else:
             kwargs['view'] = view
-            templateview = viewsreg.select(template, req, **kwargs)
             viewfunc = lambda **k: viewsreg.main_template(req, template,
-                                                          **kwargs)
-        kwargs.pop('rset')
+                                                          rset=rset, **kwargs)
         return self._test_view(viewfunc, view, template, kwargs)
 
 
@@ -991,7 +1050,7 @@
         """this method populates the database with `how_many` entities
         of each possible type. It also inserts random relations between them
         """
-        with security_enabled(self.session, read=False, write=False):
+        with self.session.security_enabled(read=False, write=False):
             self._auto_populate(how_many)
 
     def _auto_populate(self, how_many):
--- a/doc/3.15.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/3.15.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -1,6 +1,30 @@
-Whats new in CubicWeb 3.15
-==========================
+What's new in CubicWeb 3.15?
+============================
+
+New functionnalities
+--------------------
+
+* Add Zmq server, based on the cutting edge ZMQ (http://www.zeromq.org/) socket
+  library.  This allows to access distant instance, in a similar way as Pyro.
+
+* Publish/subscribe mechanism using ZMQ for communication among cubicweb
+  instances.  The new zmq-address-sub and zmq-address-pub configuration variables
+  define where this communication occurs.  As of this release this mechanism is
+  used for entity cache invalidation.
 
+* Improved WSGI support. While there is still some caveats, most of the code
+  which as twisted only is now generic and allows related functionalities to work
+  with a WSGI front-end.
+
+* Full undo/transaction support : undo of modification has eventually been
+  implemented, and the configuration simplified (basically you activate it or not
+  on an instance basis).
+
+* Controlling HTTP status code used is not much more easier :
+
+  - `WebRequest` now has a `status_out` attribut to control the response status ;
+
+  - most web-side exceptions take an optional ``status`` argument.
 
 API changes
 -----------
@@ -25,38 +49,48 @@
   * on the CubicWeb side, the `selectors` module has been renamed to
     `predicates`.
 
-  Debugging refactoring dropped the more need for the `lltrace` decorator.
-
-  There should be full backward compat with proper deprecation warnings.
-
-  Notice the `yes` predicate and `objectify_predicate` decorator, as well as the
+  Debugging refactoring dropped the more need for the `lltrace` decorator.  There
+  should be full backward compat with proper deprecation warnings.  Notice the
+  `yes` predicate and `objectify_predicate` decorator, as well as the
   `traced_selection` function should now be imported from the
   `logilab.common.registry` module.
 
+* All login forms are now submitted to <app_root>/login. Redirection to requested
+  page is now handled by the login controller (it was previously handle by the
+  session manager).
+
+* `Publisher.publish` has been renamed to `Publisher.handle_request`. This
+  method now contains generic version of logic previously handled by
+  Twisted. `Controller.publish` is **not** affected.
 
 Unintrusive API changes
 -----------------------
 
-* new 'ldapfeed' source type, designed to replace 'ldapuser' source with
+* New 'ldapfeed' source type, designed to replace 'ldapuser' source with
   data-feed (i.e. copy based) source ideas.
 
+* New 'zmqrql' source type, similar to 'pyrorql' but using ømq instead of Pyro.
 
-RQL
----
+* A new registry called `services` has appeared, where you can register
+  server-side `cubicweb.server.Service` child classes. Their `call` method can be
+  invoked from a web-side AppObject instance using new `self._cw.call_service`
+  method or a server-side one using `self.session.call_service`. This is a new
+  way to call server-side methods, much cleaner than monkey patching the
+  Repository class, which becomes a deprecated way to perform similar tasks.
 
+* a new `ajax-func` registry now hosts all remote functions (i.e. functions
+  callable through the `asyncRemoteExec` JS api). A convenience `ajaxfunc`
+  decorator will let you expose your python function easily without all the
+  appobject standard boilerplate. Backward compatibility is preserved.
+
+* the 'json' controller is now deprecated in favor of the 'ajax' one.
+
+* `WebRequest.build_url` can now take a __secure__ argument. When True cubicweb
+  try to generate an https url.
 
 
 User interface changes
 ----------------------
 
-
-
-Configuration
--------------
-
-Base schema changes
--------------------
-Email address 'read' permission is now more restrictive: only managers and
-users to which an address belong may see them. Application that wish other
-settings should set them explicitly.
-
+A new 'undohistory' view expose the undoable transactions and give access to undo
+some of them.
--- a/doc/book/en/admin/config.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/admin/config.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -70,53 +70,53 @@
 install the `postgresql-client` package on the |cubicweb| host, and others on the
 database host.
 
-.. Note::
+Database cluster
+++++++++++++++++
 
-    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).
+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``::
+To initialize a PostgreSQL cluster, use the command ``initdb``::
 
     $ 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::
+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::
 
-    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.
-
+  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::
+Once initialized, start the database server PostgreSQL with the command::
 
-    $ postgres -D /path/to/psql
+  $ 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
+  $ 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::
+Database authentication
++++++++++++++++++++++++
 
-    $ su
-    $ su - postgres
-    $ createuser -s -P username
+The database authentication is configured in `pg_hba.conf`. It 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 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
+  $ su - postgres
+  $ createuser -s -P username
 
-    $ su postgres -c "echo ALTER USER username WITH PASSWORD 'userpasswd' | psql"
+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::
 
-.. Note::
-    The authentication method can be configured in file:`pg_hba.conf`.
-
+  $ su postgres -c "echo ALTER USER username WITH PASSWORD 'userpasswd' | psql"
 
 The above login/password will be requested when you will create an instance with
 `cubicweb-ctl create` to initialize the database of your instance.
@@ -149,7 +149,6 @@
     cat /usr/share/postgresql/8.X/contrib/tsearch2.sql | psql -U username template1
 
 
-
 .. _MySqlConfiguration:
 
 MySql
@@ -196,12 +195,12 @@
 
 The ALTER DATABASE command above requires some permissions that your
 user may not have. In that case you will have to ask your local DBA to
-run the query for you. 
+run the query for you.
 
 You can check that the setting is correct by running the following
 query which must return '1'::
 
-   SELECT is_read_committed_snapshot_on 
+   SELECT is_read_committed_snapshot_on
      FROM sys.databases WHERE name='<databasename>';
 
 
@@ -210,6 +209,7 @@
 
 SQLite
 ~~~~~~
+
 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.
@@ -226,6 +226,7 @@
 
 Pyro name server
 ~~~~~~~~~~~~~~~~
+
 If you want to use Pyro to access your instance remotely, or to have multi-source
 or distributed configuration, it is required to have a Pyro name server running
 on your network. By default it is detected by a broadcast request, but you can
--- a/doc/book/en/admin/instance-config.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/admin/instance-config.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -17,6 +17,7 @@
 each option name is prefixed with its own section and followed by its
 default value if necessary, e.g. "`<section>.<option>` [value]."
 
+.. _`WebServerConfig`:
 
 Configuring the Web server
 --------------------------
--- a/doc/book/en/admin/ldap.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/admin/ldap.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -29,6 +29,15 @@
 The base functionality for this is in
 :file:`cubicweb/server/sources/ldapuser.py`.
 
+External dependencies
+---------------------
+
+You'll need the following packages to make CubicWeb interact with your LDAP /
+Active Directory server:
+
+* python-ldap
+* ldaputils if using `ldapfeed` source
+
 Configurations options
 ----------------------
 
--- a/doc/book/en/admin/setup.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/admin/setup.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -63,9 +63,9 @@
 
   deb http://download.logilab.org/production/ lucid/
 
-  Note that for Ubuntu Maverick and newer, you shall use the `lucid`
-  repository and install the ``libgecode19`` package from `lucid
-  universe <http://packages.ubuntu.com/lucid/libgecode19>`_.
+Note that for Ubuntu Maverick and newer, you shall use the `lucid`
+repository and install the ``libgecode19`` package from `lucid
+universe <http://packages.ubuntu.com/lucid/libgecode19>`_.
 
 The repositories are signed with the `Logilab's gnupg key`_. You can download
 and register the key to avoid warnings::
--- a/doc/book/en/annexes/depends.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/annexes/depends.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -45,6 +45,9 @@
 * indexer - http://www.logilab.org/project/indexer -
   http://pypi.python.org/pypi/indexer - included in the forest
 
+* passlib - https://code.google.com/p/passlib/ -
+  http://pypi.python.org/pypi/passlib
+
 To use network communication between cubicweb instances / clients:
 
 * Pyro - http://www.xs4all.nl/~irmen/pyro3/ - http://pypi.python.org/pypi/Pyro
--- a/doc/book/en/annexes/faq.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/annexes/faq.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -364,7 +364,7 @@
     >>> crypted = crypt_password('joepass')
     >>> rset = rql('Any U WHERE U is CWUser, U login "joe"')
     >>> joe = rset.get_entity(0,0)
-    >>> joe.set_attributes(upassword=Binary(crypted))
+    >>> joe.cw_set(upassword=Binary(crypted))
 
 Please, refer to the script example is provided in the `misc/examples/chpasswd.py` file.
 
--- a/doc/book/en/devrepo/entityclasses/application-logic.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/entityclasses/application-logic.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -38,7 +38,7 @@
 object was built.
 
 Setting an attribute or relation value can be done in the context of a
-Hook/Operation, using the obj.set_relations(x=42) notation or a plain
+Hook/Operation, using the obj.cw_set(x=42) notation or a plain
 RQL SET expression.
 
 In views, it would be preferable to encapsulate the necessary logic in
--- a/doc/book/en/devrepo/entityclasses/data-as-objects.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/entityclasses/data-as-objects.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -16,50 +16,47 @@
 
 `Formatting and output generation`:
 
-* `view(__vid, __registry='views', **kwargs)`, applies the given view to the entity
+* :meth:`view(__vid, __registry='views', **kwargs)`, applies the given view to the entity
   (and returns an unicode string)
 
-* `absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url
+* :meth:`absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url
 
-* `rest_path()`, returns a relative REST URL to get the entity
+* :meth:`rest_path()`, returns a relative REST URL to get the entity
 
-* `printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`,
+* :meth:`printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`,
   returns a string enabling the display of an attribute value in a given format
   (the value is automatically recovered if necessary)
 
 `Data handling`:
 
-* `as_rset()`, converts the entity into an equivalent result set simulating the
+* :meth:`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 at
+* :meth:`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
+* :meth:`get_value(name)`, returns the value associated to the attribute name given
   in parameter
 
-* `related(rtype, role='subject', limit=None, entities=False)`,
+* :meth:`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, role='subject', limit=None)`,
+* :meth:`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
+* :meth:`cw_set(**kwargs)`, updates entity's attributes and/or relation with the
+  corresponding values given named parameters. 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).
 
-* `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
+* :meth:`copy_relations(ceid)`, copies the relations of the entities having the eid
   given in the parameters on the current entity
 
-* `delete()` allows to delete the entity
+* :meth:`cw_delete()` allows to delete the entity
 
 
 The :class:`AnyEntity` class
@@ -81,40 +78,30 @@
 
 `Standard meta-data (Dublin Core)`:
 
-* `dc_title()`, returns a unicode string corresponding to the
+* :meth:`dc_title()`, returns a unicode string corresponding to the
   meta-data `Title` (used by default is the first non-meta attribute
   of the entity schema)
 
-* `dc_long_title()`, same as dc_title but can return a more
+* :meth:`dc_long_title()`, same as dc_title but can return a more
   detailed title
 
-* `dc_description(format='text/plain')`, returns a unicode string
+* :meth:`dc_description(format='text/plain')`, returns a unicode string
   corresponding to the meta-data `Description` (looks for a
   description attribute by default)
 
-* `dc_authors()`, returns a unicode string corresponding to the meta-data
+* :meth:`dc_authors()`, returns a unicode string corresponding to the meta-data
   `Authors` (owners by default)
 
-* `dc_creator()`, returns a unicode string corresponding to the
+* :meth:`dc_creator()`, returns a unicode string corresponding to the
   creator of the entity
 
-* `dc_date(date_format=None)`, returns a unicode string corresponding to
+* :meth:`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
+* :meth:`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)
+* :meth:`dc_language()`, returns the language used by the entity
 
 Inheritance
 -----------
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devrepo/fti.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,159 @@
+.. _fti:
+
+Full Text Indexing in CubicWeb
+------------------------------
+
+When an attribute is tagged as *fulltext-indexable* in the datamodel,
+CubicWeb will automatically trigger hooks to update the internal
+fulltext index (i.e the ``appears`` SQL table) each time this attribute
+is modified.
+
+CubicWeb also provides a ``db-rebuild-fti`` command to rebuild the whole
+fulltext on demand:
+
+.. sourcecode:: bash
+
+   cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance
+
+You can also rebuild the fulltext index for a given set of entity types:
+
+.. sourcecode:: bash
+
+   cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance Ticket Version
+
+In the above example, only fulltext index of entity types ``Ticket`` and ``Version``
+will be rebuilt.
+
+
+Standard FTI process
+~~~~~~~~~~~~~~~~~~~~
+
+Considering an entity type ``ET``, the default *fti* process is to :
+
+1. fetch all entities of type ``ET``
+
+2. for each entity, adapt it to ``IFTIndexable`` (see
+   :class:`~cubicweb.entities.adapters.IFTIndexableAdapter`)
+
+3. call
+   :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words` on
+   the adapter which is supposed to return a dictionary *weight* ->
+   *list of words* as expected by
+   :meth:`~logilab.database.fti.FTIndexerMixIn.index_object`. The
+   tokenization of each attribute value is done by
+   :meth:`~logilab.database.fti.tokenize`.
+
+
+See :class:`~cubicweb.entities.adapters.IFTIndexableAdapter` for more documentation.
+
+
+Yams and ``fultext_container``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It is possible in the datamodel to indicate that fulltext-indexed
+attributes defined for an entity type will be used to index not the
+entity itself but a related entity. This is especially useful for
+composite entities. Let's take a look at (a simplified version of)
+the base schema defined in CubicWeb (see :mod:`cubicweb.schemas.base`):
+
+.. sourcecode:: python
+
+  class CWUser(WorkflowableEntityType):
+      login     = String(required=True, unique=True, maxsize=64)
+      upassword = Password(required=True)
+
+  class EmailAddress(EntityType):
+      address = String(required=True,  fulltextindexed=True,
+                       indexed=True, unique=True, maxsize=128)
+
+
+  class use_email_relation(RelationDefinition):
+      name = 'use_email'
+      subject = 'CWUser'
+      object = 'EmailAddress'
+      cardinality = '*?'
+      composite = 'subject'
+
+
+The schema above states that there is a relation between ``CWUser`` and ``EmailAddress``
+and that the ``address`` field of ``EmailAddress`` is fulltext indexed. Therefore,
+in your application, if you use fulltext search to look for an email address, CubicWeb
+will return the ``EmailAddress`` itself. But the objects we'd like to index
+are more likely to be the associated ``CWUser`` than the ``EmailAddress`` itself.
+
+The simplest way to achieve that is to tag the ``use_email`` relation in
+the datamodel:
+
+.. sourcecode:: python
+
+  class use_email(RelationType):
+      fulltext_container = 'subject'
+
+
+Customizing how entities are fetched during ``db-rebuild-fti``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``db-rebuild-fti`` will call the
+:meth:`~cubicweb.entities.AnyEntity.cw_fti_index_rql_queries` class
+method on your entity type.
+
+.. automethod:: cubicweb.entities.AnyEntity.cw_fti_index_rql_queries
+
+Now, suppose you've got a _huge_ table to index, you probably don't want to
+get all entities at once. So here's a simple customized example that will
+process block of 10000 entities:
+
+.. sourcecode:: python
+
+
+    class MyEntityClass(AnyEntity):
+        __regid__ = 'MyEntityClass'
+
+    @classmethod
+    def cw_fti_index_rql_queries(cls, req):
+        # get the default RQL method and insert LIMIT / OFFSET instructions
+        base_rql = super(SearchIndex, cls).cw_fti_index_rql_queries(req)[0]
+        selected, restrictions = base_rql.split(' WHERE ')
+        rql_template = '%s ORDERBY X LIMIT %%(limit)s OFFSET %%(offset)s WHERE %s' % (
+            selected, restrictions)
+        # count how many entities you'll have to index
+        count = req.execute('Any COUNT(X) WHERE X is MyEntityClass')[0][0]
+        # iterate by blocks of 10000 entities
+        chunksize = 10000
+        for offset in xrange(0, count, chunksize):
+            print 'SENDING', rql_template % {'limit': chunksize, 'offset': offset}
+            yield rql_template % {'limit': chunksize, 'offset': offset}
+
+Since you have access to ``req``, you can more or less fetch whatever you want.
+
+
+Customizing :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can also customize the FTI process by providing your own ``get_words()``
+implementation:
+
+.. sourcecode:: python
+
+    from cubicweb.entities.adapters import IFTIndexableAdapter
+
+    class SearchIndexAdapter(IFTIndexableAdapter):
+        __regid__ = 'IFTIndexable'
+        __select__ = is_instance('MyEntityClass')
+
+        def fti_containers(self, _done=None):
+            """this should yield any entity that must be considered to
+            fulltext-index self.entity
+
+            CubicWeb's default implementation will look for yams'
+            ``fulltex_container`` property.
+            """
+            yield self.entity
+            yield self.entity.some_related_entity
+
+
+        def get_words(self):
+            # implement any logic here
+            # see http://www.postgresql.org/docs/9.1/static/textsearch-controls.html
+            # for the actual signification of 'C'
+            return {'C': ['any', 'word', 'I', 'want']}
--- a/doc/book/en/devrepo/index.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/index.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -21,3 +21,5 @@
    testing.rst
    migration.rst
    profiling.rst
+   fti.rst
+
--- a/doc/book/en/devrepo/migration.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/migration.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -139,7 +139,7 @@
 * `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.
+* `rename_relationi_type(oldname, newname, commit=True)`, renames a relation type.
 
 * `add_relation_definition(subjtype, rtype, objtype, commit=True)`, adds a new
   relation definition.
--- a/doc/book/en/devrepo/repo/hooks.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/repo/hooks.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -206,10 +206,11 @@
 Reminder
 ~~~~~~~~
 
-You should never use the `entity.foo = 42` notation to update an
-entity. It will not do what you expect (updating the
-database). Instead, use the :meth:`set_attributes` and
-:meth:`set_relations` methods.
+You should never use the `entity.foo = 42` notation to update an entity. It will
+not do what you expect (updating the database). Instead, use the
+:meth:`~cubicweb.entity.Entity.cw_set` method or direct access to entity's
+:attr:`cw_edited` attribute if you're writing a hook for 'before_add_entity' or
+'before_update_entity' event.
 
 
 How to choose between a before and an after event ?
--- a/doc/book/en/devrepo/testing.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/testing.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -70,13 +70,13 @@
 
         def test_cannot_create_cycles(self):
             # direct obvious cycle
-            self.assertRaises(ValidationError, self.kw1.set_relations,
+            self.assertRaises(ValidationError, self.kw1.cw_set,
                               subkeyword_of=self.kw1)
             # testing indirect cycles
             kw3 = self.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
                                'SK subkeyword_of K WHERE C name "classif1", K eid %s'
                                % self.kw1.eid).get_entity(0,0)
-            self.kw1.set_relations(subkeyword_of=kw3)
+            self.kw1.cw_set(subkeyword_of=kw3)
             self.assertRaises(ValidationError, self.commit)
 
 The test class defines a :meth:`setup_database` method which populates the
@@ -192,10 +192,10 @@
                                 description=u'cubicweb is beautiful')
             blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
                                              content=u'cubicweb hop')
-            blog_entry_1.set_relations(entry_of=cubicweb_blog)
+            blog_entry_1.cw_set(entry_of=cubicweb_blog)
             blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
                                              content=u'cubicweb yes')
-            blog_entry_2.set_relations(entry_of=cubicweb_blog)
+            blog_entry_2.cw_set(entry_of=cubicweb_blog)
             self.assertEqual(len(MAILBOX), 0)
             self.commit()
             self.assertEqual(len(MAILBOX), 2)
--- a/doc/book/en/devweb/ajax.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/ajax.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -7,6 +7,6 @@
 
 You can, for instance, register some python functions that will become
 callable from javascript through ajax calls. All the ajax URLs are handled
-by the ``AjaxController`` controller.
+by the :class:`cubicweb.web.views.ajaxcontroller.AjaxController` controller.
 
 .. automodule:: cubicweb.web.views.ajaxcontroller
--- a/doc/book/en/devweb/index.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/index.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -10,6 +10,7 @@
    publisher
    controllers
    request
+   searchbar
    views/index
    rtags
    ajax
--- a/doc/book/en/devweb/property.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/property.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -1,3 +1,5 @@
+.. _cwprops:
+
 The property mecanism
 ---------------------
 
--- a/doc/book/en/devweb/request.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/request.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -99,6 +99,7 @@
     document.ready(...) or another ajax-friendly one-time trigger event
   * `add_header(header, values)`: adds the header/value pair to the
     current html headers
+  * `status_out`: control the HTTP status of the response
 
 * `And more...`
 
--- a/doc/book/en/devweb/resource.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/resource.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -1,3 +1,5 @@
+.. _resources:
+
 Locate resources
 ----------------
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devweb/searchbar.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,41 @@
+.. _searchbar:
+
+RQL search bar
+--------------
+
+The RQL search bar is a visual component, hidden by default, the tiny *search*
+input being enough for common use cases.
+
+An autocompletion helper is provided to help you type valid queries, both
+in terms of syntax and in terms of schema validity.
+
+.. autoclass:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder
+
+
+How search is performed
++++++++++++++++++++++++
+
+You can use the *rql search bar* to either type RQL queries, plain text queries
+or standard shortcuts such as *<EntityType>* or *<EntityType> <attrname> <value>*.
+
+Ultimately, all queries are translated to rql since it's the only
+language understood on the server (data) side. To transform the user
+query into RQL, CubicWeb uses the so-called *magicsearch component*,
+defined in :mod:`cubicweb.web.views.magicsearch`, which in turn
+delegates to a number of query preprocessor that are responsible of
+interpreting the user query and generating corresponding RQL.
+
+The code of the main processor loop is easy to understand:
+
+.. sourcecode:: python
+
+  for proc in self.processors:
+      try:
+          return proc.process_query(uquery, req)
+      except (RQLSyntaxError, BadRQLQuery):
+          pass
+
+The idea is simple: for each query processor, try to translate the
+query. If it fails, try with the next processor, if it succeeds,
+we're done and the RQL query will be executed.
+
--- a/doc/book/en/devweb/views/index.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/views/index.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -18,10 +18,11 @@
    boxes
    table
    xmlrss
-..   editforms
    urlpublish
    breadcrumbs
    idownloadable
    wdoc
+
+..   editforms
 ..   embedding
 
--- a/doc/book/en/devweb/views/views.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/views/views.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -32,33 +32,10 @@
 Basic class for views
 ~~~~~~~~~~~~~~~~~~~~~
 
-Class `View` (`cubicweb.view`)
-```````````````````````````````
-
-This class is an abstraction of a view class, used as a base class for
-every renderable object such as views, templates and other user
-interface components.
-
-A `View` is instantiated to render a result set or part of a result
-set. `View` subclasses may be parametrized using the following class
-attributes:
+Class :class:`~cubicweb.view.View`
+``````````````````````````````````
 
-* `templatable` indicates if the view may be embedded in a main
-  template or if it has to be rendered standalone (i.e. pure XML views
-  must not be embedded in the main template of HTML pages)
-
-* if the view is not templatable, it should set the `content_type`
-  class attribute to the correct MIME type (text/xhtml being the
-  default)
-
-* the `category` attribute may be used in the interface to regroup
-  related view kinds together
-
-A view writes to its output stream thanks to its attribute `w` (the
-append method of an `UStreamIO`, except for binary views).
-
-At instantiation time, the standard `_cw` and `cw_rset` attributes are
-added and the `w` attribute will be set at rendering time.
+.. autoclass:: cubicweb.view.View
 
 The basic interface for views is as follows (remember that the result
 set has a tabular structure with rows and columns, hence cells):
@@ -88,12 +65,13 @@
 
 Other basic view classes
 ````````````````````````
-Here are some of the subclasses of `View` defined in `cubicweb.view`
+Here are some of the subclasses of :class:`~cubicweb.view.View` defined in :mod:`cubicweb.view`
 that are more concrete as they relate to data rendering within the application:
 
-* `EntityView`, view applying to lines or cell containing an entity (e.g. an eid)
-* `StartupView`, start view that does not require a result set to apply to
-* `AnyRsetView`, view applicable to any result set
+.. autoclass:: cubicweb.view.EntityView
+.. autoclass:: cubicweb.view.StartupView
+.. autoclass:: cubicweb.view.EntityStartupView
+.. autoclass:: cubicweb.view.AnyRsetView
 
 Examples of views class
 ```````````````````````
--- a/doc/book/en/tutorials/advanced/part02_security.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/tutorials/advanced/part02_security.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -196,7 +196,7 @@
 	    for eid in self.get_data():
 		entity = self.session.entity_from_eid(eid)
 		if entity.visibility == 'parent':
-		    entity.set_attributes(visibility=u'authenticated')
+		    entity.cw_set(visibility=u'authenticated')
 
     class SetVisibilityHook(hook.Hook):
 	__regid__ = 'sytweb.setvisibility'
@@ -215,7 +215,7 @@
 	    parent = self._cw.entity_from_eid(self.eidto)
 	    child = self._cw.entity_from_eid(self.eidfrom)
 	    if child.visibility == 'parent':
-		child.set_attributes(visibility=parent.visibility)
+		child.cw_set(visibility=parent.visibility)
 
 Notice:
 
@@ -344,7 +344,7 @@
 	    self.assertEquals(len(req.execute('Folder X')), 0) # restricted...
 	    # may_be_read_by propagation
 	    self.restore_connection()
-	    folder.set_relations(may_be_read_by=toto)
+	    folder.cw_set(may_be_read_by=toto)
 	    self.commit()
 	    photo1.clear_all_caches()
 	    self.failUnless(photo1.may_be_read_by)
--- a/doc/book/en/tutorials/advanced/part04_ui-base.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -294,6 +294,7 @@
 folder in which the current file (e.g. `self.entity`) is located.
 
 .. Note::
+
    The :class:`IBreadCrumbs` interface is a `breadcrumbs` method, but the default
    :class:`IBreadCrumbsAdapter` provides a default implementation for it that will look
    at the value returned by its `parent_entity` method. It also provides a
@@ -331,6 +332,7 @@
 navigate through the web site to see if everything is ok...
 
 .. Note::
+
    In the 'cubicweb-ctl i18ncube' command, `sytweb` refers to the **cube**, while
    in the two other, it refers to the **instance** (if you can't see the
    difference, reread CubicWeb's concept chapter !).
@@ -363,4 +365,4 @@
 .. _`several improvments`: http://www.cubicweb.org/blogentry/1179899
 .. _`3.8`: http://www.cubicweb.org/blogentry/917107
 .. _`first blog of this series`: http://www.cubicweb.org/blogentry/824642
-.. _`an earlier post`: http://www.cubicweb.org/867464
\ No newline at end of file
+.. _`an earlier post`: http://www.cubicweb.org/867464
--- a/entities/__init__.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/__init__.py	Tue Oct 23 15:00:53 2012 +0200
@@ -40,6 +40,24 @@
         """ return the url of the entity creation form for this entity type"""
         return req.build_url('add/%s' % cls.__regid__, **kwargs)
 
+    @classmethod
+    def cw_fti_index_rql_queries(cls, req):
+        """return the list of rql queries to fetch entities to FT-index
+
+        The default is to fetch all entities at once and to prefetch
+        indexable attributes but one could imagine iterating over
+        "smaller" resultsets if the table is very big or returning
+        a subset of entities that match some business-logic condition.
+        """
+        restrictions = ['X is %s' % cls.__regid__]
+        selected = ['X']
+        for attrschema in cls.e_schema.indexable_attributes():
+            varname = attrschema.type.upper()
+            restrictions.append('X %s %s' % (attrschema, varname))
+            selected.append(varname)
+        return ['Any %s WHERE %s' % (', '.join(selected),
+                                     ', '.join(restrictions))]
+
     # meta data api ###########################################################
 
     def dc_title(self):
--- a/entities/adapters.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/adapters.py	Tue Oct 23 15:00:53 2012 +0200
@@ -87,10 +87,20 @@
 
 
 class IFTIndexableAdapter(view.EntityAdapter):
+    """standard adapter to handle fulltext indexing
+
+    .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.fti_containers
+    .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.get_words
+    """
     __regid__ = 'IFTIndexable'
     __select__ = is_instance('Any')
 
     def fti_containers(self, _done=None):
+        """return the list of entities to index when handling ``self.entity``
+
+        The actual list of entities depends on ``fulltext_container`` usage
+        in the datamodel definition
+        """
         if _done is None:
             _done = set()
         entity = self.entity
--- a/entities/authobjs.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/authobjs.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -77,6 +77,19 @@
             self._properties = dict((p.pkey, p.value) for p in self.reverse_for_user)
             return self._properties
 
+    def prefered_language(self, language=None):
+        """return language used by this user, if explicitly defined (eg not
+        using http negociation)
+        """
+        language = language or self.property_value('ui.language')
+        vreg = self._cw.vreg
+        try:
+            vreg.config.translations[language]
+        except KeyError:
+            language = vreg.property_value('ui.language')
+            assert language in vreg.config.translations[language], language
+        return language
+
     def property_value(self, key):
         try:
             # properties stored on the user aren't correctly typed
@@ -101,7 +114,7 @@
                 kwargs['for_user'] = self
             self._cw.create_entity('CWProperty', **kwargs)
         else:
-            prop.set_attributes(value=value)
+            prop.cw_set(value=value)
 
     def matching_groups(self, groups):
         """return the number of the given group(s) in which the user is
--- a/entities/sources.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/sources.py	Tue Oct 23 15:00:53 2012 +0200
@@ -51,7 +51,7 @@
                     continue
                 raise
         cfgstr = unicode(generate_source_config(sconfig), self._cw.encoding)
-        self.set_attributes(config=cfgstr)
+        self.cw_set(config=cfgstr)
 
 
 class CWSource(_CWSourceCfgMixIn, AnyEntity):
@@ -181,5 +181,5 @@
     def write_log(self, session, **kwargs):
         if 'status' not in kwargs:
             kwargs['status'] = getattr(self, '_status', u'success')
-        self.set_attributes(log=u'<br/>'.join(self._logs), **kwargs)
+        self.cw_set(log=u'<br/>'.join(self._logs), **kwargs)
         self._logs = []
--- a/entities/test/unittest_base.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/test/unittest_base.py	Tue Oct 23 15:00:53 2012 +0200
@@ -19,6 +19,7 @@
 """unit tests for cubicweb.entities.base module
 
 """
+from __future__ import with_statement
 
 from logilab.common.testlib import unittest_main
 from logilab.common.decorators import clear_cache
@@ -57,13 +58,19 @@
         self.assertEqual(dict((str(k), v) for k, v in self.schema['State'].meta_attributes().iteritems()),
                           {'description_format': ('format', 'description')})
 
+    def test_fti_rql_method(self):
+        eclass = self.vreg['etypes'].etype_class('EmailAddress')
+        self.assertEqual(['Any X, ALIAS, ADDRESS WHERE X is EmailAddress, '
+                          'X alias ALIAS, X address ADDRESS'],
+                         eclass.cw_fti_index_rql_queries(self.request()))
+
 
 class EmailAddressTC(BaseEntityTC):
     def test_canonical_form(self):
         email1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"').get_entity(0, 0)
         email2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com"').get_entity(0, 0)
         email3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"').get_entity(0, 0)
-        email1.set_relations(prefered_form=email2)
+        email1.cw_set(prefered_form=email2)
         self.assertEqual(email1.prefered.eid, email2.eid)
         self.assertEqual(email2.prefered.eid, email2.eid)
         self.assertEqual(email3.prefered.eid, email3.eid)
@@ -97,10 +104,10 @@
         e = self.execute('CWUser U WHERE U login "member"').get_entity(0, 0)
         self.assertEqual(e.dc_title(), 'member')
         self.assertEqual(e.name(), 'member')
-        e.set_attributes(firstname=u'bouah')
+        e.cw_set(firstname=u'bouah')
         self.assertEqual(e.dc_title(), 'member')
         self.assertEqual(e.name(), u'bouah')
-        e.set_attributes(surname=u'lôt')
+        e.cw_set(surname=u'lôt')
         self.assertEqual(e.dc_title(), 'member')
         self.assertEqual(e.name(), u'bouah lôt')
 
--- a/entities/test/unittest_wfobjs.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/test/unittest_wfobjs.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -20,7 +20,6 @@
 
 from cubicweb import ValidationError
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.server.session import security_enabled
 
 
 def add_wf(self, etype, name=None, default=False):
@@ -63,7 +62,7 @@
         # gnark gnark
         bar = wf.add_state(u'bar')
         self.commit()
-        bar.set_attributes(name=u'foo')
+        bar.cw_set(name=u'foo')
         with self.assertRaises(ValidationError) as cm:
             self.commit()
         self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a state of that name'})
@@ -86,7 +85,7 @@
         # gnark gnark
         biz = wf.add_transition(u'biz', (bar,), foo)
         self.commit()
-        biz.set_attributes(name=u'baz')
+        biz.cw_set(name=u'baz')
         with self.assertRaises(ValidationError) as cm:
             self.commit()
         self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a transition of that name'})
@@ -126,8 +125,9 @@
         self.assertEqual(trs[0].destination(None).name, u'deactivated')
         # test a std user get no possible transition
         cnx = self.login('member')
+        req = self.request()
         # fetch the entity using the new session
-        trs = list(cnx.user().cw_adapt_to('IWorkflowable').possible_transitions())
+        trs = list(req.user.cw_adapt_to('IWorkflowable').possible_transitions())
         self.assertEqual(len(trs), 0)
         cnx.close()
 
@@ -154,7 +154,7 @@
         wf = add_wf(self, 'CWUser')
         s = wf.add_state(u'foo', initial=True)
         self.commit()
-        with security_enabled(self.session, write=False):
+        with self.session.security_enabled(write=False):
             with self.assertRaises(ValidationError) as cm:
                 self.session.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
                                      {'x': self.user().eid, 's': s.eid})
@@ -173,7 +173,7 @@
 
     def test_goback_transition(self):
         req = self.request()
-        wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow
+        wf = req.user.cw_adapt_to('IWorkflowable').current_workflow
         asleep = wf.add_state('asleep')
         wf.add_transition('rest', (wf.state_by_name('activated'),
                                    wf.state_by_name('deactivated')),
@@ -212,7 +212,7 @@
         req = self.request()
         iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
         iworkflowable.fire_transition('deactivate')
-        cnx.commit()
+        req.cu.commit()
         with self.assertRaises(ValidationError) as cm:
             iworkflowable.fire_transition('activate')
         self.assertEqual(cm.exception.errors, {'by_transition-subject': "transition may not be fired"})
@@ -516,7 +516,7 @@
                           ['rest'])
         self.assertEqual(parse_hist(iworkflowable.workflow_history),
                           [('asleep', 'asleep', 'rest', None)])
-        user.set_attributes(surname=u'toto') # fulfill condition
+        user.cw_set(surname=u'toto') # fulfill condition
         self.commit()
         iworkflowable.fire_transition('rest')
         self.commit()
@@ -556,13 +556,12 @@
 
     def setUp(self):
         CubicWebTC.setUp(self)
-        self.wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow
-        self.session.set_cnxset()
+        req = self.request()
+        self.wf = req.user.cw_adapt_to('IWorkflowable').current_workflow
         self.s_activated = self.wf.state_by_name('activated').eid
         self.s_deactivated = self.wf.state_by_name('deactivated').eid
         self.s_dummy = self.wf.add_state(u'dummy').eid
         self.wf.add_transition(u'dummy', (self.s_deactivated,), self.s_dummy)
-        req = self.request()
         ueid = self.create_user(req, 'stduser', commit=False).eid
         # test initial state is set
         rset = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s',
--- a/entity.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/entity.py	Tue Oct 23 15:00:53 2012 +0200
@@ -452,26 +452,13 @@
         return mainattr, needcheck
 
     @classmethod
-    def cw_instantiate(cls, execute, **kwargs):
-        """add a new entity of this given type
-
-        Example (in a shell session):
-
-        >>> companycls = vreg['etypes'].etype_class(('Company')
-        >>> personcls = vreg['etypes'].etype_class(('Person')
-        >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
-        >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
-        ...                              works_for=c)
-
-        You can also set relation where the entity has 'object' role by
-        prefixing the relation by 'reverse_'.
-        """
-        rql = 'INSERT %s X' % cls.__regid__
+    def _cw_build_entity_query(cls, kwargs):
         relations = []
         restrictions = set()
-        pending_relations = []
+        pendingrels = []
         eschema = cls.e_schema
         qargs = {}
+        attrcache = {}
         for attr, value in kwargs.items():
             if attr.startswith('reverse_'):
                 attr = attr[len('reverse_'):]
@@ -487,10 +474,13 @@
                     value = iter(value).next()
                 else:
                     # prepare IN clause
-                    pending_relations.append( (attr, role, value) )
+                    pendingrels.append( (attr, role, value) )
                     continue
             if rschema.final: # attribute
                 relations.append('X %s %%(%s)s' % (attr, attr))
+                attrcache[attr] = value
+            elif value is None:
+                pendingrels.append( (attr, role, value) )
             else:
                 rvar = attr.upper()
                 if role == 'object':
@@ -503,19 +493,52 @@
                 if hasattr(value, 'eid'):
                     value = value.eid
             qargs[attr] = value
+        rql = u''
         if relations:
-            rql = '%s: %s' % (rql, ', '.join(relations))
+            rql += ', '.join(relations)
         if restrictions:
-            rql = '%s WHERE %s' % (rql, ', '.join(restrictions))
-        created = execute(rql, qargs).get_entity(0, 0)
-        for attr, role, values in pending_relations:
+            rql += ' WHERE %s' % ', '.join(restrictions)
+        return rql, qargs, pendingrels, attrcache
+
+    @classmethod
+    def _cw_handle_pending_relations(cls, eid, pendingrels, execute):
+        for attr, role, values in pendingrels:
             if role == 'object':
                 restr = 'Y %s X' % attr
             else:
                 restr = 'X %s Y' % attr
+            if values is None:
+                execute('DELETE %s WHERE X eid %%(x)s' % restr, {'x': eid})
+                continue
             execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
                 restr, ','.join(str(getattr(r, 'eid', r)) for r in values)),
-                    {'x': created.eid}, build_descr=False)
+                    {'x': eid}, build_descr=False)
+
+    @classmethod
+    def cw_instantiate(cls, execute, **kwargs):
+        """add a new entity of this given type
+
+        Example (in a shell session):
+
+        >>> companycls = vreg['etypes'].etype_class(('Company')
+        >>> personcls = vreg['etypes'].etype_class(('Person')
+        >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
+        >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
+        ...                              works_for=c)
+
+        You can also set relations where the entity has 'object' role by
+        prefixing the relation name by 'reverse_'. Also, relation values may be
+        an entity or eid, a list of entities or eids.
+        """
+        rql, qargs, pendingrels, attrcache = cls._cw_build_entity_query(kwargs)
+        if rql:
+            rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
+        else:
+            rql = 'INSERT %s X' % (cls.__regid__)
+        created = execute(rql, qargs).get_entity(0, 0)
+        created._cw_update_attr_cache(attrcache)
+        created.cw_attr_cache.update(attrcache)
+        cls._cw_handle_pending_relations(created.eid, pendingrels, execute)
         return created
 
     def __init__(self, req, rset=None, row=None, col=0):
@@ -535,6 +558,24 @@
     def __cmp__(self, other):
         raise NotImplementedError('comparison not implemented for %s' % self.__class__)
 
+    def _cw_update_attr_cache(self, attrcache):
+        # if context is a repository session, don't consider dont-cache-attrs as
+        # the instance already hold modified values and loosing them could
+        # introduce severe problems
+        if self._cw.is_request:
+            for attr in self._cw.get_shared_data('%s.dont-cache-attrs' % self.eid,
+                                                 default=(), txdata=True, pop=True):
+                attrcache.pop(attr, None)
+                self.cw_attr_cache.pop(attr, None)
+        self.cw_attr_cache.update(attrcache)
+
+    def _cw_dont_cache_attribute(self, attr):
+        """repository side method called when some attribute have been
+        transformed by a hook, hence original value should not be cached by
+        client
+        """
+        self._cw.transaction_data.setdefault('%s.dont-cache-attrs' % self.eid, set()).add(attr)
+
     def __json_encode__(self):
         """custom json dumps hook to dump the entity's eid
         which is not part of dict structure itself
@@ -635,11 +676,12 @@
         mainattr, needcheck = self.cw_rest_attr_info()
         etype = str(self.e_schema)
         path = etype.lower()
+        fallback = False
         if mainattr != 'eid':
             value = getattr(self, mainattr)
             if not can_use_rest_path(value):
                 mainattr = 'eid'
-                path += '/eid'
+                path = None
             elif needcheck:
                 # make sure url is not ambiguous
                 try:
@@ -650,12 +692,16 @@
                     nbresults = self.__unique = self._cw.execute(rql, {'value' : value})[0][0]
                 if nbresults != 1: # ambiguity?
                     mainattr = 'eid'
-                    path += '/eid'
+                    path = None
         if mainattr == 'eid':
             if use_ext_eid:
                 value = self.cw_metainformation()['extid']
             else:
                 value = self.eid
+        if path is None:
+            # fallback url: <base-url>/<eid> url is used as cw entities uri,
+            # prefer it to <base-url>/<etype>/eid/<eid>
+            return unicode(value)
         return '%s/%s' % (path, self._cw.url_quote(value))
 
     def cw_attr_metadata(self, attr, metadata):
@@ -1107,6 +1153,9 @@
         # insert security RQL expressions granting the permission to 'add' the
         # relation into the rql syntax tree, if necessary
         rqlexprs = rdef.get_rqlexprs('add')
+        if not self.has_eid():
+            rqlexprs = [rqlexpr for rqlexpr in rqlexprs
+                        if searchedvar.name in rqlexpr.mainvars]
         if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
             # compute a varmap suitable to RQLRewriter.rewrite argument
             varmap = dict((v, v) for v in (searchedvar.name, evar.name)
@@ -1207,54 +1256,41 @@
 
     # raw edition utilities ###################################################
 
-    def set_attributes(self, **kwargs): # XXX cw_set_attributes
+    def cw_set(self, **kwargs):
+        """update this entity using given attributes / relation, working in the
+        same fashion as :meth:`cw_instantiate`.
+
+        Example (in a shell session):
+
+        >>> c = rql('Any X WHERE X is Company').get_entity(0, 0)
+        >>> p = rql('Any X WHERE X is Person').get_entity(0, 0)
+        >>> c.set(name=u'Logilab')
+        >>> p.set(firstname=u'John', lastname=u'Doe', works_for=c)
+
+        You can also set relations where the entity has 'object' role by
+        prefixing the relation name by 'reverse_'.  Also, relation values may be
+        an entity or eid, a list of entities or eids, or None (meaning that all
+        relations of the given type from or to this object should be deleted).
+        """
         _check_cw_unsafe(kwargs)
         assert kwargs
         assert self.cw_is_saved(), "should not call set_attributes while entity "\
                "hasn't been saved yet"
-        relations = ['X %s %%(%s)s' % (key, key) for key in kwargs]
-        # and now update the database
-        kwargs['x'] = self.eid
-        self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
-                         kwargs)
-        kwargs.pop('x')
+        rql, qargs, pendingrels, attrcache = self._cw_build_entity_query(kwargs)
+        if rql:
+            rql = 'SET ' + rql
+            qargs['x'] = self.eid
+            if ' WHERE ' in rql:
+                rql += ', X eid %(x)s'
+            else:
+                rql += ' WHERE X eid %(x)s'
+            self._cw.execute(rql, qargs)
         # update current local object _after_ the rql query to avoid
         # interferences between the query execution itself and the cw_edited /
         # skip_security machinery
-        self.cw_attr_cache.update(kwargs)
-
-    def set_relations(self, **kwargs): # XXX cw_set_relations
-        """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 or eid, a list of entities or eids, or None
-        (meaning that all relations of the given type from or to this object
-        should be deleted).
-        """
-        # 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:
-                self._cw.execute('DELETE %s WHERE X eid %%(x)s' % restr,
-                                 {'x': self.eid})
-                continue
-            if not isinstance(values, (tuple, list, set, frozenset)):
-                values = (values,)
-            eids = []
-            for val in values:
-                try:
-                    eids.append(str(val.eid))
-                except AttributeError:
-                    try:
-                        eids.append(str(typed_eid(val)))
-                    except (ValueError, TypeError):
-                        raise Exception('expected an Entity or eid, got %s' % val)
-            self._cw.execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
-                    restr, ','.join(eids)), {'x': self.eid})
+        self._cw_update_attr_cache(attrcache)
+        self._cw_handle_pending_relations(self.eid, pendingrels, self._cw.execute)
+        # XXX update relation cache
 
     def cw_delete(self, **kwargs):
         assert self.has_eid(), self.eid
@@ -1269,6 +1305,21 @@
 
     # deprecated stuff #########################################################
 
+    @deprecated('[3.16] use cw_set() instead')
+    def set_attributes(self, **kwargs): # XXX cw_set_attributes
+        self.cw_set(**kwargs)
+
+    @deprecated('[3.16] use cw_set() instead')
+    def set_relations(self, **kwargs): # XXX cw_set_relations
+        """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 or eid, a list of entities or eids, or None
+        (meaning that all relations of the given type from or to this object
+        should be deleted).
+        """
+        self.cw_set(**kwargs)
+
     @deprecated('[3.13] use entity.cw_clear_all_caches()')
     def clear_all_caches(self):
         return self.cw_clear_all_caches()
--- a/etwist/http.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/etwist/http.py	Tue Oct 23 15:00:53 2012 +0200
@@ -43,19 +43,3 @@
 
     def __repr__(self):
         return "<%s.%s code=%d>" % (self.__module__, self.__class__.__name__, self._code)
-
-
-def not_modified_response(twisted_request, headers_in):
-    headers_out = Headers()
-
-    for header in (
-        # Required from sec 10.3.5:
-        'date', 'etag', 'content-location', 'expires',
-        'cache-control', 'vary',
-        # Others:
-        'server', 'proxy-authenticate', 'www-authenticate', 'warning'):
-        value = headers_in.getRawHeaders(header)
-        if value is not None:
-            headers_out.setRawHeaders(header, value)
-    return HTTPResponse(twisted_request=twisted_request,
-                        headers=headers_out)
--- a/etwist/request.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/etwist/request.py	Tue Oct 23 15:00:53 2012 +0200
@@ -27,27 +27,18 @@
 from cubicweb.web.request import CubicWebRequestBase
 from cubicweb.web.httpcache import GMTOFFSET
 from cubicweb.web.http_headers import Headers
-from cubicweb.etwist.http import not_modified_response
 
 
 class CubicWebTwistedRequestAdapter(CubicWebRequestBase):
-    def __init__(self, req, vreg, https, base_url):
+    def __init__(self, req, vreg, https):
         self._twreq = req
-        self._base_url = base_url
-        super(CubicWebTwistedRequestAdapter, self).__init__(vreg, https, req.args)
+        super(CubicWebTwistedRequestAdapter, self).__init__(
+            vreg, https, req.args, headers=req.received_headers)
         for key, (name, stream) in req.files.iteritems():
             if name is None:
                 self.form[key] = (name, stream)
             else:
                 self.form[key] = (unicode(name, self.encoding), stream)
-        # XXX can't we keep received_headers?
-        self._headers_in = Headers()
-        for k, v in req.received_headers.iteritems():
-            self._headers_in.addRawHeader(k, v)
-
-    def base_url(self):
-        """return the root url of the instance"""
-        return self._base_url
 
     def http_method(self):
         """returns 'POST', 'GET', 'HEAD', etc."""
@@ -65,56 +56,3 @@
         if not includeparams:
             path = path.split('?', 1)[0]
         return path
-
-    def get_header(self, header, default=None, raw=True):
-        """return the value associated with the given input header, raise
-        KeyError if the header is not set
-        """
-        if raw:
-            return self._headers_in.getRawHeaders(header, [default])[0]
-        return self._headers_in.getHeader(header, default)
-
-    def _validate_cache(self):
-        """raise a `DirectResponse` exception if a cached page along the way
-        exists and is still usable
-        """
-        if self.get_header('Cache-Control') in ('max-age=0', 'no-cache'):
-            # Expires header seems to be required by IE7
-            self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
-            return
-        # when using both 'Last-Modified' and 'ETag' response headers
-        # (i.e. using respectively If-Modified-Since and If-None-Match request
-        # headers, see
-        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4 for
-        # reference
-        last_modified = self.headers_out.getHeader('last-modified')
-        if last_modified is not None:
-            status = self._twreq.setLastModified(last_modified)
-            if status != http.CACHED:
-                return
-        etag = self.headers_out.getRawHeaders('etag')
-        if etag is not None:
-            status = self._twreq.setETag(etag[0])
-            if status == http.CACHED:
-                response = not_modified_response(self._twreq, self._headers_in)
-                raise DirectResponse(response)
-        # Expires header seems to be required by IE7
-        self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
-
-    def header_accept_language(self):
-        """returns an ordered list of preferred languages"""
-        acceptedlangs = self.get_header('Accept-Language', raw=False) or {}
-        for lang, _ in sorted(acceptedlangs.iteritems(), key=lambda x: x[1],
-                              reverse=True):
-            lang = lang.split('-')[0]
-            yield lang
-
-    def header_if_modified_since(self):
-        """If the HTTP header If-modified-since is set, return the equivalent
-        date time value (GMT), else return None
-        """
-        mtime = self.get_header('If-modified-since', raw=False)
-        if mtime:
-            # :/ twisted is returned a localized time stamp
-            return datetime.fromtimestamp(mtime) + GMTOFFSET
-        return None
--- a/etwist/server.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/etwist/server.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -48,7 +48,7 @@
 from cubicweb import (AuthenticationError, ConfigurationError,
                       CW_EVENT_MANAGER, CubicWebException)
 from cubicweb.utils import json_dumps
-from cubicweb.web import Redirect, DirectResponse, StatusResponse, LogOut
+from cubicweb.web import DirectResponse
 from cubicweb.web.application import CubicWebPublisher
 from cubicweb.web.http_headers import generateDateTime
 from cubicweb.etwist.request import CubicWebTwistedRequestAdapter
@@ -57,7 +57,7 @@
 def start_task(interval, func):
     lc = task.LoopingCall(func)
     # wait until interval has expired to actually start the task, else we have
-    # to wait all task to be finished for the server to be actually started
+    # to wait all tasks to be finished for the server to be actually started
     lc.start(interval, now=False)
 
 def host_prefixed_baseurl(baseurl, host):
@@ -69,166 +69,6 @@
     return baseurl
 
 
-class ForbiddenDirectoryLister(resource.Resource):
-    def render(self, request):
-        return HTTPResponse(twisted_request=request,
-                            code=http.FORBIDDEN,
-                            stream='Access forbidden')
-
-
-class NoListingFile(static.File):
-    def __init__(self, config, path=None):
-        if path is None:
-            path = config.static_directory
-        static.File.__init__(self, path)
-        self.config = config
-
-    def set_expires(self, request):
-        if not self.config.debugmode:
-            # XXX: Don't provide additional resource information to error responses
-            #
-            # the HTTP RFC recommands not going further than 1 year ahead
-            expires = date.today() + timedelta(days=6*30)
-            request.setHeader('Expires', generateDateTime(mktime(expires.timetuple())))
-
-    def directoryListing(self):
-        return ForbiddenDirectoryLister()
-
-
-class DataLookupDirectory(NoListingFile):
-    def __init__(self, config, path):
-        self.md5_version = config.instance_md5_version()
-        NoListingFile.__init__(self, config, path)
-        self.here = path
-        self._defineChildResources()
-        if self.config.debugmode:
-            self.data_modconcat_basepath = '/data/??'
-        else:
-            self.data_modconcat_basepath = '/data/%s/??' % self.md5_version
-
-    def _defineChildResources(self):
-        self.putChild(self.md5_version, self)
-
-    def getChild(self, path, request):
-        if not path:
-            uri = request.uri
-            if uri.startswith('/https/'):
-                uri = uri[6:]
-            if uri.startswith(self.data_modconcat_basepath):
-                resource_relpath = uri[len(self.data_modconcat_basepath):]
-                if resource_relpath:
-                    paths = resource_relpath.split(',')
-                    try:
-                        self.set_expires(request)
-                        return ConcatFiles(self.config, paths)
-                    except ConcatFileNotFoundError:
-                        return self.childNotFound
-            return self.directoryListing()
-        childpath = join(self.here, path)
-        dirpath, rid = self.config.locate_resource(childpath)
-        if dirpath is None:
-            # resource not found
-            return self.childNotFound
-        filepath = os.path.join(dirpath, rid)
-        if os.path.isdir(filepath):
-            resource = DataLookupDirectory(self.config, childpath)
-            # cache resource for this segment path to avoid recomputing
-            # directory lookup
-            self.putChild(path, resource)
-            return resource
-        else:
-            self.set_expires(request)
-            return NoListingFile(self.config, filepath)
-
-
-class FCKEditorResource(NoListingFile):
-
-    def getChild(self, path, request):
-        pre_path = request.path.split('/')[1:]
-        if pre_path[0] == 'https':
-            pre_path.pop(0)
-            uiprops = self.config.https_uiprops
-        else:
-            uiprops = self.config.uiprops
-        return static.File(osp.join(uiprops['FCKEDITOR_PATH'], path))
-
-
-class LongTimeExpiringFile(DataLookupDirectory):
-    """overrides static.File and sets a far future ``Expires`` date
-    on the resouce.
-
-    versions handling is done by serving static files by different
-    URLs for each version. For instance::
-
-      http://localhost:8080/data-2.48.2/cubicweb.css
-      http://localhost:8080/data-2.49.0/cubicweb.css
-      etc.
-
-    """
-    def _defineChildResources(self):
-        pass
-
-
-class ConcatFileNotFoundError(CubicWebException):
-    pass
-
-
-class ConcatFiles(LongTimeExpiringFile):
-    def __init__(self, config, paths):
-        _, ext = osp.splitext(paths[0])
-        self._resources = {}
-        # create a unique / predictable filename. We don't consider cubes
-        # version since uicache is cleared at server startup, and file's dates
-        # are checked in debug mode
-        fname = 'cache_concat_' + md5(';'.join(paths)).hexdigest() + ext
-        filepath = osp.join(config.appdatahome, 'uicache', fname)
-        LongTimeExpiringFile.__init__(self, config, filepath)
-        self._concat_cached_filepath(filepath, paths)
-
-    def _resource(self, path):
-        try:
-            return self._resources[path]
-        except KeyError:
-            self._resources[path] = self.config.locate_resource(path)
-            return self._resources[path]
-
-    def _concat_cached_filepath(self, filepath, paths):
-        if not self._up_to_date(filepath, paths):
-            with open(filepath, 'wb') as f:
-                for path in paths:
-                    dirpath, rid = self._resource(path)
-                    if rid is None:
-                        # In production mode log an error, do not return a 404
-                        # XXX the erroneous content is cached anyway
-                        LOGGER.error('concatenated data url error: %r file '
-                                     'does not exist', path)
-                        if self.config.debugmode:
-                            raise ConcatFileNotFoundError(path)
-                    else:
-                        for line in open(osp.join(dirpath, rid)):
-                            f.write(line)
-                        f.write('\n')
-
-    def _up_to_date(self, filepath, paths):
-        """
-        The concat-file is considered up-to-date if it exists.
-        In debug mode, an additional check is performed to make sure that
-        concat-file is more recent than all concatenated files
-        """
-        if not osp.isfile(filepath):
-            return False
-        if self.config.debugmode:
-            concat_lastmod = os.stat(filepath).st_mtime
-            for path in paths:
-                dirpath, rid = self._resource(path)
-                if rid is None:
-                    raise ConcatFileNotFoundError(path)
-                path = osp.join(dirpath, rid)
-                if os.stat(path).st_mtime > concat_lastmod:
-                    return False
-        return True
-
-
 class CubicWebRootResource(resource.Resource):
     def __init__(self, config, vreg=None):
         resource.Resource.__init__(self)
@@ -240,9 +80,6 @@
         self.https_url = config['https-url']
         global MAX_POST_LENGTH
         MAX_POST_LENGTH = config['max-post-length']
-        self.putChild('static', NoListingFile(config))
-        self.putChild('fckeditor', FCKEditorResource(self.config, ''))
-        self.putChild('data', DataLookupDirectory(self.config, ''))
 
     def init_publisher(self):
         config = self.config
@@ -320,88 +157,28 @@
         host = request.host
         # dual http/https access handling: expect a rewrite rule to prepend
         # 'https' to the path to detect https access
+        https = False
         if origpath.split('/', 2)[1] == 'https':
             origpath = origpath[6:]
             request.uri = request.uri[6:]
             https = True
-            baseurl = self.https_url or self.base_url
-        else:
-            https = False
-            baseurl = self.base_url
-        if self.config['use-request-subdomain']:
-            baseurl = host_prefixed_baseurl(baseurl, host)
-            self.warning('used baseurl is %s for this request', baseurl)
-        req = CubicWebTwistedRequestAdapter(request, self.appli.vreg, https, baseurl)
-        if req.authmode == 'http':
-            # activate realm-based auth
-            realm = self.config['realm']
-            req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
-        try:
-            self.appli.connect(req)
-        except Redirect, ex:
-            return self.redirect(request=req, location=ex.location)
-        if https and req.session.anonymous_session and self.config['https-deny-anonymous']:
-            # don't allow anonymous on https connection
-            return self.request_auth(request=req)
         if self.url_rewriter is not None:
             # XXX should occur before authentication?
-            try:
-                path = self.url_rewriter.rewrite(host, origpath, req)
-            except Redirect, ex:
-                return self.redirect(req, ex.location)
+            path = self.url_rewriter.rewrite(host, origpath, request)
             request.uri.replace(origpath, path, 1)
         else:
             path = origpath
-        if not path or path == "/":
-            path = 'view'
+        req = CubicWebTwistedRequestAdapter(request, self.appli.vreg, https)
         try:
-            result = self.appli.publish(path, req)
+            ### Try to generate the actual request content
+            content = self.appli.handle_request(req, path)
         except DirectResponse, ex:
             return ex.response
-        except StatusResponse, ex:
-            return HTTPResponse(stream=ex.content, code=ex.status,
-                                twisted_request=req._twreq,
-                                headers=req.headers_out)
-        except AuthenticationError:
-            return self.request_auth(request=req)
-        except LogOut, ex:
-            if self.config['auth-mode'] == 'cookie' and ex.url:
-                return self.redirect(request=req, location=ex.url)
-            # in http we have to request auth to flush current http auth
-            # information
-            return self.request_auth(request=req, loggedout=True)
-        except Redirect, ex:
-            return self.redirect(request=req, location=ex.location)
-        # request may be referenced by "onetime callback", so clear its entity
-        # cache to avoid memory usage
-        req.drop_entity_cache()
-        return HTTPResponse(twisted_request=req._twreq, code=http.OK,
-                            stream=result, headers=req.headers_out)
-
-    def redirect(self, request, location):
-        self.debug('redirecting to %s', str(location))
-        request.headers_out.setHeader('location', str(location))
-        # 303 See other
-        return HTTPResponse(twisted_request=request._twreq, code=303,
-                            headers=request.headers_out)
-
-    def request_auth(self, request, loggedout=False):
-        if self.https_url and request.base_url() != self.https_url:
-            return self.redirect(request, self.https_url + 'login')
-        if self.config['auth-mode'] == 'http':
-            code = http.UNAUTHORIZED
-        else:
-            code = http.FORBIDDEN
-        if loggedout:
-            if request.https:
-                request._base_url =  self.base_url
-                request.https = False
-            content = self.appli.loggedout_content(request)
-        else:
-            content = self.appli.need_login_content(request)
-        return HTTPResponse(twisted_request=request._twreq,
-                            stream=content, code=code,
-                            headers=request.headers_out)
+        # at last: create twisted object
+        return HTTPResponse(code    = req.status_out,
+                            headers = req.headers_out,
+                            stream  = content,
+                            twisted_request=req._twreq)
 
     # these are overridden by set_log_methods below
     # only defining here to prevent pylint from complaining
--- a/etwist/test/unittest_server.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/etwist/test/unittest_server.py	Tue Oct 23 15:00:53 2012 +0200
@@ -19,8 +19,7 @@
 import os, os.path as osp, glob
 
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.etwist.server import (host_prefixed_baseurl, ConcatFiles,
-                                    ConcatFileNotFoundError)
+from cubicweb.etwist.server import host_prefixed_baseurl
 
 
 class HostPrefixedBaseURLTC(CubicWebTC):
@@ -54,30 +53,6 @@
         self._check('http://localhost:8080/hg/', 'code.cubicweb.org',
                     'http://localhost:8080/hg/')
 
-
-class ConcatFilesTC(CubicWebTC):
-
-    def tearDown(self):
-        super(ConcatFilesTC, self).tearDown()
-        self._cleanup_concat_cache()
-        self.config.debugmode = False
-
-    def _cleanup_concat_cache(self):
-        uicachedir = osp.join(self.config.apphome, 'uicache')
-        for fname in glob.glob(osp.join(uicachedir, 'cache_concat_*')):
-            os.unlink(osp.join(uicachedir, fname))
-
-    def test_cache(self):
-        concat = ConcatFiles(self.config, ('cubicweb.ajax.js', 'jquery.js'))
-        self.assertTrue(osp.isfile(concat.path))
-
-    def test_404(self):
-        # when not in debug mode, should not crash
-        ConcatFiles(self.config, ('cubicweb.ajax.js', 'dummy.js'))
-        # in debug mode, raise error
-        self.config.debugmode = True
-        try:
-            self.assertRaises(ConcatFileNotFoundError, ConcatFiles, self.config,
-                              ('cubicweb.ajax.js', 'dummy.js'))
-        finally:
-            self.config.debugmode = False
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/ext/tal.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/ext/tal.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -261,7 +261,7 @@
         return wrapped
 
     def _compiled_template(self, instance):
-        for fileordirectory in instance.config.vregistry_path():
+        for fileordirectory in instance.config.appobjects_path():
             filepath = join(fileordirectory, self.filename)
             if isdir(fileordirectory) and exists(filepath):
                 return compile_template_file(filepath)
--- a/ext/test/unittest_rest.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/ext/test/unittest_rest.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,9 +15,6 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
 from logilab.common.testlib import unittest_main
 from cubicweb.devtools.testlib import CubicWebTC
 
@@ -60,23 +57,23 @@
     def test_rql_role_with_vid(self):
         context = self.context()
         out = rest_publish(context, ':rql:`Any X WHERE X is CWUser:table`')
-        self.assert_(out.endswith('<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a>'
-                                  '</td></tr></tbody></table></div>\n</div>\n</p>\n'))
+        self.assertTrue(out.endswith('<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a>'
+                                     '</td></tr>\n</tbody></table></div></p>\n'))
 
     def test_rql_role_with_vid_empty_rset(self):
         context = self.context()
         out = rest_publish(context, ':rql:`Any X WHERE X is CWUser, X login "nono":table`')
-        self.assert_(out.endswith('<p><div class="searchMessage"><strong>No result matching query</strong></div>\n</p>\n'))
+        self.assertTrue(out.endswith('<p><div class="searchMessage"><strong>No result matching query</strong></div>\n</p>\n'))
 
     def test_rql_role_with_unknown_vid(self):
         context = self.context()
         out = rest_publish(context, ':rql:`Any X WHERE X is CWUser:toto`')
-        self.assert_(out.startswith("<p>an error occured while interpreting this rql directive: ObjectNotFound(u'toto',)</p>"))
+        self.assertTrue(out.startswith("<p>an error occured while interpreting this rql directive: ObjectNotFound(u'toto',)</p>"))
 
     def test_rql_role_without_vid(self):
         context = self.context()
         out = rest_publish(context, ':rql:`Any X WHERE X is CWUser`')
-        self.assertEqual(out, u'<p><h1>cwuser_plural</h1><div class="section"><a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div><div class="section"><a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div></p>\n')
+        self.assertEqual(out, u'<p><h1>CWUser_plural</h1><div class="section"><a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div><div class="section"><a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div></p>\n')
 
 if __name__ == '__main__':
     unittest_main()
--- a/hooks/__init__.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/__init__.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -46,7 +46,7 @@
                 session.commit()
             finally:
                 session.close()
-        if self.repo.config['undo-support']:
+        if self.repo.config['undo-enabled']:
             self.repo.looping_task(60*60*24, cleanup_old_transactions,
                                    self.repo)
         def update_feeds(repo):
--- a/hooks/bookmark.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/bookmark.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/hooks/email.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/email.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""hooks to ensure use_email / primary_email relations consistency
+"""hooks to ensure use_email / primary_email relations consistency"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.server import hook
--- a/hooks/integrity.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/integrity.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -20,12 +20,11 @@
 """
 
 __docformat__ = "restructuredtext en"
+_ = unicode
 
 from threading import Lock
 
-from yams.schema import role_name
-
-from cubicweb import ValidationError
+from cubicweb import validation_error
 from cubicweb.schema import (META_RTYPES, WORKFLOW_RTYPES,
                              RQLConstraint, RQLUniqueConstraint)
 from cubicweb.predicates import is_instance
@@ -87,11 +86,11 @@
                 continue
             if not session.execute(self.base_rql % rtype, {'x': eid}):
                 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})
+                raise validation_error(eid, {(rtype, self.role): msg},
+                                       {'rtype': rtype, 'etype': etype, 'eid': eid},
+                                       ['rtype', 'etype'])
 
 
 class _CheckSRelationOp(_CheckRequiredRelationOperation):
@@ -231,9 +230,9 @@
                 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
                 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')
-                    qname = role_name(attr, 'subject')
-                    raise ValidationError(entity.eid, {qname: msg % val})
+                    msg = _('the value "%s" is already used, use another one')
+                    raise validation_error(entity, {(attr, 'subject'): msg},
+                                           (val,))
 
 
 class DontRemoveOwnersGroupHook(IntegrityHook):
@@ -246,15 +245,12 @@
     def __call__(self):
         entity = self.entity
         if self.event == 'before_delete_entity' and entity.name == 'owners':
-            msg = self._cw._('can\'t be deleted')
-            raise ValidationError(entity.eid, {None: msg})
+            raise validation_error(entity, {None: _("can't be deleted")})
         elif self.event == 'before_update_entity' \
                  and 'name' in entity.cw_edited:
             oldname, newname = entity.cw_edited.oldnewvalue('name')
             if oldname == 'owners' and newname != oldname:
-                qname = role_name('name', 'subject')
-                msg = self._cw._('can\'t be changed')
-                raise ValidationError(entity.eid, {qname: msg})
+                raise validation_error(entity, {('name', 'subject'): _("can't be changed")})
 
 
 class TidyHtmlFields(IntegrityHook):
@@ -301,11 +297,10 @@
     def precommit_event(self):
         session = self.session
         pendingeids = session.transaction_data.get('pendingeids', ())
-        neweids = session.transaction_data.get('neweids', ())
         eids_by_etype_rtype = {}
         for eid, rtype in self.get_data():
-            # don't do anything if the entity is being created or deleted
-            if not (eid in pendingeids or eid in neweids):
+            # don't do anything if the entity is being deleted
+            if eid not in pendingeids:
                 etype = session.describe(eid)[0]
                 key = (etype, rtype)
                 if key not in eids_by_etype_rtype:
--- a/hooks/metadata.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/metadata.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -199,17 +199,12 @@
             entity = self._cw.entity_from_eid(self.eidfrom)
             # copy entity if necessary
             if not oldsource.repo_source.copy_based_source:
-                entity.complete(skip_bytes=False)
+                entity.complete(skip_bytes=False, skip_pwd=False)
                 if not entity.creation_date:
                     entity.cw_attr_cache['creation_date'] = datetime.now()
                 if not entity.modification_date:
                     entity.cw_attr_cache['modification_date'] = datetime.now()
                 entity.cw_attr_cache['cwuri'] = u'%s%s' % (self._cw.base_url(), entity.eid)
-                for rschema, attrschema in entity.e_schema.attribute_definitions():
-                    if attrschema == 'Password' and \
-                       rschema.rdef(entity.e_schema, attrschema).cardinality[0] == '1':
-                        from logilab.common.shellutils import generate_password
-                        entity.cw_attr_cache[rschema.type] = generate_password()
                 entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
                 syssource.add_entity(self._cw, entity)
             # we don't want the moved entity to be reimported later.  To
--- a/hooks/syncschema.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/syncschema.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -24,6 +24,7 @@
 """
 
 __docformat__ = "restructuredtext en"
+_ = unicode
 
 from copy import copy
 from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema
@@ -31,7 +32,7 @@
 
 from logilab.common.decorators import clear_cache
 
-from cubicweb import ValidationError
+from cubicweb import validation_error
 from cubicweb.predicates import is_instance
 from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
                              CONSTRAINTS, ETYPE_NAME_MAP, display_name)
@@ -127,10 +128,9 @@
         if attr in ro_attrs:
             origval, newval = entity.cw_edited.oldnewvalue(attr)
             if newval != origval:
-                errors[attr] = session._("can't change the %s attribute") % \
-                               display_name(session, attr)
+                errors[attr] = _("can't change this attribute")
     if errors:
-        raise ValidationError(entity.eid, errors)
+        raise validation_error(entity, errors)
 
 
 class _MockEntity(object): # XXX use a named tuple with python 2.6
@@ -755,7 +755,13 @@
         cols = ['%s%s' % (prefix, c) for c in self.cols]
         sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols)
         for sql in sqls:
-            session.system_sql(sql)
+            try:
+                session.system_sql(sql)
+            except Exception, exc: # should be ProgrammingError
+                if sql.startswith('DROP'):
+                    self.error('execute of `%s` failed (cause: %s)', sql, exc)
+                    continue
+                raise
 
     # XXX revertprecommit_event
 
@@ -907,7 +913,7 @@
         # final entities can't be deleted, don't care about that
         name = self.entity.name
         if name in CORE_TYPES:
-            raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
+            raise validation_error(self.entity, {None: _("can't be deleted")})
         # delete every entities of this type
         if name not in ETYPE_NAME_MAP:
             self._cw.execute('DELETE %s X' % name)
@@ -977,7 +983,7 @@
     def __call__(self):
         name = self.entity.name
         if name in CORE_TYPES:
-            raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
+            raise validation_error(self.entity, {None: _("can't be deleted")})
         # delete relation definitions using this relation type
         self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
                         {'x': self.entity.eid})
--- a/hooks/syncsession.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/syncsession.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -18,9 +18,9 @@
 """Core hooks: synchronize living session on persistent data changes"""
 
 __docformat__ = "restructuredtext en"
+_ = unicode
 
-from yams.schema import role_name
-from cubicweb import UnknownProperty, ValidationError, BadConnectionId
+from cubicweb import UnknownProperty, BadConnectionId, validation_error
 from cubicweb.predicates import is_instance
 from cubicweb.server import hook
 
@@ -165,13 +165,11 @@
         try:
             value = session.vreg.typed_value(key, value)
         except UnknownProperty:
-            qname = role_name('pkey', 'subject')
-            msg = session._('unknown property key %s') % key
-            raise ValidationError(self.entity.eid, {qname: msg})
+            msg = _('unknown property key %s')
+            raise validation_error(self.entity, {('pkey', 'subject'): msg}, (key,))
         except ValueError, ex:
-            qname = role_name('value', 'subject')
-            raise ValidationError(self.entity.eid,
-                                  {qname: session._(str(ex))})
+            raise validation_error(self.entity,
+                                  {('value', 'subject'): str(ex)})
         if not session.user.matching_groups('managers'):
             session.add_relation(self.entity.eid, 'for_user', session.user.eid)
         else:
@@ -196,8 +194,7 @@
         except UnknownProperty:
             return
         except ValueError, ex:
-            qname = role_name('value', 'subject')
-            raise ValidationError(entity.eid, {qname: session._(str(ex))})
+            raise validation_error(entity, {('value', 'subject'): 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,
@@ -237,10 +234,8 @@
         key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
                                      {'x': eidfrom})[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,
-                                  {qname: msg})
+            msg = _("site-wide property can't be set for user")
+            raise validation_error(eidfrom, {('for_user', 'subject'): msg})
         for session_ in get_user_sessions(session.repo, self.eidto):
             _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
                               key=key, value=value)
--- a/hooks/syncsources.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/syncsources.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2010-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -17,12 +17,13 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """hooks for repository sources synchronization"""
 
+_ = unicode
+
 from socket import gethostname
 
 from logilab.common.decorators import clear_cache
-from yams.schema import role_name
 
-from cubicweb import ValidationError
+from cubicweb import validation_error
 from cubicweb.predicates import is_instance
 from cubicweb.server import SOURCE_TYPES, hook
 
@@ -46,12 +47,15 @@
         try:
             sourcecls = SOURCE_TYPES[self.entity.type]
         except KeyError:
-            msg = self._cw._('unknown source type')
-            raise ValidationError(self.entity.eid,
-                                  {role_name('type', 'subject'): msg})
-        sourcecls.check_conf_dict(self.entity.eid, self.entity.host_config,
-                                  fail_if_unknown=not self._cw.vreg.config.repairing)
-        SourceAddedOp(self._cw, entity=self.entity)
+            msg = _('Unknown source type')
+            raise validation_error(self.entity, {('type', 'subject'): msg})
+        # ignore creation of the system source done during database
+        # initialisation, as config for this source is in a file and handling
+        # is done separatly (no need for the operation either)
+        if self.entity.name != 'system':
+            sourcecls.check_conf_dict(self.entity.eid, self.entity.host_config,
+                                      fail_if_unknown=not self._cw.vreg.config.repairing)
+            SourceAddedOp(self._cw, entity=self.entity)
 
 
 class SourceRemovedOp(hook.Operation):
@@ -65,7 +69,8 @@
     events = ('before_delete_entity',)
     def __call__(self):
         if self.entity.name == 'system':
-            raise ValidationError(self.entity.eid, {None: 'cant remove system source'})
+            msg = _("You cannot remove the system source")
+            raise validation_error(self.entity, {None: msg})
         SourceRemovedOp(self._cw, uri=self.entity.name)
 
 
@@ -116,11 +121,18 @@
     __select__ = SourceHook.__select__ & is_instance('CWSource')
     events = ('before_update_entity',)
     def __call__(self):
-        if 'config' in self.entity.cw_edited:
-            SourceConfigUpdatedOp.get_instance(self._cw).add_data(self.entity)
         if 'name' in self.entity.cw_edited:
             oldname, newname = self.entity.cw_edited.oldnewvalue('name')
+            if oldname == 'system':
+                msg = _("You cannot rename the system source")
+                raise validation_error(self.entity, {('name', 'subject'): msg})
             SourceRenamedOp(self._cw, oldname=oldname, newname=newname)
+        if 'config' in self.entity.cw_edited:
+            if self.entity.name == 'system' and self.entity.config:
+                msg = _("Configuration of the system source goes to "
+                        "the 'sources' file, not in the database")
+                raise validation_error(self.entity, {('config', 'subject'): msg})
+            SourceConfigUpdatedOp.get_instance(self._cw).add_data(self.entity)
 
 
 class SourceHostConfigUpdatedHook(SourceHook):
@@ -154,8 +166,8 @@
     events = ('before_add_relation',)
     def __call__(self):
         if not self._cw.added_in_transaction(self.eidfrom):
-            msg = self._cw._("can't change this relation")
-            raise ValidationError(self.eidfrom, {self.rtype: msg})
+            msg = _("You can't change this relation")
+            raise validation_error(self.eidfrom, {self.rtype: msg})
 
 
 class SourceMappingChangedOp(hook.DataOperationMixIn, hook.Operation):
--- a/hooks/test/unittest_hooks.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/test/unittest_hooks.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -67,10 +67,9 @@
         entity = self.request().create_entity('Workflow', name=u'wf1',
                                               description_format=u'text/html',
                                               description=u'yo')
-        entity.set_attributes(name=u'wf2')
+        entity.cw_set(name=u'wf2')
         self.assertEqual(entity.description, u'yo')
-        entity.set_attributes(description=u'R&D<p>yo')
-        entity.cw_attr_cache.pop('description')
+        entity.cw_set(description=u'R&D<p>yo')
         self.assertEqual(entity.description, u'R&amp;D<p>yo</p>')
 
     def test_metadata_cwuri(self):
@@ -171,6 +170,7 @@
         try:
             self.execute('INSERT CWUser X: X login "admin"')
         except ValidationError, ex:
+            ex.tr(unicode)
             self.assertIsInstance(ex.entity, int)
             self.assertEqual(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
 
--- a/hooks/test/unittest_syncschema.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/test/unittest_syncschema.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -294,7 +294,7 @@
     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)
+        target.cw_set(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])
--- a/hooks/test/unittest_syncsession.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/test/unittest_syncsession.py	Tue Oct 23 15:00:53 2012 +0200
@@ -31,9 +31,11 @@
     def test_unexistant_cwproperty(self):
         with self.assertRaises(ValidationError) as cm:
             self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U')
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
         with self.assertRaises(ValidationError) as cm:
             self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
 
     def test_site_wide_cwproperty(self):
--- a/hooks/workflow.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/workflow.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -18,12 +18,12 @@
 """Core hooks: workflow related hooks"""
 
 __docformat__ = "restructuredtext en"
+_ = unicode
 
 from datetime import datetime
 
-from yams.schema import role_name
 
-from cubicweb import RepositoryError, ValidationError
+from cubicweb import RepositoryError, validation_error
 from cubicweb.predicates import is_instance, adaptable
 from cubicweb.server import hook
 
@@ -92,9 +92,8 @@
         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, {qname: msg})
+                msg = _('workflow has no initial state')
+                raise validation_error(entity, {('custom_workflow', 'subject'): msg})
             if mainwf.state_by_eid(iworkflowable.current_state.eid):
                 # nothing to do
                 return
@@ -119,9 +118,8 @@
         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, {qname: msg})
+                msg = _("can't have multiple exits on the same state")
+                raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg})
             outputs.add(ep.subwf_state.eid)
 
 
@@ -137,13 +135,12 @@
         wftr = iworkflowable.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})
+            msg = _("state doesn't belong to entity's current workflow")
+            raise validation_error(self.trinfo, {('to_state', 'subject'): msg})
         tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state'])
         if tostate is not None:
             # reached an exit point
-            msg = session._('exiting from subworkflow %s')
+            msg = _('exiting from subworkflow %s')
             msg %= session._(iworkflowable.current_workflow.name)
             session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
             iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
@@ -186,9 +183,8 @@
         try:
             foreid = entity.cw_attr_cache['wf_info_for']
         except KeyError:
-            qname = role_name('wf_info_for', 'subject')
-            msg = session._('mandatory relation')
-            raise ValidationError(entity.eid, {qname: msg})
+            msg = _('mandatory relation')
+            raise validation_error(entity, {('wf_info_for', 'subject'): msg})
         forentity = session.entity_from_eid(foreid)
         # see comment in the TrInfo entity definition
         entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for)
@@ -201,13 +197,13 @@
         else:
             wf = iworkflowable.current_workflow
         if wf is None:
-            msg = session._('related entity has no workflow set')
-            raise ValidationError(entity.eid, {None: msg})
+            msg = _('related entity has no workflow set')
+            raise validation_error(entity, {None: msg})
         # then check it has a state set
         fromstate = iworkflowable.current_state
         if fromstate is None:
-            msg = session._('related entity has no state')
-            raise ValidationError(entity.eid, {None: msg})
+            msg = _('related entity has no state')
+            raise validation_error(entity, {None: msg})
         # True if we are coming back from subworkflow
         swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
         cowpowers = (session.user.is_in_group('managers')
@@ -219,47 +215,42 @@
             # 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, {qname: msg})
+                msg = _('mandatory relation')
+                raise validation_error(entity, {('by_transition', 'subject'): msg})
             deststateeid = entity.cw_attr_cache.get('to_state')
             if not deststateeid:
-                qname = role_name('by_transition', 'subject')
-                msg = session._('mandatory relation')
-                raise ValidationError(entity.eid, {qname: msg})
+                msg = _('mandatory relation')
+                raise validation_error(entity, {('by_transition', 'subject'): 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, {qname: msg})
+                msg = _("state doesn't belong to entity's workflow")
+                raise validation_error(entity, {('to_state', 'subject'): 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')
+                qname = ('by_transition', 'subject')
                 if tr is None:
-                    msg = session._("transition doesn't belong to entity's workflow")
-                    raise ValidationError(entity.eid, {qname: msg})
+                    msg = _("transition doesn't belong to entity's workflow")
+                    raise validation_error(entity, {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, {qname: msg})
+                    msg = _("transition %(tr)s isn't allowed from %(st)s")
+                    raise validation_error(entity, {qname: msg}, {
+                            'tr': tr.name, 'st': fromstate.name}, ['tr', 'st'])
                 if not tr.may_be_fired(foreid):
-                    msg = session._("transition may not be fired")
-                    raise ValidationError(entity.eid, {qname: msg})
+                    msg = _("transition may not be fired")
+                    raise validation_error(entity, {qname: msg})
             deststateeid = entity.cw_attr_cache.get('to_state')
             if deststateeid is not None:
                 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, {qname: msg})
+                    msg = _("transition isn't allowed")
+                    raise validation_error(entity, {('by_transition', 'subject'): 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, {qname: msg})
+                        msg = _("state doesn't belong to entity's workflow")
+                        raise validation_error(entity, {('to_state', 'subject'): msg})
             else:
                 deststateeid = tr.destination(forentity).eid
         # everything is ok, add missing information on the trinfo entity
@@ -307,20 +298,18 @@
         iworkflowable = entity.cw_adapt_to('IWorkflowable')
         mainwf = iworkflowable.main_workflow
         if mainwf is None:
-            msg = session._('entity has no workflow set')
-            raise ValidationError(entity.eid, {None: msg})
+            msg = _('entity has no workflow set')
+            raise validation_error(entity, {None: msg})
         for wf in mainwf.iter_workflows():
             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, {qname: msg})
+            msg = _("state doesn't belong to entity's workflow. You may "
+                    "want to set a custom workflow for this entity first.")
+            raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
         if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
-            qname = role_name('in_state', 'subject')
-            msg = session._("state doesn't belong to entity's current workflow")
-            raise ValidationError(self.eidfrom, {qname: msg})
+            msg = _("state doesn't belong to entity's current workflow")
+            raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
 
 
 class SetModificationDateOnStateChange(WorkflowHook):
@@ -335,7 +324,7 @@
             return
         entity = self._cw.entity_from_eid(self.eidfrom)
         try:
-            entity.set_attributes(modification_date=datetime.now())
+            entity.cw_set(modification_date=datetime.now())
         except RepositoryError, ex:
             # usually occurs if entity is coming from a read-only source
             # (eg ldap user)
--- a/hooks/zmq.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/zmq.py	Tue Oct 23 15:00:53 2012 +0200
@@ -46,3 +46,30 @@
             self.repo.app_instances_bus.add_subscriber(address)
         self.repo.app_instances_bus.start()
 
+
+class ZMQRepositoryServerStopHook(hook.Hook):
+    __regid__ = 'zmqrepositoryserverstop'
+    events = ('server_shutdown',)
+
+    def __call__(self):
+        server = getattr(self.repo, 'zmq_repo_server', None)
+        if server:
+            self.repo.zmq_repo_server.quit()
+
+class ZMQRepositoryServerStartHook(hook.Hook):
+    __regid__ = 'zmqrepositoryserverstart'
+    events = ('server_startup',)
+
+    def __call__(self):
+        config = self.repo.config
+        if config.name == 'repository':
+            # start-repository command already starts a zmq repo
+            return
+        address = config.get('zmq-repository-address')
+        if not address:
+            return
+        from cubicweb.server import cwzmq
+        self.repo.zmq_repo_server = server = cwzmq.ZMQRepositoryServer(self.repo)
+        server.connect(address)
+        self.repo.threaded_task(server.run)
+
--- a/i18n/de.po	Wed Feb 22 11:57:42 2012 +0100
+++ b/i18n/de.po	Tue Oct 23 15:00:53 2012 +0200
@@ -145,6 +145,10 @@
 msgid "(UNEXISTANT EID)"
 msgstr "(EID nicht gefunden)"
 
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr ""
+
 msgid "**"
 msgstr "0..n 0..n"
 
@@ -218,6 +222,10 @@
 msgid "About this site"
 msgstr "Ãœber diese Seite"
 
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
 msgid "Any"
 msgstr "irgendein"
 
@@ -263,6 +271,10 @@
 msgid "Browse by entity type"
 msgstr "nach Identitätstyp navigieren"
 
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr ""
+
 msgid "Bytes"
 msgstr "Bytes"
 
@@ -390,14 +402,6 @@
 
 #, python-format
 msgid ""
-"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
-"exists anymore in the schema."
-msgstr ""
-"Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, "
-"diese Relation existiert nicht mehr in dem Schema."
-
-#, python-format
-msgid ""
 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
 "anymore."
 msgstr ""
@@ -423,6 +427,10 @@
 msgid "Click to sort on this column"
 msgstr ""
 
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "DEBUG"
 msgstr ""
 
@@ -448,6 +456,14 @@
 msgid "Decimal_plural"
 msgstr "Dezimalzahlen"
 
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Detected problems"
 msgstr ""
 
@@ -872,12 +888,22 @@
 msgid "URLs from which content will be imported. You can put one url per line"
 msgstr ""
 
+msgid "Undoable actions"
+msgstr ""
+
+msgid "Undoing"
+msgstr ""
+
 msgid "UniqueConstraint"
 msgstr "eindeutige Einschränkung"
 
 msgid "Unreachable objects"
 msgstr "unzugängliche Objekte"
 
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Used by:"
 msgstr "benutzt von:"
 
@@ -975,9 +1001,6 @@
 msgid "abstract base class for transitions"
 msgstr "abstrakte Basisklasse für Übergänge"
 
-msgid "action menu"
-msgstr ""
-
 msgid "action(s) on this selection"
 msgstr "Aktionen(en) bei dieser Auswahl"
 
@@ -1265,6 +1288,9 @@
 msgid "bad value"
 msgstr "Unzulässiger Wert"
 
+msgid "badly formatted url"
+msgstr ""
+
 msgid "base url"
 msgstr "Basis-URL"
 
@@ -1349,6 +1375,9 @@
 msgid "can not resolve entity types:"
 msgstr "Die Typen konnten nicht ermittelt werden:"
 
+msgid "can only have one url"
+msgstr ""
+
 msgid "can't be changed"
 msgstr "kann nicht geändert werden"
 
@@ -1386,6 +1415,22 @@
 
 #, python-format
 msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+
+#, python-format
+msgid ""
 "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
 "%(card)s"
 msgstr ""
@@ -2015,9 +2060,6 @@
 msgid "date"
 msgstr "Datum"
 
-msgid "day"
-msgstr ""
-
 msgid "deactivate"
 msgstr "deaktivieren"
 
@@ -2519,6 +2561,9 @@
 msgid "foaf"
 msgstr "FOAF"
 
+msgid "focus on this selection"
+msgstr ""
+
 msgid "follow"
 msgstr "dem Link folgen"
 
@@ -2672,6 +2717,9 @@
 msgid "has_text"
 msgstr "enthält Text"
 
+msgid "header-center"
+msgstr ""
+
 msgid "header-left"
 msgstr ""
 
@@ -3031,9 +3079,6 @@
 msgid "log in"
 msgstr "anmelden"
 
-msgid "log out first"
-msgstr "Melden Sie sich zuerst ab."
-
 msgid "login"
 msgstr "Anmeldung"
 
@@ -3136,9 +3181,6 @@
 msgid "monday"
 msgstr "Montag"
 
-msgid "month"
-msgstr ""
-
 msgid "more actions"
 msgstr "weitere Aktionen"
 
@@ -3241,6 +3283,9 @@
 msgid "new"
 msgstr "neu"
 
+msgid "next page"
+msgstr ""
+
 msgid "next_results"
 msgstr "weitere Ergebnisse"
 
@@ -3442,6 +3487,9 @@
 msgid "preferences"
 msgstr "Einstellungen"
 
+msgid "previous page"
+msgstr ""
+
 msgid "previous_results"
 msgstr "vorige Ergebnisse"
 
@@ -4028,6 +4076,10 @@
 msgid "there is no previous page"
 msgstr ""
 
+#, python-format
+msgid "there is no transaction #%s"
+msgstr ""
+
 msgid "this action is not reversible!"
 msgstr "Achtung! Diese Aktion ist unumkehrbar."
 
@@ -4111,15 +4163,15 @@
 msgid "to_state_object"
 msgstr "Ãœbergang zu diesem Zustand"
 
-msgid "today"
-msgstr ""
-
 msgid "todo_by"
 msgstr "zu erledigen bis"
 
 msgid "toggle check boxes"
 msgstr "Kontrollkästchen umkehren"
 
+msgid "toggle filter"
+msgstr "filter verbergen/zeigen"
+
 msgid "tr_count"
 msgstr ""
 
@@ -4238,6 +4290,9 @@
 msgid "unauthorized value"
 msgstr "ungültiger Wert"
 
+msgid "undefined user"
+msgstr ""
+
 msgid "undo"
 msgstr "rückgängig machen"
 
@@ -4265,6 +4320,9 @@
 msgid "unknown vocabulary:"
 msgstr "Unbekanntes Wörterbuch : "
 
+msgid "unsupported protocol"
+msgstr ""
+
 msgid "upassword"
 msgstr "Passwort"
 
@@ -4474,9 +4532,6 @@
 msgid "wednesday"
 msgstr "Mittwoch"
 
-msgid "week"
-msgstr "Woche"
-
 #, python-format
 msgid "welcome %s !"
 msgstr "Willkommen %s !"
@@ -4573,45 +4628,9 @@
 msgid "you should un-inline relation %s which is supported and may be crossed "
 msgstr ""
 
-#~ msgid "(loading ...)"
-#~ msgstr "(laden...)"
-
-#~ msgid "Schema of the data model"
-#~ msgstr "Schema des Datenmodells"
-
-#~ msgid "csv entities export"
-#~ msgstr "CSV-Export von Entitäten"
-
-#~ msgid "follow this link if javascript is deactivated"
-#~ msgstr "Folgen Sie diesem Link, falls Javascript deaktiviert ist."
-
 #~ msgid ""
-#~ "how to format date and time in the ui (\"man strftime\" for format "
-#~ "description)"
+#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does "
+#~ "not exists anymore in the schema."
 #~ msgstr ""
-#~ "Wie formatiert man das Datum Interface im (\"man strftime\" für die "
-#~ "Beschreibung des neuen Formats"
-
-#~ msgid ""
-#~ "how to format date in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "Wie formatiert man das Datum im Interface (\"man strftime\" für die "
-#~ "Beschreibung des Formats)"
-
-#~ msgid ""
-#~ "how to format time in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "Wie man die Uhrzeit im Interface (\"man strftime\" für die "
-#~ "Formatbeschreibung)"
-
-#~ msgid "instance schema"
-#~ msgstr "Schema der Instanz"
-
-#~ msgid "rss"
-#~ msgstr "RSS"
-
-#~ msgid "xbel"
-#~ msgstr "XBEL"
-
-#~ msgid "xml"
-#~ msgstr "XML"
+#~ "Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, "
+#~ "diese Relation existiert nicht mehr in dem Schema."
--- a/i18n/en.po	Wed Feb 22 11:57:42 2012 +0100
+++ b/i18n/en.po	Tue Oct 23 15:00:53 2012 +0200
@@ -137,6 +137,10 @@
 msgid "(UNEXISTANT EID)"
 msgstr ""
 
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr ""
+
 msgid "**"
 msgstr "0..n 0..n"
 
@@ -207,6 +211,10 @@
 msgid "About this site"
 msgstr ""
 
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
 msgid "Any"
 msgstr ""
 
@@ -252,6 +260,10 @@
 msgid "Browse by entity type"
 msgstr ""
 
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr ""
+
 msgid "Bytes"
 msgstr "Bytes"
 
@@ -374,12 +386,6 @@
 
 #, 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 ""
@@ -399,6 +405,10 @@
 msgid "Click to sort on this column"
 msgstr ""
 
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "DEBUG"
 msgstr ""
 
@@ -424,6 +434,14 @@
 msgid "Decimal_plural"
 msgstr "Decimal numbers"
 
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Detected problems"
 msgstr ""
 
@@ -846,12 +864,22 @@
 msgid "URLs from which content will be imported. You can put one url per line"
 msgstr ""
 
+msgid "Undoable actions"
+msgstr ""
+
+msgid "Undoing"
+msgstr ""
+
 msgid "UniqueConstraint"
 msgstr "unique constraint"
 
 msgid "Unreachable objects"
 msgstr ""
 
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Used by:"
 msgstr ""
 
@@ -935,9 +963,6 @@
 msgid "abstract base class for transitions"
 msgstr ""
 
-msgid "action menu"
-msgstr ""
-
 msgid "action(s) on this selection"
 msgstr ""
 
@@ -1220,6 +1245,9 @@
 msgid "bad value"
 msgstr ""
 
+msgid "badly formatted url"
+msgstr ""
+
 msgid "base url"
 msgstr ""
 
@@ -1304,6 +1332,9 @@
 msgid "can not resolve entity types:"
 msgstr ""
 
+msgid "can only have one url"
+msgstr ""
+
 msgid "can't be changed"
 msgstr ""
 
@@ -1340,6 +1371,22 @@
 
 #, python-format
 msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+
+#, python-format
+msgid ""
 "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
 "%(card)s"
 msgstr ""
@@ -1970,9 +2017,6 @@
 msgid "date"
 msgstr ""
 
-msgid "day"
-msgstr ""
-
 msgid "deactivate"
 msgstr ""
 
@@ -2464,6 +2508,9 @@
 msgid "foaf"
 msgstr ""
 
+msgid "focus on this selection"
+msgstr ""
+
 msgid "follow"
 msgstr ""
 
@@ -2610,6 +2657,9 @@
 msgid "has_text"
 msgstr "has text"
 
+msgid "header-center"
+msgstr ""
+
 msgid "header-left"
 msgstr "header (left)"
 
@@ -2948,9 +2998,6 @@
 msgid "log in"
 msgstr ""
 
-msgid "log out first"
-msgstr ""
-
 msgid "login"
 msgstr ""
 
@@ -3052,9 +3099,6 @@
 msgid "monday"
 msgstr ""
 
-msgid "month"
-msgstr ""
-
 msgid "more actions"
 msgstr ""
 
@@ -3155,6 +3199,9 @@
 msgid "new"
 msgstr ""
 
+msgid "next page"
+msgstr ""
+
 msgid "next_results"
 msgstr "next results"
 
@@ -3355,6 +3402,9 @@
 msgid "preferences"
 msgstr ""
 
+msgid "previous page"
+msgstr ""
+
 msgid "previous_results"
 msgstr "previous results"
 
@@ -3926,6 +3976,10 @@
 msgid "there is no previous page"
 msgstr ""
 
+#, python-format
+msgid "there is no transaction #%s"
+msgstr ""
+
 msgid "this action is not reversible!"
 msgstr ""
 
@@ -4009,15 +4063,15 @@
 msgid "to_state_object"
 msgstr "transitions to this state"
 
-msgid "today"
-msgstr ""
-
 msgid "todo_by"
 msgstr "to do by"
 
 msgid "toggle check boxes"
 msgstr ""
 
+msgid "toggle filter"
+msgstr ""
+
 msgid "tr_count"
 msgstr "transition number"
 
@@ -4136,6 +4190,9 @@
 msgid "unauthorized value"
 msgstr ""
 
+msgid "undefined user"
+msgstr ""
+
 msgid "undo"
 msgstr ""
 
@@ -4163,6 +4220,9 @@
 msgid "unknown vocabulary:"
 msgstr ""
 
+msgid "unsupported protocol"
+msgstr ""
+
 msgid "upassword"
 msgstr "password"
 
@@ -4361,9 +4421,6 @@
 msgid "wednesday"
 msgstr ""
 
-msgid "week"
-msgstr ""
-
 #, python-format
 msgid "welcome %s !"
 msgstr ""
--- a/i18n/es.po	Wed Feb 22 11:57:42 2012 +0100
+++ b/i18n/es.po	Tue Oct 23 15:00:53 2012 +0200
@@ -146,6 +146,10 @@
 msgid "(UNEXISTANT EID)"
 msgstr "(EID INEXISTENTE"
 
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr ""
+
 msgid "**"
 msgstr "0..n 0..n"
 
@@ -219,6 +223,10 @@
 msgid "About this site"
 msgstr "Información del Sistema"
 
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
 msgid "Any"
 msgstr "Cualquiera"
 
@@ -264,6 +272,10 @@
 msgid "Browse by entity type"
 msgstr "Busca por tipo de entidad"
 
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr ""
+
 msgid "Bytes"
 msgstr "Bytes"
 
@@ -390,14 +402,6 @@
 
 #, python-format
 msgid ""
-"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
-"exists anymore in the schema."
-msgstr ""
-"No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta "
-"relación ya no existe en el esquema."
-
-#, python-format
-msgid ""
 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
 "anymore."
 msgstr ""
@@ -423,6 +427,10 @@
 msgid "Click to sort on this column"
 msgstr ""
 
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "DEBUG"
 msgstr ""
 
@@ -448,6 +456,14 @@
 msgid "Decimal_plural"
 msgstr "Decimales"
 
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Detected problems"
 msgstr "Problemas detectados"
 
@@ -875,12 +891,22 @@
 "URLs desde el cual el contenido sera importado. Usted puede incluir un URL "
 "por línea."
 
+msgid "Undoable actions"
+msgstr ""
+
+msgid "Undoing"
+msgstr ""
+
 msgid "UniqueConstraint"
 msgstr "Restricción de Unicidad"
 
 msgid "Unreachable objects"
 msgstr "Objetos inaccesibles"
 
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr ""
+
 msgid "Used by:"
 msgstr "Utilizado por :"
 
@@ -985,9 +1011,6 @@
 msgid "abstract base class for transitions"
 msgstr "Clase de base abstracta para la transiciones"
 
-msgid "action menu"
-msgstr ""
-
 msgid "action(s) on this selection"
 msgstr "Acción(es) en esta selección"
 
@@ -1276,6 +1299,9 @@
 msgid "bad value"
 msgstr "Valor erróneo"
 
+msgid "badly formatted url"
+msgstr ""
+
 msgid "base url"
 msgstr "Url de base"
 
@@ -1360,6 +1386,9 @@
 msgid "can not resolve entity types:"
 msgstr "Imposible de interpretar los tipos de entidades:"
 
+msgid "can only have one url"
+msgstr ""
+
 msgid "can't be changed"
 msgstr "No puede ser modificado"
 
@@ -1396,6 +1425,22 @@
 
 #, python-format
 msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+
+#, python-format
+msgid ""
 "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
 "%(card)s"
 msgstr ""
@@ -2044,9 +2089,6 @@
 msgid "date"
 msgstr "Fecha"
 
-msgid "day"
-msgstr "día"
-
 msgid "deactivate"
 msgstr "Desactivar"
 
@@ -2561,6 +2603,9 @@
 msgid "foaf"
 msgstr "Amigo de un Amigo, FOAF"
 
+msgid "focus on this selection"
+msgstr ""
+
 msgid "follow"
 msgstr "Seguir la liga"
 
@@ -2713,6 +2758,9 @@
 msgid "has_text"
 msgstr "Contiene el texto"
 
+msgid "header-center"
+msgstr ""
+
 msgid "header-left"
 msgstr "encabezado (izquierdo)"
 
@@ -3073,9 +3121,6 @@
 msgid "log in"
 msgstr "Acceder"
 
-msgid "log out first"
-msgstr "Desconéctese primero"
-
 msgid "login"
 msgstr "Usuario"
 
@@ -3177,9 +3222,6 @@
 msgid "monday"
 msgstr "Lunes"
 
-msgid "month"
-msgstr "mes"
-
 msgid "more actions"
 msgstr "Más acciones"
 
@@ -3282,6 +3324,9 @@
 msgid "new"
 msgstr "Nuevo"
 
+msgid "next page"
+msgstr ""
+
 msgid "next_results"
 msgstr "Siguientes resultados"
 
@@ -3483,6 +3528,9 @@
 msgid "preferences"
 msgstr "Preferencias"
 
+msgid "previous page"
+msgstr ""
+
 msgid "previous_results"
 msgstr "Resultados Anteriores"
 
@@ -4078,6 +4126,10 @@
 msgid "there is no previous page"
 msgstr ""
 
+#, python-format
+msgid "there is no transaction #%s"
+msgstr ""
+
 msgid "this action is not reversible!"
 msgstr "Esta acción es irreversible!."
 
@@ -4161,15 +4213,15 @@
 msgid "to_state_object"
 msgstr "Transición hacia este Estado"
 
-msgid "today"
-msgstr "hoy"
-
 msgid "todo_by"
 msgstr "Asignada a"
 
 msgid "toggle check boxes"
 msgstr "Cambiar valor"
 
+msgid "toggle filter"
+msgstr "esconder/mostrar el filtro"
+
 msgid "tr_count"
 msgstr "n° de transición"
 
@@ -4288,6 +4340,9 @@
 msgid "unauthorized value"
 msgstr "Valor no permitido"
 
+msgid "undefined user"
+msgstr ""
+
 msgid "undo"
 msgstr "Anular"
 
@@ -4315,6 +4370,9 @@
 msgid "unknown vocabulary:"
 msgstr "Vocabulario desconocido: "
 
+msgid "unsupported protocol"
+msgstr ""
+
 msgid "upassword"
 msgstr "Contraseña"
 
@@ -4522,9 +4580,6 @@
 msgid "wednesday"
 msgstr "Miércoles"
 
-msgid "week"
-msgstr "sem."
-
 #, python-format
 msgid "welcome %s !"
 msgstr "¡ Bienvenido %s  !"
@@ -4624,54 +4679,9 @@
 "usted debe  quitar la puesta en línea de la relación %s que es aceptada y "
 "puede ser cruzada"
 
-#~ msgid "(loading ...)"
-#~ msgstr "(Cargando ...)"
-
-#~ msgid "Schema of the data model"
-#~ msgstr "Esquema del modelo de datos"
-
-#~ msgid "add a CWSourceSchemaConfig"
-#~ msgstr "agregar una parte de mapeo"
-
-#~ msgid "csv entities export"
-#~ msgstr "Exportar entidades en csv"
-
-#~ msgid "follow this link if javascript is deactivated"
-#~ msgstr "Seleccione esta liga si javascript esta desactivado"
-
 #~ msgid ""
-#~ "how to format date and time in the ui (\"man strftime\" for format "
-#~ "description)"
-#~ msgstr ""
-#~ "Formato de fecha y hora que se utilizará por defecto en la interfaz "
-#~ "(\"man strftime\" para mayor información del formato)"
-
-#~ msgid ""
-#~ "how to format date in the ui (\"man strftime\" for format description)"
+#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does "
+#~ "not exists anymore in the schema."
 #~ msgstr ""
-#~ "Formato de fecha que se utilizará por defecto en la interfaz (\"man "
-#~ "strftime\" para mayor información  del formato)"
-
-#~ msgid ""
-#~ "how to format time in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "Formato de hora que se utilizará por defecto en la interfaz (\"man "
-#~ "strftime\" para mayor información del formato)"
-
-#~ msgid "instance schema"
-#~ msgstr "Esquema de la Instancia"
-
-#~ msgid "rdf"
-#~ msgstr "rdf"
-
-#~ msgid "rss"
-#~ msgstr "RSS"
-
-#~ msgid "siteinfo"
-#~ msgstr "información"
-
-#~ msgid "xbel"
-#~ msgstr "xbel"
-
-#~ msgid "xml"
-#~ msgstr "xml"
+#~ "No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta "
+#~ "relación ya no existe en el esquema."
--- a/i18n/fr.po	Wed Feb 22 11:57:42 2012 +0100
+++ b/i18n/fr.po	Tue Oct 23 15:00:53 2012 +0200
@@ -4,7 +4,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: cubicweb 2.46.0\n"
-"PO-Revision-Date: 2012-02-08 17:43+0100\n"
+"PO-Revision-Date: 2012-02-15 16:08+0100\n"
 "Last-Translator: Logilab Team <contact@logilab.fr>\n"
 "Language-Team: fr <contact@logilab.fr>\n"
 "Language: \n"
@@ -147,6 +147,10 @@
 msgid "(UNEXISTANT EID)"
 msgstr "(EID INTROUVABLE)"
 
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr "entité #%d (supprimée)"
+
 msgid "**"
 msgstr "0..n 0..n"
 
@@ -219,6 +223,10 @@
 msgid "About this site"
 msgstr "À propos de ce site"
 
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr "Relation ajoutée : %(entity_from)s %(rtype)s %(entity_to)s"
+
 msgid "Any"
 msgstr "Tous"
 
@@ -264,6 +272,10 @@
 msgid "Browse by entity type"
 msgstr "Naviguer par type d'entité"
 
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr "Par %(user)s le %(dt)s [%(undo_link)s] "
+
 msgid "Bytes"
 msgstr "Donnée binaires"
 
@@ -390,14 +402,6 @@
 
 #, 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 relation "
-"n'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 ""
@@ -423,6 +427,10 @@
 msgid "Click to sort on this column"
 msgstr "Cliquer pour trier sur cette colonne"
 
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr "Entité %(etype)s crée : %(entity)s"
+
 msgid "DEBUG"
 msgstr "DEBUG"
 
@@ -448,6 +456,14 @@
 msgid "Decimal_plural"
 msgstr "Nombres décimaux"
 
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr "Relation supprimée : %(entity_from)s %(rtype)s %(entity_to)s"
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr "Entité %(etype)s supprimée : %(entity)s"
+
 msgid "Detected problems"
 msgstr "Problèmes détectés"
 
@@ -875,12 +891,22 @@
 "URLs depuis lesquelles le contenu sera importé. Vous pouvez mettre une URL "
 "par ligne."
 
+msgid "Undoable actions"
+msgstr "Action annulables"
+
+msgid "Undoing"
+msgstr "Annuler"
+
 msgid "UniqueConstraint"
 msgstr "contrainte d'unicité"
 
 msgid "Unreachable objects"
 msgstr "Objets inaccessibles"
 
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr "Entité %(etype)s mise à jour : %(entity)s"
+
 msgid "Used by:"
 msgstr "Utilisé par :"
 
@@ -985,9 +1011,6 @@
 msgid "abstract base class for transitions"
 msgstr "classe de base abstraite pour les transitions"
 
-msgid "action menu"
-msgstr "actions"
-
 msgid "action(s) on this selection"
 msgstr "action(s) sur cette sélection"
 
@@ -1227,7 +1250,7 @@
 msgstr "anonyme"
 
 msgid "anyrsetview"
-msgstr "vues \"tous les rset\""
+msgstr "vues pour tout rset"
 
 msgid "april"
 msgstr "avril"
@@ -1277,6 +1300,9 @@
 msgid "bad value"
 msgstr "mauvaise valeur"
 
+msgid "badly formatted url"
+msgstr ""
+
 msgid "base url"
 msgstr "url de base"
 
@@ -1362,6 +1388,9 @@
 msgid "can not resolve entity types:"
 msgstr "impossible d'interpréter les types d'entités :"
 
+msgid "can only have one url"
+msgstr ""
+
 msgid "can't be changed"
 msgstr "ne peut-être modifié"
 
@@ -1398,6 +1427,28 @@
 
 #, python-format
 msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+"impossible de rétablir l'entité %(eid)s de type %(eschema)s, cible de la "
+"relation %(rtype)s (eid %(value)s) n'existe plus"
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+"impossible de rétablir la relation %(rtype)s sur l'entité %(eid)s, cette "
+"relation n'existe plus dans le schéma."
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+"impossible de rétablir l'état de l'entité %s, elle a été supprimée entre-"
+"temps"
+
+#, python-format
+msgid ""
 "can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
 "%(card)s"
 msgstr ""
@@ -2050,9 +2101,6 @@
 msgid "date"
 msgstr "date"
 
-msgid "day"
-msgstr "jour"
-
 msgid "deactivate"
 msgstr "désactiver"
 
@@ -2563,6 +2611,9 @@
 msgid "foaf"
 msgstr "foaf"
 
+msgid "focus on this selection"
+msgstr "afficher cette sélection"
+
 msgid "follow"
 msgstr "suivre le lien"
 
@@ -2716,6 +2767,9 @@
 msgid "has_text"
 msgstr "contient le texte"
 
+msgid "header-center"
+msgstr "en-tête (centre)"
+
 msgid "header-left"
 msgstr "en-tête (gauche)"
 
@@ -3074,9 +3128,6 @@
 msgid "log in"
 msgstr "s'identifier"
 
-msgid "log out first"
-msgstr "déconnecter vous d'abord"
-
 msgid "login"
 msgstr "identifiant"
 
@@ -3178,9 +3229,6 @@
 msgid "monday"
 msgstr "lundi"
 
-msgid "month"
-msgstr "mois"
-
 msgid "more actions"
 msgstr "plus d'actions"
 
@@ -3283,6 +3331,9 @@
 msgid "new"
 msgstr "nouveau"
 
+msgid "next page"
+msgstr "page suivante"
+
 msgid "next_results"
 msgstr "résultats suivants"
 
@@ -3486,6 +3537,9 @@
 msgid "preferences"
 msgstr "préférences"
 
+msgid "previous page"
+msgstr "page précédente"
+
 msgid "previous_results"
 msgstr "résultats précédents"
 
@@ -4077,10 +4131,14 @@
 msgstr "la valeur \"%s\" est déjà utilisée, veuillez utiliser une autre valeur"
 
 msgid "there is no next page"
-msgstr "il n'y a pas de page suivante"
+msgstr "Il n'y a pas de page suivante"
 
 msgid "there is no previous page"
-msgstr "il n'y a pas de page précédente"
+msgstr "Il n'y a pas de page précédente"
+
+#, python-format
+msgid "there is no transaction #%s"
+msgstr "Il n'y a pas de transaction #%s"
 
 msgid "this action is not reversible!"
 msgstr ""
@@ -4166,14 +4224,14 @@
 msgid "to_state_object"
 msgstr "transition vers cet état"
 
-msgid "today"
-msgstr "aujourd'hui"
-
 msgid "todo_by"
 msgstr "à faire par"
 
 msgid "toggle check boxes"
-msgstr "inverser les cases à cocher"
+msgstr "afficher/masquer les cases à cocher"
+
+msgid "toggle filter"
+msgstr "afficher/masquer le filtre"
 
 msgid "tr_count"
 msgstr "n° de transition"
@@ -4293,6 +4351,9 @@
 msgid "unauthorized value"
 msgstr "valeur non autorisée"
 
+msgid "undefined user"
+msgstr "utilisateur inconnu"
+
 msgid "undo"
 msgstr "annuler"
 
@@ -4320,6 +4381,9 @@
 msgid "unknown vocabulary:"
 msgstr "vocabulaire inconnu : "
 
+msgid "unsupported protocol"
+msgstr ""
+
 msgid "upassword"
 msgstr "mot de passe"
 
@@ -4526,9 +4590,6 @@
 msgid "wednesday"
 msgstr "mercredi"
 
-msgid "week"
-msgstr "semaine"
-
 #, python-format
 msgid "welcome %s !"
 msgstr "bienvenue %s !"
@@ -4628,27 +4689,26 @@
 "vous devriez enlevé la mise en ligne de la relation %s qui est supportée et "
 "peut-être croisée"
 
-#~ msgid "(loading ...)"
-#~ msgstr "(chargement ...)"
-
-#~ msgid "follow this link if javascript is deactivated"
-#~ msgstr "suivez ce lien si javascript est désactivé"
-
-#~ msgid ""
-#~ "how to format date and time in the ui (\"man strftime\" for format "
-#~ "description)"
-#~ msgstr ""
-#~ "comment formater la date dans l'interface (\"man strftime\" pour la "
-#~ "description du format)"
-
-#~ msgid ""
-#~ "how to format date in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "comment formater la date dans l'interface (\"man strftime\" pour la "
-#~ "description du format)"
-
-#~ msgid ""
-#~ "how to format time in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "comment formater l'heure dans l'interface (\"man strftime\" pour la "
-#~ "description du format)"
+#~ msgid "Action"
+#~ msgstr "Action"
+
+#~ msgid "day"
+#~ msgstr "jour"
+
+#~ msgid "jump to selection"
+#~ msgstr "afficher cette sélection"
+
+#~ msgid "log out first"
+#~ msgstr "déconnecter vous d'abord"
+
+#~ msgid "month"
+#~ msgstr "mois"
+
+#~ msgid "today"
+#~ msgstr "aujourd'hui"
+
+#~ msgid "undo last change"
+#~ msgstr "annuler dernier changement"
+
+#~ msgid "week"
+#~ msgstr "semaine"
--- a/md5crypt.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/md5crypt.py	Tue Oct 23 15:00:53 2012 +0200
@@ -51,18 +51,16 @@
         v = v >> 6
     return ret
 
-def crypt(pw, salt, magic=None):
+def crypt(pw, salt):
     if isinstance(pw, unicode):
         pw = pw.encode('utf-8')
-    if magic is None:
-        magic = MAGIC
     # Take care of the magic string if present
-    if salt[:len(magic)] == magic:
-        salt = salt[len(magic):]
+    if salt.startswith(MAGIC):
+        salt = salt[len(MAGIC):]
     # salt can have up to 8 characters:
     salt = salt.split('$', 1)[0]
     salt = salt[:8]
-    ctx = pw + magic + salt
+    ctx = pw + MAGIC + salt
     final = md5(pw + salt + pw).digest()
     for pl in xrange(len(pw), 0, -16):
         if pl > 16:
@@ -114,4 +112,4 @@
                            |(int(ord(final[10])) << 8)
                            |(int(ord(final[5]))), 4)
     passwd = passwd + to64((int(ord(final[11]))), 2)
-    return salt + '$' + passwd
+    return passwd
--- a/migration.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/migration.py	Tue Oct 23 15:00:53 2012 +0200
@@ -514,7 +514,9 @@
                     elif op == None:
                         continue
                     else:
-                        print 'unable to handle this case', oper, version, op, ver
+                        print ('unable to handle %s in %s, set to `%s %s` '
+                               'but currently up to `%s %s`' %
+                               (cube, source, oper, version, op, ver))
             # "solve" constraint satisfaction problem
             if cube not in self.cubes:
                 self.errors.append( ('add', cube, version, source) )
--- a/misc/migration/3.10.0_Any.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/3.10.0_Any.py	Tue Oct 23 15:00:53 2012 +0200
@@ -34,5 +34,5 @@
 for x in rql('Any X,XK WHERE X pkey XK, '
              'X pkey ~= "boxes.%" OR '
              'X pkey ~= "contentnavigation.%"').entities():
-    x.set_attributes(pkey=u'ctxcomponents.' + x.pkey.split('.', 1)[1])
+    x.cw_set(pkey=u'ctxcomponents.' + x.pkey.split('.', 1)[1])
 
--- a/misc/migration/3.11.0_Any.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/3.11.0_Any.py	Tue Oct 23 15:00:53 2012 +0200
@@ -81,5 +81,5 @@
         rset = session.execute('Any V WHERE X is CWProperty, X value V, X pkey %(k)s',
                                {'k': pkey})
         timestamp = int(rset[0][0])
-        sourceentity.set_attributes(latest_retrieval=datetime.fromtimestamp(timestamp))
+        sourceentity.cw_set(latest_retrieval=datetime.fromtimestamp(timestamp))
         session.execute('DELETE CWProperty X WHERE X pkey %(k)s', {'k': pkey})
--- a/misc/migration/3.12.0_Any.py	Wed Feb 22 11:57:42 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-if schema['TZDatetime'].eid is None:
-    add_entity_type('TZDatetime')
-if schema['TZTime'].eid is None:
-    add_entity_type('TZTime')
--- a/misc/migration/3.14.0_Any.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/3.14.0_Any.py	Tue Oct 23 15:00:53 2012 +0200
@@ -9,5 +9,5 @@
     expression = rqlcstr.value
     mainvars = guess_rrqlexpr_mainvars(expression)
     yamscstr = CONSTRAINTS[rqlcstr.type](expression, mainvars)
-    rqlcstr.set_attributes(value=yamscstr.serialize())
+    rqlcstr.cw_set(value=yamscstr.serialize())
     print 'updated', rqlcstr.type, rqlcstr.value.strip()
--- a/misc/migration/3.14.3_Any.py	Wed Feb 22 11:57:42 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-# keep the same behavior on existing instance but use the new one on new instance.
-config['https-deny-anonymous'] = True
--- a/misc/migration/3.14.4_Any.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/3.14.4_Any.py	Tue Oct 23 15:00:53 2012 +0200
@@ -4,6 +4,8 @@
 rdefdef = schema['CWSource'].rdef('name')
 attrtype = y2sql.type_from_constraints(dbhelper, rdefdef.object, rdefdef.constraints).split()[0]
 
-sql(dbhelper.sql_change_col_type('entities', 'asource', attrtype, False))
-sql(dbhelper.sql_change_col_type('entities', 'source', attrtype, False))
-sql(dbhelper.sql_change_col_type('deleted_entities', 'source', attrtype, False))
+cursor = session.cnxset['system']
+sql('UPDATE entities SET asource = source WHERE asource is NULL')
+dbhelper.change_col_type(cursor, 'entities', 'asource', attrtype, False)
+dbhelper.change_col_type(cursor, 'entities', 'source', attrtype, False)
+dbhelper.change_col_type(cursor, 'deleted_entities', 'source', attrtype, False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.14.7_Any.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,4 @@
+# migrate default format for TriInfo `comment_format` attribute
+sync_schema_props_perms('TrInfo')
+
+commit()
--- a/misc/migration/3.15.0_Any.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/3.15.0_Any.py	Tue Oct 23 15:00:53 2012 +0200
@@ -4,7 +4,7 @@
     config = source.dictconfig
     host = config.pop('host', u'ldap')
     protocol = config.pop('protocol', u'ldap')
-    source.set_attributes(url=u'%s://%s' % (protocol, host))
+    source.cw_set(url=u'%s://%s' % (protocol, host))
     source.update_config(skip_unknown=True, **config)
 
 commit()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.15.0_common.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,7 @@
+import ConfigParser
+try:
+    undo_actions = config.cfgfile_parser.get('MAIN', 'undo-support', False)
+except ConfigParser.NoOptionError:
+    pass # this conf. file was probably already migrated
+else:
+    config.global_set_option('undo-enabled', bool(undo_actions))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.15.4_Any.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,11 @@
+from logilab.common.shellutils import generate_password
+from cubicweb.server.utils import crypt_password
+
+for user in rql('CWUser U WHERE U cw_source S, S name "system", U upassword P, U login L').entities():
+    salt = user.upassword.getvalue()
+    if crypt_password('', salt) == salt:
+        passwd = generate_password()
+        print 'setting random password for user %s' % user.login
+        user.set_attributes(upassword=passwd)
+
+commit()
--- a/misc/migration/bootstrapmigration_repository.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/bootstrapmigration_repository.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -40,6 +40,13 @@
     sql('UPDATE entities SET asource=cw_name  '
         'FROM cw_CWSource, cw_source_relation '
         'WHERE entities.eid=cw_source_relation.eid_from AND cw_source_relation.eid_to=cw_CWSource.cw_eid')
+    commit()
+
+if schema['TZDatetime'].eid is None:
+    add_entity_type('TZDatetime', auto=False)
+if schema['TZTime'].eid is None:
+    add_entity_type('TZTime', auto=False)
+
 
 if applcubicwebversion <= (3, 14, 0) and cubicwebversion >= (3, 14, 0):
     if 'require_permission' in schema and not 'localperms'in repo.config.cubes():
--- a/misc/migration/postcreate.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/postcreate.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/misc/scripts/chpasswd.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/scripts/chpasswd.py	Tue Oct 23 15:00:53 2012 +0200
@@ -42,7 +42,7 @@
 crypted = crypt_password(pass1)
 
 cwuser = rset.get_entity(0,0)
-cwuser.set_attributes(upassword=Binary(crypted))
+cwuser.cw_set(upassword=Binary(crypted))
 commit()
 
 print("password updated.")
--- a/misc/scripts/ldapuser2ldapfeed.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/scripts/ldapuser2ldapfeed.py	Tue Oct 23 15:00:53 2012 +0200
@@ -3,6 +3,8 @@
 Once this script is run, execute c-c db-check to cleanup relation tables.
 """
 import sys
+from collections import defaultdict
+from logilab.common.shellutils import generate_password
 
 try:
     source_name, = __args__
@@ -33,44 +35,65 @@
 
 print '******************** backport entity content ***************************'
 
-todelete = {}
+todelete = defaultdict(list)
+extids = set()
+duplicates = []
 for entity in rql('Any X WHERE X cw_source S, S eid %(s)s', {'s': source.eid}).entities():
-        etype = entity.__regid__
-        if not source.support_entity(etype):
-            print "source doesn't support %s, delete %s" % (etype, entity.eid)
-        else:
-            try:
-                entity.complete()
-            except Exception:
-                print '%s %s much probably deleted, delete it (extid %s)' % (
-                    etype, entity.eid, entity.cw_metainformation()['extid'])
-            else:
-                print 'get back', etype, entity.eid
-                entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
-                if not entity.creation_date:
-                    entity.cw_edited['creation_date'] = datetime.now()
-                if not entity.modification_date:
-                    entity.cw_edited['modification_date'] = datetime.now()
-                if not entity.upassword:
-                    entity.cw_edited['upassword'] = u''
-                if not entity.cwuri:
-                    entity.cw_edited['cwuri'] = '%s/?dn=%s' % (
-                        source.urls[0], entity.cw_metainformation()['extid'])
-                print entity.cw_edited
-                system_source.add_entity(session, entity)
-                sql("UPDATE entities SET source='system' "
-                    "WHERE eid=%(eid)s", {'eid': entity.eid})
-                continue
-        todelete.setdefault(etype, []).append(entity)
+    etype = entity.__regid__
+    if not source.support_entity(etype):
+        print "source doesn't support %s, delete %s" % (etype, entity.eid)
+        todelete[etype].append(entity)
+        continue
+    try:
+        entity.complete()
+    except Exception:
+        print '%s %s much probably deleted, delete it (extid %s)' % (
+            etype, entity.eid, entity.cw_metainformation()['extid'])
+        todelete[etype].append(entity)
+        continue
+    print 'get back', etype, entity.eid
+    entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
+    if not entity.creation_date:
+        entity.cw_edited['creation_date'] = datetime.now()
+    if not entity.modification_date:
+        entity.cw_edited['modification_date'] = datetime.now()
+    if not entity.upassword:
+        entity.cw_edited['upassword'] = generate_password()
+    extid = entity.cw_metainformation()['extid']
+    if not entity.cwuri:
+        entity.cw_edited['cwuri'] = '%s/?dn=%s' % (
+            source.urls[0], extid.decode('utf-8', 'ignore'))
+    print entity.cw_edited
+    if extid in extids:
+        duplicates.append(extid)
+        continue
+    extids.add(extid)
+    system_source.add_entity(session, entity)
+    sql("UPDATE entities SET source='system' "
+        "WHERE eid=%(eid)s", {'eid': entity.eid})
 
 # only cleanup entities table, remaining stuff should be cleaned by a c-c
 # db-check to be run after this script
-for entities in todelete.values():
+if duplicates:
+    print 'found %s duplicate entries' % len(duplicates)
+    from pprint import pprint
+    pprint(duplicates)
+
+print len(todelete), 'entities will be deleted'
+for etype, entities in todelete.values():
+    print 'deleting', etype, [e.login for e in entities]
     system_source.delete_info_multi(session, entities, source_name)
 
 
+
 source_ent = rql('CWSource S WHERE S eid %(s)s', {'s': source.eid}).get_entity(0, 0)
-source_ent.set_attributes(type=u"ldapfeed", parser=u"ldapfeed")
+source_ent.cw_set(type=u"ldapfeed", parser=u"ldapfeed")
 
 
-commit()
+if raw_input('Commit ?') in 'yY':
+    print 'committing'
+    commit()
+else:
+    rollback()
+    print 'rollbacked'
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/scripts/repair_splitbrain_ldapuser_source.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,108 @@
+"""
+CAUTION: READ THIS CAREFULLY
+
+Sometimes it happens that ldap (specifically ldapuser type) source
+yield "ghost" users. The reasons may vary (server upgrade while some
+instances are still running & syncing with the ldap source, unmanaged
+updates to the upstream ldap, etc.).
+
+This script was written and refined enough times that we are confident
+in that it does something reasonnable (at least it did for the
+target application).
+
+However you should really REALLY understand what it does before
+deciding to apply it for you. And then ADAPT it tou your needs.
+
+"""
+
+import base64
+from collections import defaultdict
+
+from cubicweb.server.session import hooks_control
+
+try:
+    source_name, = __args__
+    source = repo.sources_by_uri[source_name]
+except ValueError:
+    print('you should specify the source name as script argument (i.e. after --'
+          ' on the command line)')
+    sys.exit(1)
+except KeyError:
+    print '%s is not an active source' % source_name
+    sys.exit(1)
+
+# check source is reachable before doing anything
+if not source.get_connection().cnx:
+    print '%s is not reachable. Fix this before running this script' % source_name
+    sys.exit(1)
+
+def find_dupes():
+    # XXX this retrieves entities from a source name "ldap"
+    #     you will want to adjust
+    rset = sql("SELECT eid, extid FROM entities WHERE source='%s'" % source_name)
+    extid2eids = defaultdict(list)
+    for eid, extid in rset:
+        extid2eids[extid].append(eid)
+    return dict((base64.b64decode(extid).lower(), eids)
+                for extid, eids in extid2eids.items()
+                if len(eids) > 1)
+
+def merge_dupes(dupes, docommit=False):
+    gone_eids = []
+    CWUser = schema['CWUser']
+    for extid, eids in dupes.items():
+        newest = eids.pop() # we merge everything on the newest
+        print 'merging ghosts of', extid, 'into', newest
+        # now we merge pairwise into the newest
+        for old in eids:
+            subst = {'old': old, 'new': newest}
+            print '  merging', old
+            gone_eids.append(old)
+            for rschema in CWUser.subject_relations():
+                if rschema.final or rschema == 'identity':
+                    continue
+                if CWUser.rdef(rschema, 'subject').composite == 'subject':
+                    # old 'composite' property is wiped ...
+                    # think about email addresses, excel preferences
+                    for eschema in rschema.objects():
+                        rql('DELETE %s X WHERE U %s X, U eid %%(old)s' % (eschema, rschema), subst)
+                else:
+                    # relink the new user to its old relations
+                    rql('SET NU %s X WHERE NU eid %%(new)s, NOT NU %s X, OU %s X, OU eid %%(old)s' %
+                        (rschema, rschema, rschema), subst)
+                    # delete the old relations
+                    rql('DELETE U %s X WHERE U eid %%(old)s' % rschema, subst)
+            # same thing ...
+            for rschema in CWUser.object_relations():
+                if rschema.final or rschema == 'identity':
+                    continue
+                rql('SET X %s NU WHERE NU eid %%(new)s, NOT X %s NU, X %s OU, OU eid %%(old)s' %
+                    (rschema, rschema, rschema), subst)
+                rql('DELETE X %s U WHERE U eid %%(old)s' % rschema, subst)
+    if not docommit:
+        rollback()
+        return
+    commit() # XXX flushing operations is wanted rather than really committing
+    print 'clean up entities table'
+    sql('DELETE FROM entities WHERE eid IN (%s)' % (', '.join(str(x) for x in gone_eids)))
+    commit()
+
+def main():
+    dupes = find_dupes()
+    if not dupes:
+        print 'No duplicate user'
+        return
+
+    print 'Found %s duplicate user instances' % len(dupes)
+
+    while True:
+        print 'Fix or dry-run? (f/d)  ... or Ctrl-C to break out'
+        answer = raw_input('> ')
+        if answer.lower() not in 'fd':
+            continue
+        print 'Please STOP THE APPLICATION INSTANCES (service or interactive), and press Return when done.'
+        raw_input('<I swear all running instances and workers of the application are stopped>')
+        with hooks_control(session, session.HOOKS_DENY_ALL):
+            merge_dupes(dupes, docommit=answer=='f')
+
+main()
--- a/predicates.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/predicates.py	Tue Oct 23 15:00:53 2012 +0200
@@ -352,12 +352,12 @@
     """
 
     def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
-                 **kwargs):
-        if not rset and not kwargs.get('entity'):
+                 entity=None, **kwargs):
+        if not rset and entity is None:
             return 0
         score = 0
-        if kwargs.get('entity'):
-            score = self.score_entity(kwargs['entity'])
+        if entity is not None:
+            score = self.score_entity(entity)
         elif row is None:
             col = col or 0
             if accept_none is None:
@@ -558,7 +558,7 @@
 @objectify_predicate
 def nonempty_rset(cls, req, rset=None, **kwargs):
     """Return 1 for result set containing one ore more rows."""
-    if rset is not None and rset.rowcount:
+    if rset:
         return 1
     return 0
 
@@ -567,7 +567,7 @@
 @objectify_predicate
 def empty_rset(cls, req, rset=None, **kwargs):
     """Return 1 for result set which doesn't contain any row."""
-    if rset is not None and rset.rowcount == 0:
+    if rset is not None and len(rset) == 0:
         return 1
     return 0
 
@@ -580,7 +580,7 @@
     """
     if rset is None and 'entity' in kwargs:
         return 1
-    if rset is not None and (row is not None or rset.rowcount == 1):
+    if rset is not None and (row is not None or len(rset) == 1):
         return 1
     return 0
 
@@ -608,7 +608,7 @@
         return self.operator(num, self.expected)
 
     def __call__(self, cls, req, rset=None, **kwargs):
-        return int(rset is not None and self.match_expected(rset.rowcount))
+        return int(rset is not None and self.match_expected(len(rset)))
 
 
 class multi_columns_rset(multi_lines_rset):
@@ -618,8 +618,9 @@
     """
 
     def __call__(self, cls, req, rset=None, **kwargs):
-        # 'or 0' since we *must not* return None
-        return rset and self.match_expected(len(rset.rows[0])) or 0
+        # 'or 0' since we *must not* return None. Also don't use rset.rows so
+        # this selector will work if rset is a simple list of list.
+        return rset and self.match_expected(len(rset[0])) or 0
 
 
 class paginated_rset(Predicate):
@@ -647,7 +648,7 @@
                 page_size = req.property_value('navigation.page-size')
             else:
                 page_size = int(page_size)
-        if rset.rowcount <= (page_size*self.nbpages):
+        if len(rset) <= (page_size*self.nbpages):
             return 0
         return self.nbpages
 
@@ -736,12 +737,16 @@
     See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
     class lookup / score rules according to the input context.
 
-    .. note:: when interface is an entity class, the score will reflect class
-              proximity so the most specific object will be selected.
+    .. note::
+
+       when interface is an entity class, the score will reflect class
+       proximity so the most specific object will be selected.
 
-    .. note:: deprecated in cubicweb >= 3.9, use either
-              :class:`~cubicweb.predicates.is_instance` or
-              :class:`~cubicweb.predicates.adaptable`.
+    .. note::
+
+       deprecated in cubicweb >= 3.9, use either
+       :class:`~cubicweb.predicates.is_instance` or
+       :class:`~cubicweb.predicates.adaptable`.
     """
 
     def __init__(self, *expected_ifaces, **kwargs):
@@ -1075,9 +1080,9 @@
 
     # don't use EntityPredicate.__call__ but this optimized implementation to
     # avoid considering each entity when it's not necessary
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
-        if kwargs.get('entity'):
-            return self.score_entity(kwargs['entity'])
+    def __call__(self, cls, req, rset=None, row=None, col=0, entity=None, **kwargs):
+        if entity is not None:
+            return self.score_entity(entity)
         if rset is None:
             return 0
         if row is None:
--- a/req.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/req.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -62,6 +62,8 @@
     :attribute vreg.schema: the instance's schema
     :attribute vreg.config: the instance's configuration
     """
+    is_request = True # False for repository session
+
     def __init__(self, vreg):
         self.vreg = vreg
         try:
@@ -75,6 +77,17 @@
         self.local_perm_cache = {}
         self._ = unicode
 
+    def set_language(self, lang):
+        """install i18n configuration for `lang` translation.
+
+        Raises :exc:`KeyError` if translation doesn't exist.
+        """
+        self.lang = lang
+        gettext, pgettext = self.vreg.config.translations[lang]
+        # use _cw.__ to translate a message without registering it to the catalog
+        self._ = self.__ = gettext
+        self.pgettext = pgettext
+
     def property_value(self, key):
         """return value of the property with the given key, giving priority to
         user specific value if any, else using site value
@@ -204,6 +217,9 @@
         parameters. Values are automatically URL quoted, and the
         publishing method to use may be specified or will be guessed.
 
+        if ``__secure__`` argument is True, the request will try to build a
+        https url.
+
         raises :exc:`ValueError` if None is found in arguments
         """
         # use *args since we don't want first argument to be "anonymous" to
@@ -222,7 +238,8 @@
                 method = 'view'
         base_url = kwargs.pop('base_url', None)
         if base_url is None:
-            base_url = self.base_url()
+            secure = kwargs.pop('__secure__', None)
+            base_url = self.base_url(secure=secure)
         if '_restpath' in kwargs:
             assert method == 'view', method
             path = kwargs.pop('_restpath')
@@ -415,8 +432,11 @@
             raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
                              % {'value': value, 'format': format})
 
-    def base_url(self):
-        """return the root url of the instance"""
+    def base_url(self, secure=None):
+        """return the root url of the instance
+        """
+        if secure:
+            raise NotImplementedError()
         return self.vreg.config['base-url']
 
     # abstract methods to override according to the web front-end #############
--- a/rqlrewrite.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/rqlrewrite.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -77,12 +77,26 @@
                 mytyperel = None
         possibletypes = allpossibletypes[varname]
         if mytyperel is not None:
-            # variable as already some types restriction. new possible types
-            # can only be a subset of existing ones, so only remove no more
-            # possible types
-            for cst in mytyperel.get_nodes(n.Constant):
-                if not cst.value in possibletypes:
-                    cst.parent.remove(cst)
+            if mytyperel.r_type == 'is_instance_of':
+                # turn is_instance_of relation into a is relation since we've
+                # all possible solutions and don't want to bother with
+                # potential is_instance_of incompatibility
+                mytyperel.r_type = 'is'
+                if len(possibletypes) > 1:
+                    node = n.Function('IN')
+                    for etype in possibletypes:
+                        node.append(n.Constant(etype, 'etype'))
+                else:
+                    node = n.Constant(etype, 'etype')
+                comp = mytyperel.children[1]
+                comp.replace(comp.children[0], node)
+            else:
+                # variable has already some strict types restriction. new
+                # possible types can only be a subset of existing ones, so only
+                # remove no more possible types
+                for cst in mytyperel.get_nodes(n.Constant):
+                    if not cst.value in possibletypes:
+                        cst.parent.remove(cst)
         else:
             # we have to add types restriction
             if stinfo.get('scope') is not None:
@@ -228,39 +242,44 @@
                                            if not r in sti['rhsrelations'])
                 else:
                     vi['rhs_rels'] = vi['lhs_rels'] = {}
-        parent = None
+        previous = None
         inserted = False
         for rqlexpr in rqlexprs:
             self.current_expr = rqlexpr
             if varexistsmap is None:
                 try:
-                    new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, parent)
+                    new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, previous)
                 except Unsupported:
                     continue
                 inserted = True
                 if new is not None and self._insert_scope is None:
                     self.exists_snippet[rqlexpr] = new
-                parent = parent or new
+                previous = previous or new
             else:
                 # called to reintroduce snippet due to ambiguity creation,
                 # so skip snippets which are not introducing this ambiguity
                 exists = varexistsmap[varmap]
-                if self.exists_snippet[rqlexpr] is exists:
+                if self.exists_snippet.get(rqlexpr) is exists:
                     self.insert_snippet(varmap, rqlexpr.snippet_rqlst, exists)
         if varexistsmap is None and not inserted:
             # no rql expression found matching rql solutions. User has no access right
             raise Unauthorized() # XXX may also be because of bad constraints in schema definition
 
-    def insert_snippet(self, varmap, snippetrqlst, parent=None):
+    def insert_snippet(self, varmap, snippetrqlst, previous=None):
         new = snippetrqlst.where.accept(self)
         existing = self.existingvars
         self.existingvars = None
         try:
-            return self._insert_snippet(varmap, parent, new)
+            return self._insert_snippet(varmap, previous, new)
         finally:
             self.existingvars = existing
 
-    def _insert_snippet(self, varmap, parent, new):
+    def _insert_snippet(self, varmap, previous, new):
+        """insert `new` snippet into the syntax tree, which have been rewritten
+        using `varmap`. In cases where an action is protected by several rql
+        expresssion, `previous` will be the first rql expression which has been
+        inserted, and so should be ORed with the following expressions.
+        """
         if new is not None:
             if self._insert_scope is None:
                 insert_scope = None
@@ -274,28 +293,28 @@
                 insert_scope = self._insert_scope
             if self._insert_scope is None and any(vi.get('stinfo', {}).get('optrelations')
                                                   for vi in self.varinfos):
-                assert parent is None
-                self._insert_scope = self.snippet_subquery(varmap, new)
+                assert previous is None
+                self._insert_scope, new = self.snippet_subquery(varmap, new)
                 self.insert_pending()
                 #self._insert_scope = None
-                return
+                return new
             if not isinstance(new, (n.Exists, n.Not)):
                 new = n.Exists(new)
-            if parent is None:
+            if previous is None:
                 insert_scope.add_restriction(new)
             else:
-                grandpa = parent.parent
-                or_ = n.Or(parent, new)
-                grandpa.replace(parent, or_)
+                grandpa = previous.parent
+                or_ = n.Or(previous, new)
+                grandpa.replace(previous, or_)
             if not self.removing_ambiguity:
                 try:
                     self.compute_solutions()
                 except Unsupported:
                     # some solutions have been lost, can't apply this rql expr
-                    if parent is None:
+                    if previous is None:
                         self.current_statement().remove_node(new, undefine=True)
                     else:
-                        grandpa.replace(or_, parent)
+                        grandpa.replace(or_, previous)
                         self._cleanup_inserted(new)
                     raise
                 else:
@@ -419,7 +438,7 @@
             # some solutions have been lost, can't apply this rql expr
             self.select.remove_subquery(self.select.with_[-1])
             raise
-        return subselect
+        return subselect, snippetrqlst
 
     def remove_ambiguities(self, snippets, newsolutions):
         # the snippet has introduced some ambiguities, we have to resolve them
@@ -476,11 +495,17 @@
 
     def _cleanup_inserted(self, node):
         # cleanup inserted variable references
+        removed = set()
         for vref in node.iget_nodes(n.VariableRef):
             vref.unregister_reference()
             if not vref.variable.stinfo['references']:
                 # no more references, undefine the variable
                 del self.select.defined_vars[vref.name]
+                removed.add(vref.name)
+        for key, newvar in self.rewritten.items(): # I mean items we alter it
+            if newvar in removed:
+                del self.rewritten[key]
+
 
     def _may_be_shared_with(self, sniprel, target):
         """if the snippet relation can be skipped to use a relation from the
--- a/schema.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/schema.py	Tue Oct 23 15:00:53 2012 +0200
@@ -261,30 +261,34 @@
     return self.has_local_role(action) or self.has_perm(req, action)
 PermissionMixIn.may_have_permission = may_have_permission
 
-def has_perm(self, session, action, **kwargs):
+def has_perm(self, _cw, action, **kwargs):
     """return true if the action is granted globaly or localy"""
     try:
-        self.check_perm(session, action, **kwargs)
+        self.check_perm(_cw, action, **kwargs)
         return True
     except Unauthorized:
         return False
 PermissionMixIn.has_perm = has_perm
 
-def check_perm(self, session, action, **kwargs):
-    # NB: session may be a server session or a request object check user is
-    # in an allowed group, if so that's enough internal sessions should
-    # always stop there
+def check_perm(self, _cw, action, **kwargs):
+    # NB: _cw may be a server transaction or a request object.
+    #
+    # check user is in an allowed group, if so that's enough internal
+    # transactions should always stop there
     groups = self.get_groups(action)
-    if session.user.matching_groups(groups):
+    if _cw.user.matching_groups(groups):
         return
     # if 'owners' in allowed groups, check if the user actually owns this
     # object, if so that's enough
+    #
+    # NB: give _cw to user.owns since user is not be bound to a transaction on
+    # the repository side
     if 'owners' in groups and (
           kwargs.get('creating')
-          or ('eid' in kwargs and session.user.owns(kwargs['eid']))):
+          or ('eid' in kwargs and _cw.user.owns(kwargs['eid']))):
         return
     # else if there is some rql expressions, check them
-    if any(rqlexpr.check(session, **kwargs)
+    if any(rqlexpr.check(_cw, **kwargs)
            for rqlexpr in self.get_rqlexprs(action)):
         return
     raise Unauthorized(action, str(self))
@@ -467,45 +471,45 @@
                     return True
         return False
 
-    def has_perm(self, session, action, **kwargs):
+    def has_perm(self, _cw, action, **kwargs):
         """return true if the action is granted globaly or localy"""
         if self.final:
             assert not ('fromeid' in kwargs or 'toeid' in kwargs), kwargs
             assert action in ('read', 'update')
             if 'eid' in kwargs:
-                subjtype = session.describe(kwargs['eid'])[0]
+                subjtype = _cw.describe(kwargs['eid'])[0]
             else:
                 subjtype = objtype = None
         else:
             assert not 'eid' in kwargs, kwargs
             assert action in ('read', 'add', 'delete')
             if 'fromeid' in kwargs:
-                subjtype = session.describe(kwargs['fromeid'])[0]
+                subjtype = _cw.describe(kwargs['fromeid'])[0]
             elif 'frometype' in kwargs:
                 subjtype = kwargs.pop('frometype')
             else:
                 subjtype = None
             if 'toeid' in kwargs:
-                objtype = session.describe(kwargs['toeid'])[0]
+                objtype = _cw.describe(kwargs['toeid'])[0]
             elif 'toetype' in kwargs:
                 objtype = kwargs.pop('toetype')
             else:
                 objtype = None
         if objtype and subjtype:
-            return self.rdef(subjtype, objtype).has_perm(session, action, **kwargs)
+            return self.rdef(subjtype, objtype).has_perm(_cw, action, **kwargs)
         elif subjtype:
             for tschema in self.targets(subjtype, 'subject'):
                 rdef = self.rdef(subjtype, tschema)
-                if not rdef.has_perm(session, action, **kwargs):
+                if not rdef.has_perm(_cw, action, **kwargs):
                     return False
         elif objtype:
             for tschema in self.targets(objtype, 'object'):
                 rdef = self.rdef(tschema, objtype)
-                if not rdef.has_perm(session, action, **kwargs):
+                if not rdef.has_perm(_cw, action, **kwargs):
                     return False
         else:
             for rdef in self.rdefs.itervalues():
-                if not rdef.has_perm(session, action, **kwargs):
+                if not rdef.has_perm(_cw, action, **kwargs):
                     return False
         return True
 
@@ -754,17 +758,17 @@
             return rql, found, keyarg
         return rqlst.as_string(), None, None
 
-    def _check(self, session, **kwargs):
+    def _check(self, _cw, **kwargs):
         """return True if the rql expression is matching the given relation
         between fromeid and toeid
 
-        session may actually be a request as well
+        _cw may be a request or a server side transaction
         """
         creating = kwargs.get('creating')
         if not creating and self.eid is not None:
             key = (self.eid, tuple(sorted(kwargs.iteritems())))
             try:
-                return session.local_perm_cache[key]
+                return _cw.local_perm_cache[key]
             except KeyError:
                 pass
         rql, has_perm_defs, keyarg = self.transform_has_permission()
@@ -772,50 +776,50 @@
         if creating and 'X' in self.rqlst.defined_vars:
             return True
         if keyarg is None:
-            kwargs.setdefault('u', session.user.eid)
+            kwargs.setdefault('u', _cw.user.eid)
             try:
-                rset = session.execute(rql, kwargs, build_descr=True)
+                rset = _cw.execute(rql, kwargs, build_descr=True)
             except NotImplementedError:
                 self.critical('cant check rql expression, unsupported rql %s', rql)
                 if self.eid is not None:
-                    session.local_perm_cache[key] = False
+                    _cw.local_perm_cache[key] = False
                 return False
             except TypeResolverException, ex:
                 # some expression may not be resolvable with current kwargs
                 # (type conflict)
                 self.warning('%s: %s', rql, str(ex))
                 if self.eid is not None:
-                    session.local_perm_cache[key] = False
+                    _cw.local_perm_cache[key] = False
                 return False
             except Unauthorized, ex:
                 self.debug('unauthorized %s: %s', rql, str(ex))
                 if self.eid is not None:
-                    session.local_perm_cache[key] = False
+                    _cw.local_perm_cache[key] = False
                 return False
         else:
-            rset = session.eid_rset(kwargs[keyarg])
+            rset = _cw.eid_rset(kwargs[keyarg])
         # if no special has_*_permission relation in the rql expression, just
         # check the result set contains something
         if has_perm_defs is None:
             if rset:
                 if self.eid is not None:
-                    session.local_perm_cache[key] = True
+                    _cw.local_perm_cache[key] = True
                 return True
         elif rset:
             # check every special has_*_permission relation is satisfied
-            get_eschema = session.vreg.schema.eschema
+            get_eschema = _cw.vreg.schema.eschema
             try:
                 for eaction, col in has_perm_defs:
                     for i in xrange(len(rset)):
                         eschema = get_eschema(rset.description[i][col])
-                        eschema.check_perm(session, eaction, eid=rset[i][col])
+                        eschema.check_perm(_cw, eaction, eid=rset[i][col])
                 if self.eid is not None:
-                    session.local_perm_cache[key] = True
+                    _cw.local_perm_cache[key] = True
                 return True
             except Unauthorized:
                 pass
         if self.eid is not None:
-            session.local_perm_cache[key] = False
+            _cw.local_perm_cache[key] = False
         return False
 
     @property
@@ -843,15 +847,15 @@
             rql += ', U eid %(u)s'
         return rql
 
-    def check(self, session, eid=None, creating=False, **kwargs):
+    def check(self, _cw, eid=None, creating=False, **kwargs):
         if 'X' in self.rqlst.defined_vars:
             if eid is None:
                 if creating:
-                    return self._check(session, creating=True, **kwargs)
+                    return self._check(_cw, creating=True, **kwargs)
                 return False
             assert creating == False
-            return self._check(session, x=eid, **kwargs)
-        return self._check(session, **kwargs)
+            return self._check(_cw, x=eid, **kwargs)
+        return self._check(_cw, **kwargs)
 
 
 def vargraph(rqlst):
@@ -904,7 +908,7 @@
             rql += ', U eid %(u)s'
         return rql
 
-    def check(self, session, fromeid=None, toeid=None):
+    def check(self, _cw, fromeid=None, toeid=None):
         kwargs = {}
         if 'S' in self.rqlst.defined_vars:
             if fromeid is None:
@@ -914,7 +918,7 @@
             if toeid is None:
                 return False
             kwargs['o'] = toeid
-        return self._check(session, **kwargs)
+        return self._check(_cw, **kwargs)
 
 
 # in yams, default 'update' perm for attributes granted to managers and owners.
@@ -1024,7 +1028,7 @@
                     'expression': self.expression}
             raise ValidationError(maineid, {qname: msg})
 
-    def exec_query(self, session, eidfrom, eidto):
+    def exec_query(self, _cw, eidfrom, eidto):
         if eidto is None:
             # checking constraint for an attribute relation
             expression = 'S eid %(s)s, ' + self.expression
@@ -1034,11 +1038,11 @@
             args = {'s': eidfrom, 'o': eidto}
         if 'U' in self.rqlst.defined_vars:
             expression = 'U eid %(u)s, ' + expression
-            args['u'] = session.user.eid
+            args['u'] = _cw.user.eid
         rql = 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)), expression)
         if self.distinct_query:
             rql = 'DISTINCT ' + rql
-        return session.execute(rql, args, build_descr=False)
+        return _cw.execute(rql, args, build_descr=False)
 
 
 class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint):
@@ -1061,7 +1065,7 @@
     """
     # XXX turns mainvars into a required argument in __init__
     distinct_query = True
-
+ 
     def match_condition(self, session, eidfrom, eidto):
         return len(self.exec_query(session, eidfrom, eidto)) <= 1
 
--- a/schemas/workflow.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/schemas/workflow.py	Tue Oct 23 15:00:53 2012 +0200
@@ -185,7 +185,7 @@
     # make by_transition optional because we want to allow managers to set
     # entity into an arbitrary state without having to respect wf transition
     by_transition = SubjectRelation('BaseTransition', cardinality='?*')
-    comment = RichString(fulltextindexed=True)
+    comment = RichString(fulltextindexed=True, default_format='text/plain')
     tr_count = Int(description='autocomputed attribute used to ensure transition coherency')
     # get actor and date time using owned_by and creation_date
 
--- a/server/__init__.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/__init__.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -31,10 +31,12 @@
 
 from logilab.common.modutils import LazyObject
 from logilab.common.textutils import splitstrip
+from logilab.common.registry import yes
 
 from yams import BASE_GROUPS
 
 from cubicweb import CW_SOFTWARE_ROOT
+from cubicweb.appobject import AppObject
 
 class ShuttingDown(BaseException):
     """raised when trying to access some resources while the repository is
@@ -42,7 +44,26 @@
     catch it.
     """
 
-# server-side debugging #########################################################
+# server-side services #########################################################
+
+class Service(AppObject):
+    """Base class for services.
+
+    A service is a selectable object that performs an action server-side.
+    Use :class:`cubicweb.dbapi.Connection.call_service` to call them from
+    the web-side.
+
+    When inheriting this class, do not forget to define at least the __regid__
+    attribute (and probably __select__ too).
+    """
+    __registry__ = 'services'
+    __select__ = yes()
+
+    def call(self, **kwargs):
+        raise NotImplementedError
+
+
+# server-side debugging ########################################################
 
 # server debugging flags. They may be combined using binary operators.
 
@@ -203,10 +224,6 @@
     config._cubes = None # avoid assertion error
     repo, cnx = in_memory_repo_cnx(config, login, password=pwd)
     repo.system_source.eid = ssource.eid # redo this manually
-    # trigger vreg initialisation of entity classes
-    config.cubicweb_appobject_path = set(('entities',))
-    config.cube_appobject_path = set(('entities',))
-    repo.vreg.set_schema(repo.schema)
     assert len(repo.sources) == 1, repo.sources
     handler = config.migration_handler(schema, interactive=False,
                                        cnx=cnx, repo=repo)
@@ -230,15 +247,13 @@
 
 def initialize_schema(config, schema, mhandler, event='create'):
     from cubicweb.server.schemaserial import serialize_schema
-    from cubicweb.server.session import hooks_control
     session = mhandler.session
     cubes = config.cubes()
     # 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'):
+    with session.deny_all_hooks_but('metadata', 'activeintegrity'):
         # execute cubicweb's pre<event> script
         mhandler.cmd_exec_event_script('pre%s' % event)
         # execute cubes pre<event> script if any
@@ -275,4 +290,5 @@
                 'ldapfeed': LazyObject('cubicweb.server.sources.ldapfeed', 'LDAPFeedSource'),
                 'ldapuser': LazyObject('cubicweb.server.sources.ldapuser', 'LDAPUserSource'),
                 'pyrorql': LazyObject('cubicweb.server.sources.pyrorql', 'PyroRQLSource'),
+                'zmqrql': LazyObject('cubicweb.server.sources.zmqrql', 'ZMQRQLSource'),
                 }
--- a/server/checkintegrity.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/checkintegrity.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -32,7 +32,6 @@
 
 from cubicweb.schema import PURE_VIRTUAL_RTYPES, VIRTUAL_RTYPES
 from cubicweb.server.sqlutils import SQL_PREFIX
-from cubicweb.server.session import security_enabled
 
 def notify_fixed(fix):
     if fix:
@@ -134,8 +133,12 @@
     # attribute to their current value
     source = repo.system_source
     for eschema in etypes:
-        rset = session.execute('Any X WHERE X is %s' % eschema)
-        source.fti_index_entities(session, rset.entities())
+        etype_class = session.vreg['etypes'].etype_class(str(eschema))
+        for fti_rql in etype_class.cw_fti_index_rql_queries(session):
+            rset = session.execute(fti_rql)
+            source.fti_index_entities(session, rset.entities())
+            # clear entity cache to avoid high memory consumption on big tables
+            session.drop_entity_cache()
         if withpb:
             pb.update()
 
@@ -313,7 +316,7 @@
     print 'Checking mandatory relations'
     msg = '%s #%s is missing mandatory %s relation %s (autofix will delete the entity)'
     for rschema in schema.relations():
-        if rschema.final or rschema.type in PURE_VIRTUAL_RTYPES:
+        if rschema.final or rschema in PURE_VIRTUAL_RTYPES or rschema in ('is', 'is_instance_of'):
             continue
         smandatory = set()
         omandatory = set()
@@ -390,7 +393,7 @@
     # yo, launch checks
     if checks:
         eids_cache = {}
-        with security_enabled(session, read=False, write=False): # ensure no read security
+        with session.security_enabled(read=False, write=False): # ensure no read security
             for check in checks:
                 check_func = globals()['check_%s' % check]
                 check_func(repo.schema, session, eids_cache, fix=fix)
--- a/server/cwzmq.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/cwzmq.py	Tue Oct 23 15:00:53 2012 +0200
@@ -18,12 +18,16 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
 from threading import Thread
+import cPickle
+import traceback
+
 import zmq
 from zmq.eventloop import ioloop
 import zmq.eventloop.zmqstream
 
 from logging import getLogger
 from cubicweb import set_log_methods
+from cubicweb.server.server import QuitEvent
 
 ctx = zmq.Context()
 
@@ -105,5 +109,132 @@
         self.ioloop.add_callback(lambda: self.stream.setsockopt(zmq.SUBSCRIBE, topic))
 
 
+class ZMQRepositoryServer(object):
+
+    def __init__(self, repository):
+        """make the repository available as a PyRO object"""
+        self.address = None
+        self.repo = repository
+        self.socket = None
+        self.stream = None
+        self.loop = ioloop.IOLoop()
+
+        # event queue
+        self.events = []
+
+    def connect(self, address):
+        self.address = address
+
+    def run(self):
+        """enter the service loop"""
+        # start repository looping tasks
+        self.socket = ctx.socket(zmq.REP)
+        self.stream = zmq.eventloop.zmqstream.ZMQStream(self.socket, io_loop=self.loop)
+        self.stream.bind(self.address)
+        self.info('ZMQ server bound on: %s', self.address)
+
+        self.stream.on_recv(self.process_cmds)
+
+        try:
+            self.loop.start()
+        except zmq.ZMQError:
+            self.warning('ZMQ event loop killed')
+        self.quit()
+
+    def trigger_events(self):
+        """trigger ready events"""
+        for event in self.events[:]:
+            if event.is_ready():
+                self.info('starting event %s', event)
+                event.fire(self)
+                try:
+                    event.update()
+                except Finished:
+                    self.events.remove(event)
+
+    def process_cmd(self, cmd):
+        """Delegate the given command to the repository.
+
+        ``cmd`` is a list of (method_name, args, kwargs)
+        where ``args`` is a list of positional arguments
+        and ``kwargs`` is a dictionnary of named arguments.
+
+        >>> rset = delegate_to_repo(["execute", [sessionid], {'rql': rql}])
+
+        :note1: ``kwargs`` may be ommited
+
+            >>> rset = delegate_to_repo(["execute", [sessionid, rql]])
+
+        :note2: both ``args`` and ``kwargs`` may be omitted
+
+            >>> schema = delegate_to_repo(["get_schema"])
+            >>> schema = delegate_to_repo("get_schema") # also allowed
+
+        """
+        cmd = cPickle.loads(cmd)
+        if not cmd:
+            raise AttributeError('function name required')
+        if isinstance(cmd, basestring):
+            cmd = [cmd]
+        if len(cmd) < 2:
+            cmd.append(())
+        if len(cmd) < 3:
+            cmd.append({})
+        cmd  = list(cmd) + [(), {}]
+        funcname, args, kwargs = cmd[:3]
+        result = getattr(self.repo, funcname)(*args, **kwargs)
+        return result
+
+    def process_cmds(self, cmds):
+        """Callback intended to be used with ``on_recv``.
+
+        Call ``delegate_to_repo`` on each command and send a pickled of
+        each result recursively.
+
+        Any exception are catched, pickled and sent.
+        """
+        try:
+            for cmd in cmds:
+                result = self.process_cmd(cmd)
+                self.send_data(result)
+        except Exception, exc:
+            traceback.print_exc()
+            self.send_data(exc)
+
+    def send_data(self, data):
+        self.socket.send_pyobj(data)
+
+    def quit(self, shutdown_repo=False):
+        """stop the server"""
+        self.info('Quitting ZMQ server')
+        try:
+            self.loop.add_callback(self.loop.stop)
+            self.stream.on_recv(None)
+            self.stream.close()
+        except Exception, e:
+            print e
+            pass
+        if shutdown_repo and not self.repo.shutting_down:
+            event = QuitEvent()
+            event.fire(self)
+
+    # server utilitities ######################################################
+
+    def install_sig_handlers(self):
+        """install signal handlers"""
+        import signal
+        self.info('installing signal handlers')
+        signal.signal(signal.SIGINT, lambda x, y, s=self: s.quit(shutdown_repo=True))
+        signal.signal(signal.SIGTERM, lambda x, y, s=self: s.quit(shutdown_repo=True))
+
+
+    # these are overridden by set_log_methods below
+    # only defining here to prevent pylint from complaining
+    @classmethod
+    def info(cls, msg, *a, **kw):
+        pass
+
+
 set_log_methods(Publisher, getLogger('cubicweb.zmq.pub'))
 set_log_methods(Subscriber, getLogger('cubicweb.zmq.sub'))
+set_log_methods(ZMQRepositoryServer, getLogger('cubicweb.zmq.repo'))
--- a/server/edition.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/edition.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -61,6 +61,8 @@
         # attributes, else we may accidentaly skip a desired security check
         if attr not in self:
             self.skip_security.add(attr)
+        # mark attribute as needing purge by the client
+        self.entity._cw_dont_cache_attribute(attr)
         self.edited_attribute(attr, value)
 
     def __delitem__(self, attr):
@@ -141,8 +143,7 @@
                          for rtype in self]
         try:
             entity.e_schema.check(dict_protocol_catcher(entity),
-                                  creation=creation, _=entity._cw._,
-                                  relations=relations)
+                                  creation=creation, relations=relations)
         except ValidationError, ex:
             ex.entity = self.entity
             raise
--- a/server/hook.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/hook.py	Tue Oct 23 15:00:53 2012 +0200
@@ -152,7 +152,7 @@
 
   On those events, the entity has no `cw_edited` dictionary.
 
-.. note:: `self.entity.set_attributes(age=42)` will set the `age` attribute to
+.. note:: `self.entity.cw_set(age=42)` will set the `age` attribute to
   42. But to do so, it will generate a rql query that will have to be processed,
   hence may trigger some hooks, etc. This could lead to infinitely looping hooks.
 
@@ -174,14 +174,17 @@
 Non data events
 ~~~~~~~~~~~~~~~
 
-Hooks called on server start/maintenance/stop event (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.
+Hooks called on server start/maintenance/stop event (e.g.
+`server_startup`, `server_maintenance`, `before_server_shutdown`,
+`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 but connections to the
+native source is impossible; `before_server_shutdown` handles that.
 
-Hooks called on backup/restore event (eg 'server_backup', 'server_restore') have
-a `repo` and a `timestamp` attributes, but *their `_cw` attribute is None*.
+Hooks called on backup/restore event (eg `server_backup`,
+`server_restore`) have a `repo` and a `timestamp` attributes, but
+*their `_cw` attribute is None*.
 
 Hooks called on session event (eg `session_open`, `session_close`) have no
 special attribute.
@@ -194,14 +197,12 @@
 ~~~~~~~~~~~~~
 
 It is sometimes convenient to explicitly enable or disable some hooks. For
-instance if you want to disable some integrity checking hook.  This can be
+instance if you want to disable some integrity checking hook. This can be
 controlled more finely through the `category` class attribute, which is a string
 giving a category name.  One can then uses the
-:class:`~cubicweb.server.session.hooks_control` context manager to explicitly
-enable or disable some categories.
-
-.. autoclass:: cubicweb.server.session.hooks_control
-
+:meth:`~cubicweb.server.session.Session.deny_all_hooks_but` and
+:meth:`~cubicweb.server.session.Session.allow_all_hooks_but` context managers to
+explicitly enable or disable some categories.
 
 The existing categories are:
 
@@ -227,14 +228,12 @@
 * ``bookmark``, bookmark entities handling hooks
 
 
-Nothing precludes one to invent new categories and use the
-:class:`~cubicweb.server.session.hooks_control` context manager to
-filter them in or out. Note that ending the transaction with commit()
-or rollback() will restore the hooks.
+Nothing precludes one to invent new categories and use existing mechanisms to
+filter them in or out.
 
 
-Hooks specific predicate
-~~~~~~~~~~~~~~~~~~~~~~~
+Hooks specific predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~
 .. autoclass:: cubicweb.server.hook.match_rtype
 .. autoclass:: cubicweb.server.hook.match_rtype_sets
 
@@ -265,7 +264,6 @@
 from cubicweb.cwvreg import CWRegistry, CWRegistryStore
 from cubicweb.predicates import ExpectedValuePredicate, is_instance
 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',
@@ -273,7 +271,8 @@
 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_maintenance', 'server_shutdown',
+                    'server_startup', 'server_maintenance',
+                    'server_shutdown', 'before_server_shutdown',
                     'session_open', 'session_close'))
 ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
 
@@ -322,13 +321,13 @@
             pruned = self.get_pruned_hooks(session, event,
                                            entities, eids_from_to, kwargs)
             # by default, hooks are executed with security turned off
-            with security_enabled(session, read=False):
+            with session.security_enabled(read=False):
                 for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs):
                     hooks = sorted(self.filtered_possible_objects(pruned, session, **_kwargs),
                                    key=lambda x: x.order)
-                    with security_enabled(session, write=False):
+                    with session.security_enabled(write=False):
                         for hook in hooks:
-                           hook()
+                            hook()
 
     def get_pruned_hooks(self, session, event, entities, eids_from_to, kwargs):
         """return a set of hooks that should not be considered by filtered_possible objects
@@ -469,16 +468,18 @@
     argument. The goal of this predicate is that it keeps reference to original sets,
     so modification to thoses sets are considered by the predicate. For instance
 
-    MYSET = set()
+    .. sourcecode:: python
+
+      MYSET = set()
 
-    class Hook1(Hook):
-        __regid__ = 'hook1'
-        __select__ = Hook.__select__ & match_rtype_sets(MYSET)
-        ...
+      class Hook1(Hook):
+          __regid__ = 'hook1'
+          __select__ = Hook.__select__ & match_rtype_sets(MYSET)
+          ...
 
-    class Hook2(Hook):
-        __regid__ = 'hook2'
-        __select__ = Hook.__select__ & match_rtype_sets(MYSET)
+      class Hook2(Hook):
+          __regid__ = 'hook2'
+          __select__ = Hook.__select__ & match_rtype_sets(MYSET)
 
     Client code can now change `MYSET`, this will changes the selection criteria
     of :class:`Hook1` and :class:`Hook1`.
--- a/server/ldaputils.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/ldaputils.py	Tue Oct 23 15:00:53 2012 +0200
@@ -37,7 +37,7 @@
 from ldap.filter import filter_format
 from ldapurl import LDAPUrl
 
-from cubicweb import ValidationError, AuthenticationError
+from cubicweb import ValidationError, AuthenticationError, Binary
 from cubicweb.server.sources import ConnectionWrapper
 
 _ = unicode
@@ -125,7 +125,7 @@
           }),
         ('user-attrs-map',
          {'type' : 'named',
-          'default': {'uid': 'login', 'gecos': 'email'},
+          'default': {'uid': 'login', 'gecos': 'email', 'userPassword': 'upassword'},
           'help': 'map from ldap user attributes to cubicweb attributes (with Active Directory, you want to use sAMAccountName:login,mail:email,givenName:firstname,sn:surname)',
           'group': 'ldap-source', 'level': 1,
           }),
@@ -203,7 +203,7 @@
         try:
             user = self._search(session, self.user_base_dn,
                                 self.user_base_scope, searchstr)[0]
-        except IndexError:
+        except (IndexError, ldap.SERVER_DOWN):
             # no such user
             raise AuthenticationError()
         # check password by establishing a (unused) connection
@@ -216,7 +216,7 @@
         except Exception:
             self.error('while trying to authenticate %s', user, exc_info=True)
             raise AuthenticationError()
-        eid = self.repo.extid2eid(self, user['dn'], 'CWUser', session)
+        eid = self.repo.extid2eid(self, user['dn'], 'CWUser', session, {})
         if eid < 0:
             # user has been moved away from this source
             raise AuthenticationError()
@@ -225,11 +225,12 @@
     def object_exists_in_ldap(self, dn):
         cnx = self.get_connection().cnx #session.cnxset.connection(self.uri).cnx
         if cnx is None:
-            return True # ldap unreachable, suppose it exists
+            self.warning('Could not establish connexion with LDAP server, assuming dn %s exists', dn)
+            return True # ldap unreachable, let's not touch it
         try:
             cnx.search_s(dn, self.user_base_scope)
         except ldap.PARTIAL_RESULTS:
-            pass
+            self.warning('PARTIAL RESULTS for dn %s', dn)
         except ldap.NO_SUCH_OBJECT:
             return False
         return True
@@ -249,10 +250,11 @@
         except ldap.LDAPError: # Invalid protocol version, fall back safely
             conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION2)
         # Deny auto-chasing of referrals to be safe, we handle them instead
-        #try:
-        #    connection.set_option(ldap.OPT_REFERRALS, 0)
-        #except ldap.LDAPError: # Cannot set referrals, so do nothing
-        #    pass
+        # Required for AD
+        try:
+           conn.set_option(ldap.OPT_REFERRALS, 0)
+        except ldap.LDAPError: # Cannot set referrals, so do nothing
+           pass
         #conn.set_option(ldap.OPT_NETWORK_TIMEOUT, conn_timeout)
         #conn.timeout = op_timeout
         # Now bind with the credentials given. Let exceptions propagate out.
@@ -344,14 +346,13 @@
         """Turn an ldap received item into a proper dict."""
         itemdict = {'dn': dn}
         for key, value in iterator:
-            if not isinstance(value, str):
-                try:
-                    for i in range(len(value)):
-                        value[i] = unicode(value[i], 'utf8')
-                except Exception:
-                    pass
-            if isinstance(value, list) and len(value) == 1:
-                itemdict[key] = value = value[0]
+            if self.user_attrs.get(key) == 'upassword': # XXx better password detection
+                itemdict[key] = Binary(value[0].encode('utf-8'))
+            else:
+                for i, val in enumerate(value):
+                    value[i] = unicode(val, 'utf-8', 'replace')
+                if isinstance(value, list) and len(value) == 1:
+                    itemdict[key] = value = value[0]
         return itemdict
 
     def _process_no_such_object(self, session, dn):
--- a/server/migractions.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/migractions.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1039,17 +1039,15 @@
             gmap = self.group_mapping()
             cmap = self.cstrtype_mapping()
             done = set()
-            for rdef in rschema.rdefs.itervalues():
-                if not (reposchema.has_entity(rdef.subject)
-                        and reposchema.has_entity(rdef.object)):
+            for subj, obj in rschema.rdefs:
+                if not (reposchema.has_entity(subj)
+                        and reposchema.has_entity(obj)):
                     continue
                 # symmetric relations appears twice
-                if (rdef.subject, rdef.object) in done:
+                if (subj, obj) in done:
                     continue
-                done.add( (rdef.subject, rdef.object) )
-                self._set_rdef_eid(rdef)
-                ss.execschemarql(execute, rdef,
-                                 ss.rdef2rql(rdef, cmap, gmap))
+                done.add( (subj, obj) )
+                self.cmd_add_relation_definition(subj, rtype, obj)
             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
@@ -1077,7 +1075,7 @@
         if commit:
             self.commit()
 
-    def cmd_rename_relation(self, oldname, newname, commit=True):
+    def cmd_rename_relation_type(self, oldname, newname, commit=True):
         """rename an existing relation
 
         `oldname` is a string giving the name of the existing relation
@@ -1099,9 +1097,8 @@
             print 'warning: relation %s %s %s is already known, skip addition' % (
                 subjtype, rtype, objtype)
             return
-        execute = self._cw.execute
         rdef = self._get_rdef(rschema, subjtype, objtype)
-        ss.execschemarql(execute, rdef,
+        ss.execschemarql(self._cw.execute, rdef,
                          ss.rdef2rql(rdef, self.cstrtype_mapping(),
                                      self.group_mapping()))
         if commit:
@@ -1115,7 +1112,7 @@
             schemaobj = getattr(rdef, attr)
             if getattr(schemaobj, 'eid', None) is None:
                 schemaobj.eid =  self.repo.schema[schemaobj].eid
-                assert schemaobj.eid is not None
+                assert schemaobj.eid is not None, schemaobj
         return rdef
 
     def cmd_drop_relation_definition(self, subjtype, rtype, objtype, commit=True):
@@ -1324,7 +1321,7 @@
         except Exception:
             self.cmd_create_entity('CWProperty', pkey=unicode(pkey), value=value)
         else:
-            prop.set_attributes(value=value)
+            prop.cw_set(value=value)
 
     # other data migration commands ###########################################
 
@@ -1529,6 +1526,10 @@
     def cmd_reactivate_verification_hooks(self):
         self.session.enable_hook_categories('integrity')
 
+    @deprecated("[3.15] use rename_relation_type(oldname, newname)")
+    def cmd_rename_relation(self, oldname, newname, commit=True):
+        self.cmd_rename_relation_type(oldname, newname, commit)
+
 
 class ForRqlIterator:
     """specific rql iterator to make the loop skipable"""
--- a/server/pool.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/pool.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/server/querier.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/querier.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -26,21 +26,26 @@
 from itertools import repeat
 
 from logilab.common.compat import any
-from rql import RQLSyntaxError
+from rql import RQLSyntaxError, CoercionError
 from rql.stmts import Union, Select
+from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj
 from rql.nodes import (Relation, VariableRef, Constant, SubQuery, Function,
                        Exists, Not)
+from yams import BASE_TYPES
 
 from cubicweb import ValidationError, Unauthorized, QueryError, UnknownEid
-from cubicweb import server, typed_eid
+from cubicweb import Binary, server, typed_eid
 from cubicweb.rset import ResultSet
 
-from cubicweb.utils import QueryCache
+from cubicweb.utils import QueryCache, RepeatList
 from cubicweb.server.utils import cleanup_solutions
 from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata
 from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction
 from cubicweb.server.edition import EditedEntity
-from cubicweb.server.session import security_enabled
+
+
+ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
+
 
 def empty_rset(rql, args, rqlst=None):
     """build an empty result set object"""
@@ -256,7 +261,7 @@
                 cached = True
             else:
                 noinvariant = set()
-                with security_enabled(self.session, read=False):
+                with self.session.security_enabled(read=False):
                     self._insert_security(union, noinvariant)
                 if key is not None:
                     self.session.transaction_data[key] = (union, self.args)
@@ -417,7 +422,7 @@
                         if rqlexpr.check(session, eid):
                             break
                     else:
-                        raise Unauthorized()
+                        raise Unauthorized('No read acces on %r with eid %i.' % (var, eid))
                 restricted_vars.update(localcheck)
                 localchecks.setdefault(tuple(localcheck.iteritems()), []).append(solution)
         # raise Unautorized exception if the user can't access to any solution
@@ -723,7 +728,7 @@
             rqlst = rqlst.copy()
             self._annotate(rqlst)
             if args:
-                 # different SQL generated when some argument is None or not (IS
+                # different SQL generated when some argument is None or not (IS
                 # NULL). This should be considered when computing sql cache key
                 cachekey += tuple(sorted([k for k,v in args.iteritems()
                                           if v is None]))
@@ -751,14 +756,22 @@
         if build_descr:
             if rqlst.TYPE == 'select':
                 # sample selection
-                descr = session.build_description(orig_rqlst, args, results)
+                if len(rqlst.children) == 1 and len(rqlst.children[0].solutions) == 1:
+                    # easy, all lines are identical
+                    selected = rqlst.children[0].selection
+                    solution = rqlst.children[0].solutions[0]
+                    description = _make_description(selected, args, solution)
+                    descr = RepeatList(len(results), tuple(description))
+                else:
+                    # hard, delegate the work :o)
+                    descr = manual_build_descr(session, rqlst, args, results)
             elif rqlst.TYPE == 'insert':
                 # on insert plan, some entities may have been auto-casted,
                 # so compute description manually even if there is only
                 # one solution
                 basedescr = [None] * len(plan.selected)
                 todetermine = zip(xrange(len(plan.selected)), repeat(False))
-                descr = session._build_descr(results, basedescr, todetermine)
+                descr = _build_descr(session, results, basedescr, todetermine)
             # FIXME: get number of affected entities / relations on non
             # selection queries ?
         # return a result set object
@@ -772,3 +785,77 @@
 from cubicweb import set_log_methods
 LOGGER = getLogger('cubicweb.querier')
 set_log_methods(QuerierHelper, LOGGER)
+
+
+def manual_build_descr(tx, rqlst, args, result):
+    """build a description for a given result by analysing each row
+
+    XXX could probably be done more efficiently during execution of query
+    """
+    # not so easy, looks for variable which changes from one solution
+    # to another
+    unstables = rqlst.get_variable_indices()
+    basedescr = []
+    todetermine = []
+    for i in xrange(len(rqlst.children[0].selection)):
+        ttype = _selection_idx_type(i, rqlst, args)
+        if ttype is None or ttype == 'Any':
+            ttype = None
+            isfinal = True
+        else:
+            isfinal = ttype in BASE_TYPES
+        if ttype is None or i in unstables:
+            basedescr.append(None)
+            todetermine.append( (i, isfinal) )
+        else:
+            basedescr.append(ttype)
+    if not todetermine:
+        return RepeatList(len(result), tuple(basedescr))
+    return _build_descr(tx, result, basedescr, todetermine)
+
+def _build_descr(tx, result, basedescription, todetermine):
+    description = []
+    etype_from_eid = tx.describe
+    todel = []
+    for i, row in enumerate(result):
+        row_descr = basedescription[:]
+        for index, isfinal in todetermine:
+            value = row[index]
+            if value is None:
+                # None value inserted by an outer join, no type
+                row_descr[index] = None
+                continue
+            if isfinal:
+                row_descr[index] = etype_from_pyobj(value)
+            else:
+                try:
+                    row_descr[index] = etype_from_eid(value)[0]
+                except UnknownEid:
+                    tx.error('wrong eid %s in repository, you should '
+                             'db-check the database' % value)
+                    todel.append(i)
+                    break
+        else:
+            description.append(tuple(row_descr))
+    for i in reversed(todel):
+        del result[i]
+    return description
+
+def _make_description(selected, args, solution):
+    """return a description for a result set"""
+    description = []
+    for term in selected:
+        description.append(term.get_type(solution, args))
+    return description
+
+def _selection_idx_type(i, rqlst, args):
+    """try to return type of term at index `i` of the rqlst's selection"""
+    for select in rqlst.children:
+        term = select.selection[i]
+        for solution in select.solutions:
+            try:
+                ttype = term.get_type(solution, args)
+                if ttype is not None:
+                    return ttype
+            except CoercionError:
+                return None
--- a/server/repository.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/repository.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -56,8 +56,7 @@
                       RepositoryError, UniqueTogetherError, typed_eid, onevent)
 from cubicweb import cwvreg, schema, server
 from cubicweb.server import ShuttingDown, utils, hook, pool, querier, sources
-from cubicweb.server.session import Session, InternalSession, InternalManager, \
-     security_enabled
+from cubicweb.server.session import Session, InternalSession, InternalManager
 from cubicweb.server.ssplanner import EditedEntity
 
 NO_CACHE_RELATIONS = set( [('owned_by', 'object'),
@@ -65,7 +64,7 @@
                            ('cw_source', 'object'),
                            ])
 
-def prefill_entity_caches(entity, relations):
+def prefill_entity_caches(entity):
     session = entity._cw
     # prefill entity relation caches
     for rschema in entity.e_schema.subject_relations():
@@ -109,17 +108,38 @@
     # * we don't want read permissions to be applied but we want delete
     #   permission to be checked
     if card[0] in '1?':
-        with security_enabled(session, read=False):
+        with session.security_enabled(read=False):
             session.execute('DELETE X %s Y WHERE X eid %%(x)s, '
                             'NOT Y eid %%(y)s' % rtype,
                                 {'x': eidfrom, 'y': eidto})
     if card[1] in '1?':
-        with security_enabled(session, read=False):
+        with session.security_enabled(read=False):
             session.execute('DELETE X %s Y WHERE Y eid %%(y)s, '
                             'NOT X eid %%(x)s' % rtype,
                             {'x': eidfrom, 'y': eidto})
 
 
+def preprocess_inlined_relations(session, entity):
+    """when an entity is added, check if it has some inlined relation which
+    requires to be extrated for proper call hooks
+    """
+    relations = []
+    activeintegrity = session.is_hook_category_activated('activeintegrity')
+    eschema = entity.e_schema
+    for attr in entity.cw_edited.iterkeys():
+        rschema = eschema.subjrels[attr]
+        if not rschema.final: # inlined relation
+            value = entity.cw_edited[attr]
+            relations.append((attr, value))
+            session.update_rel_cache_add(entity.eid, attr, value)
+            rdef = session.rtype_eids_rdef(attr, entity.eid, value)
+            if rdef.cardinality[1] in '1?' and activeintegrity:
+                with session.security_enabled(read=False):
+                    session.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
+                                    {'x': entity.eid, 'y': value})
+    return relations
+
+
 class NullEventBus(object):
     def publish(self, msg):
         pass
@@ -141,19 +161,22 @@
     XXX protect pyro access
     """
 
-    def __init__(self, config, vreg=None):
+    def __init__(self, config, tasks_manager=None, vreg=None):
         self.config = config
         if vreg is None:
             vreg = cwvreg.CWRegistryStore(config)
         self.vreg = vreg
+        self._tasks_manager = tasks_manager
+
         self.pyro_registered = False
         self.pyro_uri = None
         self.app_instances_bus = NullEventBus()
         self.info('starting repository from %s', self.config.apphome)
         # dictionary of opened sessions
         self._sessions = {}
+
+
         # list of functions to be called at regular interval
-        self._looping_tasks = []
         # list of running threads
         self._running_threads = []
         # initial schema, should be build or replaced latter
@@ -177,6 +200,9 @@
             self.init_cnxset_pool()
         @onevent('after-registry-reload', self)
         def fix_user_classes(self):
+            # After registery reload the 'CWUser' class used for CWEtype
+            # changed.  To any existing user object have a different class than
+            # the new loaded one. We are hot fixing this.
             usercls = self.vreg['etypes'].etype_class('CWUser')
             for session in self._sessions.values():
                 if not isinstance(session.user, InternalManager):
@@ -191,8 +217,8 @@
             # information (eg dump/restore/...)
             config._cubes = ()
             # only load hooks and entity classes in the registry
-            config.__class__.cube_appobject_path = set(('hooks', 'entities'))
-            config.__class__.cubicweb_appobject_path = set(('hooks', 'entities'))
+            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
@@ -200,16 +226,10 @@
         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
-            etdirectory = join(CW_SOFTWARE_ROOT, 'entities')
-            self.vreg.init_registration([etdirectory])
-            for modname in ('__init__', 'authobjs', 'wfobjs'):
-                self.vreg.load_file(join(etdirectory, '%s.py' % modname),
-                                    'cubicweb.entities.%s' % modname)
-            hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks')
-            self.vreg.load_file(join(hooksdirectory, 'metadata.py'),
-                                'cubicweb.hooks.metadata')
+            # trigger vreg initialisation of entity classes
+            config.cubicweb_appobject_path = set(('hooks', 'entities'))
+            config.cube_appobject_path = set(('hooks', 'entities'))
+            self.set_schema(config.load_schema())
         elif config.read_instance_schema:
             # normal start: load the instance schema from the database
             self.fill_schema()
@@ -247,8 +267,7 @@
                or not 'CWSource' in self.schema: # # 3.10 migration
             self.system_source.init_creating()
             return
-        session = self.internal_session()
-        try:
+        with self.internal_session() as session:
             # FIXME: sources should be ordered (add_entity priority)
             for sourceent in session.execute(
                 'Any S, SN, SA, SC WHERE S is_instance_of CWSource, '
@@ -259,8 +278,6 @@
                     self.system_source.init(True, sourceent)
                     continue
                 self.add_source(sourceent, add_to_cnxsets=False)
-        finally:
-            session.close()
 
     def _clear_planning_caches(self):
         for cache in ('source_defs', 'is_multi_sources_relation',
@@ -324,14 +341,13 @@
         self.schema = schema
 
     def fill_schema(self):
-        """lod schema from the repository"""
+        """load schema from the repository"""
         from cubicweb.server.schemaserial import deserialize_schema
         self.info('loading schema from the repository')
         appschema = schema.CubicWebSchema(self.config.appid)
         self.set_schema(self.config.load_bootstrap_schema(), resetvreg=False)
         self.debug('deserializing db schema into %s %#x', appschema.name, id(appschema))
-        session = self.internal_session()
-        try:
+        with self.internal_session() as session:
             try:
                 deserialize_schema(appschema, session)
             except BadSchemaDefinition:
@@ -342,11 +358,15 @@
                 raise Exception('Is the database initialised ? (cause: %s)' %
                                 (ex.args and ex.args[0].strip() or 'unknown')), \
                                 None, sys.exc_info()[-1]
-        finally:
-            session.close()
         self.set_schema(appschema)
 
-    def start_looping_tasks(self):
+
+    def _prepare_startup(self):
+        """Prepare "Repository as a server" for startup.
+
+        * trigger server startup hook,
+        * register session clean up task.
+        """
         if not (self.config.creating or self.config.repairing
                 or self.config.quick_start):
             # call instance level initialisation hooks
@@ -355,15 +375,23 @@
             self.cleanup_session_time = self.config['cleanup-session-time'] or 60 * 60 * 24
             assert self.cleanup_session_time > 0
             cleanup_session_interval = min(60*60, self.cleanup_session_time / 3)
-            self.looping_task(cleanup_session_interval, 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(self, interval, func, args)
-            self.info('starting task %s with interval %.2fs', task.name,
-                      interval)
-            task.start()
-        # ensure no tasks will be further added
-        self._looping_tasks = tuple(self._looping_tasks)
+            assert self._tasks_manager is not None, "This Repository is not intended to be used as a server"
+            self._tasks_manager.add_looping_task(cleanup_session_interval,
+                                                 self.clean_sessions)
+
+    def start_looping_tasks(self):
+        """Actual "Repository as a server" startup.
+
+        * trigger server startup hook,
+        * register session clean up task,
+        * start all tasks.
+
+        XXX Other startup related stuffs are done elsewhere. In Repository
+        XXX __init__ or in external codes (various server managers).
+        """
+        self._prepare_startup()
+        assert self._tasks_manager is not None, "This Repository is not intended to be used as a server"
+        self._tasks_manager.start()
 
     def looping_task(self, interval, func, *args):
         """register a function to be called every `interval` seconds.
@@ -371,15 +399,12 @@
         looping tasks can only be registered during repository initialization,
         once done this method will fail.
         """
-        try:
-            self._looping_tasks.append( (interval, func, args) )
-        except AttributeError:
-            raise RuntimeError("can't add looping task once the repository is started")
+        assert self._tasks_manager is not None, "This Repository is not intended to be used as a server"
+        self._tasks_manager.add_looping_task(interval, func, *args)
 
     def threaded_task(self, func):
         """start function in a separated thread"""
-        t = utils.RepoThread(func, self._running_threads)
-        t.start()
+        utils.RepoThread(func, self._running_threads).start()
 
     #@locked
     def _get_cnxset(self):
@@ -407,21 +432,21 @@
         connections
         """
         assert not self.shutting_down, 'already shutting down'
+        if not (self.config.creating or self.config.repairing
+                or self.config.quick_start):
+            # then, the system source is still available
+            self.hm.call_hooks('before_server_shutdown', repo=self)
         self.shutting_down = True
         self.system_source.shutdown()
-        if isinstance(self._looping_tasks, tuple): # if tasks have been started
-            for looptask in self._looping_tasks:
-                self.info('canceling task %s...', looptask.name)
-                looptask.cancel()
-                looptask.join()
-                self.info('task %s finished', looptask.name)
+        if self._tasks_manager is not None:
+            self._tasks_manager.stop()
+        if not (self.config.creating or self.config.repairing
+                or self.config.quick_start):
+            self.hm.call_hooks('server_shutdown', repo=self)
         for thread in self._running_threads:
             self.info('waiting thread %s...', thread.getName())
             thread.join()
             self.info('thread %s finished', thread.getName())
-        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._cnxsets_pool.empty():
             cnxset = self._cnxsets_pool.get_nowait()
@@ -514,7 +539,8 @@
         results['sql_no_cache'] = self.system_source.no_cache
         results['nb_open_sessions'] = len(self._sessions)
         results['nb_active_threads'] = threading.activeCount()
-        results['looping_tasks'] = ', '.join(str(t) for t in self._looping_tasks)
+        looping_tasks = self._tasks_manager._looping_tasks
+        results['looping_tasks'] = ', '.join(str(t) for t in looping_tasks)
         results['available_cnxsets'] = self._cnxsets_pool.qsize()
         results['threads'] = ', '.join(sorted(str(t) for t in threading.enumerate()))
         return results
@@ -612,8 +638,7 @@
         """
         from logilab.common.changelog import Version
         vcconf = {}
-        session = self.internal_session()
-        try:
+        with self.internal_session() as session:
             for pk, version in session.execute(
                 'Any K,V WHERE P is CWProperty, P value V, P pkey K, '
                 'P pkey ~="system.version.%"', build_descr=False):
@@ -632,8 +657,6 @@
                         msg = ('instance has %s version %s but %s '
                                'is installed. Run "cubicweb-ctl upgrade".')
                         raise ExecutionError(msg % (cube, version, fsversion))
-        finally:
-            session.close()
         return vcconf
 
     @cached
@@ -654,14 +677,11 @@
 
         This is a public method, not requiring a session id.
         """
-        session = self.internal_session()
-        try:
+        with self.internal_session() as session:
             # don't use session.execute, we don't want rset.req set
             return self.querier.execute(session, 'Any K,V WHERE P is CWProperty,'
                                         'P pkey K, P value V, NOT P for_user U',
                                         build_descr=False)
-        finally:
-            session.close()
 
     # XXX protect this method: anonymous should be allowed and registration
     # plugged
@@ -670,10 +690,9 @@
         given password. This method is designed to be used for anonymous
         registration on public web site.
         """
-        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:
+        with self.internal_session() as session:
+            # for consistency, keep same error as unique check hook (although not required)
+            errmsg = session._('the value "%s" is already used, use another one')
             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',
@@ -700,8 +719,6 @@
                                 'U primary_email X, U use_email X '
                                 'WHERE U login %(login)s', d, build_descr=False)
             session.commit()
-        finally:
-            session.close()
         return True
 
     def find_users(self, fetch_attrs, **query_attrs):
@@ -724,8 +741,7 @@
         for k in chain(fetch_attrs, query_attrs.iterkeys()):
             if k not in cwuserattrs:
                 raise Exception('bad input for find_user')
-        session = self.internal_session()
-        try:
+        with self.internal_session() as session:
             varmaker = rqlvar_maker()
             vars = [(attr, varmaker.next()) for attr in fetch_attrs]
             rql = 'Any %s WHERE X is CWUser, ' % ','.join(var[1] for var in vars)
@@ -734,8 +750,6 @@
                                                   for attr in query_attrs.iterkeys()),
                                    query_attrs)
             return rset.rows
-        finally:
-            session.close()
 
     def connect(self, login, **kwargs):
         """open a connection for a given user
@@ -746,14 +760,11 @@
         raise `AuthenticationError` if the authentication failed
         raise `ConnectionError` if we can't open a connection
         """
+        cnxprops = kwargs.pop('cnxprops', None)
         # use an internal connection
-        session = self.internal_session()
-        # try to get a user object
-        cnxprops = kwargs.pop('cnxprops', None)
-        try:
+        with self.internal_session() as session:
+            # try to get a user object
             user = self.authenticate_user(session, login, **kwargs)
-        finally:
-            session.close()
         session = Session(user, self, cnxprops)
         user._cw = user.cw_rset.req = session
         user.cw_clear_relation_cache()
@@ -878,28 +889,34 @@
         del self._sessions[sessionid]
         self.info('closed session %s for user %s', sessionid, session.user.login)
 
+    def call_service(self, sessionid, regid, async, **kwargs):
+        """
+        See :class:`cubicweb.dbapi.Connection.call_service`
+        and :class:`cubicweb.server.Service`
+        """
+        def task():
+            session = self._get_session(sessionid, setcnxset=True)
+            service = session.vreg['services'].select(regid, session, **kwargs)
+            try:
+                return service.call(**kwargs)
+            finally:
+                session.rollback() # free cnxset
+        if async:
+            self.info('calling service %s asynchronously', regid)
+            self.threaded_task(task)
+        else:
+            self.info('calling service %s synchronously', regid)
+            return task()
+
     def user_info(self, sessionid, props=None):
         """this method should be used by client to:
         * check session id validity
         * update user information on each user's request (i.e. groups and
           custom properties)
         """
-        session = self._get_session(sessionid, setcnxset=False)
-        if props is not None:
-            self.set_session_props(sessionid, props)
-        user = session.user
+        user = self._get_session(sessionid, setcnxset=False).user
         return user.eid, user.login, user.groups, user.properties
 
-    def set_session_props(self, sessionid, props):
-        """this method should be used by client to:
-        * check session id validity
-        * update user information on each user's request (i.e. groups and
-          custom properties)
-        """
-        session = self._get_session(sessionid, setcnxset=False)
-        for prop, value in props.items():
-            session.change_property(prop, value)
-
     def undoable_transactions(self, sessionid, ueid=None, txid=None,
                               **actionfilters):
         """See :class:`cubicweb.dbapi.Connection.undoable_transactions`"""
@@ -947,14 +964,11 @@
         * list of (etype, eid) of entities of the given types which have been
           deleted since the given timestamp
         """
-        session = self.internal_session()
-        updatetime = datetime.utcnow()
-        try:
+        with self.internal_session() as session:
+            updatetime = datetime.utcnow()
             modentities, delentities = self.system_source.modified_entities(
                 session, etypes, mtime)
             return updatetime, modentities, delentities
-        finally:
-            session.close()
 
     # session handling ########################################################
 
@@ -1205,7 +1219,7 @@
             source = self.sources_by_eid[scleanup]
         # 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):
+        with session.security_enabled(read=False, write=False):
             eid = entity.eid
             for rschema, _, role in entity.e_schema.relation_definitions():
                 rtype = rschema.type
@@ -1247,7 +1261,7 @@
             source = self.sources_by_eid[scleanup]
         # 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):
+        with session.security_enabled(read=False, write=False):
             in_eids = ','.join([str(_e.eid) for _e in entities])
             for rschema, _, role in entities[0].e_schema.relation_definitions():
                 rtype = rschema.type
@@ -1339,7 +1353,6 @@
         entity._cw_is_saved = False # entity has an eid but is not yet saved
         # init edited_attributes before calling before_add_entity hooks
         entity.cw_edited = edited
-        eschema = entity.e_schema
         source = self.locate_etype_source(entity.__regid__)
         # allocate an eid to the entity before calling hooks
         entity.eid = self.system_source.create_eid(session)
@@ -1347,22 +1360,10 @@
         extid = self.init_entity_caches(session, entity, source)
         if server.DEBUG & server.DBG_REPO:
             print 'ADD entity', self, entity.__regid__, entity.eid, edited
-        relations = []
-        prefill_entity_caches(entity, relations)
+        prefill_entity_caches(entity)
         if source.should_call_hooks:
             self.hm.call_hooks('before_add_entity', session, entity=entity)
-        activintegrity = session.is_hook_category_activated('activeintegrity')
-        for attr in edited.iterkeys():
-            rschema = eschema.subjrels[attr]
-            if not rschema.final: # inlined relation
-                value = edited[attr]
-                relations.append((attr, value))
-                session.update_rel_cache_add(entity.eid, attr, value)
-                rdef = session.rtype_eids_rdef(attr, entity.eid, value)
-                if rdef.cardinality[1] in '1?' and activintegrity:
-                    with security_enabled(session, read=False):
-                        session.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
-                                        {'x': entity.eid, 'y': value})
+        relations = preprocess_inlined_relations(session, entity)
         edited.set_defaults()
         if session.is_hook_category_activated('integrity'):
             edited.check(creation=True)
@@ -1525,7 +1526,7 @@
         activintegrity = session.is_hook_category_activated('activeintegrity')
         for rtype, eids_subj_obj in relations.iteritems():
             if server.DEBUG & server.DBG_REPO:
-                for subjeid, objeid in relations:
+                for subjeid, objeid in eids_subj_obj:
                     print 'ADD relation', subjeid, rtype, objeid
             for subjeid, objeid in eids_subj_obj:
                 source = self.locate_relation_source(session, subjeid, rtype, objeid)
@@ -1546,7 +1547,7 @@
                 rdef = session.rtype_eids_rdef(rtype, subjeid, objeid)
                 card = rdef.cardinality
                 if card[0] in '?1':
-                    with security_enabled(session, read=False):
+                    with session.security_enabled(read=False):
                         session.execute('DELETE X %s Y WHERE X eid %%(x)s, '
                                         'NOT Y eid %%(y)s' % rtype,
                                         {'x': subjeid, 'y': objeid})
@@ -1557,7 +1558,7 @@
                         continue
                     subjects[subjeid] = len(relations_by_rtype[rtype]) - 1
                 if card[1] in '?1':
-                    with security_enabled(session, read=False):
+                    with session.security_enabled(read=False):
                         session.execute('DELETE X %s Y WHERE Y eid %%(y)s, '
                                         'NOT X eid %%(x)s' % rtype,
                                         {'x': subjeid, 'y': objeid})
@@ -1671,6 +1672,7 @@
     # only defining here to prevent pylint from complaining
     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
 
+
 def pyro_unregister(config):
     """unregister the repository from the pyro name server"""
     from logilab.common.pyro_ext import ns_unregister
--- a/server/rqlannotation.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/rqlannotation.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -28,12 +28,13 @@
 from rql.utils import common_parent
 
 def _annotate_select(annotator, rqlst):
+    has_text_query = False
     for subquery in rqlst.with_:
-        annotator._annotate_union(subquery.query)
+        if annotator._annotate_union(subquery.query):
+            has_text_query = True
     #if server.DEBUG:
     #    print '-------- sql annotate', repr(rqlst)
     getrschema = annotator.schema.rschema
-    has_text_query = False
     need_distinct = rqlst.distinct
     for rel in rqlst.iget_nodes(Relation):
         if getrschema(rel.r_type).symmetric and not isinstance(rel.parent, Exists):
@@ -154,6 +155,11 @@
                     sstinfo['scope'] = common_parent(sstinfo['scope'], stinfo['scope']).scope
             except CantSelectPrincipal:
                 stinfo['invariant'] = False
+    # see unittest_rqlannotation. test_has_text_security_cache_bug
+    # XXX probably more to do, but yet that work without more...
+    for col_alias in rqlst.aliases.itervalues():
+        if col_alias.stinfo.get('ftirels'):
+            has_text_query = True
     rqlst.need_distinct = need_distinct
     return has_text_query
 
@@ -272,8 +278,7 @@
     def _annotate_union(self, union):
         has_text_query = False
         for select in union.children:
-            htq = _annotate_select(self, select)
-            if htq:
+            if _annotate_select(self, select):
                 has_text_query = True
         return has_text_query
 
--- a/server/server.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/server.py	Tue Oct 23 15:00:53 2012 +0200
@@ -26,6 +26,7 @@
 from time import localtime, mktime
 
 from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.server.utils import TasksManager
 from cubicweb.server.repository import Repository
 
 class Finished(Exception):
@@ -77,7 +78,7 @@
     def __init__(self, config):
         """make the repository available as a PyRO object"""
         self.config = config
-        self.repo = Repository(config)
+        self.repo = Repository(config, TasksManager())
         self.ns = None
         self.quiting = None
         # event queue
--- a/server/serverconfig.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/serverconfig.py	Tue Oct 23 15:00:53 2012 +0200
@@ -140,12 +140,9 @@
           'help': 'size of the parsed rql cache size.',
           'group': 'main', 'level': 3,
           }),
-        ('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.',
+        ('undo-enabled',
+         {'type' : 'yn', 'default': False,
+          'help': 'enable undo support',
           'group': 'main', 'level': 3,
           }),
         ('keep-transaction-lifetime',
@@ -207,7 +204,13 @@
 and if not set, it will be choosen randomly',
           'group': 'pyro', 'level': 3,
           }),
-
+        # zmq services config
+        ('zmq-repository-address',
+         {'type' : 'string',
+          'default': None,
+          'help': 'ZMQ URI on which the repository will be bound to.',
+          'group': 'zmq', 'level': 3,
+          }),
          ('zmq-address-sub',
           {'type' : 'csv',
            'default' : None,
--- a/server/serverctl.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/serverctl.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -35,6 +35,7 @@
 from cubicweb.toolsutils import Command, CommandHandler, underline_title
 from cubicweb.cwctl import CWCTL, check_options_consistency
 from cubicweb.server import SOURCE_TYPES
+from cubicweb.server.repository import Repository
 from cubicweb.server.serverconfig import (
     USER_OPTIONS, ServerConfiguration, SourceConfiguration,
     ask_source_config, generate_source_config)
@@ -446,7 +447,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') or '', password=system.get('db-password') or '',
                 **extra)
         except Exception, ex:
             raise ConfigurationError(
@@ -633,7 +634,7 @@
 class StartRepositoryCommand(Command):
     """Start a CubicWeb RQL server for a given instance.
 
-    The server will be accessible through pyro
+    The server will be remotely accessible through pyro or ZMQ
 
     <instance>
       the identifier of the instance to initialize.
@@ -650,12 +651,30 @@
           'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
           'help': 'debug if -D is set, error otherwise',
           }),
+        ('address',
+         {'short': 'a', 'type': 'string', 'metavar': '<protocol>://<host>:<port>',
+          'default': '',
+          'help': ('specify a ZMQ URI on which to bind, or use "pyro://"'
+                   'to create a pyro-based repository'),
+          }),
         )
 
+    def create_repo(self, config):
+        address = self['address']
+        if not address:
+            address = config.get('zmq-repository-address') or 'pyro://'
+        if address.startswith('pyro://'):
+            from cubicweb.server.server import RepositoryServer
+            return RepositoryServer(config), config['host']
+        else:
+            from cubicweb.server.utils import TasksManager
+            from cubicweb.server.cwzmq import ZMQRepositoryServer
+            repo = Repository(config, TasksManager())
+            return ZMQRepositoryServer(repo), address
+
     def run(self, args):
         from logilab.common.daemon import daemonize, setugid
         from cubicweb.cwctl import init_cmdline_log_threshold
-        from cubicweb.server.server import RepositoryServer
         appid = args[0]
         debug = self['debug']
         if sys.platform == 'win32' and not debug:
@@ -665,7 +684,7 @@
         config = ServerConfiguration.config_for(appid, debugmode=debug)
         init_cmdline_log_threshold(config, self['loglevel'])
         # create the server
-        server = RepositoryServer(config)
+        server, address = self.create_repo(config)
         # ensure the directory where the pid-file should be set exists (for
         # instance /var/run/cubicweb may be deleted on computer restart)
         pidfile = config['pid-file']
@@ -679,7 +698,7 @@
         if uid is not None:
             setugid(uid)
         server.install_sig_handlers()
-        server.connect(config['host'], 0)
+        server.connect(address)
         server.run()
 
 
@@ -974,20 +993,24 @@
 class RebuildFTICommand(Command):
     """Rebuild the full-text index of the system database of an instance.
 
-    <instance>
+    <instance> [etype(s)]
       the identifier of the instance to rebuild
+
+    If no etype is specified, cubicweb will reindex everything, otherwise
+    only specified etypes will be considered.
     """
     name = 'db-rebuild-fti'
     arguments = '<instance>'
-    min_args = max_args = 1
+    min_args = 1
 
     def run(self, args):
         from cubicweb.server.checkintegrity import reindex_entities
-        appid = args[0]
+        appid = args.pop(0)
+        etypes = args or None
         config = ServerConfiguration.config_for(appid)
         repo, cnx = repo_cnx(config)
         session = repo._get_session(cnx.sessionid, setcnxset=True)
-        reindex_entities(repo.schema, session)
+        reindex_entities(repo.schema, session, etypes=etypes)
         cnx.commit()
 
 
--- a/server/session.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/session.py	Tue Oct 23 15:00:53 2012 +0200
@@ -30,21 +30,16 @@
 from logilab.common.deprecation import deprecated
 from logilab.common.textutils import unormalize
 from logilab.common.registry import objectify_predicate
-from rql import CoercionError
-from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj
-from yams import BASE_TYPES
 
-from cubicweb import Binary, UnknownEid, QueryError, schema
+from cubicweb import UnknownEid, QueryError, schema
 from cubicweb.req import RequestSessionBase
 from cubicweb.dbapi import ConnectionProperties
-from cubicweb.utils import make_uid, RepeatList
+from cubicweb.utils import make_uid
 from cubicweb.rqlrewrite import RQLRewriter
 from cubicweb.server import ShuttingDown
 from cubicweb.server.edition import EditedEntity
 
 
-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
@@ -55,25 +50,6 @@
 NO_UNDO_TYPES.add('cw_source')
 # XXX rememberme,forgotpwd,apycot,vcsfile
 
-def _make_description(selected, args, solution):
-    """return a description for a result set"""
-    description = []
-    for term in selected:
-        description.append(term.get_type(solution, args))
-    return description
-
-def selection_idx_type(i, rqlst, args):
-    """try to return type of term at index `i` of the rqlst's selection"""
-    for select in rqlst.children:
-        term = select.selection[i]
-        for solution in select.solutions:
-            try:
-                ttype = term.get_type(solution, args)
-                if ttype is not None:
-                    return ttype
-            except CoercionError:
-                return None
-
 @objectify_predicate
 def is_user_session(cls, req, **kwargs):
     """repository side only predicate returning 1 if the session is a regular
@@ -106,7 +82,8 @@
         self.free_cnxset = free_cnxset
 
     def __enter__(self):
-        pass
+        # ensure session has a cnxset
+        self.session.set_cnxset()
 
     def __exit__(self, exctype, exc, traceback):
         if exctype:
@@ -131,6 +108,11 @@
 
        with hooks_control(self.session, self.session.HOOKS_DENY_ALL, 'integrity'):
            # ... do stuff with none but 'integrity' hooks activated
+
+    This is an internal api, you should rather use
+    :meth:`~cubicweb.server.session.Session.deny_all_hooks_but` or
+    :meth:`~cubicweb.server.session.Session.allow_all_hooks_but` session
+    methods.
     """
     def __init__(self, session, mode, *categories):
         self.session = session
@@ -240,7 +222,11 @@
 
       :attr:`running_dbapi_query`, boolean flag telling if the executing query
       is coming from a dbapi connection or is a query from within the repository
+
+    .. automethod:: cubicweb.server.session.deny_all_hooks_but
+    .. automethod:: cubicweb.server.session.all_all_hooks_but
     """
+    is_request = False
     is_internal_session = False
 
     def __init__(self, user, repo, cnxprops=None, _id=None):
@@ -252,20 +238,18 @@
         self.cnxtype = cnxprops.cnxtype
         self.timestamp = time()
         self.default_mode = 'read'
-        # support undo for Create Update Delete entity / Add Remove relation
+        # undo support
         if repo.config.creating or repo.config.repairing or self.is_internal_session:
-            self.undo_actions = ()
+            self.undo_actions = False
         else:
-            self.undo_actions = set(repo.config['undo-support'].upper())
-            if self.undo_actions - set('CUDAR'):
-                raise Exception('bad undo-support string in configuration')
+            self.undo_actions = repo.config['undo-enabled']
         # short cut to querier .execute method
         self._execute = repo.querier.execute
         # shared data, used to communicate extra information between the client
         # and the rql server
         self.data = {}
         # i18n initialization
-        self.set_language(cnxprops.lang)
+        self.set_language(user.prefered_language())
         # internals
         self._tx_data = {}
         self.__threaddata = threading.local()
@@ -463,28 +447,6 @@
             self.cnxset.reconnect(source)
             return source.doexec(self, sql, args, rollback=rollback_on_failure)
 
-    def set_language(self, language):
-        """i18n configuration for translation"""
-        language = language or self.user.property_value('ui.language')
-        try:
-            gettext, pgettext = self.vreg.config.translations[language]
-            self._ = self.__ = gettext
-            self.pgettext = pgettext
-        except KeyError:
-            language = self.vreg.property_value('ui.language')
-            try:
-                gettext, pgettext = self.vreg.config.translations[language]
-                self._ = self.__ = gettext
-                self.pgettext = pgettext
-            except KeyError:
-                self._ = self.__ = unicode
-                self.pgettext = lambda x, y: y
-        self.lang = language
-
-    def change_property(self, prop, value):
-        assert prop == 'lang' # this is the only one changeable property for now
-        self.set_language(value)
-
     def deleted_in_transaction(self, eid):
         """return True if the entity of the given eid is being deleted in the
         current transaction
@@ -508,7 +470,7 @@
 
     DEFAULT_SECURITY = object() # evaluated to true by design
 
-    def security_enabled(self, read=False, write=False):
+    def security_enabled(self, read=None, write=None):
         return security_enabled(self, read=read, write=write)
 
     def init_security(self, read, write):
@@ -847,6 +809,12 @@
         else:
             self.data[key] = value
 
+    # server-side service call #################################################
+
+    def call_service(self, regid, async=False, **kwargs):
+        return self.repo.call_service(self.id, regid, async, **kwargs)
+
+
     # request interface #######################################################
 
     @property
@@ -890,7 +858,7 @@
         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
         metas = self.repo.type_and_source_from_eid(eid, self)
         if asdict:
-            return dict(zip(('type', 'source', 'extid', 'asource'), metas)) 
+            return dict(zip(('type', 'source', 'extid', 'asource'), metas))
        # XXX :-1 for cw compat, use asdict=True for full information
         return metas[:-1]
 
@@ -1118,9 +1086,8 @@
 
     # 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 ertype_supports_undo(self, ertype):
+        return self.undo_actions  and ertype not in NO_UNDO_TYPES
 
     def transaction_uuid(self, set=True):
         try:
@@ -1148,71 +1115,6 @@
             self._threaddata._rewriter = RQLRewriter(self)
             return self._threaddata._rewriter
 
-    def build_description(self, rqlst, args, result):
-        """build a description for a given result"""
-        if len(rqlst.children) == 1 and len(rqlst.children[0].solutions) == 1:
-            # easy, all lines are identical
-            selected = rqlst.children[0].selection
-            solution = rqlst.children[0].solutions[0]
-            description = _make_description(selected, args, solution)
-            return RepeatList(len(result), tuple(description))
-        # hard, delegate the work :o)
-        return self.manual_build_descr(rqlst, args, result)
-
-    def manual_build_descr(self, rqlst, args, result):
-        """build a description for a given result by analysing each row
-
-        XXX could probably be done more efficiently during execution of query
-        """
-        # not so easy, looks for variable which changes from one solution
-        # to another
-        unstables = rqlst.get_variable_indices()
-        basedescr = []
-        todetermine = []
-        for i in xrange(len(rqlst.children[0].selection)):
-            ttype = selection_idx_type(i, rqlst, args)
-            if ttype is None or ttype == 'Any':
-                ttype = None
-                isfinal = True
-            else:
-                isfinal = ttype in BASE_TYPES
-            if ttype is None or i in unstables:
-                basedescr.append(None)
-                todetermine.append( (i, isfinal) )
-            else:
-                basedescr.append(ttype)
-        if not todetermine:
-            return RepeatList(len(result), tuple(basedescr))
-        return self._build_descr(result, basedescr, todetermine)
-
-    def _build_descr(self, result, basedescription, todetermine):
-        description = []
-        etype_from_eid = self.describe
-        todel = []
-        for i, row in enumerate(result):
-            row_descr = basedescription[:]
-            for index, isfinal in todetermine:
-                value = row[index]
-                if value is None:
-                    # None value inserted by an outer join, no type
-                    row_descr[index] = None
-                    continue
-                if isfinal:
-                    row_descr[index] = etype_from_pyobj(value)
-                else:
-                    try:
-                        row_descr[index] = etype_from_eid(value)[0]
-                    except UnknownEid:
-                        self.error('wrong eid %s in repository, you should '
-                                   'db-check the database' % value)
-                        todel.append(i)
-                        break
-            else:
-                description.append(tuple(row_descr))
-        for i in reversed(todel):
-            del result[i]
-        return description
-
     # deprecated ###############################################################
 
     @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
@@ -1272,6 +1174,12 @@
         if not safe:
             self.disable_hook_categories('integrity')
 
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exctype, excvalue, tb):
+        self.close()
+
     @property
     def cnxset(self):
         """connections set, set according to transaction mode for each query"""
@@ -1305,6 +1213,9 @@
             return 'en'
         return None
 
+    def prefered_language(self, language=None):
+        # mock CWUser.prefered_language, mainly for testing purpose
+        return self.property_value('ui.language')
 
 from logging import getLogger
 from cubicweb import set_log_methods
--- a/server/sources/__init__.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/__init__.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/server/sources/datafeed.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/datafeed.py	Tue Oct 23 15:00:53 2012 +0200
@@ -28,9 +28,9 @@
 from cookielib import CookieJar
 
 from lxml import etree
-from logilab.mtconverter import xml_escape
 
 from cubicweb import RegistryNotFound, ObjectNotFound, ValidationError, UnknownEid
+from cubicweb.server.repository import preprocess_inlined_relations
 from cubicweb.server.sources import AbstractSource
 from cubicweb.appobject import AppObject
 
@@ -68,7 +68,7 @@
           }),
         ('delete-entities',
          {'type' : 'yn',
-          'default': True,
+          'default': False,
           'help': ('Should already imported entities not found anymore on the '
                    'external source be deleted?'),
           'group': 'datafeed-source', 'level': 2,
@@ -80,6 +80,7 @@
           'group': 'datafeed-source', 'level': 2,
           }),
         )
+
     def __init__(self, repo, source_config, eid=None):
         AbstractSource.__init__(self, repo, source_config, eid)
         self.update_config(None, self.check_conf_dict(eid, source_config,
@@ -152,21 +153,24 @@
 
     def update_latest_retrieval(self, session):
         self.latest_retrieval = datetime.utcnow()
+        session.set_cnxset()
         session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s',
                         {'x': self.eid, 'date': self.latest_retrieval})
+        session.commit()
 
     def acquire_synchronization_lock(self, session):
         # XXX race condition until WHERE of SET queries is executed using
         # 'SELECT FOR UPDATE'
         now = datetime.utcnow()
+        session.set_cnxset()
         if not session.execute(
             'SET X in_synchronization %(now)s WHERE X eid %(x)s, '
             'X in_synchronization NULL OR X in_synchronization < %(maxdt)s',
             {'x': self.eid, 'now': now, 'maxdt': now - self.max_lock_lifetime}):
             self.error('concurrent synchronization detected, skip pull')
-            session.commit(free_cnxset=False)
+            session.commit()
             return False
-        session.commit(free_cnxset=False)
+        session.commit()
         return True
 
     def release_synchronization_lock(self, session):
@@ -192,30 +196,22 @@
             self.release_synchronization_lock(session)
 
     def _pull_data(self, session, force=False, raise_on_error=False):
-        if self.config['delete-entities']:
-            myuris = self.source_cwuris(session)
-        else:
-            myuris = None
         importlog = self.init_import_log(session)
+        myuris = self.source_cwuris(session)
         parser = self._get_parser(session, sourceuris=myuris, import_log=importlog)
         if self.process_urls(parser, self.urls, raise_on_error):
             self.warning("some error occured, don't attempt to delete entities")
-        elif self.config['delete-entities'] and myuris:
-            byetype = {}
-            for extid, (eid, etype) in myuris.iteritems():
-                if parser.is_deleted(extid, etype, eid):
-                    byetype.setdefault(etype, []).append(str(eid))
-            for etype, eids in byetype.iteritems():
-                self.warning('delete %s %s entities', len(eids), etype)
-                session.execute('DELETE %s X WHERE X eid IN (%s)'
-                                % (etype, ','.join(eids)))
+        else:
+            parser.handle_deletion(self.config, session, myuris)
         self.update_latest_retrieval(session)
         stats = parser.stats
         if stats.get('created'):
             importlog.record_info('added %s entities' % len(stats['created']))
         if stats.get('updated'):
             importlog.record_info('updated %s entities' % len(stats['updated']))
+        session.set_cnxset()
         importlog.write_log(session, end_timestamp=self.latest_retrieval)
+        session.commit()
         return stats
 
     def process_urls(self, parser, urls, raise_on_error=False):
@@ -259,18 +255,27 @@
         """called by the repository after an entity stored here has been
         inserted in the system table.
         """
+        relations = preprocess_inlined_relations(session, entity)
         if session.is_hook_category_activated('integrity'):
             entity.cw_edited.check(creation=True)
         self.repo.system_source.add_entity(session, entity)
         entity.cw_edited.saved = entity._cw_is_saved = True
         sourceparams['parser'].after_entity_copy(entity, sourceparams)
+        # call hooks for inlined relations
+        call_hooks = self.repo.hm.call_hooks
+        if self.should_call_hooks:
+            for attr, value in relations:
+                call_hooks('before_add_relation', session,
+                           eidfrom=entity.eid, rtype=attr, eidto=value)
+                call_hooks('after_add_relation', session,
+                           eidfrom=entity.eid, rtype=attr, eidto=value)
 
     def source_cwuris(self, session):
         sql = ('SELECT extid, eid, type FROM entities, cw_source_relation '
                'WHERE entities.eid=cw_source_relation.eid_from '
                'AND cw_source_relation.eid_to=%s' % self.eid)
         return dict((b64decode(uri), (eid, type))
-                    for uri, eid, type in session.system_sql(sql))
+                    for uri, eid, type in session.system_sql(sql).fetchall())
 
     def init_import_log(self, session, **kwargs):
         dataimport = session.create_entity('CWDataImport', cw_import_of=self,
@@ -288,8 +293,7 @@
         self.source = source
         self.sourceuris = sourceuris
         self.import_log = import_log
-        self.stats = {'created': set(),
-                      'updated': set()}
+        self.stats = {'created': set(), 'updated': set(), 'checked': set()}
 
     def normalize_url(self, url):
         from cubicweb.sobjects import URL_MAPPING # available after registration
@@ -350,7 +354,7 @@
             self.sourceuris.pop(str(uri), None)
         return session.entity_from_eid(eid, etype)
 
-    def process(self, url, partialcommit=True):
+    def process(self, url, raise_on_error=False):
         """main callback: process the url"""
         raise NotImplementedError
 
@@ -369,6 +373,9 @@
     def notify_updated(self, entity):
         return self.stats['updated'].add(entity.eid)
 
+    def notify_checked(self, entity):
+        return self.stats['checked'].add(entity.eid)
+
     def is_deleted(self, extid, etype, eid):
         """return True if the entity of given external id, entity type and eid
         is actually deleted. Always return True by default, put more sensible
@@ -376,22 +383,36 @@
         """
         return True
 
+    def handle_deletion(self, config, session, myuris):
+        if config['delete-entities'] and myuris:
+            byetype = {}
+            for extid, (eid, etype) in myuris.iteritems():
+                if self.is_deleted(extid, etype, eid):
+                    byetype.setdefault(etype, []).append(str(eid))
+            for etype, eids in byetype.iteritems():
+                self.warning('delete %s %s entities', len(eids), etype)
+                session.set_cnxset()
+                session.execute('DELETE %s X WHERE X eid IN (%s)'
+                                % (etype, ','.join(eids)))
+                session.commit()
+
     def update_if_necessary(self, entity, attrs):
-        self.notify_updated(entity)
         entity.complete(tuple(attrs))
         # check modification date and compare attribute values to only update
         # what's actually needed
+        self.notify_checked(entity)
         mdate = attrs.get('modification_date')
         if not mdate or mdate > entity.modification_date:
             attrs = dict( (k, v) for k, v in attrs.iteritems()
                           if v != getattr(entity, k))
             if attrs:
-                entity.set_attributes(**attrs)
+                entity.cw_set(**attrs)
+                self.notify_updated(entity)
 
 
 class DataFeedXMLParser(DataFeedParser):
 
-    def process(self, url, raise_on_error=False, partialcommit=True):
+    def process(self, url, raise_on_error=False):
         """IDataFeedParser main entry point"""
         try:
             parsed = self.parse(url)
@@ -401,24 +422,30 @@
             self.import_log.record_error(str(ex))
             return True
         error = False
+        # Check whether self._cw is a session or a connection
+        if getattr(self._cw, 'commit', None) is not None:
+            commit = self._cw.commit
+            set_cnxset = self._cw.set_cnxset
+            rollback = self._cw.rollback
+        else:
+            commit = self._cw.cnx.commit
+            set_cnxset = lambda: None
+            rollback = self._cw.cnx.rollback
         for args in parsed:
             try:
                 self.process_item(*args)
-                if partialcommit:
-                    # commit+set_cnxset instead of commit(free_cnxset=False) to let
-                    # other a chance to get our connections set
-                    self._cw.commit()
-                    self._cw.set_cnxset()
+                # commit+set_cnxset instead of commit(free_cnxset=False) to let
+                # other a chance to get our connections set
+                commit()
+                set_cnxset()
             except ValidationError, exc:
                 if raise_on_error:
                     raise
-                if partialcommit:
-                    self.source.error('Skipping %s because of validation error %s' % (args, exc))
-                    self._cw.rollback()
-                    self._cw.set_cnxset()
-                    error = True
-                else:
-                    raise
+                self.source.error('Skipping %s because of validation error %s'
+                                  % (args, exc))
+                rollback()
+                set_cnxset()
+                error = True
         return error
 
     def parse(self, url):
--- a/server/sources/ldapfeed.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/ldapfeed.py	Tue Oct 23 15:00:53 2012 +0200
@@ -29,7 +29,7 @@
                      datafeed.DataFeedSource):
     """LDAP feed source"""
     support_entities = {'CWUser': False}
-    use_cwuri_as_url = True
+    use_cwuri_as_url = False
 
     options = datafeed.DataFeedSource.options + ldaputils.LDAPSourceMixIn.options
 
@@ -43,4 +43,3 @@
     def _entity_update(self, source_entity):
         datafeed.DataFeedSource._entity_update(self, source_entity)
         ldaputils.LDAPSourceMixIn._entity_update(self, source_entity)
-
--- a/server/sources/native.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/native.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -55,17 +55,16 @@
 from yams.schema import role_name
 
 from cubicweb import (UnknownEid, AuthenticationError, ValidationError, Binary,
-                      UniqueTogetherError)
+                      UniqueTogetherError, QueryError, UndoTransactionException)
 from cubicweb import transaction as tx, server, neg_role
 from cubicweb.utils import QueryCache
 from cubicweb.schema import VIRTUAL_RTYPES
 from cubicweb.cwconfig import CubicWebNoAppConfiguration
 from cubicweb.server import hook
-from cubicweb.server.utils import crypt_password, eschema_eid
+from cubicweb.server.utils import crypt_password, eschema_eid, verify_and_update
 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.edition import EditedEntity
 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
 from cubicweb.server.sources.rql2sql import SQLGenerator
@@ -162,24 +161,24 @@
     allownull = rdef.cardinality[0] != '1'
     return coltype, allownull
 
-class UndoException(Exception):
+
+class _UndoException(Exception):
     """something went wrong during undoing"""
 
     def __unicode__(self):
         """Called by the unicode builtin; should return a Unicode object
 
-        Type of UndoException message must be `unicode` by design in CubicWeb.
+        Type of _UndoException message must be `unicode` by design in CubicWeb.
+        """
+        assert isinstance(self.args[0], unicode)
+        return self.args[0]
 
-        .. warning::
-            This method is not available in python2.5"""
-        assert isinstance(self.message, unicode)
-        return self.message
 
 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._(
+        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),
@@ -192,7 +191,7 @@
         try:
             entities.append(session.entity_from_eid(eid))
         except UnknownEid:
-            raise UndoException(session._(
+            raise _UndoException(session._(
                 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s"
                 " doesn't exist anymore.")
                                 % {'role': session._(role),
@@ -203,7 +202,7 @@
         rschema = session.vreg.schema.rschema(rtype)
         rdef = rschema.rdefs[(sentity.__regid__, oentity.__regid__)]
     except KeyError:
-        raise UndoException(session._(
+        raise _UndoException(session._(
             "Can't restore relation %(rtype)s between %(subj)s and "
             "%(obj)s, that relation does not exists anymore in the "
             "schema.")
@@ -614,12 +613,10 @@
         etype = entities[0].__regid__
         for attr, storage in self._storages.get(etype, {}).items():
             for entity in entities:
-                try:
+                if event == 'deleted':
+                    storage.entity_deleted(entity, attr)
+                else:
                     edited = entity.cw_edited
-                except AttributeError:
-                    assert event == 'deleted'
-                    getattr(storage, 'entity_deleted')(entity, attr)
-                else:
                     if attr in edited:
                         handler = getattr(storage, 'entity_%s' % event)
                         to_restore = handler(entity, attr)
@@ -637,7 +634,7 @@
             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__):
+            if session.ertype_supports_undo(entity.__regid__):
                 self._record_tx_action(session, 'tx_entity_actions', 'C',
                                        etype=entity.__regid__, eid=entity.eid)
 
@@ -645,7 +642,7 @@
         """replace an entity in the source"""
         with self._storage_handler(entity, 'updated'):
             attrs = self.preprocess_entity(entity)
-            if session.undoable_action('U', entity.__regid__):
+            if session.ertype_supports_undo(entity.__regid__):
                 changes = self._save_attrs(session, entity, attrs)
                 self._record_tx_action(session, 'tx_entity_actions', 'U',
                                        etype=entity.__regid__, eid=entity.eid,
@@ -657,7 +654,7 @@
     def delete_entity(self, session, entity):
         """delete an entity from the source"""
         with self._storage_handler(entity, 'deleted'):
-            if session.undoable_action('D', entity.__regid__):
+            if session.ertype_supports_undo(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]
@@ -672,14 +669,14 @@
     def add_relation(self, session, subject, rtype, object, inlined=False):
         """add a relation to the source"""
         self._add_relations(session,  rtype, [(subject, object)], inlined)
-        if session.undoable_action('A', rtype):
+        if session.ertype_supports_undo(rtype):
             self._record_tx_action(session, 'tx_relation_actions', 'A',
                                    eid_from=subject, rtype=rtype, eid_to=object)
 
     def add_relations(self, session,  rtype, subj_obj_list, inlined=False):
         """add a relations to the source"""
         self._add_relations(session, rtype, subj_obj_list, inlined)
-        if session.undoable_action('A', rtype):
+        if session.ertype_supports_undo(rtype):
             for subject, object in subj_obj_list:
                 self._record_tx_action(session, 'tx_relation_actions', 'A',
                                        eid_from=subject, rtype=rtype, eid_to=object)
@@ -712,7 +709,7 @@
         """delete a relation from the source"""
         rschema = self.schema.rschema(rtype)
         self._delete_relation(session, subject, rtype, object, rschema.inlined)
-        if session.undoable_action('R', rtype):
+        if session.ertype_supports_undo(rtype):
             self._record_tx_action(session, 'tx_relation_actions', 'R',
                                    eid_from=subject, rtype=rtype, eid_to=object)
 
@@ -1157,16 +1154,18 @@
         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):
+        with session.deny_all_hooks_but('integrity', 'activeintegrity', 'undo'):
+            with session.security_enabled(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
+        if errors:
+            raise UndoTransactionException(txuuid, errors)
+        else:
+            return
 
     def start_undoable_transaction(self, session, uuid):
         """session callback to insert a transaction record in the transactions
@@ -1219,12 +1218,53 @@
         try:
             time, ueid = cu.fetchone()
         except TypeError:
-            raise tx.NoSuchTransaction()
+            raise tx.NoSuchTransaction(txuuid)
         if not (session.user.is_in_group('managers')
                 or session.user.eid == ueid):
-            raise tx.NoSuchTransaction()
+            raise tx.NoSuchTransaction(txuuid)
         return time, ueid
 
+    def _reedit_entity(self, entity, changes, err):
+        session = entity._cw
+        eid = entity.eid
+        entity.cw_edited = edited = EditedEntity(entity)
+        # 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 changes.items():
+            rtype = column[len(SQL_PREFIX):]
+            if rtype == "eid":
+                continue # XXX should even `eid` be stored in action changes?
+            try:
+                rschema = getrschema[rtype]
+            except KeyError:
+                err(session._("can't restore relation %(rtype)s of entity %(eid)s, "
+                              "this relation does not exist in the schema anymore.")
+                    % {'rtype': rtype, 'eid': eid})
+            if not rschema.final:
+                if not rschema.inlined:
+                    assert value is None
+                # rschema is an inlined relation
+                elif value is not None:
+                    # not a deletion: we must put something in edited
+                    try:
+                        entity._cw.entity_from_eid(value) # check target exists
+                        edited[rtype] = value
+                    except UnknownEid:
+                        err(session._("can't restore entity %(eid)s of type %(eschema)s, "
+                                      "target of %(rtype)s (eid %(value)s) does not exist any longer")
+                            % locals())
+            elif eschema.destination(rtype) in ('Bytes', 'Password'):
+                changes[column] = self._binary(value)
+                edited[rtype] = Binary(value)
+            elif isinstance(value, str):
+                edited[rtype] = unicode(value, session.encoding, 'replace')
+            else:
+                edited[rtype] = value
+        # This must only be done after init_entitiy_caches : defered in calling functions
+        # edited.check()
+
     def _undo_d(self, session, action):
         """undo an entity deletion"""
         errors = []
@@ -1239,31 +1279,10 @@
             err("can't restore entity %s of type %s, type no more supported"
                 % (eid, etype))
             return errors
-        entity.cw_edited = edited = EditedEntity(entity)
-        # 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)
-                edited[rtype] = Binary(value)
-            elif isinstance(value, str):
-                edited[rtype] = unicode(value, session.encoding, 'replace')
-            else:
-                edited[rtype] = value
+        self._reedit_entity(entity, action.changes, err)
         entity.eid = eid
         session.repo.init_entity_caches(session, entity, self)
-        edited.check()
+        entity.cw_edited.check()
         self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
         # restore the entity
         action.changes['cw_eid'] = eid
@@ -1284,14 +1303,14 @@
         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:
+        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:
+                except _UndoException, ex:
                     errors.append(unicode(ex))
                     continue
         if not errors:
@@ -1344,7 +1363,22 @@
 
     def _undo_u(self, session, action):
         """undo an entity update"""
-        return ['undoing of entity updating not yet supported.']
+        errors = []
+        err = errors.append
+        try:
+            entity = session.entity_from_eid(action.eid)
+        except UnknownEid:
+            err(session._("can't restore state of entity %s, it has been "
+                          "deleted inbetween") % action.eid)
+            return errors
+        self._reedit_entity(entity, action.changes, err)
+        entity.cw_edited.check()
+        self.repo.hm.call_hooks('before_update_entity', session, entity=entity)
+        sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, action.changes,
+                                 ['cw_eid'])
+        self.doexec(session, sql, action.changes)
+        self.repo.hm.call_hooks('after_update_entity', session, entity=entity)
+        return errors
 
     def _undo_a(self, session, action):
         """undo a relation addition"""
@@ -1352,7 +1386,7 @@
         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:
+        except _UndoException, ex:
             errors.append(unicode(ex))
         else:
             rschema = rdef.rtype
@@ -1561,9 +1595,10 @@
         pass
 
 class LoginPasswordAuthentifier(BaseAuthentifier):
-    passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P"
-    auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s"
-    _sols = ({'X': 'CWUser', 'P': 'Password'},)
+    passwd_rql = 'Any P WHERE X is CWUser, X login %(login)s, X upassword P'
+    auth_rql = ('Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s, '
+                'X cw_source S, S name "system"')
+    _sols = ({'X': 'CWUser', 'P': 'Password', 'S': 'CWSource'},)
 
     def set_schema(self, schema):
         """set the instance'schema"""
@@ -1590,11 +1625,26 @@
                 # if pwd is None but a password is provided, something is wrong
                 raise AuthenticationError('bad password')
             # passwords are stored using the Bytes type, so we get a StringIO
-            args['pwd'] = Binary(crypt_password(password, pwd.getvalue()[:2]))
+            args['pwd'] = Binary(crypt_password(password, pwd.getvalue()))
         # get eid from login and (crypted) password
         rset = self.source.syntax_tree_search(session, self._auth_rqlst, args)
         try:
-            return rset[0][0]
+            user = rset[0][0]
+            # If the stored hash uses a deprecated scheme (e.g. DES or MD5 used
+            # before 3.14.7), update with a fresh one
+            if pwd.getvalue():
+                verify, newhash = verify_and_update(password, pwd.getvalue())
+                if not verify: # should not happen, but...
+                    raise AuthenticationError('bad password')
+                if newhash:
+                    session.system_sql("UPDATE %s SET %s=%%(newhash)s WHERE %s=%%(login)s" % (
+                                        SQL_PREFIX + 'CWUser',
+                                        SQL_PREFIX + 'upassword',
+                                        SQL_PREFIX + 'login'),
+                                       {'newhash': self.source._binary(newhash),
+                                        'login': login})
+                    session.commit(free_cnxset=False)
+            return user
         except IndexError:
             raise AuthenticationError('bad password')
 
@@ -1775,8 +1825,10 @@
         versions = set(self._get_versions())
         if file_versions != versions:
             self.logger.critical('Unable to restore : versions do not match')
-            self.logger.critical('Expected:\n%s', '\n'.join(list(sorted(versions))))
-            self.logger.critical('Found:\n%s', '\n'.join(list(sorted(file_versions))))
+            self.logger.critical('Expected:\n%s', '\n'.join('%s : %s' % (cube, ver)
+                                                            for cube, ver in sorted(versions)))
+            self.logger.critical('Found:\n%s', '\n'.join('%s : %s' % (cube, ver)
+                                                         for cube, ver in sorted(file_versions)))
             raise ValueError('Unable to restore : versions do not match')
         table_chunks = {}
         for name in archive.namelist():
--- a/server/sources/pyrorql.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/pyrorql.py	Tue Oct 23 15:00:53 2012 +0200
@@ -21,298 +21,56 @@
 _ = unicode
 
 import threading
-from os.path import join
-from time import mktime
-from datetime import datetime
-from base64 import b64decode
-
 from Pyro.errors import PyroError, ConnectionClosedError
 
 from logilab.common.configuration import REQUIRED
-from logilab.common.optik_ext import check_yn
-
-from yams.schema import role_name
-
-from rql.nodes import Constant
-from rql.utils import rqlvar_maker
 
-from cubicweb import dbapi, server
-from cubicweb import ValidationError, BadConnectionId, UnknownEid, ConnectionError
-from cubicweb.schema import VIRTUAL_RTYPES
-from cubicweb.cwconfig import register_persistent_options
-from cubicweb.server.sources import (AbstractSource, ConnectionWrapper,
-                                     TimedCache, dbg_st_search, dbg_results)
-from cubicweb.server.msplanner import neged_relation
+from cubicweb import dbapi
+from cubicweb import ConnectionError
+from cubicweb.server.sources import ConnectionWrapper
 
-def uidtype(union, col, etype, args):
-    select, col = union.locate_subquery(col, etype, args)
-    return getattr(select.selection[col], 'uidtype', None)
-
+from cubicweb.server.sources.remoterql import RemoteSource
 
-class ReplaceByInOperator(Exception):
-    def __init__(self, eids):
-        self.eids = eids
-
-class PyroRQLSource(AbstractSource):
+class PyroRQLSource(RemoteSource):
     """External repository source, using Pyro connection"""
 
-    # boolean telling if modification hooks should be called when something is
-    # modified in this source
-    should_call_hooks = False
-    # boolean telling if the repository should connect to this source during
-    # migration
-    connect_for_migration = False
+    CNX_TYPE = 'pyro'
 
-    options = (
+    options = RemoteSource.options + (
         # XXX pyro-ns host/port
         ('pyro-ns-id',
          {'type' : 'string',
           'default': REQUIRED,
           'help': 'identifier of the repository in the pyro name server',
-          'group': 'pyro-source', 'level': 0,
-          }),
-        ('cubicweb-user',
-         {'type' : 'string',
-          'default': REQUIRED,
-          'help': 'user to use for connection on the distant repository',
-          'group': 'pyro-source', 'level': 0,
-          }),
-        ('cubicweb-password',
-         {'type' : 'password',
-          'default': '',
-          'help': 'user to use for connection on the distant repository',
-          'group': 'pyro-source', 'level': 0,
-          }),
-        ('base-url',
-         {'type' : 'string',
-          'default': '',
-          'help': 'url of the web site for the distant repository, if you want '
-          'to generate external link to entities from this repository',
-          'group': 'pyro-source', 'level': 1,
-          }),
-        ('skip-external-entities',
-         {'type' : 'yn',
-          'default': False,
-          'help': 'should entities not local to the source be considered or not',
-          'group': 'pyro-source', 'level': 0,
+          'group': 'remote-source', 'level': 0,
           }),
         ('pyro-ns-host',
          {'type' : 'string',
           'default': None,
           'help': 'Pyro name server\'s host. If not set, default to the value \
 from all_in_one.conf. It may contains port information using <host>:<port> notation.',
-          'group': 'pyro-source', 'level': 1,
+          'group': 'remote-source', 'level': 1,
           }),
         ('pyro-ns-group',
          {'type' : 'string',
           'default': None,
           'help': 'Pyro name server\'s group where the repository will be \
 registered. If not set, default to the value from all_in_one.conf.',
-          'group': 'pyro-source', 'level': 2,
+          'group': 'remote-source', 'level': 2,
           }),
-        ('synchronization-interval',
-         {'type' : 'time',
-          'default': '5min',
-          'help': 'interval between synchronization with the external \
-repository (default to 5 minutes).',
-          'group': 'pyro-source', 'level': 2,
-          }),
-
     )
 
-    PUBLIC_KEYS = AbstractSource.PUBLIC_KEYS + ('base-url',)
-    _conn = None
-
-    def __init__(self, repo, source_config, eid=None):
-        AbstractSource.__init__(self, repo, source_config, eid)
-        self.update_config(None, self.check_conf_dict(eid, source_config,
-                                                      fail_if_unknown=False))
-        self._query_cache = TimedCache(1800)
-
-    def update_config(self, source_entity, processed_config):
-        """update configuration from source entity"""
-        # XXX get it through pyro if unset
-        baseurl = processed_config.get('base-url')
-        if baseurl and not baseurl.endswith('/'):
-            processed_config['base-url'] += '/'
-        self.config = processed_config
-        self._skip_externals = processed_config['skip-external-entities']
-        if source_entity is not None:
-            self.latest_retrieval = source_entity.latest_retrieval
-
-    def reset_caches(self):
-        """method called during test to reset potential source caches"""
-        self._query_cache = TimedCache(1800)
-
-    def init(self, activated, source_entity):
-        """method called by the repository once ready to handle request"""
-        self.load_mapping(source_entity._cw)
-        if activated:
-            interval = self.config['synchronization-interval']
-            self.repo.looping_task(interval, self.synchronize)
-            self.repo.looping_task(self._query_cache.ttl.seconds/10,
-                                   self._query_cache.clear_expired)
-            self.latest_retrieval = source_entity.latest_retrieval
-
-    def load_mapping(self, session=None):
-        self.support_entities = {}
-        self.support_relations = {}
-        self.dont_cross_relations = set(('owned_by', 'created_by'))
-        self.cross_relations = set()
-        assert self.eid is not None
-        self._schemacfg_idx = {}
-        self._load_mapping(session)
-
-    etype_options = set(('write',))
-    rtype_options = set(('maycross', 'dontcross', 'write',))
-
-    def _check_options(self, schemacfg, allowedoptions):
-        if schemacfg.options:
-            options = set(w.strip() for w in schemacfg.options.split(':'))
-        else:
-            options = set()
-        if options - allowedoptions:
-            options = ', '.join(sorted(options - allowedoptions))
-            msg = _('unknown option(s): %s' % options)
-            raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
-        return options
-
-    def add_schema_config(self, schemacfg, checkonly=False):
-        """added CWSourceSchemaConfig, modify mapping accordingly"""
-        try:
-            ertype = schemacfg.schema.name
-        except AttributeError:
-            msg = schemacfg._cw._("attribute/relation can't be mapped, only "
-                                  "entity and relation types")
-            raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
-        if schemacfg.schema.__regid__ == 'CWEType':
-            options = self._check_options(schemacfg, self.etype_options)
-            if not checkonly:
-                self.support_entities[ertype] = 'write' in options
-        else: # CWRType
-            if ertype in ('is', 'is_instance_of', 'cw_source') or ertype in VIRTUAL_RTYPES:
-                msg = schemacfg._cw._('%s relation should not be in mapped') % ertype
-                raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
-            options = self._check_options(schemacfg, self.rtype_options)
-            if 'dontcross' in options:
-                if 'maycross' in options:
-                    msg = schemacfg._("can't mix dontcross and maycross options")
-                    raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
-                if 'write' in options:
-                    msg = schemacfg._("can't mix dontcross and write options")
-                    raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
-                if not checkonly:
-                    self.dont_cross_relations.add(ertype)
-            elif not checkonly:
-                self.support_relations[ertype] = 'write' in options
-                if 'maycross' in options:
-                    self.cross_relations.add(ertype)
-        if not checkonly:
-            # add to an index to ease deletion handling
-            self._schemacfg_idx[schemacfg.eid] = ertype
-
-    def del_schema_config(self, schemacfg, checkonly=False):
-        """deleted CWSourceSchemaConfig, modify mapping accordingly"""
-        if checkonly:
-            return
-        try:
-            ertype = self._schemacfg_idx[schemacfg.eid]
-            if ertype[0].isupper():
-                del self.support_entities[ertype]
-            else:
-                if ertype in self.support_relations:
-                    del self.support_relations[ertype]
-                    if ertype in self.cross_relations:
-                        self.cross_relations.remove(ertype)
-                else:
-                    self.dont_cross_relations.remove(ertype)
-        except Exception:
-            self.error('while updating mapping consequently to removal of %s',
-                       schemacfg)
-
-    def local_eid(self, cnx, extid, session):
-        etype, dexturi, dextid = cnx.describe(extid)
-        if dexturi == 'system' or not (
-            dexturi in self.repo.sources_by_uri or self._skip_externals):
-            assert etype in self.support_entities, etype
-            eid = self.repo.extid2eid(self, str(extid), etype, session)
-            if eid > 0:
-                return eid, True
-        elif dexturi in self.repo.sources_by_uri:
-            source = self.repo.sources_by_uri[dexturi]
-            cnx = session.cnxset.connection(source.uri)
-            eid = source.local_eid(cnx, dextid, session)[0]
-            return eid, False
-        return None, None
-
-    def synchronize(self, mtime=None):
-        """synchronize content known by this repository with content in the
-        external repository
-        """
-        self.info('synchronizing pyro source %s', self.uri)
-        cnx = self.get_connection()
-        try:
-            extrepo = cnx._repo
-        except AttributeError:
-            # fake connection wrapper returned when we can't connect to the
-            # external source (hence we've no chance to synchronize...)
-            return
-        etypes = self.support_entities.keys()
-        if mtime is None:
-            mtime = self.latest_retrieval
-        updatetime, modified, deleted = extrepo.entities_modified_since(
-            etypes, mtime)
-        self._query_cache.clear()
-        repo = self.repo
-        session = repo.internal_session()
-        source = repo.system_source
-        try:
-            for etype, extid in modified:
-                try:
-                    eid = self.local_eid(cnx, extid, session)[0]
-                    if eid is not None:
-                        rset = session.eid_rset(eid, etype)
-                        entity = rset.get_entity(0, 0)
-                        entity.complete(entity.e_schema.indexable_attributes())
-                        source.index_entity(session, entity)
-                except Exception:
-                    self.exception('while updating %s with external id %s of source %s',
-                                   etype, extid, self.uri)
-                    continue
-            for etype, extid in deleted:
-                try:
-                    eid = self.repo.extid2eid(self, str(extid), etype, session,
-                                              insert=False)
-                    # entity has been deleted from external repository but is not known here
-                    if eid is not None:
-                        entity = session.entity_from_eid(eid, etype)
-                        repo.delete_info(session, entity, self.uri,
-                                         scleanup=self.eid)
-                except Exception:
-                    if self.repo.config.mode == 'test':
-                        raise
-                    self.exception('while updating %s with external id %s of source %s',
-                                   etype, extid, self.uri)
-                    continue
-            self.latest_retrieval = updatetime
-            session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s',
-                            {'x': self.eid, 'date': self.latest_retrieval})
-            session.commit()
-        finally:
-            session.close()
-
     def _get_connection(self):
         """open and return a connection to the source"""
         nshost = self.config.get('pyro-ns-host') or self.repo.config['pyro-ns-host']
         nsgroup = self.config.get('pyro-ns-group') or self.repo.config['pyro-ns-group']
         self.info('connecting to instance :%s.%s for user %s',
                   nsgroup, self.config['pyro-ns-id'], self.config['cubicweb-user'])
-        #cnxprops = ConnectionProperties(cnxtype=self.config['cnx-type'])
         return dbapi.connect(database=self.config['pyro-ns-id'],
                              login=self.config['cubicweb-user'],
                              password=self.config['cubicweb-password'],
                              host=nshost, group=nsgroup,
-                             setvreg=False) #cnxprops=cnxprops)
+                             setvreg=False)
 
     def get_connection(self):
         try:
@@ -333,373 +91,9 @@
         except AttributeError:
             # inmemory connection
             pass
-        if not isinstance(cnx, ConnectionWrapper):
-            try:
-                cnx.check()
-                return # ok
-            except (BadConnectionId, ConnectionClosedError):
-                pass
-        # try to reconnect
-        return self.get_connection()
-
-    def syntax_tree_search(self, session, union, args=None, cachekey=None,
-                           varmap=None):
-        assert dbg_st_search(self.uri, union, varmap, args, cachekey)
-        rqlkey = union.as_string(kwargs=args)
         try:
-            results = self._query_cache[rqlkey]
-        except KeyError:
-            results = self._syntax_tree_search(session, union, args)
-            self._query_cache[rqlkey] = results
-        assert dbg_results(results)
-        return results
-
-    def _syntax_tree_search(self, session, union, args):
-        """return result from this source for a rql query (actually from a rql
-        syntax tree and a solution dictionary mapping each used variable to a
-        possible type). If cachekey is given, the query necessary to fetch the
-        results (but not the results themselves) may be cached using this key.
-        """
-        if not args is None:
-            args = args.copy()
-        # get cached cursor anyway
-        cu = session.cnxset[self.uri]
-        if cu is None:
-            # this is a ConnectionWrapper instance
-            msg = session._("can't connect to source %s, some data may be missing")
-            session.set_shared_data('sources_error', msg % self.uri, txdata=True)
-            return []
-        translator = RQL2RQL(self)
-        try:
-            rql = translator.generate(session, union, args)
-        except UnknownEid, ex:
-            if server.DEBUG:
-                print '  unknown eid', ex, 'no results'
-            return []
-        if server.DEBUG & server.DBG_RQL:
-            print '  translated rql', rql
-        try:
-            rset = cu.execute(rql, args)
-        except Exception, ex:
-            self.exception(str(ex))
-            msg = session._("error while querying source %s, some data may be missing")
-            session.set_shared_data('sources_error', msg % self.uri, txdata=True)
-            return []
-        descr = rset.description
-        if rset:
-            needtranslation = []
-            rows = rset.rows
-            for i, etype in enumerate(descr[0]):
-                if (etype is None or not self.schema.eschema(etype).final
-                    or uidtype(union, i, etype, args)):
-                    needtranslation.append(i)
-            if needtranslation:
-                cnx = session.cnxset.connection(self.uri)
-                for rowindex in xrange(rset.rowcount - 1, -1, -1):
-                    row = rows[rowindex]
-                    localrow = False
-                    for colindex in needtranslation:
-                        if row[colindex] is not None: # optional variable
-                            eid, local = self.local_eid(cnx, row[colindex], session)
-                            if local:
-                                localrow = True
-                            if eid is not None:
-                                row[colindex] = eid
-                            else:
-                                # skip this row
-                                del rows[rowindex]
-                                del descr[rowindex]
-                                break
-                    else:
-                        # skip row if it only contains eids of entities which
-                        # are actually from a source we also know locally,
-                        # except if some args specified (XXX should actually
-                        # check if there are some args local to the source)
-                        if not (translator.has_local_eid or localrow):
-                            del rows[rowindex]
-                            del descr[rowindex]
-            results = rows
-        else:
-            results = []
-        return results
-
-    def _entity_relations_and_kwargs(self, session, entity):
-        relations = []
-        kwargs = {'x': self.repo.eid2extid(self, entity.eid, session)}
-        for key, val in entity.cw_attr_cache.iteritems():
-            relations.append('X %s %%(%s)s' % (key, key))
-            kwargs[key] = val
-        return relations, kwargs
-
-    def add_entity(self, session, entity):
-        """add a new entity to the source"""
-        raise NotImplementedError()
-
-    def update_entity(self, session, entity):
-        """update an entity in the source"""
-        relations, kwargs = self._entity_relations_and_kwargs(session, entity)
-        cu = session.cnxset[self.uri]
-        cu.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs)
-        self._query_cache.clear()
-        entity.cw_clear_all_caches()
-
-    def delete_entity(self, session, entity):
-        """delete an entity from the source"""
-        if session.deleted_in_transaction(self.eid):
-            # source is being deleted, don't propagate
-            self._query_cache.clear()
-            return
-        cu = session.cnxset[self.uri]
-        cu.execute('DELETE %s X WHERE X eid %%(x)s' % entity.__regid__,
-                   {'x': self.repo.eid2extid(self, entity.eid, session)})
-        self._query_cache.clear()
-
-    def add_relation(self, session, subject, rtype, object):
-        """add a relation to the source"""
-        cu = session.cnxset[self.uri]
-        cu.execute('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
-                   {'x': self.repo.eid2extid(self, subject, session),
-                    'y': self.repo.eid2extid(self, object, session)})
-        self._query_cache.clear()
-        session.entity_from_eid(subject).cw_clear_all_caches()
-        session.entity_from_eid(object).cw_clear_all_caches()
-
-    def delete_relation(self, session, subject, rtype, object):
-        """delete a relation from the source"""
-        if session.deleted_in_transaction(self.eid):
-            # source is being deleted, don't propagate
-            self._query_cache.clear()
-            return
-        cu = session.cnxset[self.uri]
-        cu.execute('DELETE X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
-                   {'x': self.repo.eid2extid(self, subject, session),
-                    'y': self.repo.eid2extid(self, object, session)})
-        self._query_cache.clear()
-        session.entity_from_eid(subject).cw_clear_all_caches()
-        session.entity_from_eid(object).cw_clear_all_caches()
-
-
-class RQL2RQL(object):
-    """translate a local rql query to be executed on a distant repository"""
-    def __init__(self, source):
-        self.source = source
-        self.repo = source.repo
-        self.current_operator = None
-
-    def _accept_children(self, node):
-        res = []
-        for child in node.children:
-            rql = child.accept(self)
-            if rql is not None:
-                res.append(rql)
-        return res
-
-    def generate(self, session, rqlst, args):
-        self._session = session
-        self.kwargs = args
-        self.need_translation = False
-        self.has_local_eid = False
-        return self.visit_union(rqlst)
-
-    def visit_union(self, node):
-        s = self._accept_children(node)
-        if len(s) > 1:
-            return ' UNION '.join('(%s)' % q for q in s)
-        return s[0]
+            return super(PyroRQLSource, self).check_connection(cnx)
+        except ConnectionClosedError:
+            # try to reconnect
+            return self.get_connection()
 
-    def visit_select(self, node):
-        """return the tree as an encoded rql string"""
-        self._varmaker = rqlvar_maker(defined=node.defined_vars.copy())
-        self._const_var = {}
-        if node.distinct:
-            base = 'DISTINCT Any'
-        else:
-            base = 'Any'
-        s = ['%s %s' % (base, ','.join(v.accept(self) for v in node.selection))]
-        if node.groupby:
-            s.append('GROUPBY %s' % ', '.join(group.accept(self)
-                                              for group in node.groupby))
-        if node.orderby:
-            s.append('ORDERBY %s' % ', '.join(self.visit_sortterm(term)
-                                              for term in node.orderby))
-        if node.limit is not None:
-            s.append('LIMIT %s' % node.limit)
-        if node.offset:
-            s.append('OFFSET %s' % node.offset)
-        restrictions = []
-        if node.where is not None:
-            nr = node.where.accept(self)
-            if nr is not None:
-                restrictions.append(nr)
-        if restrictions:
-            s.append('WHERE %s' % ','.join(restrictions))
-
-        if node.having:
-            s.append('HAVING %s' % ', '.join(term.accept(self)
-                                             for term in node.having))
-        subqueries = []
-        for subquery in node.with_:
-            subqueries.append('%s BEING (%s)' % (','.join(ca.name for ca in subquery.aliases),
-                                                 self.visit_union(subquery.query)))
-        if subqueries:
-            s.append('WITH %s' % (','.join(subqueries)))
-        return ' '.join(s)
-
-    def visit_and(self, node):
-        res = self._accept_children(node)
-        if res:
-            return ', '.join(res)
-        return
-
-    def visit_or(self, node):
-        res = self._accept_children(node)
-        if len(res) > 1:
-            return ' OR '.join('(%s)' % rql for rql in res)
-        elif res:
-            return res[0]
-        return
-
-    def visit_not(self, node):
-        rql = node.children[0].accept(self)
-        if rql:
-            return 'NOT (%s)' % rql
-        return
-
-    def visit_exists(self, node):
-        rql = node.children[0].accept(self)
-        if rql:
-            return 'EXISTS(%s)' % rql
-        return
-
-    def visit_relation(self, node):
-        try:
-            if isinstance(node.children[0], Constant):
-                # simplified rqlst, reintroduce eid relation
-                try:
-                    restr, lhs = self.process_eid_const(node.children[0])
-                except UnknownEid:
-                    # can safely skip not relation with an unsupported eid
-                    if neged_relation(node):
-                        return
-                    raise
-            else:
-                lhs = node.children[0].accept(self)
-                restr = None
-        except UnknownEid:
-            # can safely skip not relation with an unsupported eid
-            if neged_relation(node):
-                return
-            # XXX what about optional relation or outer NOT EXISTS()
-            raise
-        if node.optional in ('left', 'both'):
-            lhs += '?'
-        if node.r_type == 'eid' or not self.source.schema.rschema(node.r_type).final:
-            self.need_translation = True
-            self.current_operator = node.operator()
-            if isinstance(node.children[0], Constant):
-                self.current_etypes = (node.children[0].uidtype,)
-            else:
-                self.current_etypes = node.children[0].variable.stinfo['possibletypes']
-        try:
-            rhs = node.children[1].accept(self)
-        except UnknownEid:
-            # can safely skip not relation with an unsupported eid
-            if neged_relation(node):
-                return
-            # XXX what about optional relation or outer NOT EXISTS()
-            raise
-        except ReplaceByInOperator, ex:
-            rhs = 'IN (%s)' % ','.join(eid for eid in ex.eids)
-        self.need_translation = False
-        self.current_operator = None
-        if node.optional in ('right', 'both'):
-            rhs += '?'
-        if restr is not None:
-            return '%s %s %s, %s' % (lhs, node.r_type, rhs, restr)
-        return '%s %s %s' % (lhs, node.r_type, rhs)
-
-    def visit_comparison(self, node):
-        if node.operator in ('=', 'IS'):
-            return node.children[0].accept(self)
-        return '%s %s' % (node.operator.encode(),
-                          node.children[0].accept(self))
-
-    def visit_mathexpression(self, node):
-        return '(%s %s %s)' % (node.children[0].accept(self),
-                               node.operator.encode(),
-                               node.children[1].accept(self))
-
-    def visit_function(self, node):
-        #if node.name == 'IN':
-        res = []
-        for child in node.children:
-            try:
-                rql = child.accept(self)
-            except UnknownEid, ex:
-                continue
-            res.append(rql)
-        if not res:
-            raise ex
-        return '%s(%s)' % (node.name, ', '.join(res))
-
-    def visit_constant(self, node):
-        if self.need_translation or node.uidtype:
-            if node.type == 'Int':
-                self.has_local_eid = True
-                return str(self.eid2extid(node.value))
-            if node.type == 'Substitute':
-                key = node.value
-                # ensure we have not yet translated the value...
-                if not key in self._const_var:
-                    self.kwargs[key] = self.eid2extid(self.kwargs[key])
-                    self._const_var[key] = None
-                    self.has_local_eid = True
-        return node.as_string()
-
-    def visit_variableref(self, node):
-        """get the sql name for a variable reference"""
-        return node.name
-
-    def visit_sortterm(self, node):
-        if node.asc:
-            return node.term.accept(self)
-        return '%s DESC' % node.term.accept(self)
-
-    def process_eid_const(self, const):
-        value = const.eval(self.kwargs)
-        try:
-            return None, self._const_var[value]
-        except Exception:
-            var = self._varmaker.next()
-            self.need_translation = True
-            restr = '%s eid %s' % (var, self.visit_constant(const))
-            self.need_translation = False
-            self._const_var[value] = var
-            return restr, var
-
-    def eid2extid(self, eid):
-        try:
-            return self.repo.eid2extid(self.source, eid, self._session)
-        except UnknownEid:
-            operator = self.current_operator
-            if operator is not None and operator != '=':
-                # deal with query like "X eid > 12"
-                #
-                # The problem is that eid order in the external source may
-                # differ from the local source
-                #
-                # So search for all eids from this source matching the condition
-                # locally and then to replace the "> 12" branch by "IN (eids)"
-                #
-                # XXX we may have to insert a huge number of eids...)
-                sql = "SELECT extid FROM entities WHERE source='%s' AND type IN (%s) AND eid%s%s"
-                etypes = ','.join("'%s'" % etype for etype in self.current_etypes)
-                cu = self._session.system_sql(sql % (self.source.uri, etypes,
-                                                      operator, eid))
-                # XXX buggy cu.rowcount which may be zero while there are some
-                # results
-                rows = cu.fetchall()
-                if rows:
-                    raise ReplaceByInOperator((b64decode(r[0]) for r in rows))
-            raise
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/sources/remoterql.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,670 @@
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""Source to query another RQL remote repository"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from os.path import join
+from base64 import b64decode
+
+from logilab.common.configuration import REQUIRED
+
+from yams.schema import role_name
+
+from rql.nodes import Constant
+from rql.utils import rqlvar_maker
+
+from cubicweb import dbapi, server
+from cubicweb import ValidationError, BadConnectionId, UnknownEid
+from cubicweb.schema import VIRTUAL_RTYPES
+from cubicweb.server.sources import (AbstractSource, ConnectionWrapper,
+                                     TimedCache, dbg_st_search, dbg_results)
+from cubicweb.server.msplanner import neged_relation
+
+def uidtype(union, col, etype, args):
+    select, col = union.locate_subquery(col, etype, args)
+    return getattr(select.selection[col], 'uidtype', None)
+
+
+class ReplaceByInOperator(Exception):
+    def __init__(self, eids):
+        self.eids = eids
+
+class RemoteSource(AbstractSource):
+    """Generic external repository source"""
+
+    CNX_TYPE = None # Must be ovewritted !
+
+    # boolean telling if modification hooks should be called when something is
+    # modified in this source
+    should_call_hooks = False
+    # boolean telling if the repository should connect to this source during
+    # migration
+    connect_for_migration = False
+
+    options = (
+
+        ('cubicweb-user',
+         {'type' : 'string',
+          'default': REQUIRED,
+          'help': 'user to use for connection on the distant repository',
+          'group': 'remote-source', 'level': 0,
+          }),
+        ('cubicweb-password',
+         {'type' : 'password',
+          'default': '',
+          'help': 'user to use for connection on the distant repository',
+          'group': 'remote-source', 'level': 0,
+          }),
+        ('base-url',
+         {'type' : 'string',
+          'default': '',
+          'help': 'url of the web site for the distant repository, if you want '
+          'to generate external link to entities from this repository',
+          'group': 'remote-source', 'level': 1,
+          }),
+        ('skip-external-entities',
+         {'type' : 'yn',
+          'default': False,
+          'help': 'should entities not local to the source be considered or not',
+          'group': 'remote-source', 'level': 0,
+          }),
+        ('synchronization-interval',
+         {'type' : 'time',
+          'default': '5min',
+          'help': 'interval between synchronization with the external \
+repository (default to 5 minutes).',
+          'group': 'remote-source', 'level': 2,
+          }))
+
+    PUBLIC_KEYS = AbstractSource.PUBLIC_KEYS + ('base-url',)
+
+    _conn = None
+
+    def __init__(self, repo, source_config, eid=None):
+        super(RemoteSource, self).__init__(repo, source_config, eid)
+        self.update_config(None, self.check_conf_dict(eid, source_config,
+                                                      fail_if_unknown=False))
+        self._query_cache = TimedCache(1800)
+
+    def update_config(self, source_entity, processed_config):
+        """update configuration from source entity"""
+        baseurl = processed_config.get('base-url')
+        if baseurl and not baseurl.endswith('/'):
+            processed_config['base-url'] += '/'
+        self.config = processed_config
+        self._skip_externals = processed_config['skip-external-entities']
+        if source_entity is not None:
+            self.latest_retrieval = source_entity.latest_retrieval
+
+    def _get_connection(self):
+        """open and return a connection to the source"""
+        self.info('connecting to source %(base-url)s with user %(cubicweb-user)s',
+                  self.config)
+        cnxprops = ConnectionProperties(cnxtype=self.CNX_TYPE)
+        return dbapi.connect(login=self.config['cubicweb-user'],
+                             password=self.config['cubicweb-password'],
+                             cnxprops=cnxprops)
+
+    def get_connection(self):
+        try:
+            return self._get_connection()
+        except ConnectionError, ex:
+            self.critical("can't get connection to source %s: %s", self.uri, ex)
+            return ConnectionWrapper()
+
+
+    def reset_caches(self):
+        """method called during test to reset potential source caches"""
+        self._query_cache = TimedCache(1800)
+
+    def init(self, activated, source_entity):
+        """method called by the repository once ready to handle request"""
+        self.load_mapping(source_entity._cw)
+        if activated:
+            interval = self.config['synchronization-interval']
+            self.repo.looping_task(interval, self.synchronize)
+            self.repo.looping_task(self._query_cache.ttl.seconds/10,
+                                   self._query_cache.clear_expired)
+            self.latest_retrieval = source_entity.latest_retrieval
+
+    def load_mapping(self, session=None):
+        self.support_entities = {}
+        self.support_relations = {}
+        self.dont_cross_relations = set(('owned_by', 'created_by'))
+        self.cross_relations = set()
+        assert self.eid is not None
+        self._schemacfg_idx = {}
+        self._load_mapping(session)
+
+    etype_options = set(('write',))
+    rtype_options = set(('maycross', 'dontcross', 'write',))
+
+    def _check_options(self, schemacfg, allowedoptions):
+        if schemacfg.options:
+            options = set(w.strip() for w in schemacfg.options.split(':'))
+        else:
+            options = set()
+        if options - allowedoptions:
+            options = ', '.join(sorted(options - allowedoptions))
+            msg = _('unknown option(s): %s' % options)
+            raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
+        return options
+
+    def add_schema_config(self, schemacfg, checkonly=False):
+        """added CWSourceSchemaConfig, modify mapping accordingly"""
+        try:
+            ertype = schemacfg.schema.name
+        except AttributeError:
+            msg = schemacfg._cw._("attribute/relation can't be mapped, only "
+                                  "entity and relation types")
+            raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
+        if schemacfg.schema.__regid__ == 'CWEType':
+            options = self._check_options(schemacfg, self.etype_options)
+            if not checkonly:
+                self.support_entities[ertype] = 'write' in options
+        else: # CWRType
+            if ertype in ('is', 'is_instance_of', 'cw_source') or ertype in VIRTUAL_RTYPES:
+                msg = schemacfg._cw._('%s relation should not be in mapped') % ertype
+                raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
+            options = self._check_options(schemacfg, self.rtype_options)
+            if 'dontcross' in options:
+                if 'maycross' in options:
+                    msg = schemacfg._("can't mix dontcross and maycross options")
+                    raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
+                if 'write' in options:
+                    msg = schemacfg._("can't mix dontcross and write options")
+                    raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
+                if not checkonly:
+                    self.dont_cross_relations.add(ertype)
+            elif not checkonly:
+                self.support_relations[ertype] = 'write' in options
+                if 'maycross' in options:
+                    self.cross_relations.add(ertype)
+        if not checkonly:
+            # add to an index to ease deletion handling
+            self._schemacfg_idx[schemacfg.eid] = ertype
+
+    def del_schema_config(self, schemacfg, checkonly=False):
+        """deleted CWSourceSchemaConfig, modify mapping accordingly"""
+        if checkonly:
+            return
+        try:
+            ertype = self._schemacfg_idx[schemacfg.eid]
+            if ertype[0].isupper():
+                del self.support_entities[ertype]
+            else:
+                if ertype in self.support_relations:
+                    del self.support_relations[ertype]
+                    if ertype in self.cross_relations:
+                        self.cross_relations.remove(ertype)
+                else:
+                    self.dont_cross_relations.remove(ertype)
+        except Exception:
+            self.error('while updating mapping consequently to removal of %s',
+                       schemacfg)
+
+    def local_eid(self, cnx, extid, session):
+        etype, dexturi, dextid = cnx.describe(extid)
+        if dexturi == 'system' or not (
+            dexturi in self.repo.sources_by_uri or self._skip_externals):
+            assert etype in self.support_entities, etype
+            eid = self.repo.extid2eid(self, str(extid), etype, session)
+            if eid > 0:
+                return eid, True
+        elif dexturi in self.repo.sources_by_uri:
+            source = self.repo.sources_by_uri[dexturi]
+            cnx = session.cnxset.connection(source.uri)
+            eid = source.local_eid(cnx, dextid, session)[0]
+            return eid, False
+        return None, None
+
+    def synchronize(self, mtime=None):
+        """synchronize content known by this repository with content in the
+        external repository
+        """
+        self.info('synchronizing remote %s source %s', (self.CNX_TYPE, self.uri))
+        cnx = self.get_connection()
+        try:
+            extrepo = cnx._repo
+        except AttributeError:
+            # fake connection wrapper returned when we can't connect to the
+            # external source (hence we've no chance to synchronize...)
+            return
+        etypes = self.support_entities.keys()
+        if mtime is None:
+            mtime = self.latest_retrieval
+        updatetime, modified, deleted = extrepo.entities_modified_since(
+            etypes, mtime)
+        self._query_cache.clear()
+        repo = self.repo
+        session = repo.internal_session()
+        source = repo.system_source
+        try:
+            for etype, extid in modified:
+                try:
+                    eid = self.local_eid(cnx, extid, session)[0]
+                    if eid is not None:
+                        rset = session.eid_rset(eid, etype)
+                        entity = rset.get_entity(0, 0)
+                        entity.complete(entity.e_schema.indexable_attributes())
+                        source.index_entity(session, entity)
+                except Exception:
+                    self.exception('while updating %s with external id %s of source %s',
+                                   etype, extid, self.uri)
+                    continue
+            for etype, extid in deleted:
+                try:
+                    eid = self.repo.extid2eid(self, str(extid), etype, session,
+                                              insert=False)
+                    # entity has been deleted from external repository but is not known here
+                    if eid is not None:
+                        entity = session.entity_from_eid(eid, etype)
+                        repo.delete_info(session, entity, self.uri,
+                                         scleanup=self.eid)
+                except Exception:
+                    if self.repo.config.mode == 'test':
+                        raise
+                    self.exception('while updating %s with external id %s of source %s',
+                                   etype, extid, self.uri)
+                    continue
+            self.latest_retrieval = updatetime
+            session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s',
+                            {'x': self.eid, 'date': self.latest_retrieval})
+            session.commit()
+        finally:
+            session.close()
+
+    def get_connection(self):
+        raise NotImplementedError()
+
+    def check_connection(self, cnx):
+        """check connection validity, return None if the connection is still valid
+        else a new connection
+        """
+        if not isinstance(cnx, ConnectionWrapper):
+            try:
+                cnx.check()
+                return # ok
+            except BadConnectionId:
+                pass
+        # try to reconnect
+        return self.get_connection()
+
+    def syntax_tree_search(self, session, union, args=None, cachekey=None,
+                           varmap=None):
+        assert dbg_st_search(self.uri, union, varmap, args, cachekey)
+        rqlkey = union.as_string(kwargs=args)
+        try:
+            results = self._query_cache[rqlkey]
+        except KeyError:
+            results = self._syntax_tree_search(session, union, args)
+            self._query_cache[rqlkey] = results
+        assert dbg_results(results)
+        return results
+
+    def _syntax_tree_search(self, session, union, args):
+        """return result from this source for a rql query (actually from a rql
+        syntax tree and a solution dictionary mapping each used variable to a
+        possible type). If cachekey is given, the query necessary to fetch the
+        results (but not the results themselves) may be cached using this key.
+        """
+        if not args is None:
+            args = args.copy()
+        # get cached cursor anyway
+        cu = session.cnxset[self.uri]
+        if cu is None:
+            # this is a ConnectionWrapper instance
+            msg = session._("can't connect to source %s, some data may be missing")
+            session.set_shared_data('sources_error', msg % self.uri, txdata=True)
+            return []
+        translator = RQL2RQL(self)
+        try:
+            rql = translator.generate(session, union, args)
+        except UnknownEid, ex:
+            if server.DEBUG:
+                print '  unknown eid', ex, 'no results'
+            return []
+        if server.DEBUG & server.DBG_RQL:
+            print '  translated rql', rql
+        try:
+            rset = cu.execute(rql, args)
+        except Exception, ex:
+            self.exception(str(ex))
+            msg = session._("error while querying source %s, some data may be missing")
+            session.set_shared_data('sources_error', msg % self.uri, txdata=True)
+            return []
+        descr = rset.description
+        if rset:
+            needtranslation = []
+            rows = rset.rows
+            for i, etype in enumerate(descr[0]):
+                if (etype is None or not self.schema.eschema(etype).final
+                    or uidtype(union, i, etype, args)):
+                    needtranslation.append(i)
+            if needtranslation:
+                cnx = session.cnxset.connection(self.uri)
+                for rowindex in xrange(rset.rowcount - 1, -1, -1):
+                    row = rows[rowindex]
+                    localrow = False
+                    for colindex in needtranslation:
+                        if row[colindex] is not None: # optional variable
+                            eid, local = self.local_eid(cnx, row[colindex], session)
+                            if local:
+                                localrow = True
+                            if eid is not None:
+                                row[colindex] = eid
+                            else:
+                                # skip this row
+                                del rows[rowindex]
+                                del descr[rowindex]
+                                break
+                    else:
+                        # skip row if it only contains eids of entities which
+                        # are actually from a source we also know locally,
+                        # except if some args specified (XXX should actually
+                        # check if there are some args local to the source)
+                        if not (translator.has_local_eid or localrow):
+                            del rows[rowindex]
+                            del descr[rowindex]
+            results = rows
+        else:
+            results = []
+        return results
+
+    def _entity_relations_and_kwargs(self, session, entity):
+        relations = []
+        kwargs = {'x': self.repo.eid2extid(self, entity.eid, session)}
+        for key, val in entity.cw_attr_cache.iteritems():
+            relations.append('X %s %%(%s)s' % (key, key))
+            kwargs[key] = val
+        return relations, kwargs
+
+    def add_entity(self, session, entity):
+        """add a new entity to the source"""
+        raise NotImplementedError()
+
+    def update_entity(self, session, entity):
+        """update an entity in the source"""
+        relations, kwargs = self._entity_relations_and_kwargs(session, entity)
+        cu = session.cnxset[self.uri]
+        cu.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs)
+        self._query_cache.clear()
+        entity.cw_clear_all_caches()
+
+    def delete_entity(self, session, entity):
+        """delete an entity from the source"""
+        if session.deleted_in_transaction(self.eid):
+            # source is being deleted, don't propagate
+            self._query_cache.clear()
+            return
+        cu = session.cnxset[self.uri]
+        cu.execute('DELETE %s X WHERE X eid %%(x)s' % entity.__regid__,
+                   {'x': self.repo.eid2extid(self, entity.eid, session)})
+        self._query_cache.clear()
+
+    def add_relation(self, session, subject, rtype, object):
+        """add a relation to the source"""
+        cu = session.cnxset[self.uri]
+        cu.execute('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
+                   {'x': self.repo.eid2extid(self, subject, session),
+                    'y': self.repo.eid2extid(self, object, session)})
+        self._query_cache.clear()
+        session.entity_from_eid(subject).cw_clear_all_caches()
+        session.entity_from_eid(object).cw_clear_all_caches()
+
+    def delete_relation(self, session, subject, rtype, object):
+        """delete a relation from the source"""
+        if session.deleted_in_transaction(self.eid):
+            # source is being deleted, don't propagate
+            self._query_cache.clear()
+            return
+        cu = session.cnxset[self.uri]
+        cu.execute('DELETE X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
+                   {'x': self.repo.eid2extid(self, subject, session),
+                    'y': self.repo.eid2extid(self, object, session)})
+        self._query_cache.clear()
+        session.entity_from_eid(subject).cw_clear_all_caches()
+        session.entity_from_eid(object).cw_clear_all_caches()
+
+
+class RQL2RQL(object):
+    """translate a local rql query to be executed on a distant repository"""
+    def __init__(self, source):
+        self.source = source
+        self.repo = source.repo
+        self.current_operator = None
+
+    def _accept_children(self, node):
+        res = []
+        for child in node.children:
+            rql = child.accept(self)
+            if rql is not None:
+                res.append(rql)
+        return res
+
+    def generate(self, session, rqlst, args):
+        self._session = session
+        self.kwargs = args
+        self.need_translation = False
+        self.has_local_eid = False
+        return self.visit_union(rqlst)
+
+    def visit_union(self, node):
+        s = self._accept_children(node)
+        if len(s) > 1:
+            return ' UNION '.join('(%s)' % q for q in s)
+        return s[0]
+
+    def visit_select(self, node):
+        """return the tree as an encoded rql string"""
+        self._varmaker = rqlvar_maker(defined=node.defined_vars.copy())
+        self._const_var = {}
+        if node.distinct:
+            base = 'DISTINCT Any'
+        else:
+            base = 'Any'
+        s = ['%s %s' % (base, ','.join(v.accept(self) for v in node.selection))]
+        if node.groupby:
+            s.append('GROUPBY %s' % ', '.join(group.accept(self)
+                                              for group in node.groupby))
+        if node.orderby:
+            s.append('ORDERBY %s' % ', '.join(self.visit_sortterm(term)
+                                              for term in node.orderby))
+        if node.limit is not None:
+            s.append('LIMIT %s' % node.limit)
+        if node.offset:
+            s.append('OFFSET %s' % node.offset)
+        restrictions = []
+        if node.where is not None:
+            nr = node.where.accept(self)
+            if nr is not None:
+                restrictions.append(nr)
+        if restrictions:
+            s.append('WHERE %s' % ','.join(restrictions))
+
+        if node.having:
+            s.append('HAVING %s' % ', '.join(term.accept(self)
+                                             for term in node.having))
+        subqueries = []
+        for subquery in node.with_:
+            subqueries.append('%s BEING (%s)' % (','.join(ca.name for ca in subquery.aliases),
+                                                 self.visit_union(subquery.query)))
+        if subqueries:
+            s.append('WITH %s' % (','.join(subqueries)))
+        return ' '.join(s)
+
+    def visit_and(self, node):
+        res = self._accept_children(node)
+        if res:
+            return ', '.join(res)
+        return
+
+    def visit_or(self, node):
+        res = self._accept_children(node)
+        if len(res) > 1:
+            return ' OR '.join('(%s)' % rql for rql in res)
+        elif res:
+            return res[0]
+        return
+
+    def visit_not(self, node):
+        rql = node.children[0].accept(self)
+        if rql:
+            return 'NOT (%s)' % rql
+        return
+
+    def visit_exists(self, node):
+        rql = node.children[0].accept(self)
+        if rql:
+            return 'EXISTS(%s)' % rql
+        return
+
+    def visit_relation(self, node):
+        try:
+            if isinstance(node.children[0], Constant):
+                # simplified rqlst, reintroduce eid relation
+                try:
+                    restr, lhs = self.process_eid_const(node.children[0])
+                except UnknownEid:
+                    # can safely skip not relation with an unsupported eid
+                    if neged_relation(node):
+                        return
+                    raise
+            else:
+                lhs = node.children[0].accept(self)
+                restr = None
+        except UnknownEid:
+            # can safely skip not relation with an unsupported eid
+            if neged_relation(node):
+                return
+            # XXX what about optional relation or outer NOT EXISTS()
+            raise
+        if node.optional in ('left', 'both'):
+            lhs += '?'
+        if node.r_type == 'eid' or not self.source.schema.rschema(node.r_type).final:
+            self.need_translation = True
+            self.current_operator = node.operator()
+            if isinstance(node.children[0], Constant):
+                self.current_etypes = (node.children[0].uidtype,)
+            else:
+                self.current_etypes = node.children[0].variable.stinfo['possibletypes']
+        try:
+            rhs = node.children[1].accept(self)
+        except UnknownEid:
+            # can safely skip not relation with an unsupported eid
+            if neged_relation(node):
+                return
+            # XXX what about optional relation or outer NOT EXISTS()
+            raise
+        except ReplaceByInOperator, ex:
+            rhs = 'IN (%s)' % ','.join(eid for eid in ex.eids)
+        self.need_translation = False
+        self.current_operator = None
+        if node.optional in ('right', 'both'):
+            rhs += '?'
+        if restr is not None:
+            return '%s %s %s, %s' % (lhs, node.r_type, rhs, restr)
+        return '%s %s %s' % (lhs, node.r_type, rhs)
+
+    def visit_comparison(self, node):
+        if node.operator in ('=', 'IS'):
+            return node.children[0].accept(self)
+        return '%s %s' % (node.operator.encode(),
+                          node.children[0].accept(self))
+
+    def visit_mathexpression(self, node):
+        return '(%s %s %s)' % (node.children[0].accept(self),
+                               node.operator.encode(),
+                               node.children[1].accept(self))
+
+    def visit_function(self, node):
+        #if node.name == 'IN':
+        res = []
+        for child in node.children:
+            try:
+                rql = child.accept(self)
+            except UnknownEid, ex:
+                continue
+            res.append(rql)
+        if not res:
+            raise ex
+        return '%s(%s)' % (node.name, ', '.join(res))
+
+    def visit_constant(self, node):
+        if self.need_translation or node.uidtype:
+            if node.type == 'Int':
+                self.has_local_eid = True
+                return str(self.eid2extid(node.value))
+            if node.type == 'Substitute':
+                key = node.value
+                # ensure we have not yet translated the value...
+                if not key in self._const_var:
+                    self.kwargs[key] = self.eid2extid(self.kwargs[key])
+                    self._const_var[key] = None
+                    self.has_local_eid = True
+        return node.as_string()
+
+    def visit_variableref(self, node):
+        """get the sql name for a variable reference"""
+        return node.name
+
+    def visit_sortterm(self, node):
+        if node.asc:
+            return node.term.accept(self)
+        return '%s DESC' % node.term.accept(self)
+
+    def process_eid_const(self, const):
+        value = const.eval(self.kwargs)
+        try:
+            return None, self._const_var[value]
+        except Exception:
+            var = self._varmaker.next()
+            self.need_translation = True
+            restr = '%s eid %s' % (var, self.visit_constant(const))
+            self.need_translation = False
+            self._const_var[value] = var
+            return restr, var
+
+    def eid2extid(self, eid):
+        try:
+            return self.repo.eid2extid(self.source, eid, self._session)
+        except UnknownEid:
+            operator = self.current_operator
+            if operator is not None and operator != '=':
+                # deal with query like "X eid > 12"
+                #
+                # The problem is that eid order in the external source may
+                # differ from the local source
+                #
+                # So search for all eids from this source matching the condition
+                # locally and then to replace the "> 12" branch by "IN (eids)"
+                #
+                # XXX we may have to insert a huge number of eids...)
+                sql = "SELECT extid FROM entities WHERE source='%s' AND type IN (%s) AND eid%s%s"
+                etypes = ','.join("'%s'" % etype for etype in self.current_etypes)
+                cu = self._session.system_sql(sql % (self.source.uri, etypes,
+                                                      operator, eid))
+                # XXX buggy cu.rowcount which may be zero while there are some
+                # results
+                rows = cu.fetchall()
+                if rows:
+                    raise ReplaceByInOperator((b64decode(r[0]) for r in rows))
+            raise
+
--- a/server/sources/rql2sql.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/rql2sql.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -1284,10 +1284,10 @@
     def _visit_var_attr_relation(self, relation, rhs_vars):
         """visit an attribute relation with variable(s) in the RHS
 
-        attribute variables are used either in the selection or for
-        unification (eg X attr1 A, Y attr2 A). In case of selection,
-        nothing to do here.
+        attribute variables are used either in the selection or for unification
+        (eg X attr1 A, Y attr2 A). In case of selection, nothing to do here.
         """
+        ored = relation.ored()
         for vref in rhs_vars:
             var = vref.variable
             if var.name in self._varmap:
@@ -1298,10 +1298,21 @@
                 principal = 1
             else:
                 principal = var.stinfo.get('principal')
-            if principal is not None and principal is not relation:
+            # we've to return some sql if:
+            # 1. visited relation is ored
+            # 2. variable's principal is not this relation and not 1.
+            if ored or (principal is not None and principal is not relation
+                        and not getattr(principal, 'ored', lambda : 0)()):
                 # we have to generate unification expression
-                lhssql = self._inlined_var_sql(relation.children[0].variable,
-                                               relation.r_type)
+                if principal is relation:
+                    # take care if ored case and principal is the relation to
+                    # use the right relation in the unification term
+                    _rel = [rel for rel in var.stinfo['rhsrelations']
+                            if not rel is principal][0]
+                else:
+                    _rel = relation
+                lhssql = self._inlined_var_sql(_rel.children[0].variable,
+                                               _rel.r_type)
                 try:
                     self._state.ignore_varmap = True
                     sql = lhssql + relation.children[1].accept(self)
--- a/server/sources/storages.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/storages.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -194,16 +194,16 @@
                 # Mark the new file as added during the transaction.
                 # The file will be removed on rollback
                 AddFileOp.get_instance(entity._cw).add_data(fpath)
-        if oldpath != fpath:
-            # register the new location for the file.
+            # reinstall poped value
             if fpath is None:
                 entity.cw_edited.edited_attribute(attr, None)
             else:
+                # register the new location for the file.
                 entity.cw_edited.edited_attribute(attr, Binary(fpath))
+        if oldpath is not None and oldpath != fpath:
             # Mark the old file as useless so the file will be removed at
             # commit.
-            if oldpath is not None:
-                DeleteFileOp.get_instance(entity._cw).add_data(oldpath)
+            DeleteFileOp.get_instance(entity._cw).add_data(oldpath)
         return binary
 
     def entity_deleted(self, entity, attr):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/sources/zmqrql.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,27 @@
+# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""Source to query another RQL repository using pyro"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from cubicweb.server.sources.remoterql import RemoteSource
+
+class ZMQRQLSource(RemoteSource):
+    """External repository source, using ZMQ sockets"""
+    CNX_TYPE = 'zmq'
--- a/server/ssplanner.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/ssplanner.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -27,7 +27,6 @@
 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.edition import EditedEntity
 
 READ_ONLY_RTYPES = set(('eid', 'has_text', 'is', 'is_instance_of', 'identity'))
@@ -87,7 +86,7 @@
                 # 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):
+                    with session.security_enabled(read=False):
                         eschema(session.describe(eid)[0]).check_perm(
                             session, 'read', eid=eid)
                 eidconsts[lhs.variable] = eid
--- a/server/test/data/slapd.conf.in	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/data/slapd.conf.in	Tue Oct 23 15:00:53 2012 +0200
@@ -45,9 +45,9 @@
 suffix          "dc=cubicweb,dc=test"
 
 # rootdn directive for specifying a superuser on the database. This is needed
-# for syncrepl.
-#rootdn          "cn=admin,dc=cubicweb,dc=test"
-#rootpw          "cubicwebrocks"
+# for syncrepl. and ldapdelete easyness
+rootdn          "cn=admin,dc=cubicweb,dc=test"
+rootpw          "cw"
 # Where the database file are physically stored for database #1
 directory       "%(apphome)s/ldapdb"
 
--- a/server/test/unittest_checkintegrity.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_checkintegrity.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/server/test/unittest_datafeed.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_datafeed.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2011-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -54,7 +54,7 @@
             stats = dfsource.pull_data(session, force=True)
             self.commit()
             # test import stats
-            self.assertEqual(sorted(stats.keys()), ['created', 'updated'])
+            self.assertEqual(sorted(stats.keys()), ['checked', 'created', 'updated'])
             self.assertEqual(len(stats['created']), 1)
             entity = self.execute('Card X').get_entity(0, 0)
             self.assertIn(entity.eid, stats['created'])
--- a/server/test/unittest_hook.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_hook.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/server/test/unittest_ldapuser.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_ldapuser.py	Tue Oct 23 15:00:53 2012 +0200
@@ -16,13 +16,13 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """cubicweb.server.sources.ldapusers unit and functional tests"""
+from __future__ import with_statement
 
 import os
 import shutil
 import time
-from os.path import abspath, join, exists
+from os.path import join, exists
 import subprocess
-from socket import socket, error as socketerror
 
 from logilab.common.testlib import TestCase, unittest_main, mock_object, Tags
 
@@ -32,72 +32,70 @@
 from cubicweb.devtools.httptest import get_available_port
 from cubicweb.devtools import get_test_db_handler
 
-from cubicweb.server.sources.ldapuser import *
+from cubicweb.server.sources.ldapuser import GlobTrFunc, UnknownEid, RQL2LDAPFilter
 
-CONFIG = u'''user-base-dn=ou=People,dc=cubicweb,dc=test
-user-scope=ONELEVEL
-user-classes=top,posixAccount
-user-login-attr=uid
-user-default-group=users
-user-attrs-map=gecos:email,uid:login
-'''
+CONFIG = u'user-base-dn=ou=People,dc=cubicweb,dc=test'
 URL = None
 
-def setUpModule(*args):
-    create_slapd_configuration(LDAPUserSourceTC.config)
-
-def tearDownModule(*args):
-    terminate_slapd()
-
-def create_slapd_configuration(config):
-    global slapd_process, URL
+def create_slapd_configuration(cls):
+    global URL
+    config = cls.config
     basedir = join(config.apphome, "ldapdb")
     slapdconf = join(config.apphome, "slapd.conf")
     confin = file(join(config.apphome, "slapd.conf.in")).read()
     confstream = file(slapdconf, 'w')
     confstream.write(confin % {'apphome': config.apphome})
     confstream.close()
-    if not exists(basedir):
-        os.makedirs(basedir)
-        # fill ldap server with some data
-        ldiffile = join(config.apphome, "ldap_test.ldif")
-        print "Initing ldap database"
-        cmdline = "/usr/sbin/slapadd -f %s -l %s -c" % (slapdconf, ldiffile)
-        subprocess.call(cmdline, shell=True)
-
+    if exists(basedir):
+        shutil.rmtree(basedir)
+    os.makedirs(basedir)
+    # fill ldap server with some data
+    ldiffile = join(config.apphome, "ldap_test.ldif")
+    config.info('Initing ldap database')
+    cmdline = "/usr/sbin/slapadd -f %s -l %s -c" % (slapdconf, ldiffile)
+    subprocess.call(cmdline, shell=True)
 
     #ldapuri = 'ldapi://' + join(basedir, "ldapi").replace('/', '%2f')
     port = get_available_port(xrange(9000, 9100))
     host = 'localhost:%s' % port
     ldapuri = 'ldap://%s' % host
     cmdline = ["/usr/sbin/slapd", "-f",  slapdconf,  "-h",  ldapuri, "-d", "0"]
-    print 'Starting slapd:', ' '.join(cmdline)
-    slapd_process = subprocess.Popen(cmdline)
+    config.info('Starting slapd:', ' '.join(cmdline))
+    cls.slapd_process = subprocess.Popen(cmdline)
     time.sleep(0.2)
-    if slapd_process.poll() is None:
-        print "slapd started with pid %s" % slapd_process.pid
+    if cls.slapd_process.poll() is None:
+        config.info('slapd started with pid %s' % cls.slapd_process.pid)
     else:
         raise EnvironmentError('Cannot start slapd with cmdline="%s" (from directory "%s")' %
                                (" ".join(cmdline), os.getcwd()))
     URL = u'ldap://%s' % host
 
-def terminate_slapd():
-    global slapd_process
-    if slapd_process.returncode is None:
-        print "terminating slapd"
-        if hasattr(slapd_process, 'terminate'):
-            slapd_process.terminate()
+def terminate_slapd(cls):
+    config = cls.config
+    if cls.slapd_process and cls.slapd_process.returncode is None:
+        config.info('terminating slapd')
+        if hasattr(cls.slapd_process, 'terminate'):
+            cls.slapd_process.terminate()
         else:
             import os, signal
-            os.kill(slapd_process.pid, signal.SIGTERM)
-        slapd_process.wait()
-        print "DONE"
-    del slapd_process
+            os.kill(cls.slapd_process.pid, signal.SIGTERM)
+        cls.slapd_process.wait()
+        config.info('DONE')
+
+class LDAPTestBase(CubicWebTC):
+    loglevel = 'ERROR'
 
-
+    @classmethod
+    def setUpClass(cls):
+        from cubicweb.cwctl import init_cmdline_log_threshold
+        init_cmdline_log_threshold(cls.config, cls.loglevel)
+        create_slapd_configuration(cls)
 
+    @classmethod
+    def tearDownClass(cls):
+        terminate_slapd(cls)
 
-class LDAPFeedSourceTC(CubicWebTC):
+class DeleteStuffFromLDAPFeedSourceTC(LDAPTestBase):
     test_db_id = 'ldap-feed'
 
     @classmethod
@@ -105,7 +103,57 @@
         session.create_entity('CWSource', name=u'ldapuser', type=u'ldapfeed', parser=u'ldapfeed',
                               url=URL, config=CONFIG)
         session.commit()
-        isession = session.repo.internal_session()
+        isession = session.repo.internal_session(safe=True)
+        lfsource = isession.repo.sources_by_uri['ldapuser']
+        stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
+
+    def _pull(self):
+        with self.session.repo.internal_session() as isession:
+            lfsource = isession.repo.sources_by_uri['ldapuser']
+            stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
+            isession.commit()
+
+    def test_delete(self):
+        """ delete syt, pull, check deactivation, repull,
+        readd syt, pull, check activation
+        """
+        uri = self.repo.sources_by_uri['ldapuser'].urls[0]
+        deletecmd = ("ldapdelete -H %s 'uid=syt,ou=People,dc=cubicweb,dc=test' "
+                     "-v -x -D cn=admin,dc=cubicweb,dc=test -w'cw'" % uri)
+        os.system(deletecmd)
+        self._pull()
+        self.assertRaises(AuthenticationError, self.repo.connect, 'syt', password='syt')
+        self.assertEqual(self.execute('Any N WHERE U login "syt", '
+                                      'U in_state S, S name N').rows[0][0],
+                         'deactivated')
+        # check that it doesn't choke
+        self._pull()
+        # reset the fscking ldap thing
+        self.tearDownClass()
+        self.setUpClass()
+        self._pull()
+        # still deactivated, but a warning has been emitted ...
+        self.assertEqual(self.execute('Any N WHERE U login "syt", '
+                                      'U in_state S, S name N').rows[0][0],
+                         'deactivated')
+        # test reactivating the user isn't enough to authenticate, as the native source
+        # refuse to authenticate user from other sources
+        os.system(deletecmd)
+        self._pull()
+        user = self.execute('CWUser U WHERE U login "syt"').get_entity(0, 0)
+        user.cw_adapt_to('IWorkflowable').fire_transition('activate')
+        self.commit()
+        self.assertRaises(AuthenticationError, self.repo.connect, 'syt', password='syt')
+
+class LDAPFeedSourceTC(LDAPTestBase):
+    test_db_id = 'ldap-feed'
+
+    @classmethod
+    def pre_setup_database(cls, session, config):
+        session.create_entity('CWSource', name=u'ldapuser', type=u'ldapfeed', parser=u'ldapfeed',
+                              url=URL, config=CONFIG)
+        session.commit()
+        isession = session.repo.internal_session(safe=True)
         lfsource = isession.repo.sources_by_uri['ldapuser']
         stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
 
@@ -156,16 +204,23 @@
         self.assertEqual(len(rset), 1)
         e = rset.get_entity(0, 0)
         self.assertEqual(e.eid, eid)
-        self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system', 'use-cwuri-as-url': False},
+        self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native',
+                                                             'uri': u'system',
+                                                             'use-cwuri-as-url': False},
                                                   'type': 'CWUser',
                                                   'extid': None})
         self.assertEqual(e.cw_source[0].name, 'system')
         self.assertTrue(e.creation_date)
         self.assertTrue(e.modification_date)
-        # XXX test some password has been set
         source.pull_data(self.session)
         rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
         self.assertEqual(len(rset), 1)
+        # test some password has been set
+        cu = self.session.system_sql('SELECT cw_upassword FROM cw_CWUser WHERE cw_eid=%s' % rset[0][0])
+        value = str(cu.fetchall()[0][0])
+        self.assertEqual(value, '{SSHA}v/8xJQP3uoaTBZz1T7Y0B3qOxRN1cj7D')
+        self.assertTrue(self.repo.system_source.authenticate(
+                self.session, 'syt', password='syt'))
 
 
 class LDAPUserSourceTC(LDAPFeedSourceTC):
--- a/server/test/unittest_msplanner.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_msplanner.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/server/test/unittest_multisources.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_multisources.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -337,7 +337,7 @@
         ceid = cu.execute('INSERT Card X: X title "without wikiid to get eid based url"')[0][0]
         self.cnx2.commit()
         lc = self.sexecute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0)
-        self.assertEqual(lc.absolute_url(), 'http://extern.org/card/eid/%s' % ceid)
+        self.assertEqual(lc.absolute_url(), 'http://extern.org/%s' % ceid)
         cu.execute('DELETE Card X WHERE X eid %(x)s', {'x':ceid})
         self.cnx2.commit()
 
@@ -346,7 +346,7 @@
         ceid = cu.execute('INSERT Card X: X title "without wikiid to get eid based url"')[0][0]
         self.cnx3.commit()
         lc = self.sexecute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0)
-        self.assertEqual(lc.absolute_url(), 'http://testing.fr/cubicweb/card/eid/%s' % lc.eid)
+        self.assertEqual(lc.absolute_url(), 'http://testing.fr/cubicweb/%s' % lc.eid)
         cu.execute('DELETE Card X WHERE X eid %(x)s', {'x':ceid})
         self.cnx3.commit()
 
--- a/server/test/unittest_postgres.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_postgres.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,20 +1,20 @@
 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
-# This file is part of Logilab-common.
+# This file is part of CubicWeb.
 #
-# Logilab-common is free software: you can redistribute it and/or modify it
-# under the terms of the GNU Lesser General Public License as published by the
-# Free Software Foundation, either version 2.1 of the License, or (at your
-# option) any later version.
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
 #
-# Logilab-common is distributed in the hope that it will be useful, but WITHOUT
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
 # details.
 #
 # You should have received a copy of the GNU Lesser General Public License along
-# with Logilab-common.  If not, see <http://www.gnu.org/licenses/>.
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
 from __future__ import with_statement
 
--- a/server/test/unittest_querier.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_querier.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
 # -*- coding: iso-8859-1 -*-
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -18,6 +18,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unit tests for modules cubicweb.server.querier and cubicweb.server.ssplanner
 """
+from __future__ import with_statement
+
 from datetime import date, datetime, timedelta, tzinfo
 
 from logilab.common.testlib import TestCase, unittest_main
@@ -27,10 +29,10 @@
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.utils import crypt_password
 from cubicweb.server.sources.native import make_schema
+from cubicweb.server.querier import manual_build_descr, _make_description
 from cubicweb.devtools import get_test_db_handler, TestServerConfiguration
-
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.repotest import tuplify, BaseQuerierTC
-from unittest_session import Variable
 
 class FixedOffset(tzinfo):
     def __init__(self, hours=0):
@@ -70,24 +72,48 @@
                           ('C0 text,C1 integer', {'A': 'table0.C0', 'B': 'table0.C1'}))
 
 
-def setUpModule(*args):
+def setUpClass(cls, *args):
     global repo, cnx
     config = TestServerConfiguration(apphome=UtilsTC.datadir)
     handler = get_test_db_handler(config)
     handler.build_db_cache()
     repo, cnx = handler.get_repo_and_cnx()
+    cls.repo = repo
 
-def tearDownModule(*args):
+def tearDownClass(cls, *args):
     global repo, cnx
     cnx.close()
     repo.shutdown()
     del repo, cnx
 
 
+class Variable:
+    def __init__(self, name):
+        self.name = name
+        self.children = []
+
+    def get_type(self, solution, args=None):
+        return solution[self.name]
+    def as_string(self):
+        return self.name
+
+class Function:
+    def __init__(self, name, varname):
+        self.name = name
+        self.children = [Variable(varname)]
+    def get_type(self, solution, args=None):
+        return 'Int'
+
+class MakeDescriptionTC(TestCase):
+    def test_known_values(self):
+        solution = {'A': 'Int', 'B': 'CWUser'}
+        self.assertEqual(_make_description((Function('max', 'A'), Variable('B')), {}, solution),
+                          ['Int','CWUser'])
+
+
 class UtilsTC(BaseQuerierTC):
-    def setUp(self):
-        self.__class__.repo = repo
-        super(UtilsTC, self).setUp()
+    setUpClass = classmethod(setUpClass)
+    tearDownClass = classmethod(tearDownClass)
 
     def get_max_eid(self):
         # no need for cleanup here
@@ -240,11 +266,32 @@
         rset = self.execute('Any %(x)s', {'x': u'str'})
         self.assertEqual(rset.description[0][0], 'String')
 
+    def test_build_descr1(self):
+        rset = self.execute('(Any U,L WHERE U login L) UNION (Any G,N WHERE G name N, G is CWGroup)')
+        rset.req = self.transaction
+        orig_length = len(rset)
+        rset.rows[0][0] = 9999999
+        description = manual_build_descr(rset.req, rset.syntax_tree(), None, rset.rows)
+        self.assertEqual(len(description), orig_length - 1)
+        self.assertEqual(len(rset.rows), orig_length - 1)
+        self.assertNotEqual(rset.rows[0][0], 9999999)
+
+    def test_build_descr2(self):
+        rset = self.execute('Any X,Y WITH X,Y BEING ((Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G))')
+        for x, y in rset.description:
+            if y is not None:
+                self.assertEqual(y, 'CWGroup')
+
+    def test_build_descr3(self):
+        rset = self.execute('(Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G)')
+        for x, y in rset.description:
+            if y is not None:
+                self.assertEqual(y, 'CWGroup')
+
 
 class QuerierTC(BaseQuerierTC):
-    def setUp(self):
-        self.__class__.repo = repo
-        super(QuerierTC, self).setUp()
+    setUpClass = classmethod(setUpClass)
+    tearDownClass = classmethod(tearDownClass)
 
     def test_encoding_pb(self):
         self.assertRaises(RQLSyntaxError, self.execute,
@@ -1259,7 +1306,7 @@
         cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
                        % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
         passwd = str(cursor.fetchone()[0])
-        self.assertEqual(passwd, crypt_password('toto', passwd[:2]))
+        self.assertEqual(passwd, crypt_password('toto', passwd))
         rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
                             {'pwd': Binary(passwd)})
         self.assertEqual(len(rset.rows), 1)
@@ -1274,7 +1321,7 @@
         cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
                        % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
         passwd = str(cursor.fetchone()[0])
-        self.assertEqual(passwd, crypt_password('tutu', passwd[:2]))
+        self.assertEqual(passwd, crypt_password('tutu', passwd))
         rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
                             {'pwd': Binary(passwd)})
         self.assertEqual(len(rset.rows), 1)
@@ -1501,5 +1548,20 @@
         self.assertFalse(self.execute('Any X WHERE X is CWEType, X name %(name)s', {'name': None}))
         self.assertTrue(self.execute('Any X WHERE X is CWEType, X name %(name)s', {'name': 'CWEType'}))
 
+
+class NonRegressionTC(CubicWebTC):
+
+    def test_has_text_security_cache_bug(self):
+        req = self.request()
+        self.create_user(req, 'user', ('users',))
+        aff1 = req.create_entity('Societe', nom=u'aff1')
+        aff2 = req.create_entity('Societe', nom=u'aff2')
+        self.commit()
+        with self.login('user', password='user'):
+            res = self.execute('Any X WHERE X has_text %(text)s', {'text': 'aff1'})
+            self.assertEqual(res.rows, [[aff1.eid]])
+            res = self.execute('Any X WHERE X has_text %(text)s', {'text': 'aff2'})
+            self.assertEqual(res.rows, [[aff2.eid]])
+
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_repository.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_repository.py	Tue Oct 23 15:00:53 2012 +0200
@@ -36,7 +36,7 @@
                       UnknownEid, AuthenticationError, Unauthorized, QueryError)
 from cubicweb.predicates import is_instance
 from cubicweb.schema import CubicWebSchema, RQLConstraint
-from cubicweb.dbapi import connect, multiple_connections_unfix
+from cubicweb.dbapi import connect, multiple_connections_unfix, ConnectionProperties
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.repotest import tuplify
 from cubicweb.server import repository, hook
@@ -113,6 +113,8 @@
         self.assertRaises(AuthenticationError,
                           self.repo.connect, self.admlogin, password='nimportnawak')
         self.assertRaises(AuthenticationError,
+                          self.repo.connect, self.admlogin, password='')
+        self.assertRaises(AuthenticationError,
                           self.repo.connect, self.admlogin, password=None)
         self.assertRaises(AuthenticationError,
                           self.repo.connect, None, password=None)
@@ -379,6 +381,65 @@
             # connect monkey patch some method by default, remove them
             multiple_connections_unfix()
 
+
+    def test_zmq(self):
+        try:
+            import zmq
+        except ImportError:
+            self.skipTest("zmq in not available")
+        done = []
+        from cubicweb.devtools import TestServerConfiguration as ServerConfiguration
+        from cubicweb.server.cwzmq import ZMQRepositoryServer
+        # the client part has to be in a thread due to sqlite limitations
+        t = threading.Thread(target=self._zmq_client, args=(done,))
+        t.start()
+
+        zmq_server = ZMQRepositoryServer(self.repo)
+        zmq_server.connect('tcp://127.0.0.1:41415')
+
+        t2 = threading.Thread(target=self._zmq_quit, args=(done, zmq_server,))
+        t2.start()
+
+        zmq_server.run()
+
+        t2.join(1)
+        t.join(1)
+
+        if t.isAlive():
+            self.fail('something went wrong, thread still alive')
+
+    def _zmq_quit(self, done, srv):
+        while not done:
+            time.sleep(0.1)
+        srv.quit()
+
+    def _zmq_client(self, done):
+        cnxprops = ConnectionProperties('zmq')
+        try:
+            cnx = connect('tcp://127.0.0.1:41415', u'admin', password=u'gingkow',
+                          cnxprops=cnxprops,
+                          initlog=False) # don't reset logging configuration
+            try:
+                cnx.load_appobjects(subpath=('entities',))
+                # check we can get the schema
+                schema = cnx.get_schema()
+                self.assertTrue(cnx.vreg)
+                self.assertTrue('etypes'in cnx.vreg)
+                cu = cnx.cursor()
+                rset = cu.execute('Any U,G WHERE U in_group G')
+                user = iter(rset.entities()).next()
+                self.assertTrue(user._cw)
+                self.assertTrue(user._cw.vreg)
+                from cubicweb.entities import authobjs
+                self.assertIsInstance(user._cw.user, authobjs.CWUser)
+                cnx.close()
+                done.append(True)
+            finally:
+                # connect monkey patch some method by default, remove them
+                multiple_connections_unfix()
+        finally:
+            done.append(False)
+
     def test_internal_api(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
@@ -461,7 +522,7 @@
         self.commit()
         self.assertEqual(len(c.reverse_fiche), 1)
 
-    def test_set_attributes_in_before_update(self):
+    def test_cw_set_in_before_update(self):
         # local hook
         class DummyBeforeHook(Hook):
             __regid__ = 'dummy-before-hook'
@@ -473,31 +534,31 @@
                 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')
+                    self.entity.cw_set(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')
+            addr.cw_set(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.assertEqual(rset.rows, [[u'a@b.com', u'foo']])
 
-    def test_set_attributes_in_before_add(self):
+    def test_cw_set_in_before_add(self):
         # local hook
         class DummyBeforeHook(Hook):
             __regid__ = 'dummy-before-hook'
             __select__ = Hook.__select__ & is_instance('EmailAddress')
             events = ('before_add_entity',)
             def __call__(self):
-                # set_attributes is forbidden within before_add_entity()
-                self.entity.set_attributes(alias=u'foo')
+                # cw_set is forbidden within before_add_entity()
+                self.entity.cw_set(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')
 
-    def test_multiple_edit_set_attributes(self):
+    def test_multiple_edit_cw_set(self):
         """make sure cw_edited doesn't get cluttered
         by previous entities on multiple set
         """
@@ -603,7 +664,7 @@
         self.commit()
         rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
         self.assertEqual(rset.rows, [])
-        req.user.set_relations(use_email=toto)
+        req.user.cw_set(use_email=toto)
         self.commit()
         rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
         self.assertEqual(rset.rows, [[req.user.eid]])
@@ -613,11 +674,11 @@
         rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
         self.assertEqual(rset.rows, [])
         tutu = req.create_entity('EmailAddress', address=u'tutu@logilab.fr')
-        req.user.set_relations(use_email=tutu)
+        req.user.cw_set(use_email=tutu)
         self.commit()
         rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'})
         self.assertEqual(rset.rows, [[req.user.eid]])
-        tutu.set_attributes(address=u'hip@logilab.fr')
+        tutu.cw_set(address=u'hip@logilab.fr')
         self.commit()
         rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'})
         self.assertEqual(rset.rows, [])
@@ -729,7 +790,7 @@
             personnes.append(p)
         abraham = req.create_entity('Personne', nom=u'Abraham', prenom=u'John', sexe=u'M')
         for j in xrange(0, 2000, 100):
-            abraham.set_relations(personne_composite=personnes[j:j+100])
+            abraham.cw_set(personne_composite=personnes[j:j+100])
         t1 = time.time()
         self.info('creation: %.2gs', (t1 - t0))
         req.cnx.commit()
@@ -755,7 +816,7 @@
         t1 = time.time()
         self.info('creation: %.2gs', (t1 - t0))
         for j in xrange(100, 2000, 100):
-            abraham.set_relations(personne_composite=personnes[j:j+100])
+            abraham.cw_set(personne_composite=personnes[j:j+100])
         t2 = time.time()
         self.info('more relations: %.2gs', (t2-t1))
         req.cnx.commit()
@@ -775,7 +836,7 @@
         t1 = time.time()
         self.info('creation: %.2gs', (t1 - t0))
         for j in xrange(100, 2000, 100):
-            abraham.set_relations(personne_inlined=personnes[j:j+100])
+            abraham.cw_set(personne_inlined=personnes[j:j+100])
         t2 = time.time()
         self.info('more relations: %.2gs', (t2-t1))
         req.cnx.commit()
@@ -856,7 +917,7 @@
         p1 = req.create_entity('Personne', nom=u'Vincent')
         p2 = req.create_entity('Personne', nom=u'Florent')
         w = req.create_entity('Affaire', ref=u'wc')
-        w.set_relations(todo_by=[p1,p2])
+        w.cw_set(todo_by=[p1,p2])
         w.cw_clear_all_caches()
         self.commit()
         self.assertEqual(len(w.todo_by), 1)
@@ -867,9 +928,9 @@
         p1 = req.create_entity('Personne', nom=u'Vincent')
         p2 = req.create_entity('Personne', nom=u'Florent')
         w = req.create_entity('Affaire', ref=u'wc')
-        w.set_relations(todo_by=p1)
+        w.cw_set(todo_by=p1)
         self.commit()
-        w.set_relations(todo_by=p2)
+        w.cw_set(todo_by=p2)
         w.cw_clear_all_caches()
         self.commit()
         self.assertEqual(len(w.todo_by), 1)
--- a/server/test/unittest_rql2sql.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_rql2sql.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -219,6 +219,11 @@
 
 
 ADVANCED = [
+    ("Societe S WHERE S2 is Societe, S2 nom SN, S nom 'Logilab' OR S nom SN",
+     '''SELECT _S.cw_eid
+FROM cw_Societe AS _S, cw_Societe AS _S2
+WHERE ((_S.cw_nom=Logilab) OR (_S2.cw_nom=_S.cw_nom))'''),
+
     ("Societe S WHERE S nom 'Logilab' OR S nom 'Caesium'",
      '''SELECT _S.cw_eid
 FROM cw_Societe AS _S
--- a/server/test/unittest_rqlannotation.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_rqlannotation.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
 # -*- coding: iso-8859-1 -*-
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -350,6 +350,12 @@
         self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
         self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
+
+    def test_has_text_security_cache_bug(self):
+        rqlst = self._prepare('Any X WHERE X has_text "toto" WITH X BEING '
+                              '(Any C WHERE C is Societe, C nom CS)')
+        self.assertTrue(rqlst.parent.has_text_query)
+
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- a/server/test/unittest_security.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_security.py	Tue Oct 23 15:00:53 2012 +0200
@@ -16,54 +16,63 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """functional tests for server'security"""
+from __future__ import with_statement
 
 import sys
 
 from logilab.common.testlib import unittest_main, TestCase
-from cubicweb.devtools.testlib import CubicWebTC
+
+from rql import RQLException
 
-from cubicweb import Unauthorized, ValidationError, QueryError
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb import Unauthorized, ValidationError, QueryError, Binary
+from cubicweb.schema import ERQLExpression
 from cubicweb.server.querier import check_read_access
+from cubicweb.server.utils import _CRYPTO_CTX
+
 
 class BaseSecurityTC(CubicWebTC):
 
     def setup_database(self):
         super(BaseSecurityTC, self).setup_database()
-        req = self.request()
-        self.create_user(req, 'iaminusersgrouponly')
-        readoriggroups = self.schema['Personne'].permissions['read']
-        addoriggroups = self.schema['Personne'].permissions['add']
-        def fix_perm():
-            self.schema['Personne'].set_action_permissions('read', readoriggroups)
-            self.schema['Personne'].set_action_permissions('add', addoriggroups)
-        self.addCleanup(fix_perm)
-
+        self.create_user(self.request(), 'iaminusersgrouponly')
+        hash = _CRYPTO_CTX.encrypt('oldpassword', scheme='des_crypt')
+        self.create_user(self.request(), 'oldpassword', password=Binary(hash))
 
 class LowLevelSecurityFunctionTC(BaseSecurityTC):
 
     def test_check_read_access(self):
         rql = u'Personne U where U nom "managers"'
         rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
-        origgroups = self.schema['Personne'].get_groups('read')
-        self.schema['Personne'].set_action_permissions('read', ('users', 'managers'))
-        self.repo.vreg.solutions(self.session, rqlst, None)
-        solution = rqlst.solutions[0]
-        check_read_access(self.session, rqlst, solution, {})
-        cnx = self.login('anon')
-        cu = cnx.cursor()
-        self.assertRaises(Unauthorized,
-                          check_read_access,
-                          self.session, rqlst, solution, {})
-        self.assertRaises(Unauthorized, cu.execute, rql)
+        with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
+            self.repo.vreg.solutions(self.session, rqlst, None)
+            solution = rqlst.solutions[0]
+            check_read_access(self.session, rqlst, solution, {})
+            with self.login('anon') as cu:
+                self.assertRaises(Unauthorized,
+                                  check_read_access,
+                                  self.session, rqlst, solution, {})
+                self.assertRaises(Unauthorized, cu.execute, rql)
 
     def test_upassword_not_selectable(self):
         self.assertRaises(Unauthorized,
                           self.execute, 'Any X,P WHERE X is CWUser, X upassword P')
         self.rollback()
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        self.assertRaises(Unauthorized,
-                          cu.execute, 'Any X,P WHERE X is CWUser, X upassword P')
+        with self.login('iaminusersgrouponly') as cu:
+            self.assertRaises(Unauthorized,
+                              cu.execute, 'Any X,P WHERE X is CWUser, X upassword P')
+
+    def test_update_password(self):
+        """Ensure that if a user's password is stored with a deprecated hash, it will be updated on next login"""
+        oldhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0])
+        with self.login('oldpassword') as cu:
+            pass
+        newhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0])
+        self.assertNotEqual(oldhash, newhash)
+        self.assertTrue(newhash.startswith('$6$'))
+        with self.login('oldpassword') as cu:
+            pass
+        self.assertEqual(newhash, str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0]))
 
 
 class SecurityRewritingTC(BaseSecurityTC):
@@ -78,15 +87,14 @@
         super(SecurityRewritingTC, self).tearDown()
 
     def test_not_relation_read_security(self):
-        cnx = self.login('iaminusersgrouponly')
-        self.hijack_source_execute()
-        self.execute('Any U WHERE NOT A todo_by U, A is Affaire')
-        self.assertEqual(self.query[0][1].as_string(),
-                          'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
-        self.execute('Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
-        self.assertEqual(self.query[0][1].as_string(),
-                          'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
-        cnx.close()
+        with self.login('iaminusersgrouponly'):
+            self.hijack_source_execute()
+            self.execute('Any U WHERE NOT A todo_by U, A is Affaire')
+            self.assertEqual(self.query[0][1].as_string(),
+                              'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
+            self.execute('Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
+            self.assertEqual(self.query[0][1].as_string(),
+                              'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
 
 class SecurityTC(BaseSecurityTC):
 
@@ -100,76 +108,63 @@
         self.commit()
 
     def test_insert_security(self):
-        cnx = self.login('anon')
-        cu = cnx.cursor()
-        cu.execute("INSERT Personne X: X nom 'bidule'")
-        self.assertRaises(Unauthorized, cnx.commit)
-        self.assertEqual(cu.execute('Personne X').rowcount, 1)
-        cnx.close()
+        with self.login('anon') as cu:
+            cu.execute("INSERT Personne X: X nom 'bidule'")
+            self.assertRaises(Unauthorized, self.commit)
+            self.assertEqual(cu.execute('Personne X').rowcount, 1)
 
     def test_insert_rql_permission(self):
         # test user can only add une affaire related to a societe he owns
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        cu.execute("INSERT Affaire X: X sujet 'cool'")
-        self.assertRaises(Unauthorized, cnx.commit)
+        with self.login('iaminusersgrouponly') as cu:
+            cu.execute("INSERT Affaire X: X sujet 'cool'")
+            self.assertRaises(Unauthorized, self.commit)
         # test nothing has actually been inserted
-        self.restore_connection()
         self.assertEqual(self.execute('Affaire X').rowcount, 1)
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        cu.execute("INSERT Affaire X: X sujet 'cool'")
-        cu.execute("INSERT Societe X: X nom 'chouette'")
-        cu.execute("SET A concerne S WHERE A sujet 'cool', S nom 'chouette'")
-        cnx.commit()
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            cu.execute("INSERT Affaire X: X sujet 'cool'")
+            cu.execute("INSERT Societe X: X nom 'chouette'")
+            cu.execute("SET A concerne S WHERE A sujet 'cool', S nom 'chouette'")
+            self.commit()
 
     def test_update_security_1(self):
-        cnx = self.login('anon')
-        cu = cnx.cursor()
-        # local security check
-        cu.execute( "SET X nom 'bidulechouette' WHERE X is Personne")
-        self.assertRaises(Unauthorized, cnx.commit)
-        self.restore_connection()
+        with self.login('anon') as cu:
+            # local security check
+            cu.execute( "SET X nom 'bidulechouette' WHERE X is Personne")
+            self.assertRaises(Unauthorized, self.commit)
         self.assertEqual(self.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
 
     def test_update_security_2(self):
-        cnx = self.login('anon')
-        cu = cnx.cursor()
-        self.repo.schema['Personne'].set_action_permissions('read', ('users', 'managers'))
-        self.repo.schema['Personne'].set_action_permissions('add', ('guests', 'users', 'managers'))
-        self.assertRaises(Unauthorized, cu.execute, "SET X nom 'bidulechouette' WHERE X is Personne")
-        #self.assertRaises(Unauthorized, cnx.commit)
+        with self.temporary_permissions(Personne={'read': ('users', 'managers'),
+                                                  'add': ('guests', 'users', 'managers')}):
+            with self.login('anon') as cu:
+                self.assertRaises(Unauthorized, cu.execute, "SET X nom 'bidulechouette' WHERE X is Personne")
+                self.rollback()
+                # self.assertRaises(Unauthorized, cnx.commit)
         # test nothing has actually been inserted
-        self.restore_connection()
         self.assertEqual(self.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
 
     def test_update_security_3(self):
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        cu.execute("INSERT Personne X: X nom 'biduuule'")
-        cu.execute("INSERT Societe X: X nom 'looogilab'")
-        cu.execute("SET X travaille S WHERE X nom 'biduuule', S nom 'looogilab'")
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            cu.execute("INSERT Personne X: X nom 'biduuule'")
+            cu.execute("INSERT Societe X: X nom 'looogilab'")
+            cu.execute("SET X travaille S WHERE X nom 'biduuule', S nom 'looogilab'")
 
     def test_update_rql_permission(self):
         self.execute("SET A concerne S WHERE A is Affaire, S is Societe")
         self.commit()
         # test user can only update une affaire related to a societe he owns
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        cu.execute("SET X sujet 'pascool' WHERE X is Affaire")
-        # this won't actually do anything since the selection query won't return anything
-        cnx.commit()
-        # to actually get Unauthorized exception, try to update an entity we can read
-        cu.execute("SET X nom 'toto' WHERE X is Societe")
-        self.assertRaises(Unauthorized, cnx.commit)
-        cu.execute("INSERT Affaire X: X sujet 'pascool'")
-        cu.execute("INSERT Societe X: X nom 'chouette'")
-        cu.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
-        cu.execute("SET X sujet 'habahsicestcool' WHERE X sujet 'pascool'")
-        cnx.commit()
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            cu.execute("SET X sujet 'pascool' WHERE X is Affaire")
+            # this won't actually do anything since the selection query won't return anything
+            self.commit()
+            # to actually get Unauthorized exception, try to update an entity we can read
+            cu.execute("SET X nom 'toto' WHERE X is Societe")
+            self.assertRaises(Unauthorized, self.commit)
+            cu.execute("INSERT Affaire X: X sujet 'pascool'")
+            cu.execute("INSERT Societe X: X nom 'chouette'")
+            cu.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
+            cu.execute("SET X sujet 'habahsicestcool' WHERE X sujet 'pascool'")
+            self.commit()
 
     def test_delete_security(self):
         # FIXME: sample below fails because we don't detect "owner" can't delete
@@ -179,251 +174,223 @@
         #self.assertRaises(Unauthorized,
         #                  self.o.execute, user, "DELETE CWUser X WHERE X login 'bidule'")
         # check local security
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        self.assertRaises(Unauthorized, cu.execute, "DELETE CWGroup Y WHERE Y name 'staff'")
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            self.assertRaises(Unauthorized, cu.execute, "DELETE CWGroup Y WHERE Y name 'staff'")
+            self.rollback()
 
     def test_delete_rql_permission(self):
         self.execute("SET A concerne S WHERE A is Affaire, S is Societe")
         self.commit()
         # test user can only dele une affaire related to a societe he owns
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        # this won't actually do anything since the selection query won't return anything
-        cu.execute("DELETE Affaire X")
-        cnx.commit()
-        # to actually get Unauthorized exception, try to delete an entity we can read
-        self.assertRaises(Unauthorized, cu.execute, "DELETE Societe S")
-        self.assertRaises(QueryError, cnx.commit) # can't commit anymore
-        cnx.rollback() # required after Unauthorized
-        cu.execute("INSERT Affaire X: X sujet 'pascool'")
-        cu.execute("INSERT Societe X: X nom 'chouette'")
-        cu.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
-        cnx.commit()
+        with self.login('iaminusersgrouponly') as cu:
+            # this won't actually do anything since the selection query won't return anything
+            cu.execute("DELETE Affaire X")
+            self.commit()
+            # to actually get Unauthorized exception, try to delete an entity we can read
+            self.assertRaises(Unauthorized, cu.execute, "DELETE Societe S")
+            self.assertRaises(QueryError, self.commit) # can't commit anymore
+            self.rollback() # required after Unauthorized
+            cu.execute("INSERT Affaire X: X sujet 'pascool'")
+            cu.execute("INSERT Societe X: X nom 'chouette'")
+            cu.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
+            self.commit()
 ##         # this one should fail since it will try to delete two affaires, one authorized
 ##         # and the other not
 ##         self.assertRaises(Unauthorized, cu.execute, "DELETE Affaire X")
-        cu.execute("DELETE Affaire X WHERE X sujet 'pascool'")
-        cnx.commit()
-        cnx.close()
+            cu.execute("DELETE Affaire X WHERE X sujet 'pascool'")
+            self.commit()
 
 
     def test_insert_relation_rql_permission(self):
-        cnx = self.login('iaminusersgrouponly')
-        session = self.session
-        cu = cnx.cursor(session)
-        cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
-        # should raise Unauthorized since user don't own S
-        # though this won't actually do anything since the selection query won't return anything
-        cnx.commit()
-        # to actually get Unauthorized exception, try to insert a relation were we can read both entities
-        rset = cu.execute('Personne P')
-        self.assertEqual(len(rset), 1)
-        ent = rset.get_entity(0, 0)
-        session.set_cnxset() # necessary
-        self.assertRaises(Unauthorized, ent.cw_check_perm, 'update')
-        self.assertRaises(Unauthorized,
-                          cu.execute, "SET P travaille S WHERE P is Personne, S is Societe")
-        self.assertRaises(QueryError, cnx.commit) # can't commit anymore
-        cnx.rollback()
-        # test nothing has actually been inserted:
-        self.assertEqual(cu.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe').rowcount, 0)
-        cu.execute("INSERT Societe X: X nom 'chouette'")
-        cu.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
-        cnx.commit()
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+            # should raise Unauthorized since user don't own S though this won't
+            # actually do anything since the selection query won't return
+            # anything
+            self.commit()
+            # to actually get Unauthorized exception, try to insert a relation
+            # were we can read both entities
+            rset = cu.execute('Personne P')
+            self.assertEqual(len(rset), 1)
+            ent = rset.get_entity(0, 0)
+            self.assertFalse(cu.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe'))
+            self.assertRaises(Unauthorized, ent.cw_check_perm, 'update')
+            self.assertRaises(Unauthorized,
+                              cu.execute, "SET P travaille S WHERE P is Personne, S is Societe")
+            self.assertRaises(QueryError, self.commit) # can't commit anymore
+            self.rollback()
+            # test nothing has actually been inserted:
+            self.assertFalse(cu.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe'))
+            cu.execute("INSERT Societe X: X nom 'chouette'")
+            cu.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
+            self.commit()
 
     def test_delete_relation_rql_permission(self):
         self.execute("SET A concerne S WHERE A is Affaire, S is Societe")
         self.commit()
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        # this won't actually do anything since the selection query won't return anything
-        cu.execute("DELETE A concerne S")
-        cnx.commit()
+        with self.login('iaminusersgrouponly') as cu:
+            # this won't actually do anything since the selection query won't return anything
+            cu.execute("DELETE A concerne S")
+            self.commit()
         # to actually get Unauthorized exception, try to delete a relation we can read
-        self.restore_connection()
         eid = self.execute("INSERT Affaire X: X sujet 'pascool'")[0][0]
         self.execute('SET X owned_by U WHERE X eid %(x)s, U login "iaminusersgrouponly"', {'x': eid})
         self.execute("SET A concerne S WHERE A sujet 'pascool', S is Societe")
         self.commit()
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        self.assertRaises(Unauthorized, cu.execute, "DELETE A concerne S")
-        self.assertRaises(QueryError, cnx.commit) # can't commit anymore
-        cnx.rollback() # required after Unauthorized
-        cu.execute("INSERT Societe X: X nom 'chouette'")
-        cu.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
-        cnx.commit()
-        cu.execute("DELETE A concerne S WHERE S nom 'chouette'")
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            self.assertRaises(Unauthorized, cu.execute, "DELETE A concerne S")
+            self.assertRaises(QueryError, self.commit) # can't commit anymore
+            self.rollback() # required after Unauthorized
+            cu.execute("INSERT Societe X: X nom 'chouette'")
+            cu.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
+            self.commit()
+            cu.execute("DELETE A concerne S WHERE S nom 'chouette'")
+            self.commit()
 
 
     def test_user_can_change_its_upassword(self):
         req = self.request()
         ueid = self.create_user(req, 'user').eid
-        cnx = self.login('user')
-        cu = cnx.cursor()
-        cu.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
-                   {'x': ueid, 'passwd': 'newpwd'})
-        cnx.commit()
-        cnx.close()
+        with self.login('user') as cu:
+            cu.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
+                       {'x': ueid, 'passwd': 'newpwd'})
+            self.commit()
         cnx = self.login('user', password='newpwd')
         cnx.close()
 
     def test_user_cant_change_other_upassword(self):
         req = self.request()
         ueid = self.create_user(req, 'otheruser').eid
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        cu.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
-                   {'x': ueid, 'passwd': 'newpwd'})
-        self.assertRaises(Unauthorized, cnx.commit)
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            cu.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
+                       {'x': ueid, 'passwd': 'newpwd'})
+            self.assertRaises(Unauthorized, self.commit)
 
     # read security test
 
     def test_read_base(self):
-        self.schema['Personne'].set_action_permissions('read', ('users', 'managers'))
-        cnx = self.login('anon')
-        cu = cnx.cursor()
-        self.assertRaises(Unauthorized,
-                          cu.execute, 'Personne U where U nom "managers"')
-        cnx.close()
+        with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
+            with self.login('anon') as cu:
+                self.assertRaises(Unauthorized,
+                                  cu.execute, 'Personne U where U nom "managers"')
+                self.rollback()
 
     def test_read_erqlexpr_base(self):
         eid = self.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
         self.commit()
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        rset = cu.execute('Affaire X')
-        self.assertEqual(rset.rows, [])
-        self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
-        # cache test
-        self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
-        aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
-        soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
-        cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
-        cnx.commit()
-        rset = cu.execute('Any X WHERE X eid %(x)s', {'x': aff2})
-        self.assertEqual(rset.rows, [[aff2]])
-        # more cache test w/ NOT eid
-        rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': eid})
-        self.assertEqual(rset.rows, [[aff2]])
-        rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': aff2})
-        self.assertEqual(rset.rows, [])
-        # test can't update an attribute of an entity that can't be readen
-        self.assertRaises(Unauthorized, cu.execute, 'SET X sujet "hacked" WHERE X eid %(x)s', {'x': eid})
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            rset = cu.execute('Affaire X')
+            self.assertEqual(rset.rows, [])
+            self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
+            # cache test
+            self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
+            aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+            soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
+            cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+            self.commit()
+            rset = cu.execute('Any X WHERE X eid %(x)s', {'x': aff2})
+            self.assertEqual(rset.rows, [[aff2]])
+            # more cache test w/ NOT eid
+            rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': eid})
+            self.assertEqual(rset.rows, [[aff2]])
+            rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': aff2})
+            self.assertEqual(rset.rows, [])
+            # test can't update an attribute of an entity that can't be readen
+            self.assertRaises(Unauthorized, cu.execute, 'SET X sujet "hacked" WHERE X eid %(x)s', {'x': eid})
+            self.rollback()
 
 
     def test_entity_created_in_transaction(self):
         affschema = self.schema['Affaire']
-        origperms = affschema.permissions['read']
-        affschema.set_action_permissions('read', affschema.permissions['add'])
-        try:
-            cnx = self.login('iaminusersgrouponly')
-            cu = cnx.cursor()
-            aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
-            # entity created in transaction are readable *by eid*
-            self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
-            # XXX would be nice if it worked
-            rset = cu.execute("Affaire X WHERE X sujet 'cool'")
-            self.assertEqual(len(rset), 0)
-        finally:
-            affschema.set_action_permissions('read', origperms)
-            cnx.close()
+        with self.temporary_permissions(Affaire={'read': affschema.permissions['add']}):
+            with self.login('iaminusersgrouponly') as cu:
+                aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+                # entity created in transaction are readable *by eid*
+                self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
+                # XXX would be nice if it worked
+                rset = cu.execute("Affaire X WHERE X sujet 'cool'")
+                self.assertEqual(len(rset), 0)
+                self.assertRaises(Unauthorized, self.commit)
 
     def test_read_erqlexpr_has_text1(self):
         aff1 = self.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
         card1 = self.execute("INSERT Card X: X title 'cool'")[0][0]
         self.execute('SET X owned_by U WHERE X eid %(x)s, U login "iaminusersgrouponly"', {'x': card1})
         self.commit()
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
-        soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
-        cu.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1})
-        cnx.commit()
-        self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x':aff1})
-        self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
-        self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':card1}))
-        rset = cu.execute("Any X WHERE X has_text 'cool'")
-        self.assertEqual(sorted(eid for eid, in rset.rows),
-                          [card1, aff2])
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+            soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
+            cu.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1})
+            self.commit()
+            self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x':aff1})
+            self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
+            self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':card1}))
+            rset = cu.execute("Any X WHERE X has_text 'cool'")
+            self.assertEqual(sorted(eid for eid, in rset.rows),
+                              [card1, aff2])
+            self.rollback()
 
     def test_read_erqlexpr_has_text2(self):
         self.execute("INSERT Personne X: X nom 'bidule'")
         self.execute("INSERT Societe X: X nom 'bidule'")
         self.commit()
-        self.schema['Personne'].set_action_permissions('read', ('managers',))
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        rset = cu.execute('Any N WHERE N has_text "bidule"')
-        self.assertEqual(len(rset.rows), 1, rset.rows)
-        rset = cu.execute('Any N WITH N BEING (Any N WHERE N has_text "bidule")')
-        self.assertEqual(len(rset.rows), 1, rset.rows)
-        cnx.close()
+        with self.temporary_permissions(Personne={'read': ('managers',)}):
+            with self.login('iaminusersgrouponly') as cu:
+                rset = cu.execute('Any N WHERE N has_text "bidule"')
+                self.assertEqual(len(rset.rows), 1, rset.rows)
+                rset = cu.execute('Any N WITH N BEING (Any N WHERE N has_text "bidule")')
+                self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_read_erqlexpr_optional_rel(self):
         self.execute("INSERT Personne X: X nom 'bidule'")
         self.execute("INSERT Societe X: X nom 'bidule'")
         self.commit()
-        self.schema['Personne'].set_action_permissions('read', ('managers',))
-        cnx = self.login('anon')
-        cu = cnx.cursor()
-        rset = cu.execute('Any N,U WHERE N has_text "bidule", N owned_by U?')
-        self.assertEqual(len(rset.rows), 1, rset.rows)
-        cnx.close()
+        with self.temporary_permissions(Personne={'read': ('managers',)}):
+            with self.login('anon') as cu:
+                rset = cu.execute('Any N,U WHERE N has_text "bidule", N owned_by U?')
+                self.assertEqual(len(rset.rows), 1, rset.rows)
 
     def test_read_erqlexpr_aggregat(self):
         self.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
         self.commit()
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        rset = cu.execute('Any COUNT(X) WHERE X is Affaire')
-        self.assertEqual(rset.rows, [[0]])
-        aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
-        soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
-        cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
-        cnx.commit()
-        rset = cu.execute('Any COUNT(X) WHERE X is Affaire')
-        self.assertEqual(rset.rows, [[1]])
-        rset = cu.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN')
-        values = dict(rset)
-        self.assertEqual(values['Affaire'], 1)
-        self.assertEqual(values['Societe'], 2)
-        rset = cu.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN WITH X BEING ((Affaire X) UNION (Societe X))')
-        self.assertEqual(len(rset), 2)
-        values = dict(rset)
-        self.assertEqual(values['Affaire'], 1)
-        self.assertEqual(values['Societe'], 2)
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            rset = cu.execute('Any COUNT(X) WHERE X is Affaire')
+            self.assertEqual(rset.rows, [[0]])
+            aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+            soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
+            cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+            self.commit()
+            rset = cu.execute('Any COUNT(X) WHERE X is Affaire')
+            self.assertEqual(rset.rows, [[1]])
+            rset = cu.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN')
+            values = dict(rset)
+            self.assertEqual(values['Affaire'], 1)
+            self.assertEqual(values['Societe'], 2)
+            rset = cu.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN WITH X BEING ((Affaire X) UNION (Societe X))')
+            self.assertEqual(len(rset), 2)
+            values = dict(rset)
+            self.assertEqual(values['Affaire'], 1)
+            self.assertEqual(values['Societe'], 2)
 
 
     def test_attribute_security(self):
         # only managers should be able to edit the 'test' attribute of Personne entities
         eid = self.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org', X test TRUE")[0][0]
-        self.commit()
         self.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
         self.commit()
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org', X test TRUE")
-        self.assertRaises(Unauthorized, cnx.commit)
-        cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org', X test FALSE")
-        self.assertRaises(Unauthorized, cnx.commit)
-        eid = cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org'")[0][0]
-        cnx.commit()
-        cu.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
-        self.assertRaises(Unauthorized, cnx.commit)
-        cu.execute('SET X test TRUE WHERE X eid %(x)s', {'x': eid})
-        self.assertRaises(Unauthorized, cnx.commit)
-        cu.execute('SET X web "http://www.logilab.org" WHERE X eid %(x)s', {'x': eid})
-        cnx.commit()
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org', X test TRUE")
+            self.assertRaises(Unauthorized, self.commit)
+            cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org', X test FALSE")
+            self.assertRaises(Unauthorized, self.commit)
+            eid = cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org'")[0][0]
+            self.commit()
+            cu.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
+            self.assertRaises(Unauthorized, self.commit)
+            cu.execute('SET X test TRUE WHERE X eid %(x)s', {'x': eid})
+            self.assertRaises(Unauthorized, self.commit)
+            cu.execute('SET X web "http://www.logilab.org" WHERE X eid %(x)s', {'x': eid})
+            self.commit()
 
     def test_attribute_security_rqlexpr(self):
         # Note.para attribute editable by managers or if the note is in "todo" state
@@ -432,54 +399,62 @@
         note.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
         self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid})
         self.commit()
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid})
-        self.assertRaises(Unauthorized, cnx.commit)
-        note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
-        cnx.commit()
-        note2.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
-        cnx.commit()
-        self.assertEqual(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid})),
-                          0)
-        cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
-        self.assertRaises(Unauthorized, cnx.commit)
-        note2.cw_adapt_to('IWorkflowable').fire_transition('redoit')
-        cnx.commit()
-        cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
-        cnx.commit()
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid})
+            self.assertRaises(Unauthorized, self.commit)
+            note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
+            self.commit()
+            note2.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
+            self.commit()
+            self.assertEqual(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid})),
+                              0)
+            cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
+            self.assertRaises(Unauthorized, self.commit)
+            note2.cw_adapt_to('IWorkflowable').fire_transition('redoit')
+            self.commit()
+            cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
+            self.commit()
 
     def test_attribute_read_security(self):
         # anon not allowed to see users'login, but they can see users
-        self.repo.schema['CWUser'].set_action_permissions('read', ('guests', 'users', 'managers'))
-        self.repo.schema['CWUser'].rdef('login').set_action_permissions('read', ('users', 'managers'))
-        cnx = self.login('anon')
-        cu = cnx.cursor()
-        rset = cu.execute('CWUser X')
-        self.assertTrue(rset)
-        x = rset.get_entity(0, 0)
-        self.assertEqual(x.login, None)
-        self.assertTrue(x.creation_date)
-        x = rset.get_entity(1, 0)
-        x.complete()
-        self.assertEqual(x.login, None)
-        self.assertTrue(x.creation_date)
-        cnx.rollback()
-        cnx.close()
+        login_rdef = self.repo.schema['CWUser'].rdef('login')
+        with self.temporary_permissions((login_rdef, {'read': ('users', 'managers')}),
+                                        CWUser={'read': ('guests', 'users', 'managers')}):
+            with self.login('anon') as cu:
+                rset = cu.execute('CWUser X')
+                self.assertTrue(rset)
+                x = rset.get_entity(0, 0)
+                self.assertEqual(x.login, None)
+                self.assertTrue(x.creation_date)
+                x = rset.get_entity(1, 0)
+                x.complete()
+                self.assertEqual(x.login, None)
+                self.assertTrue(x.creation_date)
+
+    def test_yams_inheritance_and_security_bug(self):
+        with self.temporary_permissions(Division={'read': ('managers', ERQLExpression('X owned_by U'))}):
+            with self.login('iaminusersgrouponly'):
+                querier = self.repo.querier
+                rqlst = querier.parse('Any X WHERE X is_instance_of Societe')
+                querier.solutions(self.session, rqlst, {})
+                querier._annotate(rqlst)
+                plan = querier.plan_factory(rqlst, {}, self.session)
+                plan.preprocess(rqlst)
+                self.assertEqual(
+                    rqlst.as_string(),
+                    '(Any X WHERE X is IN(SubDivision, Societe)) UNION (Any X WHERE X is Division, EXISTS(X owned_by %(B)s))')
+
 
 class BaseSchemaSecurityTC(BaseSecurityTC):
     """tests related to the base schema permission configuration"""
 
     def test_user_can_delete_object_he_created(self):
         # even if some other user have changed object'state
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        # due to security test, affaire has to concerne a societe the user owns
-        cu.execute('INSERT Societe X: X nom "ARCTIA"')
-        cu.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"')
-        cnx.commit()
-        self.restore_connection()
+        with self.login('iaminusersgrouponly') as cu:
+            # due to security test, affaire has to concerne a societe the user owns
+            cu.execute('INSERT Societe X: X nom "ARCTIA"')
+            cu.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"')
+            self.commit()
         affaire = self.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
         affaire.cw_adapt_to('IWorkflowable').fire_transition('abort')
         self.commit()
@@ -488,90 +463,79 @@
         self.assertEqual(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",'
                                            'X owned_by U, U login "admin"')),
                           1) # TrInfo at the above state change
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        cu.execute('DELETE Affaire X WHERE X ref "ARCT01"')
-        cnx.commit()
-        self.assertFalse(cu.execute('Affaire X'))
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            cu.execute('DELETE Affaire X WHERE X ref "ARCT01"')
+            self.commit()
+            self.assertFalse(cu.execute('Affaire X'))
 
     def test_users_and_groups_non_readable_by_guests(self):
-        cnx = self.login('anon')
-        anon = cnx.user(self.session)
-        cu = cnx.cursor()
-        # anonymous user can only read itself
-        rset = cu.execute('Any L WHERE X owned_by U, U login L')
-        self.assertEqual(rset.rows, [['anon']])
-        rset = cu.execute('CWUser X')
-        self.assertEqual(rset.rows, [[anon.eid]])
-        # anonymous user can read groups (necessary to check allowed transitions for instance)
-        self.assert_(cu.execute('CWGroup X'))
-        # should only be able to read the anonymous user, not another one
-        origuser = self.adminsession.user
-        self.assertRaises(Unauthorized,
-                          cu.execute, 'CWUser X WHERE X eid %(x)s', {'x': origuser.eid})
-        # nothing selected, nothing updated, no exception raised
-        #self.assertRaises(Unauthorized,
-        #                  cu.execute, 'SET X login "toto" WHERE X eid %(x)s',
-        #                  {'x': self.user.eid})
+        with self.login('anon') as cu:
+            anon = cu.connection.user(self.session)
+            # anonymous user can only read itself
+            rset = cu.execute('Any L WHERE X owned_by U, U login L')
+            self.assertEqual(rset.rows, [['anon']])
+            rset = cu.execute('CWUser X')
+            self.assertEqual(rset.rows, [[anon.eid]])
+            # anonymous user can read groups (necessary to check allowed transitions for instance)
+            self.assert_(cu.execute('CWGroup X'))
+            # should only be able to read the anonymous user, not another one
+            origuser = self.adminsession.user
+            self.assertRaises(Unauthorized,
+                              cu.execute, 'CWUser X WHERE X eid %(x)s', {'x': origuser.eid})
+            # nothing selected, nothing updated, no exception raised
+            #self.assertRaises(Unauthorized,
+            #                  cu.execute, 'SET X login "toto" WHERE X eid %(x)s',
+            #                  {'x': self.user.eid})
 
-        rset = cu.execute('CWUser X WHERE X eid %(x)s', {'x': anon.eid})
-        self.assertEqual(rset.rows, [[anon.eid]])
-        # but can't modify it
-        cu.execute('SET X login "toto" WHERE X eid %(x)s', {'x': anon.eid})
-        self.assertRaises(Unauthorized, cnx.commit)
-        cnx.close()
+            rset = cu.execute('CWUser X WHERE X eid %(x)s', {'x': anon.eid})
+            self.assertEqual(rset.rows, [[anon.eid]])
+            # but can't modify it
+            cu.execute('SET X login "toto" WHERE X eid %(x)s', {'x': anon.eid})
+            self.assertRaises(Unauthorized, self.commit)
 
     def test_in_group_relation(self):
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        rql = u"DELETE U in_group G WHERE U login 'admin'"
-        self.assertRaises(Unauthorized, cu.execute, rql)
-        rql = u"SET U in_group G WHERE U login 'admin', G name 'users'"
-        self.assertRaises(Unauthorized, cu.execute, rql)
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            rql = u"DELETE U in_group G WHERE U login 'admin'"
+            self.assertRaises(Unauthorized, cu.execute, rql)
+            rql = u"SET U in_group G WHERE U login 'admin', G name 'users'"
+            self.assertRaises(Unauthorized, cu.execute, rql)
+            self.rollback()
 
     def test_owned_by(self):
         self.execute("INSERT Personne X: X nom 'bidule'")
         self.commit()
-        cnx = self.login('iaminusersgrouponly')
-        cu = cnx.cursor()
-        rql = u"SET X owned_by U WHERE U login 'iaminusersgrouponly', X is Personne"
-        self.assertRaises(Unauthorized, cu.execute, rql)
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            rql = u"SET X owned_by U WHERE U login 'iaminusersgrouponly', X is Personne"
+            self.assertRaises(Unauthorized, cu.execute, rql)
+            self.rollback()
 
     def test_bookmarked_by_guests_security(self):
         beid1 = self.execute('INSERT Bookmark B: B path "?vid=manage", B title "manage"')[0][0]
         beid2 = self.execute('INSERT Bookmark B: B path "?vid=index", B title "index", B bookmarked_by U WHERE U login "anon"')[0][0]
         self.commit()
-        cnx = self.login('anon')
-        cu = cnx.cursor()
-        anoneid = self.session.user.eid
-        self.assertEqual(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
-                                     'B bookmarked_by U, U eid %s' % anoneid).rows,
-                          [['index', '?vid=index']])
-        self.assertEqual(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
-                                     'B bookmarked_by U, U eid %(x)s', {'x': anoneid}).rows,
-                          [['index', '?vid=index']])
-        # can read others bookmarks as well
-        self.assertEqual(cu.execute('Any B where B is Bookmark, NOT B bookmarked_by U').rows,
-                          [[beid1]])
-        self.assertRaises(Unauthorized, cu.execute,'DELETE B bookmarked_by U')
-        self.assertRaises(Unauthorized,
-                          cu.execute, 'SET B bookmarked_by U WHERE U eid %(x)s, B eid %(b)s',
-                          {'x': anoneid, 'b': beid1})
-        cnx.close()
-
+        with self.login('anon') as cu:
+            anoneid = self.session.user.eid
+            self.assertEqual(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
+                                         'B bookmarked_by U, U eid %s' % anoneid).rows,
+                              [['index', '?vid=index']])
+            self.assertEqual(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
+                                         'B bookmarked_by U, U eid %(x)s', {'x': anoneid}).rows,
+                              [['index', '?vid=index']])
+            # can read others bookmarks as well
+            self.assertEqual(cu.execute('Any B where B is Bookmark, NOT B bookmarked_by U').rows,
+                              [[beid1]])
+            self.assertRaises(Unauthorized, cu.execute,'DELETE B bookmarked_by U')
+            self.assertRaises(Unauthorized,
+                              cu.execute, 'SET B bookmarked_by U WHERE U eid %(x)s, B eid %(b)s',
+                              {'x': anoneid, 'b': beid1})
+            self.rollback()
 
     def test_ambigous_ordered(self):
-        cnx = self.login('anon')
-        cu = cnx.cursor()
-        names = [t for t, in cu.execute('Any N ORDERBY lower(N) WHERE X name N')]
-        self.assertEqual(names, sorted(names, key=lambda x: x.lower()))
-        cnx.close()
+        with self.login('anon') as cu:
+            names = [t for t, in cu.execute('Any N ORDERBY lower(N) WHERE X name N')]
+            self.assertEqual(names, sorted(names, key=lambda x: x.lower()))
 
     def test_restrict_is_instance_ok(self):
-        from rql import RQLException
         rset = self.execute('Any X WHERE X is_instance_of BaseTransition')
         rqlst = rset.syntax_tree()
         select = rqlst.children[0]
@@ -596,33 +560,28 @@
         """
         eid = self.execute('INSERT Affaire X: X ref "ARCT01"')[0][0]
         self.commit()
-        cnx = self.login('iaminusersgrouponly')
-        session = self.session
-        # needed to avoid check_perm error
-        session.set_cnxset()
-        # needed to remove rql expr granting update perm to the user
-        affaire_perms = self.schema['Affaire'].permissions.copy()
-        self.schema['Affaire'].set_action_permissions('update', self.schema['Affaire'].get_groups('update'))
-        try:
-            self.assertRaises(Unauthorized,
-                              self.schema['Affaire'].check_perm, session, 'update', eid=eid)
-            cu = cnx.cursor()
-            self.schema['Affaire'].set_action_permissions('read', ('users',))
-            aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
-            aff.cw_adapt_to('IWorkflowable').fire_transition('abort')
-            cnx.commit()
-            # though changing a user state (even logged user) is reserved to managers
-            user = cnx.user(self.session)
-            # XXX wether it should raise Unauthorized or ValidationError is not clear
-            # the best would probably ValidationError if the transition doesn't exist
-            # from the current state but Unauthorized if it exists but user can't pass it
-            self.assertRaises(ValidationError,
-                              user.cw_adapt_to('IWorkflowable').fire_transition, 'deactivate')
-        finally:
-            # restore orig perms
-            for action, perms in affaire_perms.iteritems():
-                self.schema['Affaire'].set_action_permissions(action, perms)
-        cnx.close()
+        with self.login('iaminusersgrouponly') as cu:
+            session = self.session
+            # needed to avoid check_perm error
+            session.set_cnxset()
+            # needed to remove rql expr granting update perm to the user
+            affschema = self.schema['Affaire']
+            with self.temporary_permissions(Affaire={'update': affschema.get_groups('update'),
+                                                     'read': ('users',)}):
+                self.assertRaises(Unauthorized,
+                                  affschema.check_perm, session, 'update', eid=eid)
+                aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
+                aff.cw_adapt_to('IWorkflowable').fire_transition('abort')
+                self.commit()
+                # though changing a user state (even logged user) is reserved to managers
+                user = self.user(session)
+                session.set_cnxset()
+                # XXX wether it should raise Unauthorized or ValidationError is not clear
+                # the best would probably ValidationError if the transition doesn't exist
+                # from the current state but Unauthorized if it exists but user can't pass it
+                self.assertRaises(ValidationError,
+                                  user.cw_adapt_to('IWorkflowable').fire_transition, 'deactivate')
+                self.rollback() # else will fail on login cm exit
 
     def test_trinfo_security(self):
         aff = self.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0)
--- a/server/test/unittest_session.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_session.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -17,33 +17,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 from __future__ import with_statement
 
-from logilab.common.testlib import TestCase, unittest_main, mock_object
-
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.server.session import _make_description, hooks_control
-
-class Variable:
-    def __init__(self, name):
-        self.name = name
-        self.children = []
-
-    def get_type(self, solution, args=None):
-        return solution[self.name]
-    def as_string(self):
-        return self.name
-
-class Function:
-    def __init__(self, name, varname):
-        self.name = name
-        self.children = [Variable(varname)]
-    def get_type(self, solution, args=None):
-        return 'Int'
-
-class MakeDescriptionTC(TestCase):
-    def test_known_values(self):
-        solution = {'A': 'Int', 'B': 'CWUser'}
-        self.assertEqual(_make_description((Function('max', 'A'), Variable('B')), {}, solution),
-                          ['Int','CWUser'])
 
 
 class InternalSessionTC(CubicWebTC):
@@ -61,7 +35,7 @@
         self.assertEqual(session.disabled_hook_categories, set())
         self.assertEqual(session.enabled_hook_categories, set())
         self.assertEqual(len(session._tx_data), 1)
-        with hooks_control(session, session.HOOKS_DENY_ALL, 'metadata'):
+        with session.deny_all_hooks_but('metadata'):
             self.assertEqual(session.hooks_mode, session.HOOKS_DENY_ALL)
             self.assertEqual(session.disabled_hook_categories, set())
             self.assertEqual(session.enabled_hook_categories, set(('metadata',)))
@@ -73,7 +47,7 @@
             self.assertEqual(session.hooks_mode, session.HOOKS_DENY_ALL)
             self.assertEqual(session.disabled_hook_categories, set())
             self.assertEqual(session.enabled_hook_categories, set(('metadata',)))
-            with hooks_control(session, session.HOOKS_ALLOW_ALL, 'integrity'):
+            with session.allow_all_hooks_but('integrity'):
                 self.assertEqual(session.hooks_mode, session.HOOKS_ALLOW_ALL)
                 self.assertEqual(session.disabled_hook_categories, set(('integrity',)))
                 self.assertEqual(session.enabled_hook_categories, set(('metadata',))) # not changed in such case
@@ -88,27 +62,7 @@
         self.assertEqual(session.disabled_hook_categories, set())
         self.assertEqual(session.enabled_hook_categories, set())
 
-    def test_build_descr1(self):
-        rset = self.execute('(Any U,L WHERE U login L) UNION (Any G,N WHERE G name N, G is CWGroup)')
-        orig_length = len(rset)
-        rset.rows[0][0] = 9999999
-        description = self.session.build_description(rset.syntax_tree(), None, rset.rows)
-        self.assertEqual(len(description), orig_length - 1)
-        self.assertEqual(len(rset.rows), orig_length - 1)
-        self.assertFalse(rset.rows[0][0] == 9999999)
-
-    def test_build_descr2(self):
-        rset = self.execute('Any X,Y WITH X,Y BEING ((Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G))')
-        for x, y in rset.description:
-            if y is not None:
-                self.assertEqual(y, 'CWGroup')
-
-    def test_build_descr3(self):
-        rset = self.execute('(Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G)')
-        for x, y in rset.description:
-            if y is not None:
-                self.assertEqual(y, 'CWGroup')
-
 
 if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
     unittest_main()
--- a/server/test/unittest_storage.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_storage.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -99,7 +99,7 @@
         f1 = self.create_file()
         self.commit()
         self.assertEqual(file(expected_filepath).read(), 'the-data')
-        f1.set_attributes(data=Binary('the new data'))
+        f1.cw_set(data=Binary('the new data'))
         self.rollback()
         self.assertEqual(file(expected_filepath).read(), 'the-data')
         f1.cw_delete()
@@ -204,7 +204,7 @@
         # 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
+        # NOTE: do not use cw_set() 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})
@@ -218,7 +218,7 @@
         # 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.txt')
-        # NOTE: do not use set_attributes() which would automatically
+        # NOTE: do not use cw_set() which would automatically
         #       update f1's local dict. We want the pure rql version to work
         self.commit()
         old_path = self.fspath(f1)
@@ -240,7 +240,7 @@
         # 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.txt')
-        # NOTE: do not use set_attributes() which would automatically
+        # NOTE: do not use cw_set() which would automatically
         #       update f1's local dict. We want the pure rql version to work
         self.commit()
         old_path = self.fspath(f1)
@@ -265,7 +265,7 @@
         f = self.session.create_entity('Affaire', opt_attr=Binary('toto'))
         self.session.commit()
         self.session.set_cnxset()
-        f.set_attributes(opt_attr=None)
+        f.cw_set(opt_attr=None)
         self.session.commit()
 
     @tag('fs_importing', 'update')
--- a/server/test/unittest_undo.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_undo.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -22,14 +22,14 @@
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.transaction import *
 
-from cubicweb.server.sources.native import UndoException
+from cubicweb.server.sources.native import UndoTransactionException, _UndoException
 
 
 class UndoableTransactionTC(CubicWebTC):
 
     def setup_database(self):
         req = self.request()
-        self.session.undo_actions = set('CUDAR')
+        self.session.undo_actions = True
         self.toto = self.create_user(req, 'toto', password='toto', groups=('users',),
                                      commit=False)
         self.txuuid = self.commit()
@@ -48,6 +48,17 @@
             "SELECT * from tx_relation_actions WHERE tx_uuid='%s'" % txuuid)
         self.assertFalse(cu.fetchall())
 
+    def assertUndoTransaction(self, txuuid, expected_errors=None):
+        if expected_errors is None :
+            expected_errors = []
+        try:
+            self.cnx.undo_transaction(txuuid)
+        except UndoTransactionException, exn:
+            errors = exn.errors
+        else:
+            errors = []
+        self.assertEqual(errors, expected_errors)
+
     def test_undo_api(self):
         self.assertTrue(self.txuuid)
         # test transaction api
@@ -69,12 +80,14 @@
         self.assertEqual(a1.action, 'C')
         self.assertEqual(a1.eid, self.toto.eid)
         self.assertEqual(a1.etype,'CWUser')
+        self.assertEqual(a1.ertype, 'CWUser')
         self.assertEqual(a1.changes, None)
         self.assertEqual(a1.public, True)
         self.assertEqual(a1.order, 1)
         a4 = actions[3]
         self.assertEqual(a4.action, 'A')
         self.assertEqual(a4.rtype, 'in_group')
+        self.assertEqual(a4.ertype, 'in_group')
         self.assertEqual(a4.eid_from, self.toto.eid)
         self.assertEqual(a4.eid_to, self.toto.in_group[0].eid)
         self.assertEqual(a4.order, 4)
@@ -155,10 +168,9 @@
         self.assertEqual(len(actions), 1)
         toto.cw_clear_all_caches()
         e.cw_clear_all_caches()
-        errors = self.cnx.undo_transaction(txuuid)
+        self.assertUndoTransaction(txuuid)
         undotxuuid = self.commit()
         self.assertEqual(undotxuuid, None) # undo not undoable
-        self.assertEqual(errors, [])
         self.assertTrue(self.execute('Any X WHERE X eid %(x)s', {'x': toto.eid}))
         self.assertTrue(self.execute('Any X WHERE X eid %(x)s', {'x': e.eid}))
         self.assertTrue(self.execute('Any X WHERE X has_text "toto@logilab"'))
@@ -191,34 +203,32 @@
         c.cw_delete()
         txuuid = self.commit()
         c2 = session.create_entity('Card', title=u'hip', content=u'hip')
-        p.set_relations(fiche=c2)
+        p.cw_set(fiche=c2)
         self.commit()
-        errors = self.cnx.undo_transaction(txuuid)
+        self.assertUndoTransaction(txuuid, [
+            "Can't restore object relation fiche to entity "
+            "%s which is already linked using this relation." % p.eid])
         self.commit()
         p.cw_clear_all_caches()
         self.assertEqual(p.fiche[0].eid, c2.eid)
-        self.assertEqual(len(errors), 1)
-        self.assertEqual(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.toto.cw_set(in_group=g)
         self.commit()
         self.toto.cw_delete()
         txuuid = self.commit()
         g.cw_delete()
         self.commit()
-        errors = self.cnx.undo_transaction(txuuid)
-        self.assertEqual(errors,
-                          [u"Can't restore relation in_group, object entity "
-                          "%s doesn't exist anymore." % g.eid])
+        self.assertUndoTransaction(txuuid, [
+            u"Can't restore relation in_group, object entity "
+            "%s doesn't exist anymore." % g.eid])
         with self.assertRaises(ValidationError) as cm:
             self.commit()
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.entity, self.toto.eid)
         self.assertEqual(cm.exception.errors,
                           {'in_group-subject': u'at least one relation in_group is '
@@ -229,9 +239,8 @@
         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.assertUndoTransaction(txuuid)
         self.commit()
-        self.assertFalse(errors)
         self.assertFalse(self.execute('Any X WHERE X eid %(x)s', {'x': c.eid}))
         self.assertFalse(self.execute('Any X WHERE X eid %(x)s', {'x': p.eid}))
         self.assertFalse(self.execute('Any X,Y WHERE X fiche Y'))
@@ -257,7 +266,7 @@
         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)
+        tutu.cw_set(use_email=email, reverse_for_user=prop)
         self.commit()
         with self.assertRaises(ValidationError) as cm:
             self.cnx.undo_transaction(txuuid)
@@ -270,7 +279,7 @@
         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.toto.cw_set(in_group=g)
         self.commit()
         with self.assertRaises(ValidationError) as cm:
             self.cnx.undo_transaction(txuuid)
@@ -288,12 +297,135 @@
 
     # test implicit 'replacement' of an inlined relation
 
+    def test_undo_inline_rel_remove_ok(self):
+        """Undo remove relation  Personne (?) fiche (?) Card
+
+        NB: processed by `_undo_r` as expected"""
+        session = self.session
+        c = session.create_entity('Card', title=u'hop', content=u'hop')
+        p = session.create_entity('Personne', nom=u'louis', fiche=c)
+        self.commit()
+        p.cw_set(fiche=None)
+        txuuid = self.commit()
+        self.assertUndoTransaction(txuuid)
+        self.commit()
+        p.cw_clear_all_caches()
+        self.assertEqual(p.fiche[0].eid, c.eid)
+
+    def test_undo_inline_rel_remove_ko(self):
+        """Restore an inlined relation to a deleted entity, with an error.
+
+        NB: processed by `_undo_r` as expected"""
+        session = self.session
+        c = session.create_entity('Card', title=u'hop', content=u'hop')
+        p = session.create_entity('Personne', nom=u'louis', fiche=c)
+        self.commit()
+        p.cw_set(fiche=None)
+        txuuid = self.commit()
+        c.cw_delete()
+        self.commit()
+        self.assertUndoTransaction(txuuid, [
+            "Can't restore relation fiche, object entity %d doesn't exist anymore." % c.eid])
+        self.commit()
+        p.cw_clear_all_caches()
+        self.assertFalse(p.fiche)
+        self.assertIsNone(session.system_sql(
+            'SELECT cw_fiche FROM cw_Personne WHERE cw_eid=%s' % p.eid).fetchall()[0][0])
+
+    def test_undo_inline_rel_add_ok(self):
+        """Undo add relation  Personne (?) fiche (?) Card
+
+        Caution processed by `_undo_u`, not `_undo_a` !"""
+        session = self.session
+        c = session.create_entity('Card', title=u'hop', content=u'hop')
+        p = session.create_entity('Personne', nom=u'louis')
+        self.commit()
+        p.cw_set(fiche=c)
+        txuuid = self.commit()
+        self.assertUndoTransaction(txuuid)
+        self.commit()
+        p.cw_clear_all_caches()
+        self.assertFalse(p.fiche)
+
+    def test_undo_inline_rel_add_ko(self):
+        """Undo add relation  Personne (?) fiche (?) Card
+
+        Caution processed by `_undo_u`, not `_undo_a` !"""
+        session = self.session
+        c = session.create_entity('Card', title=u'hop', content=u'hop')
+        p = session.create_entity('Personne', nom=u'louis')
+        self.commit()
+        p.cw_set(fiche=c)
+        txuuid = self.commit()
+        c.cw_delete()
+        self.commit()
+        self.assertUndoTransaction(txuuid)
+
+    def test_undo_inline_rel_replace_ok(self):
+        """Undo changing relation  Personne (?) fiche (?) Card
+
+        Caution processed by `_undo_u` """
+        session = self.session
+        c1 = session.create_entity('Card', title=u'hop', content=u'hop')
+        c2 = session.create_entity('Card', title=u'hip', content=u'hip')
+        p = session.create_entity('Personne', nom=u'louis', fiche=c1)
+        self.commit()
+        p.cw_set(fiche=c2)
+        txuuid = self.commit()
+        self.assertUndoTransaction(txuuid)
+        self.commit()
+        p.cw_clear_all_caches()
+        self.assertEqual(p.fiche[0].eid, c1.eid)
+
+    def test_undo_inline_rel_replace_ko(self):
+        """Undo changing relation  Personne (?) fiche (?) Card, with an error
+
+        Caution processed by `_undo_u` """
+        session = self.session
+        c1 = session.create_entity('Card', title=u'hop', content=u'hop')
+        c2 = session.create_entity('Card', title=u'hip', content=u'hip')
+        p = session.create_entity('Personne', nom=u'louis', fiche=c1)
+        self.commit()
+        p.cw_set(fiche=c2)
+        txuuid = self.commit()
+        c1.cw_delete()
+        self.commit()
+        self.assertUndoTransaction(txuuid, [
+            "can't restore entity %s of type Personne, target of fiche (eid %s)"
+            " does not exist any longer" % (p.eid, c1.eid)])
+        self.commit()
+        p.cw_clear_all_caches()
+        self.assertFalse(p.fiche)
+
+    def test_undo_attr_update_ok(self):
+        session = self.session
+        p = session.create_entity('Personne', nom=u'toto')
+        session.commit()
+        self.session.set_cnxset()
+        p.cw_set(nom=u'titi')
+        txuuid = self.commit()
+        self.assertUndoTransaction(txuuid)
+        p.cw_clear_all_caches()
+        self.assertEqual(p.nom, u'toto')
+
+    def test_undo_attr_update_ko(self):
+        session = self.session
+        p = session.create_entity('Personne', nom=u'toto')
+        session.commit()
+        self.session.set_cnxset()
+        p.cw_set(nom=u'titi')
+        txuuid = self.commit()
+        p.cw_delete()
+        self.commit()
+        self.assertUndoTransaction(txuuid, [
+            u"can't restore state of entity %s, it has been deleted inbetween" % p.eid])
+
 
 class UndoExceptionInUnicode(CubicWebTC):
 
     # problem occurs in string manipulation for python < 2.6
     def test___unicode__method(self):
-        u = UndoException(u"voilà")
+        u = _UndoException(u"voilà")
         self.assertIsInstance(unicode(u), unicode)
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/unittest_utils.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,43 @@
+# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+"""
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.server import utils
+
+class UtilsTC(TestCase):
+    def test_crypt(self):
+        for hash in (
+            utils.crypt_password('xxx'), # default sha512
+            'ab$5UsKFxRKKN.d8iBIFBnQ80', # custom md5
+            'ab4Vlm81ZUHlg', # DES
+            ):
+            self.assertEqual(utils.crypt_password('xxx', hash), hash)
+            self.assertEqual(utils.crypt_password(u'xxx', hash), hash)
+            self.assertEqual(utils.crypt_password(u'xxx', unicode(hash)), hash)
+            self.assertEqual(utils.crypt_password('yyy', hash), '')
+
+        # accept any password for empty hashes (is it a good idea?)
+        self.assertEqual(utils.crypt_password('xxx', ''), '')
+        self.assertEqual(utils.crypt_password('yyy', ''), '')
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/server/utils.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/server/utils.py	Tue Oct 23 15:00:53 2012 +0200
@@ -20,35 +20,57 @@
 __docformat__ = "restructuredtext en"
 
 import sys
-import string
 import logging
 from threading import Timer, Thread
 from getpass import getpass
-from random import choice
-
-from cubicweb.server import SOURCE_TYPES
 
-try:
-    from crypt import crypt
-except ImportError:
-    # crypt is not available (eg windows)
-    from cubicweb.md5crypt import crypt
+from passlib.utils import handlers as uh, to_hash_str
+from passlib.context import CryptContext
+
+from cubicweb.md5crypt import crypt as md5crypt
 
 
-def getsalt(chars=string.letters + string.digits):
-    """generate a random 2-character 'salt'"""
-    return choice(chars) + choice(chars)
+class CustomMD5Crypt(uh.HasSalt, uh.GenericHandler):
+    name = 'cubicwebmd5crypt'
+    setting_kwds = ('salt',)
+    min_salt_size = 0
+    max_salt_size = 8
+    salt_chars = uh.H64_CHARS
 
+    @classmethod
+    def from_string(cls, hash):
+        salt, chk = uh.parse_mc2(hash, u'')
+        if chk is None:
+            raise ValueError('missing checksum')
+        return cls(salt=salt, checksum=chk)
+
+    def to_string(self):
+        return to_hash_str(u'%s$%s' % (self.salt, self.checksum or u''))
+
+    # passlib 1.5 wants calc_checksum, 1.6 wants _calc_checksum
+    def calc_checksum(self, secret):
+        return md5crypt(secret, self.salt.encode('ascii')).decode('utf-8')
+    _calc_checksum = calc_checksum
+
+_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1'],
+                           deprecated=['cubicwebmd5crypt', 'des_crypt'])
+verify_and_update = _CRYPTO_CTX.verify_and_update
 
 def crypt_password(passwd, salt=None):
     """return the encrypted password using the given salt or a generated one
     """
-    if passwd is None:
-        return None
     if salt is None:
-        salt = getsalt()
-    return crypt(passwd, salt)
-
+        return _CRYPTO_CTX.encrypt(passwd)
+    # empty hash, accept any password for backwards compat
+    if salt == '':
+        return salt
+    try:
+        if _CRYPTO_CTX.verify(passwd, salt):
+            return salt
+    except ValueError: # e.g. couldn't identify hash
+        pass
+    # wrong password
+    return ''
 
 def cartesian_product(seqin):
     """returns a generator which returns the cartesian product of `seqin`
@@ -122,12 +144,12 @@
 
 class LoopTask(object):
     """threaded task restarting itself once executed"""
-    def __init__(self, repo, interval, func, args):
+    def __init__(self, tasks_manager, interval, func, args):
         if interval <= 0:
             raise ValueError('Loop task interval must be > 0 '
                              '(current value: %f for %s)' % \
                              (interval, func_name(func)))
-        self.repo = repo
+        self._tasks_manager = tasks_manager
         self.interval = interval
         def auto_restart_func(self=self, func=func, args=args):
             restart = True
@@ -140,7 +162,7 @@
             except BaseException:
                 restart = False
             finally:
-                if restart and not self.repo.shutting_down:
+                if restart and tasks_manager.running:
                     self.start()
         self.func = auto_restart_func
         self.name = func_name(func)
@@ -186,3 +208,54 @@
 
     def getName(self):
         return '%s(%s)' % (self._name, Thread.getName(self))
+
+class TasksManager(object):
+    """Object dedicated manage background task"""
+
+    def __init__(self):
+        self.running = False
+        self._tasks = []
+        self._looping_tasks = []
+
+    def add_looping_task(self, interval, func, *args):
+        """register a function to be called every `interval` seconds.
+
+        looping tasks can only be registered during repository initialization,
+        once done this method will fail.
+        """
+        task = LoopTask(self, interval, func, args)
+        if self.running:
+            self._start_task(task)
+        else:
+            self._tasks.append(task)
+
+    def _start_task(self, task):
+        self._looping_tasks.append(task)
+        self.info('starting task %s with interval %.2fs', task.name,
+                  task.interval)
+        task.start()
+
+    def start(self):
+        """Start running looping task"""
+        assert self.running == False # bw compat purpose maintly
+        while self._tasks:
+            task = self._tasks.pop()
+            self._start_task(task)
+        self.running = True
+
+    def stop(self):
+        """Stop all running task.
+
+        returns when all task have been cancel and none are running anymore"""
+        if self.running:
+            while self._looping_tasks:
+                looptask = self._looping_tasks.pop()
+                self.info('canceling task %s...', looptask.name)
+                looptask.cancel()
+                looptask.join()
+                self.info('task %s finished', looptask.name)
+
+from logging import getLogger
+from cubicweb import set_log_methods
+set_log_methods(TasksManager, getLogger('cubicweb.repository'))
+
--- a/skeleton/__pkginfo__.py.tmpl	Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/__pkginfo__.py.tmpl	Tue Oct 23 15:00:53 2012 +0200
@@ -16,6 +16,12 @@
 __depends__ =  %(dependencies)s
 __recommends__ = {}
 
+classifiers = [
+    'Environment :: Web Environment',
+    'Framework :: CubicWeb',
+    'Programming Language :: Python',
+    'Programming Language :: JavaScript',
+    ]
 
 from os import listdir as _listdir
 from os.path import join, isdir
--- a/skeleton/debian/DISTNAME.prerm.tmpl	Wed Feb 22 11:57:42 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-#!/bin/sh -e
-
-delete_pyo_pyc () {
-  find /usr/share/cubicweb/cubes/%(cubename)s -name "*.pyc" | xargs rm -f
-  find /usr/share/cubicweb/cubes/%(cubename)s -name "*.pyo" | xargs rm -f
-}
-
-
-case "$1" in
-    failed-upgrade|abort-install|abort-upgrade|disappear)
-    ;;
-    upgrade)
-    delete_pyo_pyc
-    ;;
-    remove)
-    delete_pyo_pyc
-    ;;
-    purge)
-    ;;
-
-    *)
-        echo "postrm called with unknown argument \`$1'" >&2
-        exit 1
-
-esac
-
-#DEBHELPER#
--- a/skeleton/debian/compat	Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/debian/compat	Tue Oct 23 15:00:53 2012 +0200
@@ -1,1 +1,1 @@
-5
+7
--- a/skeleton/debian/control.tmpl	Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/debian/control.tmpl	Tue Oct 23 15:00:53 2012 +0200
@@ -2,13 +2,13 @@
 Section: web
 Priority: optional
 Maintainer: %(author)s <%(author-email)s>
-Build-Depends: debhelper (>= 5.0.37.1), python (>=2.4), python-support
-Standards-Version: 3.8.0
-
+Build-Depends: debhelper (>= 7), python (>=2.5), python-support
+Standards-Version: 3.9.3
+XS-Python-Version: >= 2.5
 
 Package: %(distname)s
 Architecture: all
-Depends: cubicweb-common (>= %(version)s)
+Depends: cubicweb-common (>= %(version)s), ${python:Depends}
 Description: %(shortdesc)s
  CubicWeb is a semantic web application framework.
  .
--- a/skeleton/debian/copyright.tmpl	Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/debian/copyright.tmpl	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-Upstream Author: 
+Upstream Author:
 
   %(author)s <%(author-email)s>
 
--- a/skeleton/debian/rules.tmpl	Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/debian/rules.tmpl	Tue Oct 23 15:00:53 2012 +0200
@@ -4,7 +4,10 @@
 
 # Uncomment this to turn on verbose mode.
 #export DH_VERBOSE=1
-build: build-stamp
+build: build-arch build-indep
+build-arch:
+	# Nothing to do
+build-indep: build-stamp
 build-stamp:
 	dh_testdir
 	NO_SETUPTOOLS=1 python setup.py -q build
--- a/skeleton/setup.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/setup.py	Tue Oct 23 15:00:53 2012 +0200
@@ -41,7 +41,7 @@
 
 # import required features
 from __pkginfo__ import modname, version, license, description, web, \
-     author, author_email
+     author, author_email, classifiers
 
 if exists('README'):
     long_description = file('README').read()
@@ -193,6 +193,7 @@
                  data_files = data_files,
                  ext_modules = ext_modules,
                  cmdclass = cmdclass,
+                 classifiers = classifiers,
                  **kwargs
                  )
 
--- a/sobjects/cwxmlparser.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/sobjects/cwxmlparser.py	Tue Oct 23 15:00:53 2012 +0200
@@ -183,11 +183,11 @@
 
     # import handling ##########################################################
 
-    def process(self, url, raise_on_error=False, partialcommit=True):
+    def process(self, url, raise_on_error=False):
         """IDataFeedParser main entry point"""
         if url.startswith('http'): # XXX similar loose test as in parse of sources.datafeed
             url = self.complete_url(url)
-        super(CWEntityXMLParser, self).process(url, raise_on_error, partialcommit)
+        super(CWEntityXMLParser, self).process(url, raise_on_error)
 
     def parse_etree(self, parent):
         for node in list(parent):
@@ -242,7 +242,7 @@
     def normalize_url(self, url):
         """overriden to add vid=xml"""
         url = super(CWEntityXMLParser, self).normalize_url(url)
-        if url.startswih('http'):
+        if url.startswith('http'):
             try:
                 url, qs = url.split('?', 1)
             except ValueError:
--- a/sobjects/ldapparser.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/sobjects/ldapparser.py	Tue Oct 23 15:00:53 2012 +0200
@@ -20,41 +20,84 @@
 unlike ldapuser source, this source is copy based and will import ldap content
 (beside passwords for authentication) into the system source.
 """
-from base64 import b64decode
+from __future__ import with_statement
 
 from logilab.common.decorators import cached
+from logilab.common.shellutils import generate_password
 
+from cubicweb import Binary, ConfigurationError
+from cubicweb.server.utils import crypt_password
 from cubicweb.server.sources import datafeed
 
-class DataFeedlDAPParser(datafeed.DataFeedParser):
+
+class DataFeedLDAPAdapter(datafeed.DataFeedParser):
     __regid__ = 'ldapfeed'
     # attributes that may appears in source user_attrs dict which are not
     # attributes of the cw user
     non_attribute_keys = set(('email',))
 
-    def process(self, url, raise_on_error=False, partialcommit=True):
+    def process(self, url, raise_on_error=False):
         """IDataFeedParser main entry point"""
         source = self.source
         searchstr = '(&%s)' % ''.join(source.base_filters)
-        try:
-            ldap_emailattr = source.user_rev_attrs['email']
-        except KeyError:
-            ldap_emailattr = None
+        self.warning('processing ldapfeed stuff %s %s', source, searchstr)
         for userdict in source._search(self._cw, source.user_base_dn,
                                        source.user_base_scope, searchstr):
+            self.warning('fetched user %s', userdict)
             entity = self.extid2entity(userdict['dn'], 'CWUser', **userdict)
-            if not self.created_during_pull(entity):
+            if entity is not None and not self.created_during_pull(entity):
                 self.notify_updated(entity)
                 attrs = self.ldap2cwattrs(userdict)
                 self.update_if_necessary(entity, attrs)
                 self._process_email(entity, userdict)
 
+
+    def handle_deletion(self, config, session, myuris):
+        if config['delete-entities']:
+            super(DataFeedLDAPAdapter, self).handle_deletion(config, session, myuris)
+            return
+        if myuris:
+            byetype = {}
+            for extid, (eid, etype) in myuris.iteritems():
+                if self.is_deleted(extid, etype, eid):
+                    byetype.setdefault(etype, []).append(str(eid))
+
+            for etype, eids in byetype.iteritems():
+                if etype != 'CWUser':
+                    continue
+                self.warning('deactivate %s %s entities', len(eids), etype)
+                for eid in eids:
+                    wf = session.entity_from_eid(eid).cw_adapt_to('IWorkflowable')
+                    wf.fire_transition_if_possible('deactivate')
+        session.commit(free_cnxset=False)
+
+    def update_if_necessary(self, entity, attrs):
+        # disable read security to allow password selection
+        with entity._cw.security_enabled(read=False):
+            entity.complete(tuple(attrs))
+        if entity.__regid__ == 'CWUser':
+            wf = entity.cw_adapt_to('IWorkflowable')
+            if wf.state == 'deactivated':
+                self.warning('update on deactivated user %s', entity.login)
+        mdate = attrs.get('modification_date')
+        if not mdate or mdate > entity.modification_date:
+            attrs = dict( (k, v) for k, v in attrs.iteritems()
+                          if v != getattr(entity, k))
+            if attrs:
+                entity.cw_set(**attrs)
+                self.notify_updated(entity)
+
     def ldap2cwattrs(self, sdict, tdict=None):
         if tdict is None:
             tdict = {}
         for sattr, tattr in self.source.user_attrs.iteritems():
             if tattr not in self.non_attribute_keys:
-                tdict[tattr] = sdict[sattr]
+                try:
+                    tdict[tattr] = sdict[sattr]
+                except KeyError:
+                    raise ConfigurationError('source attribute %s is not present '
+                                             'in the source, please check the '
+                                             'user-attrs-map field' % sattr)
         return tdict
 
     def before_entity_copy(self, entity, sourceparams):
@@ -62,14 +105,20 @@
             entity.cw_edited['address'] = sourceparams['address']
         else:
             self.ldap2cwattrs(sourceparams, entity.cw_edited)
+            pwd = entity.cw_edited.get('upassword')
+            if not pwd:
+                # generate a dumb password if not fetched from ldap (see
+                # userPassword)
+                pwd = crypt_password(generate_password())
+                entity.cw_edited['upassword'] = Binary(pwd)
         return entity
 
     def after_entity_copy(self, entity, sourceparams):
-        super(DataFeedlDAPParser, self).after_entity_copy(entity, sourceparams)
+        super(DataFeedLDAPAdapter, self).after_entity_copy(entity, sourceparams)
         if entity.__regid__ == 'EmailAddress':
             return
         groups = [self._get_group(n) for n in self.source.user_default_groups]
-        entity.set_relations(in_group=groups)
+        entity.cw_set(in_group=groups)
         self._process_email(entity, sourceparams)
 
     def is_deleted(self, extid, etype, eid):
@@ -77,7 +126,7 @@
             extid, _ = extid.rsplit('@@', 1)
         except ValueError:
             pass
-        return self.source.object_exists_in_ldap(extid)
+        return not self.source.object_exists_in_ldap(extid)
 
     def _process_email(self, entity, userdict):
         try:
@@ -96,9 +145,13 @@
                 email = self.extid2entity(emailextid, 'EmailAddress',
                                           address=emailaddr)
                 if entity.primary_email:
-                    entity.set_relations(use_email=email)
+                    entity.cw_set(use_email=email)
                 else:
-                    entity.set_relations(primary_email=email)
+                    entity.cw_set(primary_email=email)
+            elif self.sourceuris:
+                # pop from sourceuris anyway, else email may be removed by the
+                # source once import is finished
+                self.sourceuris.pop(str(userdict['dn'] + '@@' + emailaddr), None)
             # XXX else check use_email relation?
 
     @cached
--- a/sobjects/test/unittest_cwxmlparser.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/sobjects/test/unittest_cwxmlparser.py	Tue Oct 23 15:00:53 2012 +0200
@@ -197,7 +197,7 @@
                           })
         session = self.repo.internal_session(safe=True)
         stats = dfsource.pull_data(session, force=True, raise_on_error=True)
-        self.assertEqual(sorted(stats.keys()), ['created', 'updated'])
+        self.assertEqual(sorted(stats.keys()), ['checked', 'created', 'updated'])
         self.assertEqual(len(stats['created']), 2)
         self.assertEqual(stats['updated'], set())
 
@@ -233,14 +233,16 @@
         with session.security_enabled(read=False): # avoid Unauthorized due to password selection
             stats = dfsource.pull_data(session, force=True, raise_on_error=True)
         self.assertEqual(stats['created'], set())
-        self.assertEqual(len(stats['updated']), 2)
+        self.assertEqual(len(stats['updated']), 0)
+        self.assertEqual(len(stats['checked']), 2)
         self.repo._type_source_cache.clear()
         self.repo._extid_cache.clear()
         session.set_cnxset()
         with session.security_enabled(read=False): # avoid Unauthorized due to password selection
             stats = dfsource.pull_data(session, force=True, raise_on_error=True)
         self.assertEqual(stats['created'], set())
-        self.assertEqual(len(stats['updated']), 2)
+        self.assertEqual(len(stats['updated']), 0)
+        self.assertEqual(len(stats['checked']), 2)
         session.commit()
 
         # test move to system source
--- a/test/data/views.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/test/data/views.py	Tue Oct 23 15:00:53 2012 +0200
@@ -17,3 +17,17 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 from cubicweb.web.views import xmlrss
 xmlrss.RSSIconBox.visible = True
+
+
+from cubicweb.predicates import match_user_groups
+from cubicweb.server import Service
+
+
+class TestService(Service):
+    __regid__ = 'test_service'
+    __select__ = Service.__select__ & match_user_groups('managers')
+    passed_here = []
+
+    def call(self, msg):
+        self.passed_here.append(msg)
+        return 'babar'
--- a/test/unittest_cwconfig.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_cwconfig.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -101,10 +101,10 @@
         self.assertEqual(self.config.expand_cubes(('email', 'comment')),
                           ['email', 'comment', 'file'])
 
-    def test_vregistry_path(self):
+    def test_appobjects_path(self):
         self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR]
         self.config.adjust_sys_path()
-        self.assertEqual([unabsolutize(p) for p in self.config.vregistry_path()],
+        self.assertEqual([unabsolutize(p) for p in self.config.appobjects_path()],
                           ['entities', 'web/views', 'sobjects', 'hooks',
                            'file/entities', 'file/views.py', 'file/hooks',
                            'email/entities.py', 'email/views', 'email/hooks.py',
--- a/test/unittest_dbapi.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_dbapi.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -23,10 +23,11 @@
 
 from logilab.common import tempattr
 
-from cubicweb import ConnectionError, cwconfig
+from cubicweb import ConnectionError, cwconfig, NoSelectableObject
 from cubicweb.dbapi import ProgrammingError
 from cubicweb.devtools.testlib import CubicWebTC
 
+
 class DBAPITC(CubicWebTC):
 
     def test_public_repo_api(self):
@@ -82,6 +83,20 @@
             req.ajax_replace_url('domid') # don't crash
             req.user.cw_adapt_to('IBreadCrumbs') # don't crash
 
+    def test_call_service(self):
+        ServiceClass = self.vreg['services']['test_service'][0]
+        for _cw in (self.request(), self.session):
+            ret_value = _cw.call_service('test_service', msg='coucou')
+            self.assertEqual('coucou', ServiceClass.passed_here.pop())
+            self.assertEqual('babar', ret_value)
+        with self.login('anon') as ctm:
+            for _cw in (self.request(), self.session):
+                with self.assertRaises(NoSelectableObject):
+                    _cw.call_service('test_service', msg='toto')
+                self.rollback()
+                self.assertEqual([], ServiceClass.passed_here)
+
+
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- a/test/unittest_entity.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_entity.py	Tue Oct 23 15:00:53 2012 +0200
@@ -18,6 +18,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unit tests for cubicweb.web.views.entities module"""
 
+from __future__ import with_statement
+
 from datetime import datetime
 
 from logilab.common import tempattr
@@ -28,7 +30,7 @@
 from cubicweb.mttransforms import HAS_TAL
 from cubicweb.entities import fetch_config
 from cubicweb.uilib import soup2xhtml
-from cubicweb.schema import RQLVocabularyConstraint
+from cubicweb.schema import RQLVocabularyConstraint, RRQLExpression
 
 class EntityTC(CubicWebTC):
 
@@ -361,6 +363,18 @@
             'NOT (S connait AD, AD nom "toto"), AD is Personne, '
             'EXISTS(S travaille AE, AE nom "tutu")')
 
+    def test_unrelated_rql_security_rel_perms(self):
+        '''check `connait` add permission has no effect for a new entity on the
+        unrelated rql'''
+        rdef = self.schema['Personne'].rdef('connait')
+        perm_rrqle = RRQLExpression('U has_update_permission S')
+        with self.temporary_permissions((rdef, {'add': (perm_rrqle,)})):
+            person = self.vreg['etypes'].etype_class('Personne')(self.request())
+            rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
+        self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
+                         'O is Personne, O nom AA, O prenom AB, '
+                         'O modification_date AC')
+
     def test_unrelated_rql_constraints_edition_subject(self):
         person = self.request().create_entity('Personne', nom=u'sylvain')
         rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
@@ -673,37 +687,37 @@
         # ambiguity test
         person2 = req.create_entity('Personne', prenom=u'remi', nom=u'doe')
         person.cw_clear_all_caches()
-        self.assertEqual(person.rest_path(), 'personne/eid/%s' % person.eid)
-        self.assertEqual(person2.rest_path(), 'personne/eid/%s' % person2.eid)
+        self.assertEqual(person.rest_path(), unicode(person.eid))
+        self.assertEqual(person2.rest_path(), unicode(person2.eid))
         # unique attr with None value (wikiid in this case)
         card1 = req.create_entity('Card', title=u'hop')
-        self.assertEqual(card1.rest_path(), 'card/eid/%s' % card1.eid)
+        self.assertEqual(card1.rest_path(), unicode(card1.eid))
         # don't use rest if we have /, ? or & in the path (breaks mod_proxy)
         card2 = req.create_entity('Card', title=u'pod', wikiid=u'zo/bi')
-        self.assertEqual(card2.rest_path(), 'card/eid/%d' % card2.eid)
+        self.assertEqual(card2.rest_path(), unicode(card2.eid))
         card3 = req.create_entity('Card', title=u'pod', wikiid=u'zo&bi')
-        self.assertEqual(card3.rest_path(), 'card/eid/%d' % card3.eid)
+        self.assertEqual(card3.rest_path(), unicode(card3.eid))
         card4 = req.create_entity('Card', title=u'pod', wikiid=u'zo?bi')
-        self.assertEqual(card4.rest_path(), 'card/eid/%d' % card4.eid)
+        self.assertEqual(card4.rest_path(), unicode(card4.eid))
 
 
-    def test_set_attributes(self):
+    def test_cw_set_attributes(self):
         req = self.request()
         person = req.create_entity('Personne', nom=u'di mascio', prenom=u'adrien')
         self.assertEqual(person.prenom, u'adrien')
         self.assertEqual(person.nom, u'di mascio')
-        person.set_attributes(prenom=u'sylvain', nom=u'thénault')
+        person.cw_set(prenom=u'sylvain', nom=u'thénault')
         person = self.execute('Personne P').get_entity(0, 0) # XXX retreival needed ?
         self.assertEqual(person.prenom, u'sylvain')
         self.assertEqual(person.nom, u'thénault')
 
-    def test_set_relations(self):
+    def test_cw_set_relations(self):
         req = self.request()
         person = req.create_entity('Personne', nom=u'chauvat', prenom=u'nicolas')
         note = req.create_entity('Note', type=u'x')
-        note.set_relations(ecrit_par=person)
+        note.cw_set(ecrit_par=person)
         note = req.create_entity('Note', type=u'y')
-        note.set_relations(ecrit_par=person.eid)
+        note.cw_set(ecrit_par=person.eid)
         self.assertEqual(len(person.reverse_ecrit_par), 2)
 
     def test_metainformation_and_external_absolute_url(self):
@@ -723,7 +737,7 @@
         req = self.request()
         card = req.create_entity('Card', wikiid=u'', title=u'test')
         self.assertEqual(card.absolute_url(),
-                          'http://testing.fr/cubicweb/card/eid/%s' % card.eid)
+                          'http://testing.fr/cubicweb/%s' % card.eid)
 
     def test_create_entity(self):
         req = self.request()
--- a/test/unittest_migration.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_migration.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -108,7 +108,13 @@
         self.assertEqual(source['db-driver'], 'sqlite')
         handler = get_test_db_handler(config)
         handler.init_test_database()
-
+        handler.build_db_cache()
+        repo, cnx = handler.get_repo_and_cnx()
+        cu = cnx.cursor()
+        self.assertEqual(cu.execute('Any SN WHERE X is CWUser, X login "admin", X in_state S, S name SN').rows,
+                          [['activated']])
+        cnx.close()
+        repo.shutdown()
 
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_req.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_req.py	Tue Oct 23 15:00:53 2012 +0200
@@ -36,7 +36,7 @@
         req = RequestSessionBase(None)
         req.from_controller = lambda : 'view'
         req.relative_path = lambda includeparams=True: None
-        req.base_url = lambda : 'http://testing.fr/cubicweb/'
+        req.base_url = lambda secure=None: 'http://testing.fr/cubicweb/'
         self.assertEqual(req.build_url(), u'http://testing.fr/cubicweb/view')
         self.assertEqual(req.build_url(None), u'http://testing.fr/cubicweb/view')
         self.assertEqual(req.build_url('one'), u'http://testing.fr/cubicweb/one')
--- a/test/unittest_rqlrewrite.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_rqlrewrite.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -183,9 +183,9 @@
         self.assertEqual(rqlst.as_string(),
                          "Any A,C,T WHERE A documented_by C?, A is Affaire "
                          "WITH C,T BEING (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, "
-                         "EXISTS(C in_state E, E name 'public'))")
+                         "(EXISTS(C in_state B, D in_group F, G require_state B, G name 'read', G require_group F)) "
+                         "OR (EXISTS(C in_state E, E name 'public')), "
+                         "D eid %(A)s, C is Card)")
 
     def test_optional_var_4(self):
         constraint1 = 'A created_by U, X documented_by A'
@@ -199,8 +199,8 @@
                              u'Any X,LA,Y WHERE LA? documented_by X, LA concerne Y, B eid %(C)s, '
                              'EXISTS(X created_by B), EXISTS(Y created_by B), '
                              'X is Card, Y is IN(Division, Note, Societe) '
-                             'WITH LA BEING (Any LA WHERE EXISTS(A created_by B, LA documented_by A), '
-                             'B eid %(D)s, LA is Affaire, EXISTS(E created_by B, LA concerne E))')
+                             'WITH LA BEING (Any LA WHERE (EXISTS(A created_by B, LA documented_by A)) OR (EXISTS(E created_by B, LA concerne E)), '
+                             'B eid %(D)s, LA is Affaire)')
 
     def test_optional_var_inlined(self):
         c1 = ('X require_permission P')
@@ -431,6 +431,33 @@
         self.assertEqual(rqlst.as_string(),
                          u'Any A WHERE NOT EXISTS(A documented_by C, EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire')
 
+    def test_rqlexpr_multiexpr_outerjoin(self):
+        c1 = RRQLExpression('X owned_by Z, Z login "hop"', 'X')
+        c2 = RRQLExpression('X owned_by Z, Z login "hip"', 'X')
+        c3 = RRQLExpression('X owned_by Z, Z login "momo"', 'X')
+        rqlst = rqlhelper.parse('Any A WHERE A documented_by C?', annotate=False)
+        rewrite(rqlst, {('C', 'X'): (c1, c2, c3)}, {}, 'X')
+        self.assertEqual(rqlst.as_string(),
+                         u'Any A WHERE A documented_by C?, A is Affaire '
+                         'WITH C BEING (Any C WHERE ((EXISTS(C owned_by B, B login "hop")) '
+                         'OR (EXISTS(C owned_by D, D login "momo"))) '
+                         'OR (EXISTS(C owned_by A, A login "hip")), C is Card)')
+
+    def test_multiple_erql_one_bad(self):
+        #: reproduce bug #2236985
+        #: (rqlrewrite fails to remove rewritten entry for unsupported constraint and then crash)
+        #:
+        #: This check a very rare code path triggered by the four condition below
+
+        # 1. c_ok introduce an ambiguity
+        c_ok = ERQLExpression('X concerne R')
+        # 2. c_bad is just plain wrong and won't be kept
+        # 3. but it declare a new variable
+        # 4. this variable require a rewrite
+        c_bad = ERQLExpression('X documented_by R, A in_state R')
+
+        rqlst = parse('Any A, R WHERE A ref R, S is Affaire')
+        rewrite(rqlst, {('A', 'X'): (c_ok, c_bad)}, {})
 
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_schema.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_schema.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -348,6 +348,10 @@
         self.assertEqual(cstr.repo_check(self.session, 1, self.session.user.eid),
         None) # no validation error, constraint checked
 
+class WorkflowShemaTC(CubicWebTC):
+    def test_trinfo_default_format(self):
+        tr = self.request().user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
+        self.assertEqual(tr.comment_format, 'text/plain')
 
 if __name__ == '__main__':
     unittest_main()
--- a/transaction.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/transaction.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -21,7 +21,6 @@
 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.
-
 """
 __docformat__ = "restructuredtext en"
 _ = unicode
@@ -39,8 +38,12 @@
 
 
 class NoSuchTransaction(RepositoryError):
-    pass
+    # Used by CubicWebException
+    msg = _("there is no transaction #%s")
 
+    def __init__(self, txuuid):
+        super(RepositoryError, self).__init__(txuuid)
+        self.txuuid = txuuid
 
 class Transaction(object):
     """an undoable transaction"""
@@ -82,6 +85,11 @@
     def label(self):
         return ACTION_LABELS[self.action]
 
+    @property
+    def ertype(self):
+        """ Return the entity or relation type this action is related to"""
+        raise NotImplementedError(self)
+
 
 class EntityAction(AbstractAction):
     def __init__(self, action, public, order, etype, eid, changes):
@@ -95,6 +103,11 @@
             self.label, self.eid, self.changes,
             self.public and 'dbapi' or 'hook')
 
+    @property
+    def ertype(self):
+        """ Return the entity or relation type this action is related to"""
+        return self.etype
+
 
 class RelationAction(AbstractAction):
     def __init__(self, action, public, order, rtype, eidfrom, eidto):
@@ -107,3 +120,8 @@
         return '<%s: %s %s %s (%s)>' % (
             self.label, self.eid_from, self.rtype, self.eid_to,
             self.public and 'dbapi' or 'hook')
+
+    @property
+    def ertype(self):
+        """ Return the entity or relation type this action is related to"""
+        return self.rtype
--- a/uilib.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/uilib.py	Tue Oct 23 15:00:53 2012 +0200
@@ -108,7 +108,7 @@
     elif value.days > 2 or value.days < -2:
         return req._('%d days') % int(value.days)
     else:
-        minus = 1 if value.days > 0 else -1
+        minus = 1 if value.days >= 0 else -1
         if value.seconds > 3600:
             return req._('%d hours') % (int(value.seconds // 3600) * minus)
         elif value.seconds >= 120:
--- a/view.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/view.py	Tue Oct 23 15:00:53 2012 +0200
@@ -90,19 +90,30 @@
 # base view object ############################################################
 
 class View(AppObject):
-    """abstract view class, used as base for every renderable object such
-    as views, templates, some components...web
+    """This class is an abstraction of a view class, used as a base class for
+    every renderable object such as views, templates and other user interface
+    components.
 
-    A view is instantiated to render a [part of a] result set. View
-    subclasses may be parametred using the following class attributes:
+    A `View` is instantiated to render a result set or part of a result
+    set. `View` subclasses may be parametrized using the following class
+    attributes:
 
-    * `templatable` indicates if the view may be embeded in a main
-      template or if it has to be rendered standalone (i.e. XML for
-      instance)
-    * if the view is not templatable, it should set the `content_type` class
-      attribute to the correct MIME type (text/xhtml by default)
-    * the `category` attribute may be used in the interface to regroup related
-      objects together
+    :py:attr:`templatable` indicates if the view may be embedded in a main
+      template or if it has to be rendered standalone (i.e. pure XML views must
+      not be embedded in the main template of HTML pages)
+    :py:attr:`content_type` if the view is not templatable, it should set the
+      `content_type` class attribute to the correct MIME type (text/xhtml being
+      the default)
+    :py:attr:`category` this attribute may be used in the interface to regroup
+      related objects (view kinds) together
+
+    :py:attr:`paginable`
+
+    :py:attr:`binary`
+
+
+    A view writes to its output stream thanks to its attribute `w` (the
+    append method of an `UStreamIO`, except for binary views).
 
     At instantiation time, the standard `_cw`, and `cw_rset` attributes are
     added and the `w` attribute will be set at rendering time to a write
--- a/web/_exceptions.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/_exceptions.py	Tue Oct 23 15:00:53 2012 +0200
@@ -20,59 +20,90 @@
 
 __docformat__ = "restructuredtext en"
 
+import httplib
+
 from cubicweb._exceptions import *
 from cubicweb.utils import json_dumps
 
+
+class DirectResponse(Exception):
+    """Used to supply a twitted HTTP Response directly"""
+    def __init__(self, response):
+        self.response = response
+
+class InvalidSession(CubicWebException):
+    """raised when a session id is found but associated session is not found or
+    invalid"""
+
+# Publish related exception
+
 class PublishException(CubicWebException):
     """base class for publishing related exception"""
 
+    def __init__(self, *args, **kwargs):
+        self.status = kwargs.pop('status', httplib.OK)
+        super(PublishException, self).__init__(*args, **kwargs)
+
+class LogOut(PublishException):
+    """raised to ask for deauthentication of a logged in user"""
+    def __init__(self, url=None):
+        super(LogOut, self).__init__()
+        self.url = url
+
+class Redirect(PublishException):
+    """raised to redirect the http request"""
+    def __init__(self, location, status=httplib.SEE_OTHER):
+        super(Redirect, self).__init__(status=status)
+        self.location = location
+
+class StatusResponse(PublishException):
+
+    def __init__(self, status, content=''):
+        super(StatusResponse, self).__init__(status=status)
+        self.content = content
+
+    def __repr__(self):
+        return '%s(%r, %r)' % (self.__class__.__name__, self.status, self.content)
+        self.url = url
+
+# Publish related error
+
 class RequestError(PublishException):
     """raised when a request can't be served because of a bad input"""
 
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.BAD_REQUEST)
+        super(RequestError, self).__init__(*args, **kwargs)
+
+
 class NothingToEdit(RequestError):
     """raised when an edit request doesn't specify any eid to edit"""
 
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.BAD_REQUEST)
+        super(NothingToEdit, self).__init__(*args, **kwargs)
+
 class ProcessFormError(RequestError):
     """raised when posted data can't be processed by the corresponding field
     """
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.BAD_REQUEST)
+        super(ProcessFormError, self).__init__(*args, **kwargs)
 
 class NotFound(RequestError):
-    """raised when a 404 error should be returned"""
-
-class Redirect(PublishException):
-    """raised to redirect the http request"""
-    def __init__(self, location):
-        self.location = location
-
-class DirectResponse(Exception):
-    def __init__(self, response):
-        self.response = response
+    """raised when something was not found. In most case,
+       a 404 error should be returned"""
 
-class StatusResponse(Exception):
-    def __init__(self, status, content=''):
-        self.status = int(status)
-        self.content = content
-
-    def __repr__(self):
-        return '%s(%r, %r)' % (self.__class__.__name__, self.status, self.content)
-
-class InvalidSession(CubicWebException):
-    """raised when a session id is found but associated session is not found or
-    invalid
-    """
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.NOT_FOUND)
+        super(NotFound, self).__init__(*args, **kwargs)
 
 class RemoteCallFailed(RequestError):
     """raised when a json remote call fails
     """
-    def __init__(self, reason=''):
-        super(RemoteCallFailed, self).__init__()
+    def __init__(self, reason='', status=httplib.INTERNAL_SERVER_ERROR):
+        super(RemoteCallFailed, self).__init__(status=status)
         self.reason = reason
 
     def dumps(self):
         return json_dumps({'reason': self.reason})
-
-class LogOut(PublishException):
-    """raised to ask for deauthentication of a logged in user"""
-    def __init__(self, url):
-        super(LogOut, self).__init__()
-        self.url = url
--- a/web/application.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/application.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -24,6 +24,9 @@
 import sys
 from time import clock, time
 from contextlib import contextmanager
+from warnings import warn
+
+import httplib
 
 from logilab.common.deprecation import deprecated
 
@@ -39,6 +42,8 @@
     StatusResponse, DirectResponse, Redirect, NotFound, LogOut,
     RemoteCallFailed, InvalidSession, RequestError)
 
+from cubicweb.web.request import CubicWebRequestBase
+
 # make session manager available through a global variable so the debug view can
 # print information about web session
 SESSION_MANAGER = None
@@ -288,11 +293,11 @@
         if config['query-log-file']:
             from threading import Lock
             self._query_log = open(config['query-log-file'], 'a')
-            self.publish = self.log_publish
+            self.handle_request = self.log_handle_request
             self._logfile_lock = Lock()
         else:
             self._query_log = None
-            self.publish = self.main_publish
+            self.handle_request = self.main_handle_request
         # instantiate session and url resolving helpers
         self.session_handler = session_handler_fact(self)
         self.set_urlresolver()
@@ -311,12 +316,12 @@
 
     # publish methods #########################################################
 
-    def log_publish(self, path, req):
+    def log_handle_request(self, req, path):
         """wrapper around _publish to log all queries executed for a given
         accessed path
         """
         try:
-            return self.main_publish(path, req)
+            return self.main_handle_request(req, path)
         finally:
             cnx = req.cnx
             if cnx:
@@ -332,7 +337,79 @@
                     except Exception:
                         self.exception('error while logging queries')
 
-    def main_publish(self, path, req):
+
+
+    def main_handle_request(self, req, path):
+        if not isinstance(req, CubicWebRequestBase):
+            warn('[3.15] Application entry poin arguments are now (req, path) '
+                 'not (path, req)', DeprecationWarning, 2)
+            req, path = path, req
+        if req.authmode == 'http':
+            # activate realm-based auth
+            realm = self.vreg.config['realm']
+            req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
+        content = ''
+        try:
+            self.connect(req)
+            # DENY https acces for anonymous_user
+            if (req.https
+                and req.session.anonymous_session
+                and self.vreg.config['https-deny-anonymous']):
+                # don't allow anonymous on https connection
+                raise AuthenticationError()
+            # nested try to allow LogOut to delegate logic to AuthenticationError
+            # handler
+            try:
+                ### Try to generate the actual request content
+                content = self.core_handle(req, path)
+            # Handle user log-out
+            except LogOut, ex:
+                # When authentification is handled by cookie the code that
+                # raised LogOut must has invalidated the cookie. We can just
+                # reload the original url without authentification
+                if self.vreg.config['auth-mode'] == 'cookie' and ex.url:
+                    req.headers_out.setHeader('location', str(ex.url))
+                if ex.status is not None:
+                    req.status_out = httplib.SEE_OTHER
+                # When the authentification is handled by http we must
+                # explicitly ask for authentification to flush current http
+                # authentification information
+                else:
+                    # Render "logged out" content.
+                    # assignement to ``content`` prevent standard
+                    # AuthenticationError code to overwrite it.
+                    content = self.loggedout_content(req)
+                    # let the explicitly reset http credential
+                    raise AuthenticationError()
+        except Redirect, ex:
+            # authentication needs redirection (eg openid)
+            content = self.redirect_handler(req, ex)
+        # Wrong, absent or Reseted credential
+        except AuthenticationError:
+            # If there is an https url configured and
+            # the request do not used https, redirect to login form
+            https_url = self.vreg.config['https-url']
+            if https_url and req.base_url() != https_url:
+                req.status_out = httplib.SEE_OTHER
+                req.headers_out.setHeader('location', https_url + 'login')
+            else:
+                # We assume here that in http auth mode the user *May* provide
+                # Authentification Credential if asked kindly.
+                if self.vreg.config['auth-mode'] == 'http':
+                    req.status_out = httplib.UNAUTHORIZED
+                # In the other case (coky auth) we assume that there is no way
+                # for the user to provide them...
+                # XXX But WHY ?
+                else:
+                    req.status_out = httplib.FORBIDDEN
+                # If previous error handling already generated a custom content
+                # do not overwrite it. This is used by LogOut Except
+                # XXX ensure we don't actually serve content
+                if not content:
+                    content = self.need_login_content(req)
+        return content
+
+    def core_handle(self, req, path):
         """method called by the main publisher to process <path>
 
         should return a string containing the resulting page or raise a
@@ -347,89 +424,93 @@
         :rtype: str
         :return: the result of the pusblished url
         """
-        path = path or 'view'
         # don't log form values they may contains sensitive information
-        self.info('publish "%s" (%s, form params: %s)',
-                  path, req.session.sessionid, req.form.keys())
+        self.debug('publish "%s" (%s, form params: %s)',
+                   path, req.session.sessionid, req.form.keys())
         # remove user callbacks on a new request (except for json controllers
         # to avoid callbacks being unregistered before they could be called)
         tstart = clock()
         commited = False
         try:
+            ### standard processing of the request
             try:
                 ctrlid, rset = self.url_resolver.process(req, path)
                 try:
                     controller = self.vreg['controllers'].select(ctrlid, req,
                                                                  appli=self)
                 except NoSelectableObject:
-                    if ctrlid == 'login':
-                        raise Unauthorized(req._('log out first'))
                     raise Unauthorized(req._('not authorized'))
                 req.update_search_state()
                 result = controller.publish(rset=rset)
-                if req.cnx:
-                    # no req.cnx if anonymous aren't allowed and we are
-                    # displaying some anonymous enabled view such as the cookie
-                    # authentication form
-                    req.cnx.commit()
-                    commited = True
-            except (StatusResponse, DirectResponse):
-                if req.cnx:
-                    req.cnx.commit()
-                raise
-            except (AuthenticationError, LogOut):
-                raise
-            except Redirect:
-                # redirect is raised by edit controller when everything went fine,
-                # so try to commit
-                try:
-                    if req.cnx:
-                        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:
-                    req.data['errmsg'] = req._('You\'re not authorized to access this page. '
-                                               'If you think you should, please contact the site administrator.')
-                    self.error_handler(req, ex, tb=False)
-                except Exception, ex:
-                    self.error_handler(req, ex, tb=True)
-                else:
-                    # delete validation errors which may have been previously set
-                    if '__errorurl' in req.form:
-                        req.session.data.pop(req.form['__errorurl'], None)
-                    raise
-            except RemoteCallFailed, ex:
-                req.set_header('content-type', 'application/json')
-                raise StatusResponse(500, ex.dumps())
-            except NotFound:
-                raise StatusResponse(404, self.notfound_content(req))
-            except ValidationError, ex:
-                self.validation_error_handler(req, ex)
-            except Unauthorized, ex:
-                self.error_handler(req, ex, tb=False, code=403)
-            except (BadRQLQuery, RequestError), ex:
-                self.error_handler(req, ex, tb=False)
-            except BaseException, ex:
-                self.error_handler(req, ex, tb=True)
-            except:
-                self.critical('Catch all triggered!!!')
-                self.exception('this is what happened')
-                result = 'oops'
+            except StatusResponse, ex:
+                warn('StatusResponse is deprecated use req.status_out',
+                     DeprecationWarning)
+                result = ex.content
+                req.status_out = ex.status
+            except Redirect, ex:
+                # Redirect may be raised by edit controller when everything went
+                # fine, so attempt to commit
+                result = self.redirect_handler(req, ex)
+            if req.cnx:
+                txuuid = req.cnx.commit()
+                commited = True
+                if txuuid is not None:
+                    req.data['last_undoable_transaction'] = txuuid
+        ### error case
+        except NotFound, ex:
+            result = self.notfound_content(req)
+            req.status_out = ex.status
+        except ValidationError, ex:
+            result = self.validation_error_handler(req, ex)
+        except RemoteCallFailed, ex:
+            result = self.ajax_error_handler(req, ex)
+        except Unauthorized, ex:
+            req.data['errmsg'] = req._('You\'re not authorized to access this page. '
+                                       'If you think you should, please contact the site administrator.')
+            req.status_out = httplib.UNAUTHORIZED
+            result = self.error_handler(req, ex, tb=False)
+        except (BadRQLQuery, RequestError), ex:
+            result = self.error_handler(req, ex, tb=False)
+        ### pass through exception
+        except DirectResponse:
+            if req.cnx:
+                req.cnx.commit()
+            raise
+        except (AuthenticationError, LogOut):
+            # the rollback is handled in the finally
+            raise
+        ### Last defense line
+        except BaseException, ex:
+            result = self.error_handler(req, ex, tb=True)
         finally:
             if req.cnx and not commited:
                 try:
                     req.cnx.rollback()
                 except Exception:
                     pass # ignore rollback error at this point
-        self.info('query %s executed in %s sec', req.relative_path(), clock() - tstart)
+            # request may be referenced by "onetime callback", so clear its entity
+            # cache to avoid memory usage
+            req.drop_entity_cache()
+        self.add_undo_link_to_msg(req)
+        self.debug('query %s executed in %s sec', req.relative_path(), clock() - tstart)
         return result
 
+    # Error handlers
+
+    def redirect_handler(self, req, ex):
+        """handle redirect
+        - comply to ex status
+        - set header field
+        - return empty content
+        """
+        self.debug('redirecting to %s', str(ex.location))
+        req.headers_out.setHeader('location', str(ex.location))
+        assert 300 <= ex.status < 400
+        req.status_out = ex.status
+        return ''
+
     def validation_error_handler(self, req, ex):
-        ex.errors = dict((k, v) for k, v in ex.errors.items())
+        ex.tr(req._) # translate messages using ui language
         if '__errorurl' in req.form:
             forminfo = {'error': ex,
                         'values': req.form,
@@ -440,18 +521,23 @@
             # session key is 'url + #<form dom id', though we usually don't want
             # the browser to move to the form since it hides the global
             # messages.
-            raise Redirect(req.form['__errorurl'].rsplit('#', 1)[0])
-        self.error_handler(req, ex, tb=False)
+            location = req.form['__errorurl'].rsplit('#', 1)[0]
+            req.headers_out.setHeader('location', str(location))
+            req.status_out = httplib.SEE_OTHER
+            return ''
+        req.status_out = httplib.CONFLICT
+        return self.error_handler(req, ex, tb=False)
 
-    def error_handler(self, req, ex, tb=False, code=500):
+    def error_handler(self, req, ex, tb=False):
         excinfo = sys.exc_info()
-        self.exception(repr(ex))
+        if tb:
+            self.exception(repr(ex))
         req.set_header('Cache-Control', 'no-cache')
         req.remove_header('Etag')
         req.reset_message()
         req.reset_headers()
         if req.ajax_request:
-            raise RemoteCallFailed(unicode(ex))
+            return ajax_error_handler(req, ex)
         try:
             req.data['ex'] = ex
             if tb:
@@ -462,7 +548,27 @@
             content = self.vreg['views'].main_template(req, template, view=errview)
         except Exception:
             content = self.vreg['views'].main_template(req, 'error-template')
-        raise StatusResponse(code, content)
+        if getattr(ex, 'status', None) is not None:
+            req.status_out = ex.status
+        return content
+
+    def add_undo_link_to_msg(self, req):
+        txuuid = req.data.get('last_undoable_transaction')
+        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)
+
+    def ajax_error_handler(self, req, ex):
+        req.set_header('content-type', 'application/json')
+        status = ex.status
+        if status is None:
+            status = httplib.INTERNAL_SERVER_ERROR
+        json_dumper = getattr(ex, 'dumps', lambda : unicode(ex))
+        req.status_out = status
+        return json_dumper()
+
+    # special case handling
 
     def need_login_content(self, req):
         return self.vreg['views'].main_template(req, 'login')
@@ -476,6 +582,8 @@
         template = self.main_template_id(req)
         return self.vreg['views'].main_template(req, template, view=view)
 
+    # template stuff
+
     def main_template_id(self, req):
         template = req.form.get('__template', req.property_value('ui.main-template'))
         if template not in self.vreg['views']:
--- a/web/data/cubicweb.ajax.box.js	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.ajax.box.js	Tue Oct 23 15:00:53 2012 +0200
@@ -13,11 +13,11 @@
     if (separator) {
         value = $.map(value.split(separator), jQuery.trim);
     }
-    var d = loadRemote('json', ajaxFuncArgs(fname, null, eid, value));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs(fname, null, eid, value));
     d.addCallback(function() {
             $('#' + holderid).empty();
             var formparams = ajaxFuncArgs('render', null, 'ctxcomponents', boxid, eid);
-            $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+            $('#' + cw.utils.domid(boxid) + eid).loadxhtml(AJAX_BASE_URL, formparams);
             if (msg) {
                 document.location.hash = '#header';
                 updateMessage(msg);
@@ -26,10 +26,10 @@
 }
 
 function ajaxBoxRemoveLinkedEntity(boxid, eid, relatedeid, delfname, msg) {
-    var d = loadRemote('json', ajaxFuncArgs(delfname, null, eid, relatedeid));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs(delfname, null, eid, relatedeid));
     d.addCallback(function() {
             var formparams = ajaxFuncArgs('render', null, 'ctxcomponents', boxid, eid);
-            $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+            $('#' + cw.utils.domid(boxid) + eid).loadxhtml(AJAX_BASE_URL, formparams);
             if (msg) {
                 document.location.hash = '#header';
                 updateMessage(msg);
@@ -69,7 +69,7 @@
     }
     else {
         var inputid = holderid + 'Input';
-        var deferred = loadRemote('json', ajaxFuncArgs(unrelfname, null, eid));
+        var deferred = loadRemote(AJAX_BASE_URL, ajaxFuncArgs(unrelfname, null, eid));
         deferred.addCallback(function (unrelated) {
             var input = INPUT({'type': 'text', 'id': inputid, 'size': 20});
             holder.append(input).show();
--- a/web/data/cubicweb.ajax.js	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.ajax.js	Tue Oct 23 15:00:53 2012 +0200
@@ -86,8 +86,9 @@
 
 });
 
-
+var AJAX_PREFIX_URL = 'ajax';
 var JSON_BASE_URL = baseuri() + 'json?';
+var AJAX_BASE_URL = baseuri() + AJAX_PREFIX_URL + '?';
 
 
 jQuery.extend(cw.ajax, {
@@ -180,8 +181,17 @@
                 // compute concat-like url for missing resources and append <link>
                 // element to $head
                 if (missingStylesheetsUrl) {
-                    $srcnode.attr('href', missingStylesheetsUrl);
-                    $srcnode.appendTo($head);
+                    // IE has problems with dynamic CSS insertions. One symptom (among others)
+                    // is a "1 item remaining" message in the status bar. (cf. #2356261)
+                    // document.createStyleSheet needs to be used for this, although it seems
+                    // that IE can't create more than 31 additional stylesheets with
+                    // document.createStyleSheet.
+                    if ($.browser.msie) {
+                        document.createStyleSheet(missingStylesheetsUrl);
+                    } else {
+                        $srcnode.attr('href', missingStylesheetsUrl);
+                        $srcnode.appendTo($head);
+                    }
                 }
             }
         });
@@ -371,7 +381,7 @@
 /**
  * .. function:: loadRemote(url, form, reqtype='GET', sync=false)
  *
- * Asynchronously (unless `async` argument is set to false) load an url or path
+ * Asynchronously (unless `sync` argument is set to true) load an url or path
  * and return a deferred whose callbacks args are decoded according to the
  * Content-Type response header. `form` should be additional form params
  * dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
@@ -439,7 +449,7 @@
  * emulation of gettext's _ shortcut
  */
 function _(message) {
-    return loadRemote('json', ajaxFuncArgs('i18n', null, [message]), 'GET', true)[0];
+    return loadRemote(AJAX_BASE_URL, ajaxFuncArgs('i18n', null, [message]), 'GET', true)[0];
 }
 
 /**
@@ -495,19 +505,19 @@
         }
         extraparams['rql'] = rql;
         extraparams['vid'] = vid;
-        $fragment.loadxhtml('json', ajaxFuncArgs('view', extraparams));
+        $fragment.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('view', extraparams));
     }
 }
 function unloadPageData() {
     // NOTE: do not make async calls on unload if you want to avoid
     //       strange bugs
-    loadRemote('json', ajaxFuncArgs('unload_page_data'), 'GET', true);
+    loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unload_page_data'), 'GET', true);
 }
 
 function removeBookmark(beid) {
-    var d = loadRemote('json', ajaxFuncArgs('delete_bookmark', null, beid));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('delete_bookmark', null, beid));
     d.addCallback(function(boxcontent) {
-        $('#bookmarks_box').loadxhtml('json',
+        $('#bookmarks_box').loadxhtml(AJAX_BASE_URL,
                                       ajaxFuncArgs('render', null, 'ctxcomponents',
                                                    'bookmarks_box'));
         document.location.hash = '#header';
@@ -517,7 +527,7 @@
 
 function userCallback(cbname) {
     setProgressCursor();
-    var d = loadRemote('json', ajaxFuncArgs('user_callback', null, cbname));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('user_callback', null, cbname));
     d.addCallback(resetCursor);
     d.addErrback(resetCursor);
     d.addErrback(remoteCallFailed);
@@ -527,7 +537,7 @@
 function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
     var d = userCallback(cbname);
     d.addCallback(function() {
-        $('#' + nodeid).loadxhtml('json', ajaxFuncArgs('render', {'rql': rql},
+        $('#' + nodeid).loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {'rql': rql},
                                                        registry, compid));
         if (msg) {
             updateMessage(msg);
@@ -553,7 +563,7 @@
  */
 function unregisterUserCallback(cbname) {
     setProgressCursor();
-    var d = loadRemote('json', ajaxFuncArgs('unregister_user_callback',
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unregister_user_callback',
                                             null, cbname));
     d.addCallback(resetCursor);
     d.addErrback(resetCursor);
@@ -679,7 +689,7 @@
 	var compid = this.id.replace("_", ".").rstrip(creationEid);
 	var params = ajaxFuncArgs('render', null, 'ctxcomponents',
 				  compid, actualEid);
-	$(this).loadxhtml('json', params, null, 'swap', true);
+	$(this).loadxhtml(AJAX_BASE_URL, params, null, 'swap', true);
     });
     $compsholder.attr('id', context + actualEid);
 }
@@ -694,7 +704,7 @@
     var ajaxArgs = ['render', formparams, registry, compid];
     ajaxArgs = ajaxArgs.concat(cw.utils.sliceList(arguments, 4));
     var params = ajaxFuncArgs.apply(null, ajaxArgs);
-    return $('#'+domid).loadxhtml('json', params, null, 'swap');
+    return $('#'+domid).loadxhtml(AJAX_BASE_URL, params, null, 'swap', true);
 }
 
 /* ajax tabs ******************************************************************/
@@ -738,8 +748,8 @@
         nodeid = nodeid || (compid + 'Component');
         extraargs = extraargs || {};
         var node = cw.jqNode(nodeid);
-        return node.loadxhtml('json', ajaxFuncArgs('component', null, compid,
-                                                   rql, registry, extraargs));
+        return node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('component', null, compid,
+                                                          rql, registry, extraargs));
     }
 );
 
@@ -775,7 +785,7 @@
             // passing `props` directly to loadxml because replacePageChunk
             // is sometimes called (abusively) with some extra parameters in `vid`
             var mode = swap ? 'swap': 'replace';
-            var url = JSON_BASE_URL + asURL(props);
+            var url = AJAX_BASE_URL + asURL(props);
             jQuery(node).loadxhtml(url, params, 'get', mode);
         } else {
             cw.log('Node', nodeId, 'not found');
@@ -798,7 +808,7 @@
         arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
     };
     var result = jQuery.ajax({
-        url: JSON_BASE_URL,
+        url: AJAX_BASE_URL,
         data: props,
         async: false,
         traditional: true
@@ -818,7 +828,7 @@
         arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
     };
     // XXX we should inline the content of loadRemote here
-    var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
+    var deferred = loadRemote(AJAX_BASE_URL, props, 'POST');
     deferred = deferred.addErrback(remoteCallFailed);
     deferred = deferred.addErrback(resetCursor);
     deferred = deferred.addCallback(resetCursor);
--- a/web/data/cubicweb.css	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.css	Tue Oct 23 15:00:53 2012 +0200
@@ -39,6 +39,12 @@
   padding: %(h3Padding)s;
 }
 
+
+h4 {
+  font-size: %(h4FontSize)s;
+}
+
+
 div.tabbedprimary + h1,
 h1.plain {
  border-bottom: none;
@@ -214,9 +220,6 @@
     font-style: italic;
 }
 
-.align-center{
-    text-align: center;
-}
 
 /***************************************/
 /*   LAYOUT                            */
@@ -239,13 +242,21 @@
 
 table#header td#header-right {
   padding-top: 1em;
-  float: right;
+  white-space: nowrap;
 }
 
 table#header img#logo{
   vertical-align: middle;
 }
 
+table#header td#headtext {
+  white-space: nowrap;
+}
+
+table#header td#header-center{
+ width: 100%;
+}
+
 span#appliName {
   font-weight: bold;
   color: %(defaultColor)s;
@@ -534,6 +545,16 @@
   padding-left: 2em;
 }
 
+/* actions around tables */
+.tableactions span {
+  padding: 0 18px;
+  height: 24px;
+  background: #F8F8F8;
+  border: 1px solid #DFDFDF;
+  border-bottom: none;
+  border-radius: 4px 4px 0 0;
+}
+
 /* custom boxes */
 
 .search_box div.boxBody {
@@ -978,11 +999,20 @@
 /********************************/
 
 img.align-right {
-  margin-left: 1.5em;
+  margin-left: auto;
+  display:block;
 }
 
 img.align-left {
-  margin-right: 1.5em;
+  margin-right: auto;
+  display:block;
+}
+
+img.align-center{
+  text-align: center;
+  margin-left: auto;
+  margin-right: auto;
+  display:block;
 }
 
 /******************************/
--- a/web/data/cubicweb.edition.js	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.edition.js	Tue Oct 23 15:00:53 2012 +0200
@@ -28,7 +28,7 @@
             pageid: pageid,
             arg: $.map([key, varname, tabindex], jQuery.toJSON)
         };
-        cw.jqNode('div:value:' + varname).loadxhtml(JSON_BASE_URL, args, 'post');
+        cw.jqNode('div:value:' + varname).loadxhtml(AJAX_BASE_URL, args, 'post');
     }
 }
 
@@ -170,8 +170,8 @@
     // add hidden parameter
     var entityForm = jQuery('#entityForm');
     var oid = optionNode.id.substring(2); // option id is prefixed by "id"
-    loadRemote('json', ajaxFuncArgs('add_pending_inserts', null,
-                                    [oid.split(':')]), 'GET', true);
+    loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_inserts', null,
+                                           [oid.split(':')]), 'GET', true);
     var selectNode = optionNode.parentNode;
     // remove option node
     selectNode.removeChild(optionNode);
@@ -209,8 +209,8 @@
         }
     }
     elementId = elementId.substring(2, elementId.length);
-    loadRemote('json', ajaxFuncArgs('remove_pending_insert', null,
-                                    elementId.split(':')), 'GET', true);
+    loadRemote(AJAX_BASE_URL, ajaxFuncArgs('remove_pending_insert', null,
+                                           elementId.split(':')), 'GET', true);
 }
 
 /**
@@ -234,7 +234,7 @@
  * * `nodeId`, eid_from:r_type:eid_to
  */
 function addPendingDelete(nodeId, eid) {
-    var d = loadRemote('json', ajaxFuncArgs('add_pending_delete', null, nodeId.split(':')));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_delete', null, nodeId.split(':')));
     d.addCallback(function() {
         // and strike entity view
         cw.jqNode('span' + nodeId).addClass('pendingDelete');
@@ -249,7 +249,7 @@
  * * `nodeId`, eid_from:r_type:eid_to
  */
 function cancelPendingDelete(nodeId, eid) {
-    var d = loadRemote('json', ajaxFuncArgs('remove_pending_delete', null, nodeId.split(':')));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('remove_pending_delete', null, nodeId.split(':')));
     d.addCallback(function() {
         // reset link's CSS class
         cw.jqNode('span' + nodeId).removeClass('pendingDelete');
@@ -275,7 +275,7 @@
 function selectForAssociation(tripletIdsString, originalEid) {
     var tripletlist = $.map(tripletIdsString.split('-'),
                             function(x) { return [x.split(':')] ;});
-    var d = loadRemote('json', ajaxFuncArgs('add_pending_inserts', null, tripletlist));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_inserts', null, tripletlist));
     d.addCallback(function() {
         var args = {
             vid: 'edition',
@@ -308,7 +308,7 @@
 function addInlineCreationForm(peid, petype, ttype, rtype, role, i18nctx, insertBefore) {
     insertBefore = insertBefore || cw.getNode('add' + rtype + ':' + peid + 'link').parentNode;
     var args = ajaxFuncArgs('inline_creation_form', null, peid, petype, ttype, rtype, role, i18nctx);
-    var d = loadRemote('json', args);
+    var d = loadRemote(AJAX_BASE_URL, args);
     d.addCallback(function(response) {
         var dom = getDomFromResponse(response);
         loadAjaxHtmlHead(dom);
@@ -435,11 +435,15 @@
         }
     }
     if (globalerrors.length) {
-        if (globalerrors.length == 1) {
-            var innernode = SPAN(null, globalerrors[0]);
-        } else {
-            var innernode = UL(null, $.map(globalerrors, partial(LI, null)));
-        }
+       if (globalerrors.length == 1) {
+           var innernode = SPAN(null, globalerrors[0]);
+       } else {
+           var linodes =[];
+           for(var i=0; i<globalerrors.length; i++){
+             linodes.push(LI(null, globalerrors[i]));
+           }
+           var innernode = UL(null, linodes);
+       }
         // insert DIV and innernode before the form
         var div = DIV({
             'class': "errorMessage",
@@ -587,7 +591,7 @@
     try {
         var zipped = cw.utils.formContents(formid);
         var args = ajaxFuncArgs('validate_form', null, action, zipped[0], zipped[1]);
-        var d = loadRemote('json', args, 'POST');
+        var d = loadRemote(AJAX_BASE_URL, args, 'POST');
     } catch(ex) {
         cw.log('got exception', ex);
         return false;
--- a/web/data/cubicweb.facets.js	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.facets.js	Tue Oct 23 15:00:53 2012 +0200
@@ -56,7 +56,7 @@
     var zipped = facetFormContent($form);
     zipped[0].push('facetargs');
     zipped[1].push(vidargs);
-    var d = loadRemote('json', ajaxFuncArgs('filter_build_rql', null, zipped[0], zipped[1]));
+    var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('filter_build_rql', null, zipped[0], zipped[1]));
     d.addCallback(function(result) {
         var rql = result[0];
         var $bkLink = jQuery('#facetBkLink');
@@ -68,6 +68,14 @@
             var bkUrl = $bkLink.attr('cubicweb:target') + '&path=' + encodeURIComponent(bkPath);
             $bkLink.attr('href', bkUrl);
         }
+        var $focusLink = jQuery('#focusLink');
+        if ($focusLink.length) {
+            var url = baseuri()+ 'view?rql=' + encodeURIComponent(rql);
+            if (vid) {
+                url += '&vid=' + encodeURIComponent(vid);
+            }
+            $focusLink.attr('href', url);
+        }
         var toupdate = result[1];
         var extraparams = vidargs;
         if (paginate) { extraparams['paginate'] = '1'; } // XXX in vidargs
@@ -87,7 +95,7 @@
         if (vid) { // XXX see copyParam above. Need cleanup
             extraparams['vid'] = vid;
         }
-        d = $('#' + divid).loadxhtml('json', ajaxFuncArgs('view', extraparams),
+        d = $('#' + divid).loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('view', extraparams),
                                      null, 'swap');
         d.addCallback(function() {
             // XXX rql/vid in extraparams
@@ -99,14 +107,14 @@
             // now
             var $node = jQuery('#edit_box');
             if ($node.length) {
-                $node.loadxhtml('json', ajaxFuncArgs('render', {
+                $node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {
                     'rql': rql
                 },
                 'ctxcomponents', 'edit_box'));
             }
             $node = jQuery('#breadcrumbs');
             if ($node.length) {
-                $node.loadxhtml('json', ajaxFuncArgs('render', {
+                $node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {
                     'rql': rql
                 },
                 'ctxcomponents', 'breadcrumbs'));
@@ -118,7 +126,7 @@
             mainvar = zipped[1][index];
         }
 
-        var d = loadRemote('json', ajaxFuncArgs('filter_select_content', null, toupdate, rql, mainvar));
+        var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('filter_select_content', null, toupdate, rql, mainvar));
         d.addCallback(function(updateMap) {
             for (facetName in updateMap) {
                 var values = updateMap[facetName];
--- a/web/data/cubicweb.old.css	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.old.css	Tue Oct 23 15:00:53 2012 +0200
@@ -43,7 +43,7 @@
 }
 
 h4 {
-  font-size: 120%;
+  font-size: %(h4FontSize)s;
   margin: 0.2em 0px;
 }
 
@@ -64,9 +64,12 @@
   text-decoration: underline;
 }
 
-a img, img {
+a img{
+  text-align: center;
+}
+
+img{
   border: none;
-  text-align: center;
 }
 
 img.prevnext {
@@ -161,6 +164,9 @@
   color: #000;
   background-color: #f2f2f2;
   border: 1px solid #ccc;
+  margin: 10px 0;
+  padding-bottom: 12px;
+  padding-left: 5px;
 }
 
 code {
@@ -213,25 +219,44 @@
   visibility: hidden;
 }
 
-li.invisible { list-style: none; background: none; padding: 0px 0px
-1px 1px; }
+li.invisible {
+  list-style: none;
+  background: none;
+  padding: 0px 0px 1px 1px;
+}
 
 li.invisible div {
   display: inline;
 }
 
 .caption {
-    font-weight: bold;
+  font-weight: bold;
 }
 
 .legend{
-    font-style: italic;
+  font-style: italic;
+}
+
+/* rest related image classes generated with align: directive */
+
+img.align-right {
+  margin-left: auto;
+  display:block;
 }
 
-.align-center{
-    text-align: center;
+img.align-left {
+  margin-right: auto;
+  display:block;
 }
 
+img.align-center{
+  text-align: center;
+  margin-left: auto;
+  margin-right: auto;
+  display:block;
+}
+
+
 /***************************************/
 /*   LAYOUT                            */
 /***************************************/
@@ -250,10 +275,16 @@
 table#header a {
   color: #000;
 }
+table#header td#headtext {
+  white-space: nowrap;
+}
 
 table#header td#header-right {
   padding-top: 1em;
-  float: right;
+  white-space: nowrap;
+}
+table#header td#header-center{
+  width: 100%;
 }
 
 span#appliName {
@@ -868,6 +899,16 @@
   padding-left: 0.5em;
 }
 
+/* actions around tables */
+.tableactions span {
+  padding: 0 18px;
+  height: 24px;
+  background: #F8F8F8;
+  border: 1px solid #DFDFDF;
+  border-bottom: none;
+  border-radius: 4px 4px 0 0;
+}
+
 /***************************************/
 /* error view (views/management.py)    */
 /***************************************/
--- a/web/data/cubicweb.reledit.js	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.reledit.js	Tue Oct 23 15:00:53 2012 +0200
@@ -53,7 +53,7 @@
                 return;
             }
         }
-        jQuery('#'+params.divid+'-reledit').loadxhtml(JSON_BASE_URL, params, 'post');
+        jQuery('#'+params.divid+'-reledit').loadxhtml(AJAX_BASE_URL, params, 'post');
         jQuery(cw).trigger('reledit-reloaded', params);
     },
 
@@ -69,7 +69,7 @@
                     pageid: pageid, action: action,
                     eid: eid, divid: divid, formid: formid,
                     reload: reload, vid: vid};
-        var d = jQuery('#'+divid+'-reledit').loadxhtml(JSON_BASE_URL, args, 'post');
+        var d = jQuery('#'+divid+'-reledit').loadxhtml(AJAX_BASE_URL, args, 'post');
         d.addCallback(function () {cw.reledit.showInlineEditionForm(divid);});
     }
 });
--- a/web/data/cubicweb.widgets.js	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.widgets.js	Tue Oct 23 15:00:53 2012 +0200
@@ -45,11 +45,11 @@
 });
 
 function postJSON(url, data, callback) {
-    return jQuery.post(url, data, callback, 'json');
+    return jQuery.post(url, data, callback, AJAX_BASE_URL);
 }
 
 function getJSON(url, data, callback) {
-    return jQuery.get(url, data, callback, 'json');
+    return jQuery.get(url, data, callback, AJAX_BASE_URL);
 }
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/fullcalendar.locale.js	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,38 @@
+/*
+ translations for fullCalendar plugin
+ */
+
+$.fullCalendar.regional = function(lng, options){
+    var defaults = {'fr' : {
+     monthNames:
+       ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'],
+     monthNamesShort: ['janv.','févr.','mars','avr.','mai','juin','juil.','août','sept.','oct.','nov.','déc.'],
+     dayNames: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'],
+     dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
+     titleFormat: {
+ 	month: 'MMMM yyyy', // ex : Janvier 2010
+ 	week: "d[ MMMM][ yyyy]{ - d MMMM yyyy}", // ex : 10 — 16 Janvier 2010,
+ 	day: 'dddd d MMMM yyyy' // ex : Jeudi 14 Janvier 2010
+     },
+     columnFormat: {'month': 'dddd',
+                  'agendaWeek': 'dddd dd/M/yyyy',
+                  'agendaDay': 'dddd dd/M/yyyy'},
+     axisFormat: 'H:mm',
+     timeFormat: {
+	'': 'H:mm',
+	agenda: 'H:mm{ - H:mm}'},
+     allDayText: 'journée',
+     axisFormat: 'H:mm',
+     buttonText: {
+        today: "aujourd'hui",
+        month: 'mois',
+        week: 'semaine',
+       day: 'jour'
+     }
+  }};
+  if(lng in defaults){
+    return $.extend({}, defaults[lng], options);
+   }
+   else {return options;};
+  };
+;
\ No newline at end of file
--- a/web/data/uiprops.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/uiprops.py	Tue Oct 23 15:00:53 2012 +0200
@@ -89,20 +89,22 @@
 # h3 { font-size:1.30769em; }
 
 # h
-h1FontSize = '1.5em' # 18px
+h1FontSize = '2.3em' # 25.3833px
 h1Padding = '0 0 0.14em 0 '
 h1Margin = '0.8em 0 0.5em'
 h1Color = '#000'
 h1BorderBottomStyle = lazystr('0.06em solid %(h1Color)s')
 
-h2FontSize = '1.33333em'
-h2Padding = '0.4em 0 0.35em 0'
+h2FontSize = '2em' #
+h2Padding = '0.4em 0 0.35em 0' # 22.0667px
 h2Margin = '0'
 
-h3FontSize = '1.16667em'
+h3FontSize = '1.7em' #18.75px
 h3Padding = '0.5em 0 0.57em 0'
 h3Margin = '0'
 
+h4FontSize = '1.4em' # 15.45px
+
 # links
 aColor = '#e6820e'
 
--- a/web/form.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/form.py	Tue Oct 23 15:00:53 2012 +0200
@@ -88,21 +88,19 @@
 
     def __init__(self, req, rset=None, row=None, col=None,
                  submitmsg=None, mainform=True, **kwargs):
-        super(Form, self).__init__(req, rset=rset, row=row, col=col)
+        # process kwargs first so we can properly pass them to Form and match
+        # order expectation (ie cw_extra_kwargs populated almost first)
+        hiddens, extrakw = self._process_kwargs(kwargs)
+        # now call ancestor init
+        super(Form, self).__init__(req, rset=rset, row=row, col=col, **extrakw)
+        # then continue with further specific initialization
         self.fields = list(self.__class__._fields_)
+        for key, val in hiddens:
+            self.add_hidden(key, val)
         if mainform:
-            self.add_hidden(u'__form_id', kwargs.pop('formvid', self.__regid__))
-        for key, val in kwargs.iteritems():
-            if key in controller.NAV_FORM_PARAMETERS:
-                self.add_hidden(key, val)
-            elif key == 'redirect_path':
-                self.add_hidden(u'__redirectpath', val)
-            elif hasattr(self.__class__, key) and not key[0] == '_':
-                setattr(self, key, val)
-            else:
-                self.cw_extra_kwargs[key] = val
-            # skip other parameters, usually given for selection
-            # (else write a custom class to handle them)
+            formid = kwargs.pop('formvid', self.__regid__)
+            self.add_hidden(u'__form_id', formid)
+            self._posting = self._cw.form.get('__form_id') == formid
         if mainform:
             self.add_hidden(u'__errorurl', self.session_key())
             self.add_hidden(u'__domid', self.domid)
@@ -117,6 +115,22 @@
         if submitmsg is not None:
             self.set_message(submitmsg)
 
+    def _process_kwargs(self, kwargs):
+        hiddens = []
+        extrakw = {}
+        # search for navigation parameters and customization of existing
+        # attributes; remaining stuff goes in extrakwargs
+        for key, val in kwargs.iteritems():
+            if key in controller.NAV_FORM_PARAMETERS:
+                hiddens.append( (key, val) )
+            elif key == 'redirect_path':
+                hiddens.append( (u'__redirectpath', val) )
+            elif hasattr(self.__class__, key) and not key[0] == '_':
+                setattr(self, key, val)
+            else:
+                extrakw[key] = val
+        return hiddens, extrakw
+
     def set_message(self, submitmsg):
         """sets a submitmsg if exists, using _cwmsgid mechanism """
         cwmsgid = self._cw.set_redirect_message(submitmsg)
@@ -145,6 +159,16 @@
             return getattr(self, '_form_previous_values', {})
         return self.parent_form.form_previous_values
 
+    @property
+    def posting(self):
+        """return True if the form is being posted, False if it is being
+        generated.
+        """
+        # XXX check behaviour on regeneration after error
+        if self.parent_form is None:
+            return self._posting
+        return self.parent_form.posting
+
     @iclassmethod
     def _fieldsattr(cls_or_self):
         if isinstance(cls_or_self, type):
--- a/web/formfields.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/formfields.py	Tue Oct 23 15:00:53 2012 +0200
@@ -313,6 +313,7 @@
 
     def role_name(self):
         """return <field.name>-<field.role> if role is specified, else field.name"""
+        assert self.name, 'field without a name (give it to constructor for explicitly built fields)'
         if self.role is not None:
             return role_name(self.name, self.role)
         return self.name
@@ -360,7 +361,7 @@
         if self.eidparam and self.role is not None:
             if form._cw.vreg.schema.rschema(self.name).final:
                 return form.edited_entity.e_schema.default(self.name)
-            return ()
+            return form.linked_to.get((self.name, self.role), ())
         return None
 
     def example_format(self, req):
--- a/web/formwidgets.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/formwidgets.py	Tue Oct 23 15:00:53 2012 +0200
@@ -672,10 +672,11 @@
     """
     needs_js = ('jquery.ui.js', )
     needs_css = ('jquery.ui.css',)
+    default_size = 10
 
     def __init__(self, datestr=None, **kwargs):
         super(JQueryDatePicker, self).__init__(**kwargs)
-        self.datestr = datestr
+        self.value = datestr
 
     def _render(self, form, field, renderer):
         req = form._cw
@@ -689,44 +690,36 @@
                        '{buttonImage: "%s", dateFormat: "%s", firstDay: 1,'
                        ' showOn: "button", buttonImageOnly: true})' % (
                            domid, req.uiprops['CALENDAR_ICON'], fmt))
-        if self.datestr is None:
+        return self._render_input(form, field, domid)
+
+    def _render_input(self, form, field, domid):
+        if self.value is None:
             value = self.values(form, field)[0]
         else:
-            value = self.datestr
-        attrs = {}
-        if self.settabindex:
-            attrs['tabindex'] = req.next_tabindex()
-        return tags.input(id=domid, name=domid, value=value,
-                          type='text', size='10', **attrs)
+            value = self.value
+        attrs = self.attributes(form, field)
+        attrs.setdefault('size', unicode(self.default_size))
+        return tags.input(name=domid, value=value, type='text', **attrs)
 
 
-class JQueryTimePicker(FieldWidget):
+class JQueryTimePicker(JQueryDatePicker):
     """Use jquery.timePicker to define a time picker. Will return the time as an
     unicode string.
     """
     needs_js = ('jquery.timePicker.js',)
     needs_css = ('jquery.timepicker.css',)
+    default_size = 5
 
     def __init__(self, timestr=None, timesteps=30, separator=u':', **kwargs):
-        super(JQueryTimePicker, self).__init__(**kwargs)
-        self.timestr = timestr
+        super(JQueryTimePicker, self).__init__(timestr, **kwargs)
         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'cw.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:
-            value = self.timestr
-        attrs = {}
-        if self.settabindex:
-            attrs['tabindex'] = req.next_tabindex()
-        return tags.input(id=domid, name=domid, value=value,
-                          type='text', size='5')
+        form._cw.add_onload(u'cw.jqNode("%s").timePicker({step: %s, separator: "%s"})' % (
+                domid, self.timesteps, self.separator))
+        return self._render_input(form, field, domid)
 
 
 class JQueryDateTimePicker(FieldWidget):
--- a/web/http_headers.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/http_headers.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1290,11 +1290,13 @@
             self._raw_headers[name] = r
         return r
 
-    def hasHeader(self, name):
+    def __contains__(self, name):
         """Does a header with the given name exist?"""
         name=name.lower()
         return self._raw_headers.has_key(name)
 
+    hasHeader = __contains__
+
     def getRawHeaders(self, name, default=None):
         """Returns a list of headers matching the given name as the raw string given."""
 
--- a/web/httpcache.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/httpcache.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -147,3 +147,39 @@
 
 viewmod.StartupView.http_cache_manager = MaxAgeHTTPCacheManager
 viewmod.StartupView.cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
+
+
+### HTTP Cache validator ############################################
+
+
+
+def get_validators(headers_in):
+    """return a list of http condition validator relevant to this request
+    """
+    result = []
+    for header, func in VALIDATORS:
+        value = headers_in.getHeader(header)
+        if value is not None:
+            result.append((func, value))
+    return result
+
+
+def if_modified_since(ref_date, headers_out):
+    last_modified = headers_out.getHeader('last-modified')
+    if last_modified is None:
+        return True
+    return ref_date < last_modified
+
+def if_none_match(tags, headers_out):
+    etag = headers_out.getHeader('etag')
+    if etag is None:
+        return True
+    return not ((etag in tags) or ('*' in tags))
+
+VALIDATORS = [
+    ('if-modified-since', if_modified_since),
+    #('if-unmodified-since', if_unmodified_since),
+    ('if-none-match', if_none_match),
+    #('if-modified-since', if_modified_since),
+]
+
--- a/web/request.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/request.py	Tue Oct 23 15:00:53 2012 +0200
@@ -25,8 +25,9 @@
 from hashlib import sha1 # pylint: disable=E0611
 from Cookie import SimpleCookie
 from calendar import timegm
-from datetime import date
+from datetime import date, datetime
 from urlparse import urlsplit
+import httplib
 from itertools import count
 from warnings import warn
 
@@ -43,8 +44,8 @@
 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
                           RequestError, StatusResponse)
-from cubicweb.web.httpcache import GMTOFFSET
-from cubicweb.web.http_headers import Headers, Cookie
+from cubicweb.web.httpcache import GMTOFFSET, get_validators
+from cubicweb.web.http_headers import Headers, Cookie, parseDateTime
 
 _MARKER = object()
 
@@ -81,34 +82,53 @@
 
 
 class CubicWebRequestBase(DBAPIRequest):
-    """abstract HTTP request, should be extended according to the HTTP backend"""
+    """abstract HTTP request, should be extended according to the HTTP backend
+    Immutable attributes that describe the received query and generic configuration
+    """
     ajax_request = False # to be set to True by ajax controllers
 
-    def __init__(self, vreg, https, form=None):
+    def __init__(self, vreg, https=False, form=None, headers={}):
+        """
+        :vreg: Vregistry,
+        :https: boolean, s this a https request
+        :form: Forms value
+        """
         super(CubicWebRequestBase, self).__init__(vreg)
+        #: (Boolean) Is this an https request.
         self.https = https
+        #: User interface property (vary with https) (see :ref:`uiprops`)
+        self.uiprops = None
+        #: url for serving datadir (vary with https) (see :ref:`resources`)
+        self.datadir_url = None
         if https:
             self.uiprops = vreg.config.https_uiprops
             self.datadir_url = vreg.config.https_datadir_url
         else:
             self.uiprops = vreg.config.uiprops
             self.datadir_url = vreg.config.datadir_url
-        # raw html headers that can be added from any view
+        #: raw html headers that can be added from any view
         self.html_headers = HTMLHead(self)
-        # form parameters
+        #: received headers
+        self._headers_in = Headers()
+        for k, v in headers.iteritems():
+            self._headers_in.addRawHeader(k, v)
+        #: form parameters
         self.setup_params(form)
-        # dictionary that may be used to store request data that has to be
-        # shared among various components used to publish the request (views,
-        # controller, application...)
+        #: dictionary that may be used to store request data that has to be
+        #: shared among various components used to publish the request (views,
+        #: controller, application...)
         self.data = {}
-        # search state: 'normal' or 'linksearch' (eg searching for an object
-        # to create a relation with another)
+        #:  search state: 'normal' or 'linksearch' (eg searching for an object
+        #:  to create a relation with another)
         self.search_state = ('normal',)
-        # page id, set by htmlheader template
+        #: page id, set by htmlheader template
         self.pageid = None
         self._set_pageid()
         # prepare output header
+        #: Header used for the final response
         self.headers_out = Headers()
+        #: HTTP status use by the final response
+        self.status_out  = 200
 
     def _set_pageid(self):
         """initialize self.pageid
@@ -131,10 +151,30 @@
         self.ajax_request = value
     json_request = property(_get_json_request, _set_json_request)
 
+    def base_url(self, secure=None):
+        """return the root url of the instance
+
+        secure = False -> base-url
+        secure = None  -> https-url if req.https
+        secure = True  -> https if it exist
+        """
+        if secure is None:
+            secure = self.https
+        base_url = None
+        if secure:
+            base_url = self.vreg.config.get('https-url')
+        if base_url is None:
+            base_url = super(CubicWebRequestBase, self).base_url()
+        return base_url
+
     @property
     def authmode(self):
+        """Authentification mode of the instance
+        (see :ref:`WebServerConfig`)"""
         return self.vreg.config['auth-mode']
 
+    # Various variable generator.
+
     @property
     def varmaker(self):
         """the rql varmaker is exposed both as a property and as the
@@ -186,14 +226,6 @@
         # 3. default language
         self.set_default_language(vreg)
 
-    def set_language(self, lang):
-        gettext, self.pgettext = self.translations[lang]
-        self._ = self.__ = gettext
-        self.lang = lang
-        self.debug('request language: %s', lang)
-        if self.cnx:
-            self.cnx.set_session_props(lang=lang)
-
     # input form parameters management ########################################
 
     # common form parameters which should be protected against html values
@@ -269,7 +301,6 @@
             form = self.form
         return list_form_param(form, param, pop)
 
-
     def reset_headers(self):
         """used by AutomaticWebTest to clear html headers between tests on
         the same resultset
@@ -326,7 +357,7 @@
     def update_search_state(self):
         """update the current search state"""
         searchstate = self.form.get('__mode')
-        if not searchstate and self.cnx:
+        if not searchstate:
             searchstate = self.session.data.get('search_state', 'normal')
         self.set_search_state(searchstate)
 
@@ -337,8 +368,7 @@
         else:
             self.search_state = ('linksearch', searchstate.split(':'))
             assert len(self.search_state[-1]) == 4
-        if self.cnx:
-            self.session.data['search_state'] = searchstate
+        self.session.data['search_state'] = searchstate
 
     def match_search_state(self, rset):
         """when searching an entity to create a relation, return True if entities in
@@ -711,14 +741,33 @@
         return 'view'
 
     def validate_cache(self):
-        """raise a `DirectResponse` exception if a cached page along the way
+        """raise a `StatusResponse` exception if a cached page along the way
         exists and is still usable.
 
         calls the client-dependant implementation of `_validate_cache`
         """
-        self._validate_cache()
-        if self.http_method() == 'HEAD':
-            raise StatusResponse(200, '')
+        modified = True
+        if self.get_header('Cache-Control') not in ('max-age=0', 'no-cache'):
+            # Here, we search for any invalid 'not modified' condition
+            # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3
+            validators = get_validators(self._headers_in)
+            if validators: # if we have no
+                modified = any(func(val, self.headers_out) for func, val in validators)
+        # Forge expected response
+        if modified:
+            if 'Expires' not in self.headers_out:
+                # Expires header seems to be required by IE7 -- Are you sure ?
+                self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
+            if self.http_method() == 'HEAD':
+                raise StatusResponse(200, '')
+            # /!\ no raise, the function returns and we keep processing the request)
+        else:
+            # overwrite headers_out to forge a brand new not-modified response
+            self.headers_out = self._forge_cached_headers()
+            if self.http_method() in ('HEAD', 'GET'):
+                raise StatusResponse(httplib.NOT_MODIFIED)
+            else:
+                raise StatusResponse(httplib.PRECONDITION_FAILED)
 
     # abstract methods to override according to the web front-end #############
 
@@ -726,11 +775,19 @@
         """returns 'POST', 'GET', 'HEAD', etc."""
         raise NotImplementedError()
 
-    def _validate_cache(self):
-        """raise a `DirectResponse` exception if a cached page along the way
-        exists and is still usable
-        """
-        raise NotImplementedError()
+    def _forge_cached_headers(self):
+        # overwrite headers_out to forge a brand new not-modified response
+        headers = Headers()
+        for header in (
+            # Required from sec 10.3.5:
+            'date', 'etag', 'content-location', 'expires',
+            'cache-control', 'vary',
+            # Others:
+            'server', 'proxy-authenticate', 'www-authenticate', 'warning'):
+            value = self._headers_in.getRawHeaders(header)
+            if value is not None:
+                headers.setRawHeaders(header, value)
+        return headers
 
     def relative_path(self, includeparams=True):
         """return the normalized path of the request (ie at least relative
@@ -742,12 +799,37 @@
         """
         raise NotImplementedError()
 
-    def get_header(self, header, default=None):
-        """return the value associated with the given input HTTP header,
-        raise KeyError if the header is not set
+    # http headers ############################################################
+
+    ### incoming headers
+
+    def get_header(self, header, default=None, raw=True):
+        """return the value associated with the given input header, raise
+        KeyError if the header is not set
         """
-        raise NotImplementedError()
+        if raw:
+            return self._headers_in.getRawHeaders(header, [default])[0]
+        return self._headers_in.getHeader(header, default)
+
+    def header_accept_language(self):
+        """returns an ordered list of preferred languages"""
+        acceptedlangs = self.get_header('Accept-Language', raw=False) or {}
+        for lang, _ in sorted(acceptedlangs.iteritems(), key=lambda x: x[1],
+                              reverse=True):
+            lang = lang.split('-')[0]
+            yield lang
 
+    def header_if_modified_since(self):
+        """If the HTTP header If-modified-since is set, return the equivalent
+        date time value (GMT), else return None
+        """
+        mtime = self.get_header('If-modified-since', raw=False)
+        if mtime:
+            # :/ twisted is returned a localized time stamp
+            return datetime.fromtimestamp(mtime) + GMTOFFSET
+        return None
+
+    ### outcoming headers
     def set_header(self, header, value, raw=True):
         """set an output HTTP header"""
         if raw:
@@ -795,12 +877,6 @@
         values = _parse_accept_header(accepteds, value_parser, value_sort_key)
         return (raw_value for (raw_value, parsed_value, score) in values)
 
-    def header_if_modified_since(self):
-        """If the HTTP header If-modified-since is set, return the equivalent
-        mx date time value (GMT), else return None
-        """
-        raise NotImplementedError()
-
     def demote_to_html(self):
         """helper method to dynamically set request content type to text/html
 
@@ -815,6 +891,8 @@
             self.set_content_type('text/html')
             self.main_stream.set_doctype(TRANSITIONAL_DOCTYPE_NOEXT)
 
+    # xml doctype #############################################################
+
     def set_doctype(self, doctype, reset_xmldecl=True):
         """helper method to dynamically change page doctype
 
--- a/web/test/data/views.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/data/views.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,18 +15,16 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
 
-"""
 from cubicweb.web import Redirect
 from cubicweb.web.application import CubicWebPublisher
 
-# proof of concept : monkey patch publish method so that if we are in an
+# proof of concept : monkey patch handle method so that if we are in an
 # anonymous session and __fblogin is found is req.form, the user with the
 # given login is created if necessary and then a session is opened for that
 # user
 # NOTE: this require "cookie" authentication mode
-def auto_login_publish(self, path, req):
+def auto_login_handle_request(self, req, path):
     if (not req.cnx or req.cnx.anonymous_connection) and req.form.get('__fblogin'):
         login = password = req.form.pop('__fblogin')
         self.repo.register_user(login, password)
@@ -40,7 +38,7 @@
         except Redirect:
             pass
         assert req.user.login == login
-    return orig_publish(self, path, req)
+    return orig_handle(self, req, path)
 
-orig_publish = CubicWebPublisher.main_publish
-CubicWebPublisher.main_publish = auto_login_publish
+orig_handle = CubicWebPublisher.main_handle_request
+CubicWebPublisher.main_handle_request = auto_login_handle_request
--- a/web/test/unittest_application.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_application.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -184,12 +184,12 @@
 
     def test_nonregr_publish1(self):
         req = self.request(u'CWEType X WHERE X final FALSE, X meta FALSE')
-        self.app.publish('view', req)
+        self.app.handle_request(req, 'view')
 
     def test_nonregr_publish2(self):
         req = self.request(u'Any count(N) WHERE N todo_by U, N is Note, U eid %s'
                            % self.user().eid)
-        self.app.publish('view', req)
+        self.app.handle_request(req, 'view')
 
     def test_publish_validation_error(self):
         req = self.request()
@@ -202,7 +202,7 @@
              # just a sample, missing some necessary information for real life
             '__errorurl': 'view?vid=edition...'
             }
-        path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         forminfo = req.session.data['view?vid=edition...']
         eidmap = forminfo['eidmap']
         self.assertEqual(eidmap, {})
@@ -232,7 +232,7 @@
                     # necessary to get validation error handling
                     '__errorurl': 'view?vid=edition...',
                     }
-        path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         forminfo = req.session.data['view?vid=edition...']
         self.assertEqual(set(forminfo['eidmap']), set('XY'))
         self.assertEqual(forminfo['eidmap']['X'], None)
@@ -261,7 +261,7 @@
                     # necessary to get validation error handling
                     '__errorurl': 'view?vid=edition...',
                     }
-        path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         forminfo = req.session.data['view?vid=edition...']
         self.assertEqual(set(forminfo['eidmap']), set('XY'))
         self.assertIsInstance(forminfo['eidmap']['X'], int)
@@ -274,7 +274,7 @@
 
     def _test_cleaned(self, kwargs, injected, cleaned):
         req = self.request(**kwargs)
-        page = self.app.publish('view', req)
+        page = self.app.handle_request(req, 'view')
         self.assertFalse(injected in page, (kwargs, injected))
         self.assertTrue(cleaned in page, (kwargs, cleaned))
 
@@ -308,12 +308,6 @@
         self.commit()
         self.assertEqual(vreg.property_value('ui.language'), 'en')
 
-    def test_login_not_available_to_authenticated(self):
-        req = self.request()
-        with self.assertRaises(Unauthorized) as cm:
-            self.app_publish(req, 'login')
-        self.assertEqual(str(cm.exception), 'log out first')
-
     def test_fb_login_concept(self):
         """see data/views.py"""
         self.set_auth_mode('cookie', 'anon')
@@ -321,7 +315,7 @@
         req = self.request()
         origcnx = req.cnx
         req.form['__fblogin'] = u'turlututu'
-        page = self.app_publish(req)
+        page = self.app.handle_request(req, '')
         self.assertFalse(req.cnx is origcnx)
         self.assertEqual(req.user.login, 'turlututu')
         self.assertTrue('turlututu' in page, page)
@@ -332,25 +326,28 @@
     def test_http_auth_no_anon(self):
         req, origsession = self.init_authentication('http')
         self.assertAuthFailure(req)
-        self.assertRaises(AuthenticationError, self.app_publish, req, 'login')
+        self.assertRaises(AuthenticationError, self.app_handle_request, req, 'login')
         self.assertEqual(req.cnx, None)
         authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword))
         req.set_request_header('Authorization', 'basic %s' % authstr)
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_cookie_auth_no_anon(self):
         req, origsession = self.init_authentication('cookie')
         self.assertAuthFailure(req)
-        form = self.app_publish(req, 'login')
+        try:
+            form = self.app_handle_request(req, 'login')
+        except Redirect, redir:
+            self.fail('anonymous user should get login form')
         self.assertTrue('__login' in form)
         self.assertTrue('__password' in form)
         self.assertEqual(req.cnx, None)
         req.form['__login'] = self.admlogin
         req.form['__password'] = self.admpassword
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_login_by_email(self):
@@ -370,7 +367,7 @@
         req.form['__login'] = address
         req.form['__password'] = self.admpassword
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def _reset_cookie(self, req):
@@ -410,7 +407,7 @@
         authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword))
         req.set_request_header('Authorization', 'basic %s' % authstr)
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_cookie_auth_anon_allowed(self):
@@ -422,7 +419,7 @@
         req.form['__login'] = self.admlogin
         req.form['__password'] = self.admpassword
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_anonymized_request(self):
@@ -441,7 +438,7 @@
         req = self.request()
         # expect a rset with None in [0][0]
         req.form['rql'] = 'rql:Any OV1, X WHERE X custom_workflow OV1?'
-        self.app_publish(req)
+        self.app_handle_request(req)
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_formfields.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_formfields.py	Tue Oct 23 15:00:53 2012 +0200
@@ -147,7 +147,7 @@
     def test_property_key_field(self):
         from cubicweb.web.views.cwproperties import PropertyKeyField
         req = self.request()
-        field = PropertyKeyField()
+        field = PropertyKeyField(name='test')
         e = self.vreg['etypes'].etype_class('CWProperty')(req)
         renderer = self.vreg['formrenderers'].select('base', req)
         form = EntityFieldsForm(req, entity=e)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_http.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,282 @@
+from logilab.common.testlib import TestCase, unittest_main, tag, Tags
+
+from cubicweb.web import StatusResponse
+from cubicweb.devtools.fake import FakeRequest
+
+
+def _test_cache(hin, hout, method='GET'):
+    """forge and process a request
+
+    return status code and the request object
+
+    status is None is no cache is involved
+    """
+    # forge request
+    req = FakeRequest(method=method)
+    for key, value in hin:
+        req._headers_in.addRawHeader(key, str(value))
+    for key, value in hout:
+        req.headers_out.addRawHeader(key, str(value))
+    # process
+    status = None
+    try:
+        req.validate_cache()
+    except StatusResponse, ex:
+        status = ex.status
+    return status, req
+
+class HTTPCache(TestCase):
+    """Check that the http cache logiac work as expected
+    (as far as we understood the RFC)
+
+    """
+    tags = TestCase.tags | Tags('http', 'cache')
+
+
+    def assertCache(self, expected, status, situation=''):
+        """simple assert for nicer message"""
+        if expected != status:
+            if expected is None:
+                expected = "MODIFIED"
+            if status is None:
+                status = "MODIFIED"
+            msg = 'expected %r got %r' % (expected, status)
+            if situation:
+                msg = "%s - when: %s" % (msg, situation)
+            self.fail(msg)
+
+    def test_IN_none_OUT_none(self):
+        #: test that no caching is requested when not data is available
+        #: on any side
+        status, req =_test_cache((),())
+        self.assertIsNone(status)
+
+    def test_IN_Some_OUT_none(self):
+        #: test that no caching is requested when no data is available
+        #: server (origin) side
+        hin = [('if-modified-since','Sat, 14 Apr 2012 14:39:32 GM'),
+              ]
+        status, req = _test_cache(hin, ())
+        self.assertIsNone(status)
+        hin = [('if-none-match','babar/huitre'),
+              ]
+        status, req = _test_cache(hin, ())
+        self.assertIsNone(status)
+        hin = [('if-modified-since','Sat, 14 Apr 2012 14:39:32 GM'),
+               ('if-none-match','babar/huitre'),
+              ]
+        status, req = _test_cache(hin, ())
+        self.assertIsNone(status)
+
+    def test_IN_none_OUT_Some(self):
+        #: test that no caching is requested when no data is provided
+        #: by the client
+        hout = [('last-modified','Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache((), hout)
+        self.assertIsNone(status)
+        hout = [('etag','babar/huitre'),
+               ]
+        status, req = _test_cache((), hout)
+        self.assertIsNone(status)
+        hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+                ('etag','babar/huitre'),
+               ]
+        status, req = _test_cache((), hout)
+        self.assertIsNone(status)
+
+    @tag('last_modified')
+    def test_last_modified_newer(self):
+        #: test the proper behavior of modification date only
+        # newer
+        hin  = [('if-modified-since', 'Sat, 13 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'origin is newer than client')
+
+    @tag('last_modified')
+    def test_last_modified_older(self):
+        # older
+        hin  = [('if-modified-since', 'Sat, 15 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'origin is older than client')
+
+    @tag('last_modified')
+    def test_last_modified_same(self):
+        # same
+        hin  = [('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'origin is equal to client')
+
+    @tag('etag')
+    def test_etag_mismatch(self):
+        #: test the proper behavior of etag only
+        # etag mismatch
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'celestine'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'etag mismatch')
+
+    @tag('etag')
+    def test_etag_match(self):
+        # etag match
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'etag match')
+        # etag match in multiple
+        hin  = [('if-none-match', 'loutre'),
+                ('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'etag match in multiple')
+        # client use "*" as etag
+        hin  = [('if-none-match', '*'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'client use "*" as etag')
+
+    @tag('etag', 'last_modified')
+    def test_both(self):
+        #: test the proper behavior of etag only
+        # both wrong
+        hin  = [('if-none-match', 'babar'),
+                ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('etag', 'loutre'),
+                ('last-modified', 'Sat, 15 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'both wrong')
+
+    @tag('etag', 'last_modified')
+    def test_both_etag_mismatch(self):
+        # both etag mismatch
+        hin  = [('if-none-match', 'babar'),
+                ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('etag', 'loutre'),
+                ('last-modified', 'Sat, 13 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'both  but etag mismatch')
+
+    @tag('etag', 'last_modified')
+    def test_both_but_modified(self):
+        # both but modified
+        hin  = [('if-none-match', 'babar'),
+                ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('etag', 'babar'),
+                ('last-modified', 'Sat, 15 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'both  but modified')
+
+    @tag('etag', 'last_modified')
+    def test_both_ok(self):
+        # both ok
+        hin  = [('if-none-match', 'babar'),
+                ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+               ]
+        hout = [('etag', 'babar'),
+                ('last-modified', 'Sat, 13 Apr 2012 14:39:32 GM'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'both ok')
+
+    @tag('etag', 'HEAD')
+    def test_head_verb(self):
+        #: check than FOUND 200 is properly raise without content on HEAD request
+        #: This logic does not really belong here :-/
+        # modified
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'rhino/really-not-babar'),
+               ]
+        status, req = _test_cache(hin, hout, method='HEAD')
+        self.assertCache(200, status, 'modifier HEAD verb')
+        # not modified
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout, method='HEAD')
+        self.assertCache(304, status, 'not modifier HEAD verb')
+
+    @tag('etag', 'POST')
+    def test_post_verb(self):
+        # modified
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'rhino/really-not-babar'),
+               ]
+        status, req = _test_cache(hin, hout, method='POST')
+        self.assertCache(None, status, 'modifier HEAD verb')
+        # not modified
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout, method='POST')
+        self.assertCache(412, status, 'not modifier HEAD verb')
+
+    @tag('expires')
+    def test_expires_added(self):
+        #: Check that Expires header is added:
+        #: - when the page is modified
+        #: - when none was already present
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'rhino/really-not-babar'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'modifier HEAD verb')
+        value = req.headers_out.getHeader('expires')
+        self.assertIsNotNone(value)
+
+    @tag('expires')
+    def test_expires_not_added(self):
+        #: Check that Expires header is not added if NOT-MODIFIED
+        hin  = [('if-none-match', 'babar'),
+               ]
+        hout = [('etag', 'babar'),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(304, status, 'not modifier HEAD verb')
+        value = req.headers_out.getHeader('expires')
+        self.assertIsNone(value)
+
+    @tag('expires')
+    def test_expires_no_overwrite(self):
+        #: Check that cache does not overwrite existing Expires header
+        hin  = [('if-none-match', 'babar'),
+               ]
+        DATE = 'Sat, 13 Apr 2012 14:39:32 GM'
+        hout = [('etag', 'rhino/really-not-babar'),
+                ('expires', DATE),
+               ]
+        status, req = _test_cache(hin, hout)
+        self.assertCache(None, status, 'not modifier HEAD verb')
+        value = req.headers_out.getRawHeaders('expires')
+        self.assertEqual(value, [DATE])
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/web/test/unittest_magicsearch.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_magicsearch.py	Tue Oct 23 15:00:53 2012 +0200
@@ -230,5 +230,118 @@
         self.assertEqual(rset.rql, 'Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s')
         self.assertEqual(rset.args, {'text': u'utilisateur Smith'})
 
+
+class RQLSuggestionsBuilderTC(CubicWebTC):
+    def suggestions(self, rql):
+        req = self.request()
+        rbs = self.vreg['components'].select('rql.suggestions', req)
+        return rbs.build_suggestions(rql)
+
+    def test_no_restrictions_rql(self):
+        self.assertListEqual([], self.suggestions(''))
+        self.assertListEqual([], self.suggestions('An'))
+        self.assertListEqual([], self.suggestions('Any X'))
+        self.assertListEqual([], self.suggestions('Any X, Y'))
+
+    def test_invalid_rql(self):
+        self.assertListEqual([], self.suggestions('blabla'))
+        self.assertListEqual([], self.suggestions('Any X WHERE foo, bar'))
+
+    def test_is_rql(self):
+        self.assertListEqual(['Any X WHERE X is %s' % eschema
+                              for eschema in sorted(self.vreg.schema.entities())
+                              if not eschema.final],
+                             self.suggestions('Any X WHERE X is'))
+
+        self.assertListEqual(['Any X WHERE X is Personne', 'Any X WHERE X is Project'],
+                             self.suggestions('Any X WHERE X is P'))
+
+        self.assertListEqual(['Any X WHERE X is Personne, Y is Personne',
+                              'Any X WHERE X is Personne, Y is Project'],
+                             self.suggestions('Any X WHERE X is Personne, Y is P'))
+
+
+    def test_relations_rql(self):
+        self.assertListEqual(['Any X WHERE X is Personne, X ass A',
+                              'Any X WHERE X is Personne, X datenaiss A',
+                              'Any X WHERE X is Personne, X description A',
+                              'Any X WHERE X is Personne, X fax A',
+                              'Any X WHERE X is Personne, X nom A',
+                              'Any X WHERE X is Personne, X prenom A',
+                              'Any X WHERE X is Personne, X promo A',
+                              'Any X WHERE X is Personne, X salary A',
+                              'Any X WHERE X is Personne, X sexe A',
+                              'Any X WHERE X is Personne, X tel A',
+                              'Any X WHERE X is Personne, X test A',
+                              'Any X WHERE X is Personne, X titre A',
+                              'Any X WHERE X is Personne, X travaille A',
+                              'Any X WHERE X is Personne, X web A',
+                              ],
+                             self.suggestions('Any X WHERE X is Personne, X '))
+        self.assertListEqual(['Any X WHERE X is Personne, X tel A',
+                              'Any X WHERE X is Personne, X test A',
+                              'Any X WHERE X is Personne, X titre A',
+                              'Any X WHERE X is Personne, X travaille A',
+                              ],
+                             self.suggestions('Any X WHERE X is Personne, X t'))
+        # try completion on selected
+        self.assertListEqual(['Any X WHERE X is Personne, Y is Societe, X tel A',
+                              'Any X WHERE X is Personne, Y is Societe, X test A',
+                              'Any X WHERE X is Personne, Y is Societe, X titre A',
+                              'Any X WHERE X is Personne, Y is Societe, X travaille Y',
+                              ],
+                             self.suggestions('Any X WHERE X is Personne, Y is Societe, X t'))
+        # invalid relation should not break
+        self.assertListEqual([],
+                             self.suggestions('Any X WHERE X is Personne, X asdasd'))
+
+    def test_attribute_vocabulary_rql(self):
+        self.assertListEqual(['Any X WHERE X is Personne, X promo "bon"',
+                              'Any X WHERE X is Personne, X promo "pasbon"',
+                              ],
+                             self.suggestions('Any X WHERE X is Personne, X promo "'))
+        self.assertListEqual(['Any X WHERE X is Personne, X promo "pasbon"',
+                              ],
+                             self.suggestions('Any X WHERE X is Personne, X promo "p'))
+        # "bon" should be considered complete, hence no suggestion
+        self.assertListEqual([],
+                             self.suggestions('Any X WHERE X is Personne, X promo "bon"'))
+        # no valid vocabulary starts with "po"
+        self.assertListEqual([],
+                             self.suggestions('Any X WHERE X is Personne, X promo "po'))
+
+    def test_attribute_value_rql(self):
+        # suggestions should contain any possible value for
+        # a given attribute (limited to 10)
+        req = self.request()
+        for i in xrange(15):
+            req.create_entity('Personne', nom=u'n%s' % i, prenom=u'p%s' % i)
+        self.assertListEqual(['Any X WHERE X is Personne, X nom "n0"',
+                              'Any X WHERE X is Personne, X nom "n1"',
+                              'Any X WHERE X is Personne, X nom "n10"',
+                              'Any X WHERE X is Personne, X nom "n11"',
+                              'Any X WHERE X is Personne, X nom "n12"',
+                              'Any X WHERE X is Personne, X nom "n13"',
+                              'Any X WHERE X is Personne, X nom "n14"',
+                              'Any X WHERE X is Personne, X nom "n2"',
+                              'Any X WHERE X is Personne, X nom "n3"',
+                              'Any X WHERE X is Personne, X nom "n4"',
+                              'Any X WHERE X is Personne, X nom "n5"',
+                              'Any X WHERE X is Personne, X nom "n6"',
+                              'Any X WHERE X is Personne, X nom "n7"',
+                              'Any X WHERE X is Personne, X nom "n8"',
+                              'Any X WHERE X is Personne, X nom "n9"',
+                              ],
+                             self.suggestions('Any X WHERE X is Personne, X nom "'))
+        self.assertListEqual(['Any X WHERE X is Personne, X nom "n1"',
+                              'Any X WHERE X is Personne, X nom "n10"',
+                              'Any X WHERE X is Personne, X nom "n11"',
+                              'Any X WHERE X is Personne, X nom "n12"',
+                              'Any X WHERE X is Personne, X nom "n13"',
+                              'Any X WHERE X is Personne, X nom "n14"',
+                              ],
+                             self.suggestions('Any X WHERE X is Personne, X nom "n1'))
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_reledit.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_reledit.py	Tue Oct 23 15:00:53 2012 +0200
@@ -175,8 +175,8 @@
 
     def setup_database(self):
         super(ClickAndEditFormUICFGTC, self).setup_database()
-        self.tick.set_relations(concerns=self.proj)
-        self.proj.set_relations(manager=self.toto)
+        self.tick.cw_set(concerns=self.proj)
+        self.proj.cw_set(manager=self.toto)
 
     def test_with_uicfg(self):
         old_rctl = reledit_ctrl._tagdefs.copy()
--- a/web/test/unittest_request.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_request.py	Tue Oct 23 15:00:53 2012 +0200
@@ -5,7 +5,9 @@
 
 from functools import partial
 
-from cubicweb.web.request import (_parse_accept_header,
+from cubicweb.devtools.fake import FakeConfig
+
+from cubicweb.web.request import (CubicWebRequestBase, _parse_accept_header,
                                   _mimetype_sort_key, _mimetype_parser, _charset_sort_key)
 
 
@@ -65,5 +67,23 @@
                           ('utf-8', 'utf-8', 0.7),
                           ('*', '*', 0.7)])
 
+    def test_base_url(self):
+        dummy_vreg = type('DummyVreg', (object,), {})()
+        dummy_vreg.config = FakeConfig()
+        dummy_vreg.config['base-url'] = 'http://babar.com/'
+        dummy_vreg.config['https-url'] = 'https://toto.com/'
+
+        req = CubicWebRequestBase(dummy_vreg, https=False)
+        self.assertEqual('http://babar.com/', req.base_url())
+        self.assertEqual('http://babar.com/', req.base_url(False))
+        self.assertEqual('https://toto.com/', req.base_url(True))
+
+        req = CubicWebRequestBase(dummy_vreg, https=True)
+        self.assertEqual('https://toto.com/', req.base_url())
+        self.assertEqual('http://babar.com/', req.base_url(False))
+        self.assertEqual('https://toto.com/', req.base_url(True))
+
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_urlrewrite.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_urlrewrite.py	Tue Oct 23 15:00:53 2012 +0200
@@ -105,9 +105,9 @@
     def setup_database(self):
         req = self.request()
         self.p1 = self.create_user(req, u'user1')
-        self.p1.set_attributes(firstname=u'joe', surname=u'Dalton')
+        self.p1.cw_set(firstname=u'joe', surname=u'Dalton')
         self.p2 = self.create_user(req, u'user2')
-        self.p2.set_attributes(firstname=u'jack', surname=u'Dalton')
+        self.p2.cw_set(firstname=u'jack', surname=u'Dalton')
 
     def test_rgx_action_with_transforms(self):
         class TestSchemaBasedRewriter(SchemaBasedRewriter):
--- a/web/test/unittest_views_basecontrollers.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_views_basecontrollers.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -19,6 +19,12 @@
 
 from __future__ import with_statement
 
+from urlparse import urlsplit, urlunsplit, urljoin
+# parse_qs is deprecated in cgi and has been moved to urlparse in Python 2.6
+try:
+    from urlparse import parse_qs as url_parse_query
+except ImportError:
+    from cgi import parse_qs as url_parse_query
 from logilab.common.testlib import unittest_main, mock_object
 from logilab.common.decorators import monkeypatch
 
@@ -32,6 +38,7 @@
 from cubicweb.web.views.autoform import get_pending_inserts, get_pending_deletes
 from cubicweb.web.views.basecontrollers import JSonController, xhtmlize, jsonize
 from cubicweb.web.views.ajaxcontroller import ajaxfunc, AjaxFunction
+import cubicweb.transaction as tx
 
 u = unicode
 
@@ -70,6 +77,7 @@
                     }
         with self.assertRaises(ValidationError) as cm:
             self.ctrl_publish(req)
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.errors, {'login-subject': 'the value "admin" is already used, use another one'})
 
     def test_user_editing_itself(self):
@@ -89,7 +97,7 @@
             'firstname-subject:'+eid:   u'Sylvain',
             'in_group-subject:'+eid:  groups,
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}).get_entity(0, 0)
         self.assertEqual(e.firstname, u'Sylvain')
         self.assertEqual(e.surname, u'Th\xe9nault')
@@ -108,7 +116,7 @@
             'upassword-subject:'+eid: 'tournicoton',
             'upassword-subject-confirm:'+eid: 'tournicoton',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         cnx.commit() # commit to check we don't get late validation error for instance
         self.assertEqual(path, 'cwuser/user')
         self.assertFalse('vid' in params)
@@ -129,7 +137,7 @@
             'firstname-subject:'+eid: u'Th\xe9nault',
             'surname-subject:'+eid:   u'Sylvain',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}).get_entity(0, 0)
         self.assertEqual(e.login, user.login)
         self.assertEqual(e.firstname, u'Th\xe9nault')
@@ -155,7 +163,7 @@
                     'address-subject:Y': u'dima@logilab.fr',
                     'use_email-object:Y': 'X',
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         # should be redirected on the created person
         self.assertEqual(path, 'cwuser/adim')
         e = self.execute('Any P WHERE P surname "Di Mascio"').get_entity(0, 0)
@@ -177,7 +185,7 @@
                     'address-subject:Y': u'dima@logilab.fr',
                     'use_email-object:Y': peid,
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         # should be redirected on the created person
         self.assertEqual(path, 'cwuser/adim')
         e = self.execute('Any P WHERE P surname "Di Masci"').get_entity(0, 0)
@@ -197,7 +205,7 @@
                     'address-subject:'+emaileid: u'adim@logilab.fr',
                     'use_email-object:'+emaileid: peid,
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         email.cw_clear_all_caches()
         self.assertEqual(email.address, 'adim@logilab.fr')
 
@@ -242,6 +250,7 @@
                 }
         with self.assertRaises(ValidationError) as cm:
             self.ctrl_publish(req)
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.errors, {'amount-subject': 'value -10 must be >= 0'})
         req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
@@ -252,6 +261,7 @@
                     }
         with self.assertRaises(ValidationError) as cm:
             self.ctrl_publish(req)
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.errors, {'amount-subject': 'value 110 must be <= 100'})
         req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
@@ -260,7 +270,7 @@
                     'amount-subject:X': u'10',
                     'described_by_test-subject:X': u(feid),
                     }
-        self.expect_redirect_publish(req, 'edit')
+        self.expect_redirect_handle_request(req, 'edit')
         # should be redirected on the created
         #eid = params['rql'].split()[-1]
         e = self.execute('Salesterm X').get_entity(0, 0)
@@ -272,7 +282,7 @@
         user = self.user()
         req = self.request(**req_form(user))
         req.session.data['pending_insert'] = set([(user.eid, 'in_group', tmpgroup.eid)])
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
         self.assertItemsEqual(usergroups, ['managers', 'test'])
@@ -291,7 +301,7 @@
         # now try to delete the relation
         req = self.request(**req_form(user))
         req.session.data['pending_delete'] = set([(user.eid, 'in_group', groupeid)])
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
         self.assertItemsEqual(usergroups, ['managers'])
@@ -311,7 +321,7 @@
             '__form_id': 'edition',
             '__action_apply': '',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertTrue(path.startswith('blogentry/'))
         eid = path.split('/')[1]
         self.assertEqual(params['vid'], 'edition')
@@ -333,7 +343,7 @@
             '__redirectparams': 'toto=tutu&tata=titi',
             '__form_id': 'edition',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'view')
         self.assertEqual(params['rql'], redirectrql)
         self.assertEqual(params['vid'], 'primary')
@@ -345,7 +355,7 @@
         eid = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
         req.form = {'eid': u(eid), '__type:%s'%eid: 'BlogEntry',
                     '__action_delete': ''}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'blogentry')
         self.assertIn('_cwmsgid', params)
         eid = req.create_entity('EmailAddress', address=u'hop@logilab.fr').eid
@@ -355,7 +365,7 @@
         req = req
         req.form = {'eid': u(eid), '__type:%s'%eid: 'EmailAddress',
                     '__action_delete': ''}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'cwuser/admin')
         self.assertIn('_cwmsgid', params)
         eid1 = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
@@ -365,7 +375,7 @@
                     '__type:%s'%eid1: 'BlogEntry',
                     '__type:%s'%eid2: 'EmailAddress',
                     '__action_delete': ''}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'view')
         self.assertIn('_cwmsgid', params)
 
@@ -381,7 +391,7 @@
                     'title-subject:X': u'entry1-copy',
                     'content-subject:X': u'content1',
                     }
-        self.expect_redirect_publish(req, 'edit')
+        self.expect_redirect_handle_request(req, 'edit')
         blogentry2 = req.find_one_entity('BlogEntry', title=u'entry1-copy')
         self.assertEqual(blogentry2.entry_of[0].eid, blog.eid)
 
@@ -399,7 +409,7 @@
                         'title-subject:X': u'entry1-copy',
                         'content-subject:X': u'content1',
                         }
-            self.expect_redirect_publish(req, 'edit')
+            self.expect_redirect_handle_request(req, 'edit')
             blogentry2 = req.find_one_entity('BlogEntry', title=u'entry1-copy')
             # entry_of should not be copied
             self.assertEqual(len(blogentry2.entry_of), 0)
@@ -425,7 +435,7 @@
             'read_permission-subject:'+cwetypeeid:  groups,
             }
         try:
-            path, params = self.expect_redirect_publish(req, 'edit')
+            path, params = self.expect_redirect_handle_request(req, 'edit')
             e = self.execute('Any X WHERE X eid %(x)s', {'x': cwetypeeid}).get_entity(0, 0)
             self.assertEqual(e.name, 'CWEType')
             self.assertEqual(sorted(g.eid for g in e.read_permission), groupeids)
@@ -445,7 +455,7 @@
             '__type:A': 'BlogEntry', '_cw_entity_fields:A': 'title-subject,content-subject',
             'title-subject:A': u'"13:03:40"',
             'content-subject:A': u'"13:03:43"',}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertTrue(path.startswith('blogentry/'))
         eid = path.split('/')[1]
         e = self.execute('Any C, T WHERE C eid %(x)s, C content T', {'x': eid}).get_entity(0, 0)
@@ -483,7 +493,7 @@
                     'login-subject:X': u'toto',
                     'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto',
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'cwuser/toto')
         e = self.execute('Any X WHERE X is CWUser, X login "toto"').get_entity(0, 0)
         self.assertEqual(e.login, 'toto')
@@ -513,12 +523,12 @@
             #    which fires a Redirect
             # 2/ When re-publishing the copy form, the publisher implicitly commits
             try:
-                self.app_publish(req, 'edit')
+                self.app_handle_request(req, 'edit')
             except Redirect:
                 req = self.request()
                 req.form['rql'] = 'Any X WHERE X eid %s' % p.eid
                 req.form['vid'] = 'copy'
-                self.app_publish(req, 'view')
+                self.app_handle_request(req, 'view')
             rset = self.execute('CWUser P WHERE P surname "Boom"')
             self.assertEqual(len(rset), 0)
         finally:
@@ -688,38 +698,44 @@
         @ajaxfunc
         def foo(self, x, y):
             return 'hello'
-        self.assertTrue(issubclass(foo, AjaxFunction))
-        self.assertEqual(foo.__regid__, 'foo')
-        self.assertEqual(foo.check_pageid, False)
-        self.assertEqual(foo.output_type, None)
+        self.assertEqual(foo(object, 1, 2), 'hello')
+        appobject = foo.__appobject__
+        self.assertTrue(issubclass(appobject, AjaxFunction))
+        self.assertEqual(appobject.__regid__, 'foo')
+        self.assertEqual(appobject.check_pageid, False)
+        self.assertEqual(appobject.output_type, None)
         req = self.request()
-        f = foo(req)
+        f = appobject(req)
         self.assertEqual(f(12, 13), 'hello')
 
     def test_ajaxfunc_checkpageid(self):
-        @ajaxfunc( check_pageid=True)
+        @ajaxfunc(check_pageid=True)
         def foo(self, x, y):
-            pass
-        self.assertTrue(issubclass(foo, AjaxFunction))
-        self.assertEqual(foo.__regid__, 'foo')
-        self.assertEqual(foo.check_pageid, True)
-        self.assertEqual(foo.output_type, None)
+            return 'hello'
+        self.assertEqual(foo(object, 1, 2), 'hello')
+        appobject = foo.__appobject__
+        self.assertTrue(issubclass(appobject, AjaxFunction))
+        self.assertEqual(appobject.__regid__, 'foo')
+        self.assertEqual(appobject.check_pageid, True)
+        self.assertEqual(appobject.output_type, None)
         # no pageid
         req = self.request()
-        f = foo(req)
+        f = appobject(req)
         self.assertRaises(RemoteCallFailed, f, 12, 13)
 
     def test_ajaxfunc_json(self):
         @ajaxfunc(output_type='json')
         def foo(self, x, y):
             return x + y
-        self.assertTrue(issubclass(foo, AjaxFunction))
-        self.assertEqual(foo.__regid__, 'foo')
-        self.assertEqual(foo.check_pageid, False)
-        self.assertEqual(foo.output_type, 'json')
+        self.assertEqual(foo(object, 1, 2), 3)
+        appobject = foo.__appobject__
+        self.assertTrue(issubclass(appobject, AjaxFunction))
+        self.assertEqual(appobject.__regid__, 'foo')
+        self.assertEqual(appobject.check_pageid, False)
+        self.assertEqual(appobject.output_type, 'json')
         # no pageid
         req = self.request()
-        f = foo(req)
+        f = appobject(req)
         self.assertEqual(f(12, 13), '25')
 
 
@@ -768,5 +784,86 @@
         res, req = self.remote_call('foo')
         self.assertEqual(res, '12')
 
+    def test_monkeypatch_jsoncontroller_stdfunc(self):
+        @monkeypatch(JSonController)
+        @jsonize
+        def js_reledit_form(self):
+            return 12
+        res, req = self.remote_call('reledit_form')
+        self.assertEqual(res, '12')
+
+
+class UndoControllerTC(CubicWebTC):
+
+    def setup_database(self):
+        req = self.request()
+        self.session.undo_actions = True
+        self.toto = self.create_user(req, 'toto', password='toto', groups=('users',),
+                                     commit=False)
+        self.txuuid_toto = self.commit()
+        self.toto_email = self.session.create_entity('EmailAddress',
+                                       address=u'toto@logilab.org',
+                                       reverse_use_email=self.toto)
+        self.txuuid_toto_email = self.commit()
+
+    def test_no_such_transaction(self):
+        req = self.request()
+        txuuid = u"12345acbd"
+        req.form['txuuid'] = txuuid
+        controller = self.vreg['controllers'].select('undo', req)
+        with self.assertRaises(tx.NoSuchTransaction) as cm:
+            result = controller.publish(rset=None)
+        self.assertEqual(cm.exception.txuuid, txuuid)
+
+    def assertURLPath(self, url, expected_path, expected_params=None):
+        """ This assert that the path part of `url` matches  expected path
+
+        TODO : implement assertion on the expected_params too
+        """
+        req = self.request()
+        scheme, netloc, path, query, fragment = urlsplit(url)
+        query_dict = url_parse_query(query)
+        expected_url = urljoin(req.base_url(), expected_path)
+        self.assertEqual( urlunsplit((scheme, netloc, path, None, None)), expected_url)
+
+    def test_redirect_redirectpath(self):
+        "Check that the potential __redirectpath is honored"
+        req = self.request()
+        txuuid = self.txuuid_toto_email
+        req.form['txuuid'] = txuuid
+        rpath = "toto"
+        req.form['__redirectpath'] = rpath
+        controller = self.vreg['controllers'].select('undo', req)
+        with self.assertRaises(Redirect) as cm:
+            result = controller.publish(rset=None)
+        self.assertURLPath(cm.exception.location, rpath)
+
+    def test_redirect_default(self):
+        req = self.request()
+        txuuid = self.txuuid_toto_email
+        req.form['txuuid'] = txuuid
+        req.session.data['breadcrumbs'] = [ urljoin(req.base_url(), path)
+                                            for path in ('tata', 'toto',)]
+        controller = self.vreg['controllers'].select('undo', req)
+        with self.assertRaises(Redirect) as cm:
+            result = controller.publish(rset=None)
+        self.assertURLPath(cm.exception.location, 'toto')
+
+
+class LoginControllerTC(CubicWebTC):
+
+    def test_login_with_dest(self):
+        req = self.request()
+        req.form = {'postlogin_path': 'elephants/babar'}
+        with self.assertRaises(Redirect) as cm:
+            self.ctrl_publish(req, ctrl='login')
+        self.assertEqual(req.build_url('elephants/babar'), cm.exception.location)
+
+    def test_login_no_dest(self):
+        req = self.request()
+        with self.assertRaises(Redirect) as cm:
+            self.ctrl_publish(req, ctrl='login')
+        self.assertEqual(req.base_url(), cm.exception.location)
+
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_views_basetemplates.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_views_basetemplates.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -35,6 +35,13 @@
         self.set_option('allow-email-login', 'no')
         self.assertEqual(self._login_labels(), ['login', 'password'])
 
+
+class MainNoTopTemplateTC(CubicWebTC):
+
+    def test_valid_xhtml(self):
+        self.view('index', template='main-no-top')
+
+
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- a/web/test/unittest_views_searchrestriction.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_views_searchrestriction.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_views_staticcontrollers.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,97 @@
+from __future__ import with_statement
+
+from logilab.common.testlib import tag, Tags
+from cubicweb.devtools.testlib import CubicWebTC
+
+import os
+import os.path as osp
+import glob
+
+from cubicweb.utils import HTMLHead
+from cubicweb.web.views.staticcontrollers import ConcatFilesHandler
+
+class StaticControllerCacheTC(CubicWebTC):
+
+    tags = CubicWebTC.tags | Tags('static_controller', 'cache', 'http')
+
+
+    def _publish_static_files(self, url, header={}):
+        req = self.request(headers=header)
+        req._url = url
+        return self.app_handle_request(req, url), req
+
+    def test_static_file_are_cached(self):
+        _, req = self._publish_static_files('data/cubicweb.css')
+        self.assertEqual(200, req.status_out)
+        self.assertIn('last-modified', req.headers_out)
+        next_headers = {
+            'if-modified-since': req.get_response_header('last-modified', raw=True),
+        }
+        _, req = self._publish_static_files('data/cubicweb.css', next_headers)
+        self.assertEqual(304, req.status_out)
+
+
+class ConcatFilesTC(CubicWebTC):
+
+    tags = CubicWebTC.tags | Tags('static_controller', 'concat')
+
+    def tearDown(self):
+        super(ConcatFilesTC, self).tearDown()
+        self._cleanup_concat_cache()
+
+    def _cleanup_concat_cache(self):
+        uicachedir = osp.join(self.config.apphome, 'uicache')
+        for fname in glob.glob(osp.join(uicachedir, 'cache_concat_*')):
+            os.unlink(osp.join(uicachedir, fname))
+
+    def _publish_js_files(self, js_files):
+        req = self.request()
+        head = HTMLHead(req)
+        url = head.concat_urls([req.data_url(js_file) for js_file in js_files])[len(req.base_url()):]
+        req._url = url
+        return self.app_handle_request(req, url), req
+
+    def expected_content(self, js_files):
+        content = u''
+        for js_file in js_files:
+            dirpath, rid = self.config.locate_resource(js_file)
+            if dirpath is not None: # ignore resources not found
+                with open(osp.join(dirpath, rid)) as f:
+                    content += f.read() + '\n'
+        return content
+
+    def test_cache(self):
+        js_files = ('cubicweb.ajax.js', 'jquery.js')
+        result, req = self._publish_js_files(js_files)
+        self.assertNotEqual(404, req.status_out)
+        # check result content
+        self.assertEqual(result, self.expected_content(js_files))
+        # make sure we kept a cached version on filesystem
+        concat_hander = ConcatFilesHandler(self.config)
+        filepath = concat_hander.build_filepath(js_files)
+        self.assertTrue(osp.isfile(filepath))
+
+
+    def test_invalid_file_in_debug_mode(self):
+        js_files = ('cubicweb.ajax.js', 'dummy.js')
+        # in debug mode, an error is raised
+        self.config.debugmode = True
+        try:
+            result, req = self._publish_js_files(js_files)
+            #print result
+            self.assertEqual(404, req.status_out)
+        finally:
+            self.config.debugmode = False
+
+    def test_invalid_file_in_production_mode(self):
+        js_files = ('cubicweb.ajax.js', 'dummy.js')
+        result, req = self._publish_js_files(js_files)
+        self.assertNotEqual(404, req.status_out)
+        # check result content
+        self.assertEqual(result, self.expected_content(js_files))
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
+
--- a/web/test/unittest_viewselector.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_viewselector.py	Tue Oct 23 15:00:53 2012 +0200
@@ -30,7 +30,7 @@
     primary, baseviews, tableview, editforms, calendar, management, embedding,
     actions, startup, cwuser, schema, xbel, vcard, owl, treeview, idownloadable,
     wdoc, debug, cwuser, cwproperties, cwsources, workflow, xmlrss, rdf,
-    csvexport, json)
+    csvexport, json, undohistory)
 
 from cubes.folder import views as folderviews
 
@@ -102,6 +102,7 @@
                               ('siteinfo', debug.SiteInfoView),
                               ('systempropertiesform', cwproperties.SystemCWPropertiesForm),
                               ('tree', folderviews.FolderTreeView),
+                              ('undohistory', undohistory.UndoHistoryView),
                               ])
 
     def test_possible_views_noresult(self):
--- a/web/views/actions.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/actions.py	Tue Oct 23 15:00:53 2012 +0200
@@ -82,6 +82,18 @@
                 return 1
     return 0
 
+class has_undoable_transactions(EntityPredicate):
+    "Select entities having public (i.e. end-user) undoable transactions."
+
+    def score_entity(self, entity):
+        if not entity._cw.vreg.config['undo-enabled']:
+            return 0
+        if entity._cw.cnx.undoable_transactions(eid=entity.eid):
+            return 1
+        else:
+            return 0
+
+
 # generic 'main' actions #######################################################
 
 class SelectAction(action.Action):
@@ -420,6 +432,7 @@
         self._cw.add_js('cubicweb.rhythm.js')
         return 'rhythm'
 
+
 ## default actions ui configuration ###########################################
 
 addmenu = uicfg.actionbox_appearsin_addmenu
--- a/web/views/ajaxcontroller.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/ajaxcontroller.py	Tue Oct 23 15:00:53 2012 +0200
@@ -19,7 +19,7 @@
 # (disable pylint msg for client obj access to protected member as in obj._cw)
 # pylint: disable=W0212
 """The ``ajaxcontroller`` module defines the :class:`AjaxController`
-controller and the ``ajax-funcs`` cubicweb registry.
+controller and the ``ajax-func`` cubicweb registry.
 
 .. autoclass:: cubicweb.web.views.ajaxcontroller.AjaxController
    :members:
@@ -28,7 +28,7 @@
 functions that can be called from the javascript world.
 
 To register a new remote function, either decorate your function
-with the :ref:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator:
+with the :func:`~cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator:
 
 .. sourcecode:: python
 
@@ -39,7 +39,7 @@
     def list_users(self):
         return [u for (u,) in self._cw.execute('Any L WHERE U login L')]
 
-or inherit from :class:`cubicwbe.web.views.ajaxcontroller.AjaxFunction` and
+or inherit from :class:`~cubicweb.web.views.ajaxcontroller.AjaxFunction` and
 implement the ``__call__`` method:
 
 .. sourcecode:: python
@@ -63,6 +63,7 @@
 
 __docformat__ = "restructuredtext en"
 
+from warnings import warn
 from functools import partial
 
 from logilab.common.date import strptime
@@ -114,22 +115,20 @@
             fname = self._cw.form['fname']
         except KeyError:
             raise RemoteCallFailed('no method specified')
+        # 1/ check first for old-style (JSonController) ajax func for bw compat
         try:
-            func = self._cw.vreg['ajax-func'].select(fname, self._cw)
-        except ObjectNotFound:
-            # function not found in the registry, inspect JSonController for
-            # backward compatibility
+            func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
+            func = partial(func, self)
+        except AttributeError:
+            # 2/ check for new-style (AjaxController) ajax func
             try:
-                func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
-                func = partial(func, self)
-            except AttributeError:
+                func = self._cw.vreg['ajax-func'].select(fname, self._cw)
+            except ObjectNotFound:
                 raise RemoteCallFailed('no %s method' % fname)
-            else:
-                self.warning('remote function %s found on JSonController, '
-                             'use AjaxFunction / @ajaxfunc instead', fname)
-        except NoSelectableObject:
-            raise RemoteCallFailed('method %s not available in this context'
-                                   % fname)
+        else:
+            warn('[3.15] remote function %s found on JSonController, '
+                 'use AjaxFunction / @ajaxfunc instead' % fname,
+                 DeprecationWarning, stacklevel=2)
         # no <arg> attribute means the callback takes no argument
         args = self._cw.form.get('arg', ())
         if not isinstance(args, (list, tuple)):
@@ -283,11 +282,21 @@
                 if data is None:
                     raise RemoteCallFailed(self._cw._('pageid-not-found'))
             return self.serialize(implementation(self, *args, **kwargs))
+
     AnAjaxFunc.__name__ = implementation.__name__
     # make sure __module__ refers to the original module otherwise
     # vreg.register(obj) will ignore ``obj``.
     AnAjaxFunc.__module__ = implementation.__module__
-    return AnAjaxFunc
+    # relate the ``implementation`` object to its wrapper appobject
+    # will be used by e.g.:
+    #   import base_module
+    #   @ajaxfunc
+    #   def foo(self):
+    #       return 42
+    #   assert foo(object) == 42
+    #   vreg.register_and_replace(foo, base_module.older_foo)
+    implementation.__appobject__ = AnAjaxFunc
+    return implementation
 
 
 def ajaxfunc(implementation=None, selector=yes(), output_type=None,
--- a/web/views/authentication.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/authentication.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/web/views/basecomponents.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/basecomponents.py	Tue Oct 23 15:00:53 2012 +0200
@@ -59,6 +59,14 @@
         # display multilines query as one line
         rql = rset is not None and rset.printable_rql(encoded=False) or req.form.get('rql', '')
         rql = rql.replace(u"\n", u" ")
+        rql_suggestion_comp = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw)
+        if rql_suggestion_comp is not None:
+            # enable autocomplete feature only if the rql
+            # suggestions builder is available
+            self._cw.add_css('jquery.ui.css')
+            self._cw.add_js(('cubicweb.ajax.js', 'jquery.ui.js'))
+            self._cw.add_onload('$("#rql").autocomplete({source: "%s"});'
+                                % (req.build_url('json', fname='rql_suggest')))
         self.w(u'''<div id="rqlinput" class="%s"><form action="%s"><fieldset>
 <input type="text" id="rql" name="rql" value="%s"  title="%s" tabindex="%s" accesskey="q" class="searchField" />
 ''' % (not self.cw_propval('visible') and 'hidden' or '',
@@ -77,10 +85,10 @@
     __abstract__ = True
     cw_property_defs = component.override_ctx(
         component.CtxComponent,
-        vocabulary=['header-left', 'header-right'])
+        vocabulary=['header-center', 'header-left', 'header-right', ])
     # don't want user to hide this component using an cwproperty
     site_wide = True
-    context = _('header-left')
+    context = _('header-center')
 
 
 class ApplLogo(HeaderComponent):
@@ -88,6 +96,7 @@
     __regid__ = 'logo'
     __select__ = yes() # no need for a cnx
     order = -1
+    context = _('header-left')
 
     def render(self, w):
         w(u'<a href="%s"><img id="logo" src="%s" alt="logo"/></a>'
--- a/web/views/basecontrollers.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/basecontrollers.py	Tue Oct 23 15:00:53 2012 +0200
@@ -27,14 +27,14 @@
 from logilab.common.deprecation import deprecated
 
 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
-                      AuthenticationError, typed_eid)
+                      AuthenticationError, typed_eid, UndoTransactionException)
 from cubicweb.utils import json_dumps
 from cubicweb.predicates import (authenticated_user, anonymous_user,
                                 match_form_params)
 from cubicweb.web import Redirect, RemoteCallFailed
-from cubicweb.web.controller import Controller
+from cubicweb.web.controller import Controller, append_url_params
 from cubicweb.web.views import vid_from_rset
-
+import cubicweb.transaction as tx
 
 @deprecated('[3.15] jsonize is deprecated, use AjaxFunction appobjects instead')
 def jsonize(func):
@@ -84,6 +84,17 @@
             # Cookie authentication
             return self.appli.need_login_content(self._cw)
 
+class LoginControllerForAuthed(Controller):
+    __regid__ = 'login'
+    __select__ = ~anonymous_user()
+
+    def publish(self, rset=None):
+        """log in the instance"""
+        path = self._cw.form.get('postlogin_path', '')
+        # redirect expect an url, not a path. Also path may contains a query
+        # string, hence should not be given to _cw.build_url()
+        raise Redirect(self._cw.base_url() + path)
+
 
 class LogoutController(Controller):
     __regid__ = 'logout'
@@ -179,6 +190,7 @@
 
 def _validation_error(req, ex):
     req.cnx.rollback()
+    ex.tr(req._) # translate messages using ui language
     # XXX necessary to remove existant validation error?
     # imo (syt), it's not necessary
     req.session.data.pop(req.form.get('__errorurl'), None)
@@ -203,7 +215,7 @@
         return (False, _validation_error(req, ex), ctrl._edited_entity)
     except Redirect, ex:
         try:
-            req.cnx.commit() # ValidationError may be raise on commit
+            txuuid = req.cnx.commit() # ValidationError may be raised on commit
         except ValidationError, ex:
             return (False, _validation_error(req, ex), ctrl._edited_entity)
         except Exception, ex:
@@ -211,6 +223,8 @@
             req.exception('unexpected error while validating form')
             return (False, str(ex).decode('utf-8'), ctrl._edited_entity)
         else:
+            if txuuid is not None:
+                req.data['last_undoable_transaction'] = txuuid
             # complete entity: it can be used in js callbacks where we might
             # want every possible information
             if ctrl._edited_entity:
@@ -275,17 +289,17 @@
 
     def publish(self, rset=None):
         txuuid = self._cw.form['txuuid']
-        errors = self._cw.cnx.undo_transaction(txuuid)
-        if not errors:
-            self.redirect()
-        raise ValidationError(None, {None: '\n'.join(errors)})
+        try:
+            self._cw.cnx.undo_transaction(txuuid)
+        except UndoTransactionException, exc:
+            errors = exc.errors
+            #This will cause a rollback in main_publish
+            raise ValidationError(None, {None: '\n'.join(errors)})
+        else :
+            self.redirect() # Will raise Redirect
 
     def redirect(self, msg=None):
         req = self._cw
         msg = msg or req._("transaction undone")
-        breadcrumbs = req.session.data.get('breadcrumbs', None)
-        if breadcrumbs is not None and len(breadcrumbs) > 1:
-            url = req.rebuild_url(breadcrumbs[-2], __message=msg)
-        else:
-            url = req.build_url(__message=msg)
-        raise Redirect(url)
+        self._return_to_lastpage( dict(_cwmsgid= req.set_redirect_message(msg)) )
+
--- a/web/views/basetemplates.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/basetemplates.py	Tue Oct 23 15:00:53 2012 +0200
@@ -23,6 +23,7 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.deprecation import class_renamed
 from logilab.common.registry import objectify_predicate
+from logilab.common.decorators import classproperty
 
 from cubicweb.predicates import match_kwargs, no_cnx, anonymous_user
 from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW, StartupView
@@ -256,10 +257,10 @@
         whead(u'\n'.join(additional_headers) + u'\n')
         self.wview('htmlheader', rset=self.cw_rset)
         w = self.w
-        w(u'<title>%s</title>\n' % xml_escape(page_title))
+        whead(u'<title>%s</title>\n' % xml_escape(page_title))
         w(u'<body>\n')
         w(u'<div id="page">')
-        w(u'<table width="100%" height="100%" border="0"><tr>\n')
+        w(u'<table width="100%" border="0" id="mainLayout"><tr>\n')
         w(u'<td id="navColumnLeft">\n')
         self.topleft_header()
         boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects(
@@ -270,11 +271,7 @@
                 box.render(w=w)
             self.w(u'</div>\n')
         w(u'</td>')
-        w(u'<td id="contentcol" rowspan="2">')
-        w(u'<div id="pageContent">\n')
-        vtitle = self._cw.form.get('vtitle')
-        if vtitle:
-            w(u'<div class="vtitle">%s</div>' % xml_escape(vtitle))
+        w(u'<td id="contentColumn" rowspan="2">')
 
     def topleft_header(self):
         logo = self._cw.vreg['components'].select_or_none('logo', self._cw,
@@ -332,7 +329,9 @@
     __regid__ = 'header'
     main_cell_components = ('appliname', 'breadcrumbs')
     headers = (('headtext', 'header-left'),
-               ('header-right', 'header-right'))
+               ('header-center', 'header-center'),
+               ('header-right', 'header-right')
+               )
 
     def call(self, view, **kwargs):
         self.main_header(view)
@@ -421,26 +420,56 @@
                 comp.render(w=self.w, view=view)
             self.w(u'</div>')
 
+class BaseLogForm(forms.FieldsForm):
+    """Abstract Base login form to be used by any login form
+    """
+    __abstract__ = True
 
-class LogForm(forms.FieldsForm):
     __regid__ = 'logform'
     domid = 'loginForm'
     needs_css = ('cubicweb.login.css',)
-    onclick = "javascript: cw.htmlhelpers.popupLoginBox('%s', '%s');"
+
+    onclick_base = "javascript: cw.htmlhelpers.popupLoginBox('%s', '%s');"
+    onclick_args = (None, None)
+
+    @classproperty
+    def form_buttons(cls):
+        # we use a property because sub class will need to define their own onclick_args.
+        # Therefor we can't juste make the string formating when instanciating this class
+        onclick = cls.onclick_base % cls.onclick_args
+        form_buttons = [fw.SubmitButton(label=_('log in'),
+                                    attrs={'class': 'loginButton'}),
+                        fw.ResetButton(label=_('cancel'),
+                                       attrs={'class': 'loginButton',
+                                              'onclick': onclick}),]
+        ## Can't shortcut next access because __dict__ is a "dictproxy" which 
+        ## does not support items assignement.
+        # cls.__dict__['form_buttons'] = form_buttons
+        return form_buttons
+
+    def form_action(self):
+        if self.action is None:
+            # reuse existing redirection if it exists
+            target = self._cw.form.get('postlogin_path',
+                                       self._cw.relative_path())
+            url_args = {}
+            if target and target != '/':
+                url_args['postlogin_path'] = target
+            return self._cw.build_url('login', __secure__=True, **url_args)
+        return super(LogForm, self).form_action()
+
+class LogForm(BaseLogForm):
+    """Simple login form that send username and password
+    """
+    __regid__ = 'logform'
+    domid = 'loginForm'
+    needs_css = ('cubicweb.login.css',)
     # XXX have to recall fields name since python is mangling __login/__password
     __login = ff.StringField('__login', widget=fw.TextInput({'class': 'data'}))
     __password = ff.StringField('__password', label=_('password'),
                                 widget=fw.PasswordSingleInput({'class': 'data'}))
-    form_buttons = [fw.SubmitButton(label=_('log in'),
-                                    attrs={'class': 'loginButton'}),
-                    fw.ResetButton(label=_('cancel'),
-                                   attrs={'class': 'loginButton',
-                                          'onclick': onclick % ('popupLoginBox', '__login')}),]
 
-    def form_action(self):
-        if self.action is None:
-            return login_form_url(self._cw)
-        return super(LogForm, self).form_action()
+    onclick_args =  ('popupLoginBox', '__login')
 
 
 class LogFormView(View):
@@ -486,12 +515,3 @@
         cw.html_headers.add_onload('jQuery("#__login:visible").focus()')
 
 LogFormTemplate = class_renamed('LogFormTemplate', LogFormView)
-
-
-def login_form_url(req):
-    if req.https:
-        return req.url()
-    httpsurl = req.vreg.config.get('https-url')
-    if httpsurl:
-        return req.url().replace(req.base_url(), httpsurl)
-    return req.url()
--- a/web/views/baseviews.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/baseviews.py	Tue Oct 23 15:00:53 2012 +0200
@@ -173,9 +173,9 @@
 class OutOfContextView(EntityView):
     """:__regid__: *outofcontext*
 
-    This view is used whenthe entity should be considered as displayed out of
-    its context. By default it produces the result of ``entity.dc_long_title()`` wrapped
-    in a link leading to the primary view of the entity.
+    This view is used when the entity should be considered as displayed out of
+    its context. By default it produces the result of ``entity.dc_long_title()``
+    wrapped in a link leading to the primary view of the entity.
     """
     __regid__ = 'outofcontext'
 
@@ -612,18 +612,18 @@
     def group_key(self, entity, **kwargs):
         value = super(AuthorView, self).group_key(entity, **kwargs)
         if value:
-            return value.login
-        return value
+            return (value.name(), value.login)
+        return (None, None)
 
     def index_link(self, basepath, key, items):
-        label = u'%s [%s]' % (key, len(items))
+        label = u'%s [%s]' % (key[0], len(items))
         etypes = set(entity.__regid__ for entity in items)
         vtitle = self._cw._('%(etype)s by %(author)s') % {
             'etype': ', '.join(display_name(self._cw, etype, 'plural')
                                for etype in etypes),
             'author': label}
-        url = self.index_url(basepath, key, vtitle=vtitle)
-        title = self._cw._('archive for %(author)s') % {'author': key}
+        url = self.index_url(basepath, key[1], vtitle=vtitle)
+        title = self._cw._('archive for %(author)s') % {'author': key[0]}
         return tags.a(label, href=url, title=title)
 
 
--- a/web/views/boxes.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/boxes.py	Tue Oct 23 15:00:53 2012 +0200
@@ -48,17 +48,17 @@
 BoxTemplate = box.BoxTemplate
 BoxHtml = htmlwidgets.BoxHtml
 
-class EditBox(component.CtxComponent): # XXX rename to ActionsBox
+class EditBox(component.CtxComponent):
     """
     box with all actions impacting the entity displayed: edit, copy, delete
     change state, add related entities...
     """
     __regid__ = 'edit_box'
-    __select__ = component.CtxComponent.__select__ & non_final_entity()
 
     title = _('actions')
     order = 2
     contextual = True
+    __select__ = component.CtxComponent.__select__ & non_final_entity()
 
     def init_rendering(self):
         super(EditBox, self).init_rendering()
--- a/web/views/calendar.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/calendar.py	Tue Oct 23 15:00:53 2012 +0200
@@ -178,42 +178,31 @@
 
     fullcalendar_options = {
         'firstDay': 1,
+        'firstHour': 8,
+        'defaultView': 'month',
+        'editable': True,
         'header': {'left': 'prev,next today',
                    'center': 'title',
                    'right': 'month,agendaWeek,agendaDay',
                    },
-        'editable': True,
-        'defaultView': 'month',
-        'timeFormat': {'month': '',
-                       '': 'H:mm'},
-        'firstHour': 8,
-        'axisFormat': 'H:mm',
-        'columnFormat': {'month': 'dddd',
-                         'agendaWeek': 'dddd yyyy/M/dd',
-                         'agendaDay': 'dddd yyyy/M/dd'}
         }
 
-
     def call(self):
         self._cw.demote_to_html()
         self._cw.add_css(('fullcalendar.css', 'cubicweb.calendar.css'))
-        self._cw.add_js(('jquery.ui.js', 'fullcalendar.min.js', 'jquery.qtip.min.js'))
+        self._cw.add_js(('jquery.ui.js', 'fullcalendar.min.js', 'jquery.qtip.min.js', 'fullcalendar.locale.js'))
         self.calendar_id = 'cal' + make_uid('uid')
         self.add_onload()
         # write calendar div to load jquery fullcalendar object
         self.w(u'<div id="%s"></div>' % self.calendar_id)
 
-
     def add_onload(self):
         fullcalendar_options = self.fullcalendar_options.copy()
         fullcalendar_options['events'] = self.get_events()
-        fullcalendar_options['buttonText'] = {'today': self._cw._('today'),
-                                              'month': self._cw._('month'),
-                                              'week': self._cw._('week'),
-                                              'day': self._cw._('day')}
+        # i18n
         # js callback to add a tooltip and to put html in event's title
         js = """
-        var options = %s;
+        var options = $.fullCalendar.regional('%s', %s);
         options.eventRender = function(event, $element) {
           // add a tooltip for each event
           var div = '<div class="tooltip">'+ event.description+ '</div>';
@@ -223,8 +212,7 @@
         };
         $("#%s").fullCalendar(options);
         """ #"
-        self._cw.add_onload(js % (json_dumps(fullcalendar_options), self.calendar_id))
-
+        self._cw.add_onload(js % (self._cw.lang, json_dumps(fullcalendar_options), self.calendar_id))
 
     def get_events(self):
         events = []
--- a/web/views/cwsources.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/cwsources.py	Tue Oct 23 15:00:53 2012 +0200
@@ -373,8 +373,9 @@
         w(u'<label>%s</label>' % self._cw._(u'Message threshold'))
         w(u'<select class="log_filter" onchange="filterLog(\'%s\', this.options[this.selectedIndex].value)">'
           % self.view.domid)
-        for level in ('Debug', 'Info', 'Warning', 'Error', 'Fatal'):
-            w('<option value="%s">%s</option>' % (level, self._cw._(level)))
+        for level in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'FATAL'):
+            w('<option value="%s">%s</option>' % (level.capitalize(),
+                                                  self._cw._(level)))
         w(u'</select>')
         w(u'</fieldset></form>')
         super(LogTableLayout, self).render_table(w, actions, paginate)
@@ -421,7 +422,9 @@
     class URLRenderer(pyviews.PyValTableColRenderer):
         def render_cell(self, w, rownum):
             url = self.data[rownum][1]
-            w(url and tags.a(url, href=url) or u'&#160;')
+            if url and url.startswith('http'):
+                url = tags.a(url, href=url)
+            w(url or u'&#160;')
 
     class LineRenderer(pyviews.PyValTableColRenderer):
         def render_cell(self, w, rownum):
--- a/web/views/editcontroller.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/editcontroller.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/web/views/facets.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/facets.py	Tue Oct 23 15:00:53 2012 +0200
@@ -26,6 +26,7 @@
 from logilab.common.decorators import cachedproperty
 from logilab.common.registry import objectify_predicate, yes
 
+from cubicweb import tags
 from cubicweb.predicates import (non_final_entity, multi_lines_rset,
                                  match_context_prop, relation_possible)
 from cubicweb.utils import json_dumps
@@ -234,6 +235,7 @@
             vid = req.form.get('vid')
         if self.bk_linkbox_template and req.vreg.schema['Bookmark'].has_perm(req, 'add'):
             w(self.bookmark_link(rset))
+        w(self.focus_link(rset))
         hiddens = {}
         for param in ('subvid', 'vtitle'):
             if param in req.form:
@@ -269,6 +271,9 @@
                 req._('bookmark this search'))
         return self.bk_linkbox_template % bk_link
 
+    def focus_link(self, rset):
+        return self.bk_linkbox_template % tags.a(self._cw._('focus on this selection'),
+                                                 href=self._cw.url(), id='focusLink')
 
 class FilterTable(FacetFilterMixIn, AnyRsetView):
     __regid__ = 'facet.filtertable'
--- a/web/views/formrenderers.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/formrenderers.py	Tue Oct 23 15:00:53 2012 +0200
@@ -117,7 +117,9 @@
         errormsg = self.error_message(form)
         if errormsg:
             data.insert(0, errormsg)
-        w(''.join(data))
+        # NOTE: we call unicode because `tag` objects may be found within data
+        #       e.g. from the cwtags library
+        w(''.join(unicode(x) for x in data))
 
     def render_content(self, w, form, values):
         if self.display_progress_div:
@@ -492,9 +494,30 @@
     entity's form.
     """
     __regid__ = 'inline'
+    fieldset_css_class = 'subentity'
+
+    def render_title(self, w, form, values):
+        w(u'<div class="iformTitle">')
+        w(u'<span>%(title)s</span> '
+          '#<span class="icounter">%(counter)s</span> ' % values)
+        if values['removejs']:
+            values['removemsg'] = self._cw._('remove-inlined-entity-form')
+            w(u'[<a href="javascript: %(removejs)s;$.noop();">%(removemsg)s</a>]'
+              % values)
+        w(u'</div>')
 
     def render(self, w, form, values):
         form.add_media()
+        self.open_form(w, form, values)
+        self.render_title(w, form, values)
+        # XXX that stinks
+        # cleanup values
+        for key in ('title', 'removejs', 'removemsg'):
+            values.pop(key, None)
+        self.render_fields(w, form, values)
+        self.close_form(w, form, values)
+
+    def open_form(self, w, form, values):
         try:
             w(u'<div id="div-%(divid)s" onclick="%(divonclick)s">' % values)
         except KeyError:
@@ -503,29 +526,15 @@
             w(u'<div id="notice-%s" class="notice">%s</div>' % (
                 values['divid'], self._cw._('click on the box to cancel the deletion')))
         w(u'<div class="iformBody">')
-        eschema = form.edited_entity.e_schema
-        if values['removejs']:
-            values['removemsg'] = self._cw._('remove-inlined-entity-form')
-            w(u'<div class="iformTitle"><span>%(title)s</span> '
-              '#<span class="icounter">%(counter)s</span> '
-              '[<a href="javascript: %(removejs)s;$.noop();">%(removemsg)s</a>]</div>'
-              % values)
-        else:
-            w(u'<div class="iformTitle"><span>%(title)s</span> '
-              '#<span class="icounter">%(counter)s</span></div>'
-              % values)
-        # XXX that stinks
-        # cleanup values
-        for key in ('title', 'removejs', 'removemsg'):
-            values.pop(key, None)
-        self.render_fields(w, form, values)
+
+    def close_form(self, w, form, values):
         w(u'</div></div>')
 
     def render_fields(self, w, form, values):
         w(u'<fieldset id="fs-%(divid)s">' % values)
         fields = self._render_hidden_fields(w, form)
         w(u'</fieldset>')
-        w(u'<fieldset class="subentity">')
+        w(u'<fieldset class="%s">' % self.fieldset_css_class)
         if fields:
             self._render_fields(fields, w, form)
         self.render_child_forms(w, form, values)
--- a/web/views/magicsearch.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/magicsearch.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,19 +15,23 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""a query processor to handle quick search shortcuts for cubicweb"""
+"""a query processor to handle quick search shortcuts for cubicweb
+"""
 
 __docformat__ = "restructuredtext en"
 
 import re
 from logging import getLogger
-from warnings import warn
+
+from yams.interfaces import IVocabularyConstraint
 
 from rql import RQLSyntaxError, BadRQLQuery, parse
+from rql.utils import rqlvar_maker
 from rql.nodes import Relation
 
 from cubicweb import Unauthorized, typed_eid
 from cubicweb.view import Component
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
 
 LOGGER = getLogger('cubicweb.magicsearch')
 
@@ -408,3 +412,247 @@
             # explicitly specified processor: don't try to catch the exception
             return proc.process_query(uquery)
         raise BadRQLQuery(self._cw._('sorry, the server is unable to handle this query'))
+
+
+
+## RQL suggestions builder ####################################################
+class RQLSuggestionsBuilder(Component):
+    """main entry point is `build_suggestions()` which takes
+    an incomplete RQL query and returns a list of suggestions to complete
+    the query.
+
+    This component is enabled by default and is used to provide autocompletion
+    in the RQL search bar. If you don't want this feature in your application,
+    just unregister it or make it unselectable.
+
+    .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.build_suggestions
+    .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.etypes_suggestion_set
+    .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_etypes
+    .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_relations
+    .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.vocabulary
+    """
+    __regid__ = 'rql.suggestions'
+
+    #: maximum number of results to fetch when suggesting attribute values
+    attr_value_limit = 20
+
+    def build_suggestions(self, user_rql):
+        """return a list of suggestions to complete `user_rql`
+
+        :param user_rql: an incomplete RQL query
+        """
+        req = self._cw
+        try:
+            if 'WHERE' not in user_rql: # don't try to complete if there's no restriction
+                return []
+            variables, restrictions = [part.strip() for part in user_rql.split('WHERE', 1)]
+            if ',' in restrictions:
+                restrictions, incomplete_part = restrictions.rsplit(',', 1)
+                user_rql = '%s WHERE %s' % (variables, restrictions)
+            else:
+                restrictions, incomplete_part = '', restrictions
+                user_rql = variables
+            select = parse(user_rql, print_errors=False).children[0]
+            req.vreg.rqlhelper.annotate(select)
+            req.vreg.solutions(req, select, {})
+            if restrictions:
+                return ['%s, %s' % (user_rql, suggestion)
+                        for suggestion in self.rql_build_suggestions(select, incomplete_part)]
+            else:
+                return ['%s WHERE %s' % (user_rql, suggestion)
+                        for suggestion in self.rql_build_suggestions(select, incomplete_part)]
+        except Exception, exc: # we never want to crash
+            self.debug('failed to build suggestions: %s', exc)
+            return []
+
+    ## actual completion entry points #########################################
+    def rql_build_suggestions(self, select, incomplete_part):
+        """
+        :param select: the annotated select node (rql syntax tree)
+        :param incomplete_part: the part of the rql query that needs
+                                to be completed, (e.g. ``X is Pr``, ``X re``)
+        """
+        chunks = incomplete_part.split(None, 2)
+        if not chunks: # nothing to complete
+            return []
+        if len(chunks) == 1: # `incomplete` looks like "MYVAR"
+            return self._complete_rqlvar(select, *chunks)
+        elif len(chunks) == 2: # `incomplete` looks like "MYVAR some_rel"
+            return self._complete_rqlvar_and_rtype(select, *chunks)
+        elif len(chunks) == 3: # `incomplete` looks like "MYVAR some_rel something"
+            return self._complete_relation_object(select, *chunks)
+        else: # would be anything else, hard to decide what to do here
+            return []
+
+    # _complete_* methods are considered private, at least while the API
+    # isn't stabilized.
+    def _complete_rqlvar(self, select, rql_var):
+        """return suggestions for "variable only" incomplete_part
+
+        as in :
+
+        - Any X WHERE X
+        - Any X WHERE X is Project, Y
+        - etc.
+        """
+        return ['%s %s %s' % (rql_var, rtype, dest_var)
+                for rtype, dest_var in self.possible_relations(select, rql_var)]
+
+    def _complete_rqlvar_and_rtype(self, select, rql_var, user_rtype):
+        """return suggestions for "variable + rtype" incomplete_part
+
+        as in :
+
+        - Any X WHERE X is
+        - Any X WHERE X is Person, X firstn
+        - etc.
+        """
+        # special case `user_type` == 'is', return every possible type.
+        if user_rtype == 'is':
+            return self._complete_is_relation(select, rql_var)
+        else:
+            return ['%s %s %s' % (rql_var, rtype, dest_var)
+                    for rtype, dest_var in self.possible_relations(select, rql_var)
+                    if rtype.startswith(user_rtype)]
+
+    def _complete_relation_object(self, select, rql_var, user_rtype, user_value):
+        """return suggestions for "variable + rtype + some_incomplete_value"
+
+        as in :
+
+        - Any X WHERE X is Per
+        - Any X WHERE X is Person, X firstname "
+        - Any X WHERE X is Person, X firstname "Pa
+        - etc.
+        """
+        # special case `user_type` == 'is', return every possible type.
+        if user_rtype == 'is':
+            return self._complete_is_relation(select, rql_var, user_value)
+        elif user_value:
+            if user_value[0] in ('"', "'"):
+                # if finished string, don't suggest anything
+                if len(user_value) > 1 and user_value[-1] == user_value[0]:
+                    return []
+                user_value = user_value[1:]
+                return ['%s %s "%s"' % (rql_var, user_rtype, value)
+                        for value in self.vocabulary(select, rql_var,
+                                                     user_rtype, user_value)]
+        return []
+
+    def _complete_is_relation(self, select, rql_var, prefix=''):
+        """return every possible types for rql_var
+
+        :param prefix: if specified, will only return entity types starting
+                       with the specified value.
+        """
+        return ['%s is %s' % (rql_var, etype)
+                for etype in self.possible_etypes(select, rql_var, prefix)]
+
+    def etypes_suggestion_set(self):
+        """returns the list of possible entity types to suggest
+
+        The default is to return any non-final entity type available
+        in the schema.
+
+        Can be overridden for instance if an application decides
+        to restrict this list to a meaningful set of business etypes.
+        """
+        schema = self._cw.vreg.schema
+        return set(eschema.type for eschema in schema.entities() if not eschema.final)
+
+    def possible_etypes(self, select, rql_var, prefix=''):
+        """return all possible etypes for `rql_var`
+
+        The returned list will always be a subset of meth:`etypes_suggestion_set`
+
+        :param select: the annotated select node (rql syntax tree)
+        :param rql_var: the variable name for which we want to know possible types
+        :param prefix: if specified, will only return etypes starting with it
+        """
+        available_etypes = self.etypes_suggestion_set()
+        possible_etypes = set()
+        for sol in select.solutions:
+            if rql_var in sol and sol[rql_var] in available_etypes:
+                possible_etypes.add(sol[rql_var])
+        if not possible_etypes:
+            # `Any X WHERE X is Person, Y is`
+            # -> won't have a solution, need to give all etypes
+            possible_etypes = available_etypes
+        return sorted(etype for etype in possible_etypes if etype.startswith(prefix))
+
+    def possible_relations(self, select, rql_var, include_meta=False):
+        """returns a list of couple (rtype, dest_var) for each possible
+        relations with `rql_var` as subject.
+
+        ``dest_var`` will be picked among availabel variables if types match,
+        otherwise a new one will be created.
+        """
+        schema = self._cw.vreg.schema
+        relations = set()
+        untyped_dest_var = rqlvar_maker(defined=select.defined_vars).next()
+        # for each solution
+        # 1. find each possible relation
+        # 2. for each relation:
+        #    2.1. if the relation is meta, skip it
+        #    2.2. for each possible destination type, pick up possible
+        #         variables for this type or use a new one
+        for sol in select.solutions:
+            etype = sol[rql_var]
+            sol_by_types = {}
+            for varname, var_etype in sol.items():
+                # don't push subject var to avoid "X relation X" suggestion
+                if varname != rql_var:
+                    sol_by_types.setdefault(var_etype, []).append(varname)
+            for rschema in schema[etype].subject_relations():
+                if include_meta or not rschema.meta:
+                    for dest in rschema.objects(etype):
+                        for varname in sol_by_types.get(dest.type, (untyped_dest_var,)):
+                            suggestion = (rschema.type, varname)
+                            if suggestion not in relations:
+                                relations.add(suggestion)
+        return sorted(relations)
+
+    def vocabulary(self, select, rql_var, user_rtype, rtype_incomplete_value):
+        """return acceptable vocabulary for `rql_var` + `user_rtype` in `select`
+
+        Vocabulary is either found from schema (Yams) definition or
+        directly from database.
+        """
+        schema = self._cw.vreg.schema
+        vocab = []
+        for sol in select.solutions:
+            # for each solution :
+            # - If a vocabulary constraint exists on `rql_var+user_rtype`, use it
+            #   to define possible values
+            # - Otherwise, query the database to fetch available values from
+            #   database (limiting results to `self.attr_value_limit`)
+            try:
+                eschema = schema.eschema(sol[rql_var])
+                rdef = eschema.rdef(user_rtype)
+            except KeyError: # unknown relation
+                continue
+            cstr = rdef.constraint_by_interface(IVocabularyConstraint)
+            if cstr is not None:
+                # a vocabulary is found, use it
+                vocab += [value for value in cstr.vocabulary()
+                          if value.startswith(rtype_incomplete_value)]
+            elif rdef.final:
+                # no vocab, query database to find possible value
+                vocab_rql = 'DISTINCT Any V LIMIT %s WHERE X is %s, X %s V' % (
+                    self.attr_value_limit, eschema.type, user_rtype)
+                vocab_kwargs = {}
+                if rtype_incomplete_value:
+                    vocab_rql += ', X %s LIKE %%(value)s' % user_rtype
+                    vocab_kwargs['value'] = '%s%%' % rtype_incomplete_value
+                vocab += [value for value, in
+                          self._cw.execute(vocab_rql, vocab_kwargs)]
+        return sorted(set(vocab))
+
+
+
+@ajaxfunc(output_type='json')
+def rql_suggest(self):
+    rql_builder = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw)
+    if rql_builder:
+        return rql_builder.build_suggestions(self._cw.form['term'])
+    return []
--- a/web/views/navigation.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/navigation.py	Tue Oct 23 15:00:53 2012 +0200
@@ -364,11 +364,13 @@
 
     @property
     def prev_icon(self):
-        return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_prev.png'))
+        return '<img src="%s" alt="%s" />' % (
+            xml_escape(self._cw.data_url('go_prev.png')), self._cw._('previous page'))
 
     @property
     def next_icon(self):
-        return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_next.png'))
+        return '<img src="%s" alt="%s" />' % (
+            xml_escape(self._cw.data_url('go_next.png')), self._cw._('next page'))
 
     def init_rendering(self):
         adapter = self.entity.cw_adapt_to('IPrevNext')
--- a/web/views/sessions.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/sessions.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -95,18 +95,6 @@
         #      reopening. Is it actually a problem?
         if 'last_login_time' in req.vreg.schema:
             self._update_last_login_time(req)
-        args = req.form
-        for forminternal_key in ('__form_id', '__domid', '__errorurl'):
-            args.pop(forminternal_key, None)
-        path = req.relative_path(False)
-        if path in ('login', 'logout') or req.form.get('vid') == 'loggedout':
-            path = 'view'
-            args['__message'] = req._('welcome %s !') % req.user.login
-            if 'vid' in req.form and req.form['vid'] != 'loggedout':
-                args['vid'] = req.form['vid']
-            if 'rql' in req.form:
-                args['rql'] = req.form['rql']
-            raise Redirect(req.build_url(path, **args))
         req.set_message(req._('welcome %s !') % req.user.login)
 
     def _update_last_login_time(self, req):
--- a/web/views/startup.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/startup.py	Tue Oct 23 15:00:53 2012 +0200
@@ -53,6 +53,7 @@
     add_etype_links = ()
     skip_startup_views = set( ('index', 'manage', 'schema', 'owl', 'changelog',
                                'systempropertiesform', 'propertiesform',
+                               'loggedout', 'login',
                                'cw.users-and-groups-management', 'cw.groups-management', 
                                'cw.users-management', 'cw.sources-management',
                                'siteinfo', 'info', 'registry', 'gc',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/staticcontrollers.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,250 @@
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""Set of static resources controllers for :
+
+- /data/...
+- /static/...
+- /fckeditor/...
+
+"""
+from __future__ import with_statement
+
+import os
+import os.path as osp
+import hashlib
+import mimetypes
+from time import mktime
+from datetime import datetime, timedelta
+from logging import getLogger
+
+from cubicweb import Unauthorized
+from cubicweb.web import NotFound
+from cubicweb.web.http_headers import generateDateTime
+from cubicweb.web.controller import Controller
+from cubicweb.web.views.urlrewrite import URLRewriter
+
+
+
+class StaticFileController(Controller):
+    """an abtract class to serve static file
+
+    Make sure to add your subclass to the STATIC_CONTROLLERS list"""
+    __abstract__ = True
+    directory_listing_allowed = False
+
+    def max_age(self, path):
+        """max cache TTL"""
+        return 60*60*24*7
+
+    def static_file(self, path):
+        """Return full content of a static file.
+
+        XXX iterable content would be better
+        """
+        debugmode = self._cw.vreg.config.debugmode
+        if osp.isdir(path):
+            if self.directory_listing_allowed:
+                return u''
+            raise Unauthorized(path)
+        if not osp.isfile(path):
+            raise NotFound()
+        if not debugmode:
+            # XXX: Don't provide additional resource information to error responses
+            #
+            # the HTTP RFC recommands not going further than 1 year ahead
+            expires = datetime.now() + timedelta(days=6*30)
+            self._cw.set_header('Expires', generateDateTime(mktime(expires.timetuple())))
+
+        # XXX system call to os.stats could be cached once and for all in
+        # production mode (where static files are not expected to change)
+        #
+        # Note that: we do a osp.isdir + osp.isfile before and a potential
+        # os.read after. Improving this specific call will not help
+        #
+        # Real production environment should use dedicated static file serving.
+        self._cw.set_header('last-modified', generateDateTime(os.stat(path).st_mtime))
+        self._cw.validate_cache()
+        # XXX elif uri.startswith('/https/'): uri = uri[6:]
+        mimetype, encoding = mimetypes.guess_type(path)
+        if mimetype is None:
+            mimetype = 'application/octet-stream'
+        self._cw.set_content_type(mimetype, osp.basename(path), encoding)
+        with open(path, 'rb') as resource:
+            return resource.read()
+
+    @property
+    def relpath(self):
+        """path of a requested file relative to the controller"""
+        path = self._cw.form.get('static_relative_path')
+        if path is None:
+            path = self._cw.relative_path(includeparams=True)
+        return path
+
+
+class ConcatFilesHandler(object):
+    """Emulating the behavior of modconcat
+
+    this serve multiple file as a single one.
+    """
+
+    def __init__(self, config):
+        self._resources = {}
+        self.config = config
+        self.logger = getLogger('cubicweb.web')
+
+    def _resource(self, path):
+        """get the resouce"""
+        try:
+            return self._resources[path]
+        except KeyError:
+            self._resources[path] = self.config.locate_resource(path)
+            return self._resources[path]
+
+    def _up_to_date(self, filepath, paths):
+        """
+        The concat-file is considered up-to-date if it exists.
+        In debug mode, an additional check is performed to make sure that
+        concat-file is more recent than all concatenated files
+        """
+        if not osp.isfile(filepath):
+            return False
+        if self.config.debugmode:
+            concat_lastmod = os.stat(filepath).st_mtime
+            for path in paths:
+                dirpath, rid = self._resource(path)
+                if rid is None:
+                    raise NotFound(path)
+                path = osp.join(dirpath, rid)
+                if os.stat(path).st_mtime > concat_lastmod:
+                    return False
+        return True
+
+    def build_filepath(self, paths):
+        """return the filepath that will be used to cache concatenation of `paths`
+        """
+        _, ext = osp.splitext(paths[0])
+        fname = 'cache_concat_' + hashlib.md5(';'.join(paths)).hexdigest() + ext
+        return osp.join(self.config.appdatahome, 'uicache', fname)
+
+    def concat_cached_filepath(self, paths):
+        filepath = self.build_filepath(paths)
+        if not self._up_to_date(filepath, paths):
+            with open(filepath, 'wb') as f:
+                for path in paths:
+                    dirpath, rid = self._resource(path)
+                    if rid is None:
+                        # In production mode log an error, do not return a 404
+                        # XXX the erroneous content is cached anyway
+                        self.logger.error('concatenated data url error: %r file '
+                                          'does not exist', path)
+                        if self.config.debugmode:
+                            raise NotFound(path)
+                    else:
+                        with open(osp.join(dirpath, rid), 'rb') as source:
+                            for line in source:
+                                f.write(line)
+                        f.write('\n')
+        return filepath
+
+
+class DataController(StaticFileController):
+    """Controller in charge of serving static file in /data/
+
+    Handle modeconcat like url.
+    """
+
+    __regid__ = 'data'
+
+    def __init__(self, *args, **kwargs):
+        super(DataController, self).__init__(*args, **kwargs)
+        config = self._cw.vreg.config
+        md5_version = config.instance_md5_version()
+        self.base_datapath = config.data_relpath()
+        self.data_modconcat_basepath = '%s??' % self.base_datapath
+        self.concat_files_registry = ConcatFilesHandler(config)
+
+    def publish(self, rset=None):
+        config = self._cw.vreg.config
+        # includeparams=True for modconcat-like urls
+        relpath = self.relpath
+        if relpath.startswith(self.data_modconcat_basepath):
+            paths = relpath[len(self.data_modconcat_basepath):].split(',')
+            filepath = self.concat_files_registry.concat_cached_filepath(paths)
+        else:
+            # skip leading '/data/' and url params
+            relpath = relpath[len(self.base_datapath):].split('?', 1)[0]
+            dirpath, rid = config.locate_resource(relpath)
+            if dirpath is None:
+                raise NotFound()
+            filepath = osp.join(dirpath, rid)
+        return self.static_file(filepath)
+
+
+class FCKEditorController(StaticFileController):
+    """Controller in charge of serving FCKEditor related file
+
+    The motivational for a dedicated controller have been lost.
+    """
+
+    __regid__ = 'fckeditor'
+
+    def publish(self, rset=None):
+        config = self._cw.vreg.config
+        if self._cw.https:
+            uiprops = config.https_uiprops
+        else:
+            uiprops = config.uiprops
+        relpath = self.relpath
+        if relpath.startswith('fckeditor/'):
+            relpath = relpath[len('fckeditor/'):]
+        relpath = relpath.split('?', 1)[0]
+        return self.static_file(osp.join(uiprops['FCKEDITOR_PATH'], relpath))
+
+
+class StaticDirectoryController(StaticFileController):
+    """Controller in charge of serving static file in /static/
+    """
+    __regid__ = 'static'
+
+    def publish(self, rset=None):
+        staticdir = self._cw.vreg.config.static_directory
+        relpath = self.relpath
+        return self.static_file(osp.join(staticdir, relpath))
+
+STATIC_CONTROLLERS = [DataController, FCKEditorController,
+                      StaticDirectoryController]
+
+class StaticControlerRewriter(URLRewriter):
+    """a quick and dirty rewritter in charge of server static file.
+
+    This is a work around the flatness of url handling in cubicweb."""
+
+    __regid__ = 'static'
+
+    priority = 10
+
+    def rewrite(self, req, uri):
+        for ctrl in STATIC_CONTROLLERS:
+            if uri.startswith('/%s/' % ctrl.__regid__):
+                break
+        else:
+            self.debug("not a static file uri: %s", uri)
+            raise KeyError(uri)
+        relpath = self._cw.relative_path(includeparams=False)
+        self._cw.form['static_relative_path'] = self._cw.relative_path(includeparams=True)
+        return ctrl.__regid__, None
--- a/web/views/tableview.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/tableview.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -135,7 +135,7 @@
 
     * `header_column_idx`, if not `None`, should be a colum index or a set of
       column index where <th> tags should be generated instead of <td>
-    """
+    """ #'# make emacs happier
     __regid__ = 'table_layout'
     cssclass = "listing"
     needs_css = ('cubicweb.tableview.css',)
@@ -174,8 +174,8 @@
 
     @cachedproperty
     def initial_load(self):
-        """We detect a bit heuristically if we are built for the first time.
-        or from subsequent calls by the form filter or by the pagination hooks.
+        """We detect a bit heuristically if we are built for the first time or
+        from subsequent calls by the form filter or by the pagination hooks.
         """
         form = self._cw.form
         return 'fromformfilter' not in form and '__fromnavigation' not in form
@@ -290,20 +290,17 @@
         return attrs
 
     def render_actions(self, w, actions):
-        box = MenuWidget('', '', _class='tableActionsBox', islist=False)
-        label = tags.span(self._cw._('action menu'))
-        menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox',
-                            ident='%sActions' % self.view.domid)
-        box.append(menu)
+        w(u'<div class="tableactions">')
         for action in actions:
-            menu.append(action)
-        box.render(w=w)
-        w(u'<div class="clear"></div>')
+            w(u'<span>')
+            action.render(w)
+            w(u'</span>')
+        w(u'</div>')
 
     def show_hide_filter_actions(self, currentlydisplayed=False):
         divid = self.view.domid
         showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:]
-                             for what in ('Form', 'Show', 'Hide', 'Actions'))
+                             for what in ('Form', 'Actions'))
         showhide = 'javascript:' + showhide
         self._cw.add_onload(u'''\
 $(document).ready(function() {
@@ -313,10 +310,8 @@
     $('#%(id)sShow').attr('class', 'hidden');
   }
 });''' % {'id': divid})
-        showlabel = self._cw._('show filter form')
-        hidelabel = self._cw._('hide filter form')
-        return [component.Link(showhide, showlabel, id='%sShow' % divid),
-                component.Link(showhide, hidelabel, id='%sHide' % divid)]
+        showlabel = self._cw._('toggle filter')
+        return [component.Link(showhide, showlabel, id='%sToggle' % divid)]
 
 
 class AbstractColumnRenderer(object):
@@ -332,14 +327,14 @@
     :attr: `header`, the column header. If None, default to `_(colid)`
     :attr: `addcount`, if True, add the table size in parenthezis beside the header
     :attr: `trheader`, should the header be translated
-    :attr: `escapeheader`, should the header be xml_escape'd
+    :attr: `escapeheader`, should the header be xml_escaped
     :attr: `sortable`, tell if the column is sortable
     :attr: `view`, the table view
     :attr: `_cw`, the request object
     :attr: `colid`, the column identifier
     :attr: `attributes`, dictionary of attributes to put on the HTML tag when
             the cell is rendered
-    """
+    """ #'# make emacs
     attributes = {}
     empty_cell_content = u'&#160;'
 
@@ -576,7 +571,7 @@
     renderer.
 
     .. autoclass:: RsetTableColRenderer
-    """
+    """    #'# make emacs happier
     __regid__ = 'table'
     # selector trick for bw compath with the former :class:TableView
     __select__ = AnyRsetView.__select__ & (~match_kwargs(
@@ -599,16 +594,19 @@
         # may be listed in possible views
         return self.__regid__ == 'table'
 
-    def call(self, headers=None, displaycols=None, cellvids=None, **kwargs):
+    def call(self, headers=None, displaycols=None, cellvids=None,
+             paginate=None, **kwargs):
         if self.headers:
             self.headers = [h and self._cw._(h) for h in self.headers]
-        if (headers or displaycols or cellvids):
+        if (headers or displaycols or cellvids or paginate):
             if headers is not None:
                 self.headers = headers
             if displaycols is not None:
                 self.displaycols = displaycols
             if cellvids is not None:
                 self.cellvids = cellvids
+            if paginate is not None:
+                self.paginable = paginate
         if kwargs:
             # old table view arguments that we can safely ignore thanks to
             # selectors
@@ -883,7 +881,7 @@
     default_column_renderer_class = EntityTableColRenderer
     columns = None # to be defined in concret class
 
-    def call(self, columns=None):
+    def call(self, columns=None, **kwargs):
         if columns is not None:
             self.columns = columns
         self.layout_render(self.w)
--- a/web/views/tabs.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/tabs.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -47,7 +47,15 @@
         """a lazy version of wview"""
         w = w or self.w
         self._cw.add_js('cubicweb.ajax.js')
+        # the form is copied into urlparams to please the inner views
+        # that might want to take params from it
+        # beware of already present rql or eid elements
+        # to be safe of collision a proper argument passing protocol
+        # (with namespaces) should be used instead of the current
+        # ad-hockery
         urlparams = self._cw.form.copy()
+        urlparams.pop('rql', None)
+        urlparams.pop('eid', None)
         urlparams.update({'vid' : vid, 'fname' : 'view'})
         if rql:
             urlparams['rql'] = rql
@@ -82,6 +90,7 @@
 
 class TabsMixin(LazyViewMixin):
     """a tab mixin to easily get jQuery based, lazy, ajax tabs"""
+    lazy = True
 
     @property
     def cookie_name(self):
@@ -114,7 +123,7 @@
             vid = tabkwargs.get('vid', tabid)
             domid = uilib.domid(tabid)
             try:
-                viewsvreg.select(vid, self._cw, **tabkwargs)
+                viewsvreg.select(vid, self._cw, tabid=domid, **tabkwargs)
             except NoSelectableObject:
                 continue
             selected_tabs.append((tabid, domid, tabkwargs))
@@ -149,17 +158,20 @@
         w(u'</ul>')
         for tabid, domid, tabkwargs in tabs:
             w(u'<div id="%s">' % domid)
-            tabkwargs.setdefault('tabid', domid)
-            tabkwargs.setdefault('vid', tabid)
-            tabkwargs.setdefault('rset', self.cw_rset)
-            self.lazyview(**tabkwargs)
+            if self.lazy:
+                tabkwargs.setdefault('tabid', domid)
+                tabkwargs.setdefault('vid', tabid)
+                self.lazyview(**tabkwargs)
+            else:
+                self._cw.view(tabid, w=self.w, **tabkwargs)
             w(u'</div>')
         w(u'</div>')
         # call the setTab() JS function *after* each tab is generated
         # because the callback binding needs to be done before
         # XXX make work history: true
-        self._cw.add_onload(u"""
-  jQuery('#entity-tabs-%(eeid)s').tabs(
+        if self.lazy:
+            self._cw.add_onload(u"""
+  jQuery('#entity-tabs-%(uid)s').tabs(
     { selected: %(tabindex)s,
       select: function(event, ui) {
         setTab(ui.panel.id, '%(cookiename)s');
@@ -167,9 +179,13 @@
     });
   setTab('%(domid)s', '%(cookiename)s');
 """ % {'tabindex'   : active_tab_idx,
-       'domid'        : active_tab,
-       'eeid'       : (entity and entity.eid or uid),
+       'domid'      : active_tab,
+       'uid'        : uid,
        'cookiename' : self.cookie_name})
+        else:
+            self._cw.add_onload(
+                u"jQuery('#entity-tabs-%(uid)s').tabs({selected: %(tabindex)s});"
+                % {'tabindex': active_tab_idx, 'uid': uid})
 
 
 class EntityRelationView(EntityView):
@@ -210,8 +226,7 @@
     tabs = [_('main_tab')]
     default_tab = 'main_tab'
 
-    def cell_call(self, row, col):
-        entity = self.cw_rset.complete_entity(row, col)
+    def render_entity(self, entity):
         self.render_entity_toolbox(entity)
         self.w(u'<div class="tabbedprimary"></div>')
         self.render_entity_title(entity)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/undohistory.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,224 @@
+# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+
+from logilab.common.registry import Predicate
+
+from cubicweb import UnknownEid, tags, transaction as tx
+from cubicweb.view import View, StartupView
+from cubicweb.predicates import match_kwargs, ExpectedValuePredicate
+from cubicweb.schema import display_name
+
+
+class undoable_action(Predicate):
+    """Select only undoable actions depending on filters provided. Undo Action
+    is expected to be specified by the `tx_action` argument.
+
+    Currently the only implemented filter is:
+
+    :param action_type: chars among CUDAR (standing for Create, Update, Delete,
+                        Add, Remove)
+    """
+
+    # XXX FIXME : this selector should be completed to allow selection on the
+    # entity or relation types and public / private.
+    def __init__(self, action_type='CUDAR'):
+        assert not set(action_type) - set('CUDAR')
+        self.action_type = action_type
+
+    def __str__(self):
+        return '%s(%s)' % (self.__class__.__name__, ', '.join(
+            "%s=%v" % (str(k), str(v)) for k, v in kwargs.iteritems() ))
+
+    def __call__(self, cls, req, tx_action=None, **kwargs):
+        # tx_action is expected to be a transaction.AbstractAction
+        if not isinstance(tx_action, tx.AbstractAction):
+            return 0
+        # Filter according to action type
+        return int(tx_action.action in self.action_type)
+
+
+class UndoHistoryView(StartupView):
+    __regid__ = 'undohistory'
+    title = _('Undoing')
+    item_vid = 'undoable-transaction-view'
+    cache_max_age = 0
+
+    redirect_path = 'view' #TODO
+    redirect_params = dict(vid='undohistory') #TODO
+    public_actions_only = True
+
+    # TODO Allow to choose if if want all actions or only the public ones
+    # (default)
+
+    def call(self, **kwargs):
+        txs = self._cw.cnx.undoable_transactions()
+        if txs :
+            self.w(u"<ul class='undo-transactions'>")
+            for tx in txs:
+                self.cell_call(tx)
+            self.w(u"</ul>")
+
+    def cell_call(self, tx):
+        self.w(u'<li>')
+        self.wview(self.item_vid, None, txuuid=tx.uuid,
+                   public=self.public_actions_only,
+                   redirect_path=self.redirect_path,
+                   redirect_params=self.redirect_params)
+        self.w(u'</li>\n')
+
+
+class UndoableTransactionView(View):
+    __regid__ = 'undoable-transaction-view'
+    __select__ = View.__select__ & match_kwargs('txuuid')
+
+    item_vid = 'undoable-action-list-view'
+    cache_max_age = 0
+
+    def build_undo_link(self, txuuid,
+                        redirect_path=None, redirect_params=None):
+        """ the kwargs are passed to build_url"""
+        _ = self._cw._
+        redirect = {}
+        if redirect_path:
+            redirect['__redirectpath'] = redirect_path
+        if redirect_params:
+            if isinstance(redirect_params, dict):
+                redirect['__redirectparams'] = self._cw.build_url_params(**redirect_params)
+            else:
+                redirect['__redirectparams'] = redirect_params
+        link_url = self._cw.build_url('undo', txuuid=txuuid, **redirect)
+        msg = u"<span class='undo'>%s</span>" % tags.a( _('undo'), href=link_url)
+        return msg
+
+    def call(self, txuuid, public=True,
+             redirect_path=None, redirect_params=None):
+        _ = self._cw._
+        txinfo = self._cw.cnx.transaction_info(txuuid)
+        try:
+            #XXX Under some unknown circumstances txinfo.user_eid=-1
+            user = self._cw.entity_from_eid(txinfo.user_eid)
+        except UnknownEid:
+            user = None
+        undo_url = self.build_undo_link(txuuid,
+                                        redirect_path=redirect_path,
+                                        redirect_params=redirect_params)
+        txinfo_dict = dict( dt = self._cw.format_date(txinfo.datetime, time=True),
+                            user_eid = txinfo.user_eid,
+                            user = user and user.view('outofcontext') or _("undefined user"),
+                            txuuid = txuuid,
+                            undo_link = undo_url)
+        self.w( _("By %(user)s on %(dt)s [%(undo_link)s]") % txinfo_dict)
+
+        tx_actions = txinfo.actions_list(public=public)
+        if tx_actions :
+            self.wview(self.item_vid, None, tx_actions=tx_actions)
+
+
+class UndoableActionListView(View):
+    __regid__ = 'undoable-action-list-view'
+    __select__ = View.__select__ & match_kwargs('tx_actions')
+    title = _('Undoable actions')
+    item_vid = 'undoable-action-view'
+    cache_max_age = 0
+
+    def call(self, tx_actions):
+        if tx_actions :
+            self.w(u"<ol class='undo-actions'>")
+            for action in tx_actions:
+                self.cell_call(action)
+            self.w(u"</ol>")
+
+    def cell_call(self, action):
+        self.w(u'<li>')
+        self.wview(self.item_vid, None, tx_action=action)
+        self.w(u'</li>\n')
+
+
+class UndoableActionBaseView(View):
+    __regid__ = 'undoable-action-view'
+    __abstract__ = True
+
+    def call(self, tx_action):
+        raise NotImplementedError(self)
+
+    def _build_entity_link(self, eid):
+        try:
+            entity = self._cw.entity_from_eid(eid)
+            return entity.view('outofcontext')
+        except UnknownEid:
+            return _("(suppressed) entity #%d") % eid
+
+    def _build_relation_info(self, rtype, eid_from,  eid_to):
+        return dict( rtype=display_name(self._cw, rtype),
+                     entity_from=self._build_entity_link(eid_from),
+                     entity_to=self._build_entity_link(eid_to) )
+
+    def _build_entity_info(self, etype, eid, changes):
+        return dict( etype=display_name(self._cw, etype),
+                     entity=self._build_entity_link(eid),
+                     eid=eid,
+                     changes=changes)
+
+
+class UndoableAddActionView(UndoableActionBaseView):
+    __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='A')
+
+    def call(self, tx_action):
+        _ = self._cw._
+        self.w(_("Added relation : %(entity_from)s %(rtype)s %(entity_to)s") %
+               self._build_relation_info(tx_action.rtype, tx_action.eid_from, tx_action.eid_to))
+
+
+class UndoableRemoveActionView(UndoableActionBaseView):
+    __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='R')
+
+    def call(self, tx_action):
+        _ = self._cw._
+        self.w(_("Delete relation : %(entity_from)s %(rtype)s %(entity_to)s") %
+               self._build_relation_info(tx_action.rtype, tx_action.eid_from, tx_action.eid_to))
+
+
+class UndoableCreateActionView(UndoableActionBaseView):
+    __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='C')
+
+    def call(self, tx_action):
+        _ = self._cw._
+        self.w(_("Created %(etype)s : %(entity)s") % #  : %(changes)s
+               self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes) )
+
+
+class UndoableDeleteActionView(UndoableActionBaseView):
+    __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='D')
+
+    def call(self, tx_action):
+        _ = self._cw._
+        self.w(_("Deleted %(etype)s : %(entity)s") %
+               self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes))
+
+
+class UndoableUpdateActionView(UndoableActionBaseView):
+    __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='U')
+
+    def call(self, tx_action):
+        _ = self._cw._
+        self.w(_("Updated %(etype)s : %(entity)s") %
+               self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes))
--- a/web/views/urlpublishing.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/urlpublishing.py	Tue Oct 23 15:00:53 2012 +0200
@@ -106,7 +106,8 @@
         :param req: the request object
 
         :type path: str
-        :param path: the path of the resource to publish
+        :param path: the path of the resource to publish. If empty, None or "/"
+                     "view" is used as the default path.
 
         :rtype: tuple(str, `cubicweb.rset.ResultSet` or None)
         :return: the publishing method identifier and an optional result set
--- a/web/views/workflow.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/workflow.py	Tue Oct 23 15:00:53 2012 +0200
@@ -315,7 +315,7 @@
     wf = req.entity_from_eid(wfeid)
     rschema = req.vreg.schema[field.name]
     param = 'toeid' if field.role == 'subject' else 'fromeid'
-    return sorted((e.view('combobox'), e.eid)
+    return sorted((e.view('combobox'), unicode(e.eid))
                   for e in getattr(wf, 'reverse_%s' % wfrelation)
                   if rschema.has_perm(req, 'add', **{param: e.eid}))
 
@@ -330,12 +330,14 @@
 
 def transition_states_vocabulary(form, field):
     entity = form.edited_entity
-    if not entity.has_eid():
+    if entity.has_eid():
+        wfeid = entity.transition_of[0].eid
+    else:
         eids = form.linked_to.get(('transition_of', 'subject'))
         if not eids:
             return []
-        return _wf_items_for_relation(form._cw, eids[0], 'state_of', field)
-    return field.relvoc_unrelated(form)
+        wfeid = eids[0]
+    return _wf_items_for_relation(form._cw, wfeid, 'state_of', field)
 
 _afs.tag_subject_of(('*', 'destination_state', '*'), 'main', 'attributes')
 _affk.tag_subject_of(('*', 'destination_state', '*'),
@@ -348,12 +350,14 @@
 
 def state_transitions_vocabulary(form, field):
     entity = form.edited_entity
-    if not entity.has_eid():
+    if entity.has_eid():
+        wfeid = entity.state_of[0].eid
+    else :
         eids = form.linked_to.get(('state_of', 'subject'))
-        if eids:
-            return _wf_items_for_relation(form._cw, eids[0], 'transition_of', field)
-        return []
-    return field.relvoc_unrelated(form)
+        if not eids:
+            return []
+        wfeid = eids[0]
+    return _wf_items_for_relation(form._cw, wfeid, 'transition_of', field)
 
 _afs.tag_subject_of(('State', 'allowed_transition', '*'), 'main', 'attributes')
 _affk.tag_subject_of(('State', 'allowed_transition', '*'),
--- a/web/wdoc/bookmarks_fr.rst	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/wdoc/bookmarks_fr.rst	Tue Oct 23 15:00:53 2012 +0200
@@ -27,8 +27,4 @@
 ayez le droit de les modifier.
 
 
-Pour plus de détails sur les relations possibles, veuillez vous réferer au
-schéma_ du composant signet.
-
-.. _`schéma`: eetype/Bookmark?vid=eschema
 .. _`préférences utilisateurs`: myprefs
--- a/web/webconfig.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/web/webconfig.py	Tue Oct 23 15:00:53 2012 +0200
@@ -21,7 +21,7 @@
 _ = unicode
 
 import os
-from os.path import join, exists, split
+from os.path import join, exists, split, isdir
 from warnings import warn
 
 from logilab.common.decorators import cached
@@ -321,17 +321,19 @@
         if not (self.repairing or self.creating):
             self.global_set_option('base-url', baseurl)
         httpsurl = self['https-url']
-        if (self.debugmode or self.mode == 'test'):
-            datadir_path = 'data/'
-        else:
-            datadir_path = 'data/%s/' % self.instance_md5_version()
+        data_relpath = self.data_relpath()
         if httpsurl:
             if httpsurl[-1] != '/':
                 httpsurl += '/'
                 if not self.repairing:
                     self.global_set_option('https-url', httpsurl)
-            self.https_datadir_url = httpsurl + datadir_path
-        self.datadir_url = baseurl + datadir_path
+            self.https_datadir_url = httpsurl + data_relpath
+        self.datadir_url = baseurl + data_relpath
+
+    def data_relpath(self):
+        if self.mode == 'test':
+            return 'data/'
+        return 'data/%s/' % self.instance_md5_version()
 
     def _build_ui_properties(self):
         # self.datadir_url[:-1] to remove trailing /
@@ -405,7 +407,8 @@
         rdir, filename = split(rpath)
         if rdir:
             staticdir = join(staticdir, rdir)
-            os.makedirs(staticdir)
+            if not isdir(staticdir) and 'w' in mode:
+                os.makedirs(staticdir)
         return file(join(staticdir, filename), mode)
 
     def static_file_add(self, rpath, data):
--- a/wsgi/handler.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/wsgi/handler.py	Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,14 +15,16 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""WSGI request handler for cubicweb
+"""WSGI request handler for cubicweb"""
 
-"""
+
 
 __docformat__ = "restructuredtext en"
 
+from itertools import chain, repeat, izip
+
 from cubicweb import AuthenticationError
-from cubicweb.web import Redirect, DirectResponse, StatusResponse, LogOut
+from cubicweb.web import DirectResponse
 from cubicweb.web.application import CubicWebPublisher
 from cubicweb.wsgi.request import CubicWebWsgiRequest
 
@@ -71,7 +73,6 @@
     505: 'HTTP VERSION NOT SUPPORTED',
 }
 
-
 class WSGIResponse(object):
     """encapsulates the wsgi response parameters
     (code, headers and body if there is one)
@@ -79,7 +80,9 @@
     def __init__(self, code, req, body=None):
         text = STATUS_CODE_TEXT.get(code, 'UNKNOWN STATUS CODE')
         self.status =  '%s %s' % (code, text)
-        self.headers = [(str(k), str(v)) for k, v in req.headers_out.items()]
+        self.headers = list(chain(*[izip(repeat(k), v)
+                                    for k, v in req.headers_out.getAllRawHeaders()]))
+        self.headers = [(str(k), str(v)) for k, v in self.headers]
         if body:
             self.body = [body]
         else:
@@ -103,95 +106,31 @@
     def __init__(self, config, vreg=None):
         self.appli = CubicWebPublisher(config, vreg=vreg)
         self.config = config
-        self.base_url = None
-#         self.base_url = config['base-url'] or config.default_base_url()
-#         assert self.base_url[-1] == '/'
-#         self.https_url = config['https-url']
-#         assert not self.https_url or self.https_url[-1] == '/'
+        self.base_url = config['base-url']
+        self.https_url = config['https-url']
         self.url_rewriter = self.appli.vreg['components'].select_or_none('urlrewriter')
 
     def _render(self, req):
         """this function performs the actual rendering
-        XXX missing: https handling, url rewriting, cache management,
-                     authentication
         """
         if self.base_url is None:
             self.base_url = self.config._base_url = req.base_url()
-        # XXX https handling needs to be implemented
-        if req.authmode == 'http':
-            # activate realm-based auth
-            realm = self.config['realm']
-            req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
         try:
-            self.appli.connect(req)
-        except Redirect, ex:
-            return self.redirect(req, ex.location)
-        path = req.path
-        if not path or path == "/":
-            path = 'view'
-        try:
-            result = self.appli.publish(path, req)
+            path = req.path
+            result = self.appli.handle_request(req, path)
         except DirectResponse, ex:
-            return WSGIResponse(200, req, ex.response)
-        except StatusResponse, ex:
-            return WSGIResponse(ex.status, req, ex.content)
-        except AuthenticationError:  # must be before AuthenticationError
-            return self.request_auth(req)
-        except LogOut:
-            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 Redirect, ex:
-            return self.redirect(req, ex.location)
-        if not result:
-            # no result, something went wrong...
-            self.error('no data (%s)', req)
-            # 500 Internal server error
-            return self.redirect(req, req.build_url('error'))
-        return WSGIResponse(200, req, result)
+            return ex.response
+        return WSGIResponse(req.status_out, req, result)
 
 
     def __call__(self, environ, start_response):
         """WSGI protocol entry point"""
-        req = CubicWebWsgiRequest(environ, self.appli.vreg, self.base_url)
+        req = CubicWebWsgiRequest(environ, self.appli.vreg)
         response = self._render(req)
         start_response(response.status, response.headers)
         return response.body
 
-    def redirect(self, req, location):
-        """convenience function which builds a redirect WSGIResponse"""
-        self.debug('redirecting to %s', location)
-        req.set_header('location', str(location))
-        return WSGIResponse(303, req)
 
-    def request_auth(self, req, loggedout=False):
-        """returns the appropriate WSGIResponse to require the user to log in
-        """
-#         if self.https_url and req.base_url() != self.https_url:
-#             return self.redirect(self.https_url + 'login')
-        if self.config['auth-mode'] == 'http':
-            code = 401 # UNAUTHORIZED
-        else:
-            code = 403 # FORBIDDEN
-        if loggedout:
-#             if req.https:
-#                 req._base_url =  self.base_url
-#                 req.https = False
-            content = self.appli.loggedout_content(req)
-        else:
-            content = self.appli.need_login_content(req)
-        return WSGIResponse(code, req, content)
 
     # these are overridden by set_log_methods below
     # only defining here to prevent pylint from complaining
--- a/wsgi/request.py	Wed Feb 22 11:57:42 2012 +0100
+++ b/wsgi/request.py	Tue Oct 23 15:00:53 2012 +0200
@@ -32,7 +32,8 @@
 
 from cubicweb.web.request import CubicWebRequestBase
 from cubicweb.wsgi import (pformat, qs2dict, safe_copyfileobj, parse_file_upload,
-                        normalize_header)
+                           normalize_header)
+from cubicweb.web.http_headers import Headers
 
 
 
@@ -40,22 +41,23 @@
     """most of this code COMES FROM DJANO
     """
 
-    def __init__(self, environ, vreg, base_url=None):
+    def __init__(self, environ, vreg):
         self.environ = environ
         self.path = environ['PATH_INFO']
         self.method = environ['REQUEST_METHOD'].upper()
-        self._headers = dict([(normalize_header(k[5:]), v) for k, v in self.environ.items()
-                              if k.startswith('HTTP_')])
+
+        headers_in = dict((normalize_header(k[5:]), v) for k, v in self.environ.items()
+                          if k.startswith('HTTP_'))
         https = environ.get("HTTPS") in ('yes', 'on', '1')
-        self._base_url = base_url or self.instance_uri()
         post, files = self.get_posted_data()
-        super(CubicWebWsgiRequest, self).__init__(vreg, https, post)
+
+        super(CubicWebWsgiRequest, self).__init__(vreg, https, post,
+                                                  headers= headers_in)
         if files is not None:
             for key, (name, _, stream) in files.iteritems():
-                name = unicode(name, self.encoding)
+                if name is not None:
+                    name = unicode(name, self.encoding)
                 self.form[key] = (name, stream)
-        # prepare output headers
-        self.headers_out = {}
 
     def __repr__(self):
         # Since this is called as part of error handling, we need to be very
@@ -67,9 +69,6 @@
 
     ## cubicweb request interface ################################################
 
-    def base_url(self):
-        return self._base_url
-
     def http_method(self):
         """returns 'POST', 'GET', 'HEAD', etc."""
         return self.method
@@ -91,31 +90,6 @@
 
         return path
 
-    def get_header(self, header, default=None):
-        """return the value associated with the given input HTTP header,
-        raise KeyError if the header is not set
-        """
-        return self._headers.get(normalize_header(header), default)
-
-    def set_header(self, header, value, raw=True):
-        """set an output HTTP header"""
-        assert raw, "don't know anything about non-raw headers for wsgi requests"
-        self.headers_out[header] = value
-
-    def add_header(self, header, value):
-        """add an output HTTP header"""
-        self.headers_out[header] = value
-
-    def remove_header(self, header):
-        """remove an output HTTP header"""
-        self.headers_out.pop(header, None)
-
-    def header_if_modified_since(self):
-        """If the HTTP header If-modified-since is set, return the equivalent
-        mx date time value (GMT), else return None
-        """
-        return None
-
     ## wsgi request helpers ###################################################
 
     def instance_uri(self):
@@ -146,6 +120,8 @@
             and self.environ['wsgi.url_scheme'] == 'https'
 
     def get_posted_data(self):
+        # The WSGI spec says 'QUERY_STRING' may be absent.
+        post = qs2dict(self.environ.get('QUERY_STRING', ''))
         files = None
         if self.method == 'POST':
             if self.environ.get('CONTENT_TYPE', '').startswith('multipart'):
@@ -153,12 +129,10 @@
                                    for k, v in self.environ.items()
                                    if k.startswith('HTTP_'))
                 header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '')
-                post, files = parse_file_upload(header_dict, self.raw_post_data)
+                post_, files = parse_file_upload(header_dict, self.raw_post_data)
+                post.update(post_)
             else:
-                post = qs2dict(self.raw_post_data)
-        else:
-            # The WSGI spec says 'QUERY_STRING' may be absent.
-            post = qs2dict(self.environ.get('QUERY_STRING', ''))
+                post.update(qs2dict(self.raw_post_data))
         return post, files
 
     @property
@@ -176,20 +150,3 @@
         postdata = buf.getvalue()
         buf.close()
         return postdata
-
-    def _validate_cache(self):
-        """raise a `DirectResponse` exception if a cached page along the way
-        exists and is still usable
-        """
-        # XXX
-#         if self.get_header('Cache-Control') in ('max-age=0', 'no-cache'):
-#             # Expires header seems to be required by IE7
-#             self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
-#             return
-#         try:
-#             http.checkPreconditions(self._twreq, _PreResponse(self))
-#         except http.HTTPError, ex:
-#             self.info('valid http cache, no actual rendering')
-#             raise DirectResponse(ex.response)
-        # Expires header seems to be required by IE7
-        self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/zmqclient.py	Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,58 @@
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""Source to query another RQL repository using pyro"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from functools import partial
+import zmq
+
+
+# XXX hack to overpass old zmq limitation that force to have
+# only one context per python process
+try:
+    from cubicweb.server.cwzmq import ctx
+except ImportError:
+    ctx = zmq.Context()
+
+class ZMQRepositoryClient(object):
+    """
+    This class delegate the overall repository stuff to a remote source.
+
+    So calling a method of this repository will results on calling the
+    corresponding method of the remote source repository.
+
+    Any raised exception on the remote source is propagated locally.
+
+    ZMQ is used as the transport layer and cPickle is used to serialize data.
+    """
+
+    def __init__(self, zmq_address):
+        self.socket = ctx.socket(zmq.REQ)
+        self.socket.connect(zmq_address)
+
+    def __zmqcall__(self, name, *args, **kwargs):
+         self.socket.send_pyobj([name, args, kwargs])
+         result = self.socket.recv_pyobj()
+         if isinstance(result, BaseException):
+             raise result
+         return result
+
+    def __getattr__(self, name):
+        return partial(self.__zmqcall__, name)