Merge with 3.24 branch
authorDenis Laxalde <denis.laxalde@logilab.fr>
Mon, 20 Mar 2017 10:28:01 +0100
changeset 12068 a96c770b2ef1
parent 12064 0c06abcf4f82 (diff)
parent 12067 4ea6d7b23aff (current diff)
child 12069 82f7d583e5a3
Merge with 3.24 branch
cubicweb.spec
cubicweb/__pkginfo__.py
cubicweb/cwctl.py
--- a/MANIFEST.in	Mon Mar 20 09:40:24 2017 +0100
+++ b/MANIFEST.in	Mon Mar 20 10:28:01 2017 +0100
@@ -62,6 +62,8 @@
 recursive-include cubicweb/web/test/data bootstrap_cubes pouet.css *.py
 recursive-include cubicweb/web/test/data/static/jstests *.js *.html *.json
 
+include cubicweb/pyramid/development.ini.tmpl
+
 include cubicweb/web/data/jquery-treeview/*.md
 
 recursive-include cubicweb/skeleton *.py *.css *.js *.po compat *.tmpl rules
--- a/cubicweb.spec	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb.spec	Mon Mar 20 10:28:01 2017 +0100
@@ -27,7 +27,7 @@
 Requires:       %{python}-rql >= 0.34.0
 Requires:       %{python}-yams >= 0.44.0
 Requires:       %{python}-logilab-database >= 1.15.0
-Requires:       %{python}-passlib < 2.0
+Requires:       %{python}-passlib => 1.7.0
 Requires:       %{python}-lxml
 Requires:       %{python}-twisted-web < 16.0.0
 Requires:       %{python}-markdown
--- a/cubicweb/__init__.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/__init__.py	Mon Mar 20 10:28:01 2017 +0100
@@ -24,8 +24,8 @@
 import logging
 import os
 import pickle
-import pkgutil
 import sys
+import types
 import warnings
 import zlib
 
@@ -282,6 +282,37 @@
 
 # Import hook for "legacy" cubes ##############################################
 
+class _CubesLoader(object):
+
+    def __init__(self, *modinfo):
+        self.modinfo = modinfo
+
+    def load_module(self, fullname):
+        try:
+            # If there is an existing module object named 'fullname' in
+            # sys.modules , the loader must use that existing module.
+            # Otherwise, the reload() builtin will not work correctly.
+            return sys.modules[fullname]
+        except KeyError:
+            pass
+        if fullname == 'cubes':
+            mod = sys.modules[fullname] = types.ModuleType(
+                fullname, doc='CubicWeb cubes')
+        else:
+            modname, file, pathname, description = self.modinfo
+            try:
+                mod = sys.modules[fullname] = imp.load_module(
+                    modname, file, pathname, description)
+            finally:
+                # https://docs.python.org/2/library/imp.html#imp.load_module
+                # Important: the caller is responsible for closing the file
+                # argument, if it was not None, even when an exception is
+                # raised. This is best done using a try ... finally statement
+                if file is not None:
+                    file.close()
+        return mod
+
+
 class _CubesImporter(object):
     """Module finder handling redirection of import of "cubes.<name>"
     to "cubicweb_<name>".
@@ -294,11 +325,13 @@
             sys.meta_path.append(self)
 
     def find_module(self, fullname, path=None):
-        if fullname.startswith('cubes.'):
+        if fullname == 'cubes':
+            return _CubesLoader()
+        elif fullname.startswith('cubes.') and fullname.count('.') == 1:
             modname = 'cubicweb_' + fullname.split('.', 1)[1]
             try:
                 modinfo = imp.find_module(modname)
             except ImportError:
                 return None
             else:
-                return pkgutil.ImpLoader(fullname, *modinfo)
+                return _CubesLoader(modname, *modinfo)
--- a/cubicweb/__pkginfo__.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/__pkginfo__.py	Mon Mar 20 10:28:01 2017 +0100
@@ -27,8 +27,8 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 24, 6)
-version = '.'.join(str(num) for num in numversion)
+numversion = (3, 25, 0)
+version = '.'.join(str(num) for num in numversion) + '.dev0'
 
 description = "a repository of entities / relations for knowledge management"
 author = "Logilab"
--- a/cubicweb/cwconfig.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/cwconfig.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -177,16 +177,15 @@
 
    Directory where pid files will be written
 """
+
 from __future__ import print_function
 
-
-
 import importlib
 import logging
 import logging.config
 import os
-from os.path import (exists, join, expanduser, abspath, normpath, realpath,
-                     basename, isdir, dirname, splitext)
+from os.path import (exists, join, expanduser, abspath, normpath,
+                     basename, isdir, dirname, splitext, realpath)
 import pkgutil
 import pkg_resources
 import re
@@ -194,7 +193,7 @@
 import stat
 import sys
 from threading import Lock
-from warnings import warn, filterwarnings
+from warnings import filterwarnings
 
 from six import text_type
 
@@ -221,13 +220,15 @@
     except IndexError:
         raise ConfigurationError('no such config %r (check it exists with "cubicweb-ctl list")' % name)
 
+
 def possible_configurations(directory):
     """return a list of installed configurations in a directory
     according to \*-ctl files
     """
-    return [name for name in ('repository', 'all-in-one')
+    return [name for name in ('repository', 'all-in-one', 'pyramid')
             if exists(join(directory, '%s.conf' % name))]
 
+
 def guess_configuration(directory):
     """try to guess the configuration to use for a directory. If multiple
     configurations are found, ConfigurationError is raised
@@ -238,6 +239,7 @@
                                  % (directory, modes))
     return modes[0]
 
+
 def _find_prefix(start_path=None):
     """Return the prefix path of CubicWeb installation.
 
@@ -274,6 +276,40 @@
     return cube
 
 
+def _expand_modname(modname):
+    """expand modules names `modname` if exists by walking non package submodules
+    and yield (submodname, filepath) including `modname` itself
+
+    If the file ends with .pyc or .pyo (python bytecode) also check that the
+    corresponding source .py file exists before yielding.
+    """
+    try:
+        loader = pkgutil.find_loader(modname)
+    except ImportError:
+        return
+    if not loader:
+        return
+
+    def check_source_file(filepath):
+        if filepath[-4:] in ('.pyc', '.pyo'):
+            if not exists(filepath[:-1]):
+                return False
+        return True
+
+    filepath = loader.get_filename()
+    if not check_source_file(filepath):
+        return
+    yield modname, filepath
+    if loader.is_package(modname):
+        path = dirname(filepath)
+        for subloader, subname, ispkg in pkgutil.walk_packages([path]):
+            # ignore subpackages (historical behavior)
+            if not ispkg:
+                filepath = subloader.find_module(subname).get_filename()
+                if check_source_file(filepath):
+                    yield modname + '.' + subname, filepath
+
+
 # persistent options definition
 PERSISTENT_OPTIONS = (
     ('encoding',
@@ -467,6 +503,7 @@
     @classmethod
     def available_cubes(cls):
         cubes = set()
+        prefix = 'cubicweb_'
         for entry_point in pkg_resources.iter_entry_points(
                 group='cubicweb.cubes', name=None):
             try:
@@ -475,11 +512,11 @@
                 continue
             else:
                 modname = module.__name__
-                if not modname.startswith('cubicweb_'):
+                if not modname.startswith(prefix):
                     cls.warning('entry point %s does not appear to be a cube',
                                 entry_point)
                     continue
-                cubes.add(modname)
+                cubes.add(modname[len(prefix):])
         # Legacy cubes.
         for directory in cls.cubes_search_path():
             if not exists(directory):
@@ -667,23 +704,18 @@
         """update python path if necessary"""
         from cubicweb import _CubesImporter
         _CubesImporter.install()
-        cubes_parent_dir = normpath(join(cls.CUBES_DIR, '..'))
-        if not cubes_parent_dir in sys.path:
-            sys.path.insert(0, cubes_parent_dir)
-        try:
-            import cubes
-            cubes.__path__ = cls.cubes_search_path()
-        except ImportError:
-            return # cubes dir doesn't exists
+        import cubes
+        cubes.__path__ = cls.cubes_search_path()
 
     @classmethod
     def load_available_configs(cls):
         for confmod in ('web.webconfig',  'etwist.twconfig',
-                        'server.serverconfig',):
+                        'server.serverconfig', 'pyramid.config'):
             try:
                 __import__('cubicweb.%s' % confmod)
-            except ImportError:
-                pass
+            except ImportError as exc:
+                cls.warning('failed to load config module %s (%s)',
+                            confmod, exc)
 
     @classmethod
     def load_cwctl_plugins(cls):
@@ -692,7 +724,9 @@
                        'devtools.devctl', 'pyramid.pyramidctl'):
             try:
                 __import__('cubicweb.%s' % ctlmod)
-            except ImportError:
+            except ImportError as exc:
+                cls.warning('failed to load cubicweb-ctl plugin %s (%s)',
+                            ctlmod, exc)
                 continue
             cls.info('loaded cubicweb-ctl plugin %s', ctlmod)
         for cube in cls.available_cubes():
@@ -778,57 +812,26 @@
         # configure simpleTal logger
         logging.getLogger('simpleTAL').setLevel(logging.ERROR)
 
-    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.
+    def schema_modnames(self):
+        modnames = []
+        for name in ('bootstrap', 'base', 'workflow', 'Bookmark'):
+            modnames.append(('cubicweb', 'cubicweb.schemas.' + name))
+        for cube in reversed(self.cubes()):
+            for modname, filepath in _expand_modname('cubes.{0}.schema'.format(cube)):
+                modnames.append((cube, modname))
+        if self.apphome:
+            apphome = realpath(self.apphome)
+            for modname, filepath in _expand_modname('schema'):
+                if realpath(filepath).startswith(apphome):
+                    modnames.append(('data', modname))
+        return modnames
+
+    def appobjects_modnames(self):
+        """return a list of modules 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, cubes=()):
@@ -1116,9 +1119,11 @@
     # config -> repository
 
     def repository(self, vreg=None):
+        """Return a new bootstrapped repository."""
         from cubicweb.server.repository import Repository
-        from cubicweb.server.utils import TasksManager
-        return Repository(self, TasksManager(), vreg=vreg)
+        repo = Repository(self, vreg=vreg)
+        repo.bootstrap()
+        return repo
 
     # instance methods used to get instance specific resources #############
 
@@ -1309,21 +1314,49 @@
             try:
                 tr = translation('cubicweb', path, languages=[language])
                 self.translations[language] = (tr.ugettext, tr.upgettext)
-            except (ImportError, AttributeError, IOError):
+            except IOError:
                 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 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_appobjects_path(templpath)
+    @staticmethod
+    def _sorted_appobjects(appobjects):
+        appobjects = sorted(appobjects)
+        try:
+            index = appobjects.index('entities')
+        except ValueError:
+            pass
+        else:
+            # put entities first
+            appobjects.insert(0, appobjects.pop(index))
+        return appobjects
+
+    def appobjects_cube_modnames(self, cube):
+        modnames = []
+        cube_submodnames = self._sorted_appobjects(self.cube_appobject_path)
+        for name in cube_submodnames:
+            for modname, filepath in _expand_modname('.'.join(['cubes', cube, name])):
+                modnames.append(modname)
+        return modnames
+
+    def appobjects_modnames(self):
+        modnames = []
+        for name in self._sorted_appobjects(self.cubicweb_appobject_path):
+            for modname, filepath in _expand_modname('cubicweb.' + name):
+                modnames.append(modname)
+        for cube in reversed(self.cubes()):
+            modnames.extend(self.appobjects_cube_modnames(cube))
+        if self.apphome:
+            cube_submodnames = self._sorted_appobjects(self.cube_appobject_path)
+            apphome = realpath(self.apphome)
+            for name in cube_submodnames:
+                for modname, filepath in _expand_modname(name):
+                    # ensure file is in apphome
+                    if realpath(filepath).startswith(apphome):
+                        modnames.append(modname)
+        return modnames
 
     def set_sources_mode(self, sources):
         if not 'all' in sources:
--- a/cubicweb/cwctl.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/cwctl.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -20,15 +20,13 @@
 """
 from __future__ import print_function
 
-
-
 # *ctl module should limit the number of import to be imported as quickly as
 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
 # completion). So import locally in command helpers.
 import sys
 from warnings import warn, filterwarnings
 from os import remove, listdir, system, pathsep
-from os.path import exists, join, isfile, isdir, dirname, abspath
+from os.path import exists, join, isdir, dirname, abspath
 
 try:
     from os import kill, getpgid
@@ -43,7 +41,7 @@
 from logilab.common.clcommands import CommandLine
 from logilab.common.shellutils import ASK
 from logilab.common.configuration import merge_options
-from logilab.common.deprecation import deprecated
+from logilab.common.decorators import clear_cache
 
 from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage
 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg, CWDEV, CONFIGURATIONS
@@ -54,6 +52,7 @@
 CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.',
                     version=version, check_duplicated_command=False)
 
+
 def wait_process_end(pid, maxtry=10, waittime=1):
     """wait for a process to actually die"""
     import signal
@@ -62,19 +61,21 @@
     while nbtry < maxtry:
         try:
             kill(pid, signal.SIGUSR1)
-        except (OSError, AttributeError): # XXX win32
+        except (OSError, AttributeError):  # XXX win32
             break
         nbtry += 1
         sleep(waittime)
     else:
         raise ExecutionError('can\'t kill process %s' % pid)
 
+
 def list_instances(regdir):
     if isdir(regdir):
         return sorted(idir for idir in listdir(regdir) if isdir(join(regdir, idir)))
     else:
         return []
 
+
 def detect_available_modes(templdir):
     modes = []
     for fname in ('schema', 'schema.py'):
@@ -103,13 +104,6 @@
         )
     actionverb = None
 
-    @deprecated('[3.22] startorder is not used any more')
-    def ordered_instances(self):
-        """return list of known instances
-        """
-        regdir = cwcfg.instances_dir()
-        return list_instances(regdir)
-
     def run(self, args):
         """run the <command>_method on each argument (a list of instance
         identifiers)
@@ -332,7 +326,7 @@
           }),
         ('config',
          {'short': 'c', 'type' : 'choice', 'metavar': '<install type>',
-          'choices': ('all-in-one', 'repository'),
+          'choices': ('all-in-one', 'repository', 'pyramid'),
           'default': 'all-in-one',
           'help': 'installation type, telling which part of an instance '
           'should be installed. You can list available configurations using the'
@@ -519,8 +513,8 @@
             msg = (
                 "Twisted is required by the 'start' command\n"
                 "Either install it, or use one of the alternative commands:\n"
-                "- '{ctl} wsgi {appid}'\n"
-                "- '{ctl} pyramid {appid}' (requires the pyramid cube)\n")
+                "- '{ctl} pyramid {appid}'\n"
+                "- '{ctl} wsgi {appid}'\n")
             raise ExecutionError(msg.format(ctl='cubicweb-ctl', appid=appid))
         config = cwcfg.config_for(appid, debugmode=self['debug'])
         # override config file values with cmdline options
@@ -757,6 +751,7 @@
             with mih.cnx:
                 with mih.cnx.security_enabled(False, False):
                     mih.migrate(vcconf, reversed(toupgrade), self.config)
+            clear_cache(config, 'instance_md5_version')
         else:
             print('-> no data migration needed for instance %s.' % appid)
         # rewrite main configuration file
--- a/cubicweb/cwvreg.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/cwvreg.py	Mon Mar 20 10:28:01 2017 +0100
@@ -29,7 +29,7 @@
 
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.deprecation import class_deprecated
-from logilab.common.modutils import cleanup_sys_modules
+from logilab.common.modutils import clean_sys_modules
 from logilab.common.registry import RegistryStore, Registry, ObjectNotFound, RegistryNotFound
 
 from rql import RQLHelper
@@ -417,7 +417,7 @@
         """set instance'schema and load application objects"""
         self._set_schema(schema)
         # now we can load application's web objects
-        self.reload(self.config.appobjects_path(), force_reload=False)
+        self.reload(self.config.appobjects_modnames(), force_reload=False)
         # map lowered entity type names to their actual name
         self.case_insensitive_etypes = {}
         for eschema in self.schema.entities():
@@ -426,13 +426,28 @@
             clear_cache(eschema, 'ordered_relations')
             clear_cache(eschema, 'meta_attributes')
 
+    def is_reload_needed(self, modnames):
+        """overriden to handle modules names instead of directories"""
+        lastmodifs = self._lastmodifs
+        for modname in modnames:
+            if modname not in sys.modules:
+                # new module to load
+                return True
+            filepath = sys.modules[modname].__file__
+            if filepath.endswith('.py'):
+                mdate = self._mdate(filepath)
+                if filepath not in lastmodifs or lastmodifs[filepath] < mdate:
+                    self.info('File %s changed since last visit', filepath)
+                    return True
+        return False
+
     def reload_if_needed(self):
-        path = self.config.appobjects_path()
-        if self.is_reload_needed(path):
-            self.reload(path)
+        modnames = self.config.appobjects_modnames()
+        if self.is_reload_needed(modnames):
+            self.reload(modnames)
 
-    def _cleanup_sys_modules(self, path):
-        """Remove submodules of `directories` from `sys.modules` and cleanup
+    def _cleanup_sys_modules(self, modnames):
+        """Remove modules and submodules of `modnames` from `sys.modules` and cleanup
         CW_EVENT_MANAGER accordingly.
 
         We take care to properly remove obsolete registry callbacks.
@@ -446,18 +461,18 @@
                 # for non-function callable, we do nothing interesting
                 module = getattr(func, '__module__', None)
                 caches[id(callback)] = module
-        deleted_modules = set(cleanup_sys_modules(path))
+        deleted_modules = set(clean_sys_modules(modnames))
         for callbacklist in callbackdata:
             for callback in callbacklist[:]:
                 module = caches[id(callback)]
                 if module and module in deleted_modules:
                     callbacklist.remove(callback)
 
-    def reload(self, path, force_reload=True):
+    def reload(self, modnames, force_reload=True):
         """modification detected, reset and reload the vreg"""
         CW_EVENT_MANAGER.emit('before-registry-reload')
         if force_reload:
-            self._cleanup_sys_modules(path)
+            self._cleanup_sys_modules(modnames)
             cubes = self.config.cubes()
             # if the fs code use some cubes not yet registered into the instance
             # we should cleanup sys.modules for those as well to avoid potential
@@ -465,9 +480,9 @@
             cfg = self.config
             for cube in cfg.expand_cubes(cubes, with_recommends=True):
                 if not cube in cubes:
-                    cpath = cfg.build_appobjects_cube_path([cfg.cube_dir(cube)])
-                    self._cleanup_sys_modules(cpath)
-        self.register_objects(path)
+                    cube_modnames = cfg.appobjects_cube_modnames(cube)
+                    self._cleanup_sys_modules(cube_modnames)
+        self.register_modnames(modnames)
         CW_EVENT_MANAGER.emit('after-registry-reload')
 
     def load_file(self, filepath, modname):
--- a/cubicweb/dataimport/importer.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/dataimport/importer.py	Mon Mar 20 10:28:01 2017 +0100
@@ -265,7 +265,7 @@
     :param extid2eid: optional {extid: eid} dictionary giving information on existing entities. It
         will be completed during import. You may want to use :func:`cwuri2eid` to build it.
 
-    :param existing_relation: optional {rtype: set((subj eid, obj eid))} mapping giving information
+    :param existing_relations: optional {rtype: set((subj eid, obj eid))} mapping giving information
         on existing relations of a given type. You may want to use :class:`RelationMapping` to build
         it.
 
--- a/cubicweb/dataimport/massive_store.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/dataimport/massive_store.py	Mon Mar 20 10:28:01 2017 +0100
@@ -72,7 +72,6 @@
 
         self.uuid = text_type(uuid4()).replace('-', '')
         self.slave_mode = slave_mode
-        self.eids_seq_range = eids_seq_range
         if metagen is None:
             metagen = stores.MetadataGenerator(cnx)
         self.metagen = metagen
@@ -81,7 +80,7 @@
         self.sql = cnx.system_sql
         self.schema = cnx.vreg.schema
         self.default_values = get_default_values(self.schema)
-        self.get_next_eid = lambda g=self._get_eid_gen(): next(g)
+        self.get_next_eid = lambda g=self._get_eid_gen(eids_seq_range): next(g)
         self._source_dbhelper = cnx.repo.system_source.dbhelper
         self._dbh = PGHelper(cnx)
 
@@ -89,13 +88,13 @@
         self._data_relations = defaultdict(list)
         self._initialized = {}
 
-    def _get_eid_gen(self):
+    def _get_eid_gen(self, eids_seq_range):
         """ Function getting the next eid. This is done by preselecting
         a given number of eids from the 'entities_id_seq', and then
         storing them"""
         while True:
-            last_eid = self._cnx.repo.system_source.create_eid(self._cnx, self.eids_seq_range)
-            for eid in range(last_eid - self.eids_seq_range + 1, last_eid + 1):
+            last_eid = self._cnx.repo.system_source.create_eid(self._cnx, eids_seq_range)
+            for eid in range(last_eid - eids_seq_range + 1, last_eid + 1):
                 yield eid
 
     # master/slaves specific API
--- a/cubicweb/dataimport/stores.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/dataimport/stores.py	Mon Mar 20 10:28:01 2017 +0100
@@ -149,11 +149,6 @@
     def commit(self):
         return self._commit()
 
-    @property
-    def session(self):
-        warnings.warn('[3.19] deprecated property.', DeprecationWarning, stacklevel=2)
-        return self._cnx.repo._get_session(self._cnx.sessionid)
-
     @deprecated("[3.19] use cnx.find(*args, **kwargs).entities() instead")
     def find_entities(self, *args, **kwargs):
         return self._cnx.find(*args, **kwargs).entities()
--- a/cubicweb/dataimport/test/test_massive_store.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/dataimport/test/test_massive_store.py	Mon Mar 20 10:28:01 2017 +0100
@@ -16,8 +16,6 @@
 # with this program. If not, see <http://www.gnu.org/licenses/>.
 """Massive store test case"""
 
-import itertools
-
 from cubicweb.devtools import testlib, PostgresApptestConfiguration
 from cubicweb.devtools import startpgcluster, stoppgcluster
 from cubicweb.dataimport import ucsvreader, stores
--- a/cubicweb/devtools/__init__.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/__init__.py	Mon Mar 20 10:28:01 2017 +0100
@@ -108,7 +108,6 @@
     * system source is shutdown
     """
     if not repo._needs_refresh:
-        repo.close_sessions()
         for cnxset in repo.cnxsets:
             cnxset.close(True)
         repo.system_source.shutdown()
@@ -125,9 +124,7 @@
     if repo._needs_refresh:
         for cnxset in repo.cnxsets:
             cnxset.reconnect()
-        repo._type_cache = {}
-        repo.querier._rql_cache = {}
-        repo.system_source.reset_caches()
+        repo.clear_caches()
         repo._needs_refresh = False
 
 
--- a/cubicweb/devtools/devctl.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/devctl.py	Mon Mar 20 10:28:01 2017 +0100
@@ -37,6 +37,7 @@
 from six.moves import input
 
 from logilab.common import STD_BLACKLIST
+from logilab.common.modutils import clean_sys_modules
 from logilab.common.fileutils import ensure_fs_mode
 from logilab.common.shellutils import find
 
@@ -100,24 +101,6 @@
         return None
 
 
-def cleanup_sys_modules(config):
-    # cleanup sys.modules, required when we're updating multiple cubes
-    appobjects_path = config.appobjects_path()
-    for name, mod in list(sys.modules.items()):
-        if mod is None:
-            # duh ? logilab.common.os for instance
-            del sys.modules[name]
-            continue
-        if not hasattr(mod, '__file__'):
-            continue
-        if mod.__file__ is None:
-            # odd/rare but real
-            continue
-        for path in appobjects_path:
-            if mod.__file__.startswith(path):
-                del sys.modules[name]
-                break
-
 def generate_schema_pot(w, cubedir=None):
     """generate a pot file with schema specific i18n messages
 
@@ -136,7 +119,7 @@
     else:
         config = DevConfiguration()
         cube = libconfig = None
-    cleanup_sys_modules(config)
+    clean_sys_modules(config.appobjects_modnames())
     schema = config.load_schema(remove_unused_rtypes=False)
     vreg = CWRegistryStore(config)
     # set_schema triggers objects registrations
@@ -161,7 +144,7 @@
         # (cubicweb incl.)
         from cubicweb.cwvreg import CWRegistryStore
         libschema = libconfig.load_schema(remove_unused_rtypes=False)
-        cleanup_sys_modules(libconfig)
+        clean_sys_modules(libconfig.appobjects_modnames())
         libvreg = CWRegistryStore(libconfig)
         libvreg.set_schema(libschema) # trigger objects registration
         libafss = libvreg['uicfg']['autoform_section']
--- a/cubicweb/devtools/fake.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/fake.py	Mon Mar 20 10:28:01 2017 +0100
@@ -34,7 +34,6 @@
 class FakeConfig(dict, BaseApptestConfiguration):
     translations = {}
     uiprops = {}
-    https_uiprops = {}
     apphome = None
     debugmode = False
     def __init__(self, appid='data', apphome=None, cubes=()):
@@ -46,7 +45,6 @@
         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
@@ -69,7 +67,6 @@
     def __init__(self, *args, **kwargs):
         if not (args or 'vreg' in kwargs):
             kwargs['vreg'] = FakeCWRegistryStore(FakeConfig(), initlog=False)
-        kwargs['https'] = False
         self._http_method = kwargs.pop('method', 'GET')
         self._url = kwargs.pop('url', None)
         if self._url is None:
@@ -135,7 +132,7 @@
         return True
 
 
-class FakeSession(RequestSessionBase):
+class FakeConnection(RequestSessionBase):
 
     def __init__(self, repo=None, user=None, vreg=None):
         self.repo = repo
@@ -154,8 +151,7 @@
 
     def commit(self, *args):
         self.transaction_data.clear()
-    def close(self, *args):
-        pass
+
     def system_sql(self, sql, args=None):
         pass
 
@@ -187,9 +183,6 @@
         self.vreg = vreg or FakeCWRegistryStore(self.config, initlog=False)
         self.vreg.schema = schema
 
-    def internal_session(self):
-        return FakeSession(self)
-
 
 class FakeSource(object):
     dbhelper = get_db_helper('sqlite')
--- a/cubicweb/devtools/repotest.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/repotest.py	Mon Mar 20 10:28:01 2017 +0100
@@ -21,13 +21,13 @@
 """
 from __future__ import print_function
 
+from contextlib import contextmanager
 from pprint import pprint
 
-from logilab.common.decorators import cachedproperty
 from logilab.common.testlib import SkipTest
 
 from cubicweb.devtools.testlib import RepoAccess
-
+from cubicweb.entities.authobjs import user_session_cache_key
 
 def tuplify(mylist):
     return [tuple(item) for item in mylist]
@@ -39,7 +39,7 @@
 
 
 def check_plan(self, rql, expected, kwargs=None):
-    with self.session.new_cnx() as cnx:
+    with self.admin_access.cnx() as cnx:
         plan = self._prepare_plan(cnx, rql, kwargs)
         self.planner.build_plan(plan)
         try:
@@ -135,10 +135,9 @@
 from rql import RQLHelper
 
 from cubicweb.devtools.testlib import BaseTestCase
-from cubicweb.devtools.fake import FakeRepo, FakeConfig, FakeSession, FakeRequest
+from cubicweb.devtools.fake import FakeRepo, FakeConfig, FakeRequest, FakeConnection
 from cubicweb.server import set_debug, debugged
 from cubicweb.server.querier import QuerierHelper
-from cubicweb.server.session import Session
 from cubicweb.server.sources.rql2sql import SQLGenerator, remove_unused_solutions
 
 class RQLGeneratorTC(BaseTestCase):
@@ -182,7 +181,7 @@
         #print '********* solutions', solutions
         self.rqlhelper.simplify(union)
         #print '********* simplified', union.as_string()
-        plan = self.qhelper.plan_factory(union, {}, FakeSession(self.repo))
+        plan = self.qhelper.plan_factory(union, {}, FakeConnection(self.repo))
         plan.preprocess(union)
         for select in union.children:
             select.solutions.sort(key=lambda x: list(x.items()))
@@ -193,14 +192,10 @@
 class BaseQuerierTC(TestCase):
     repo = None # set this in concrete class
 
-    @cachedproperty
-    def session(self):
-        return self._access._session
-
     def setUp(self):
         self.o = self.repo.querier
-        self._access = RepoAccess(self.repo, 'admin', FakeRequest)
-        self.ueid = self.session.user.eid
+        self.admin_access = RepoAccess(self.repo, 'admin', FakeRequest)
+        self.ueid = self.admin_access._user.eid
         assert self.ueid != -1
         self.repo._type_cache = {} # clear cache
         self.maxeid = self.get_max_eid()
@@ -208,18 +203,18 @@
         self._dumb_sessions = []
 
     def get_max_eid(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             return cnx.execute('Any MAX(X)')[0][0]
 
     def cleanup(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             cnx.execute('DELETE Any X WHERE X eid > %s' % self.maxeid)
             cnx.commit()
 
     def tearDown(self):
         undo_monkey_patch()
         self.cleanup()
-        assert self.session.user.eid != -1
+        assert self.admin_access._user.eid != -1
 
     def set_debug(self, debug):
         set_debug(debug)
@@ -250,17 +245,17 @@
         rqlst.solutions = remove_unused_solutions(rqlst, rqlst.solutions, self.repo.schema)[0]
         return rqlst
 
+    @contextmanager
     def user_groups_session(self, *groups):
         """lightweight session using the current user with hi-jacked groups"""
-        # use self.session.user.eid to get correct owned_by relation, unless explicit eid
-        with self.session.new_cnx() as cnx:
-            user_eid = self.session.user.eid
-            session = Session(self.repo._build_user(cnx, user_eid), self.repo)
-            session.data['%s-groups' % user_eid] = set(groups)
-            return session
+        # use cnx.user.eid to get correct owned_by relation, unless explicit eid
+        with self.admin_access.cnx() as cnx:
+            user_eid = cnx.user.eid
+            cnx.user._cw.data[user_session_cache_key(user_eid, 'groups')] = set(groups)
+            yield cnx
 
     def qexecute(self, rql, args=None, build_descr=True):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             try:
                 return self.o.execute(cnx, rql, args, build_descr)
             finally:
@@ -273,7 +268,6 @@
     def setup(self):
         # XXX source_defs
         self.o = self.repo.querier
-        self.session = self.repo._sessions.values()[0]
         self.schema = self.o.schema
         self.system = self.repo.system_source
         do_monkey_patch()
@@ -283,8 +277,8 @@
         undo_monkey_patch()
 
     def _prepare_plan(self, cnx, rql, kwargs=None):
-        rqlst = self.o.parse(rql, annotate=True)
-        self.o.solutions(cnx, rqlst, kwargs)
+        rqlst = self.repo.vreg.rqlhelper.parse(rql, annotate=True)
+        self.repo.vreg.solutions(cnx, rqlst, kwargs)
         if rqlst.TYPE == 'select':
             self.repo.vreg.rqlhelper.annotate(rqlst)
             for select in rqlst.children:
--- a/cubicweb/devtools/stresstester.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/stresstester.py	Mon Mar 20 10:28:01 2017 +0100
@@ -54,19 +54,23 @@
 
 from logilab.common.fileutils import lines
 from logilab.common.ureports import Table, TextWriter
+
+from cubicweb import repoapi
 from cubicweb.server.repository import Repository
 
 TB_LOCK = threading.Lock()
 
+
 class QueryExecutor:
-    def __init__(self, session, times, queries, reporter = None):
-        self._session = session
+
+    def __init__(self, cnx, times, queries, reporter = None):
+        self._cnx = cnx
         self._times = times
         self._queries = queries
         self._reporter = reporter
 
     def run(self):
-        with self._session.new_cnx() as cnx:
+        with self._cnx as cnx:
             times = self._times
             while times:
                 for index, query in enumerate(self._queries):
@@ -168,12 +172,13 @@
     # get local access to the repository
     print("Creating repo", prof_file)
     repo = Repository(config, prof_file)
-    session = repo.new_session(user, password=password)
+    repo.bootstrap()
+    cnx = repoapi.connect(repo, user, password=password)
     reporter = ProfileReporter(queries)
     if threads > 1:
         executors = []
         while threads:
-            qe = QueryExecutor(session, repeat, queries, reporter = reporter)
+            qe = QueryExecutor(cnx, repeat, queries, reporter=reporter)
             executors.append(qe)
             thread = threading.Thread(target=qe.run)
             qe.thread = thread
@@ -184,7 +189,7 @@
 ##         for qe in executors:
 ##             print qe.thread, repeat - qe._times, 'times'
     else:
-        QueryExecutor(session, repeat, queries, reporter = reporter).run()
+        QueryExecutor(cnx, repeat, queries, reporter=reporter).run()
     reporter.dump_report(report_output)
 
 
--- a/cubicweb/devtools/test/unittest_devctl.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/test/unittest_devctl.py	Mon Mar 20 10:28:01 2017 +0100
@@ -20,11 +20,11 @@
 import os
 import os.path as osp
 import sys
-import tempfile
-import shutil
 from subprocess import Popen, PIPE, STDOUT, check_output
 from unittest import TestCase
 
+from cubicweb.devtools.testlib import TemporaryDirectory
+
 
 def newcube(directory, name):
     cmd = ['cubicweb-ctl', 'newcube', '--directory', directory, name]
@@ -51,8 +51,7 @@
         expected_package_content = ['i18n', 'hooks.py', 'views.py',
                                     'migration', 'entities.py', 'schema.py',
                                     '__init__.py', 'data', '__pkginfo__.py']
-        tmpdir = tempfile.mkdtemp(prefix="temp-cwctl-newcube")
-        try:
+        with TemporaryDirectory(prefix="temp-cwctl-newcube") as tmpdir:
             retcode, stdout = newcube(tmpdir, 'foo')
             self.assertEqual(retcode, 0, msg=to_unicode(stdout))
             project_dir = osp.join(tmpdir, 'cubicweb-foo')
@@ -61,27 +60,21 @@
             package_content = os.listdir(package_dir)
             self.assertItemsEqual(project_content, expected_project_content)
             self.assertItemsEqual(package_content, expected_package_content)
-        finally:
-            shutil.rmtree(tmpdir, ignore_errors=True)
 
     def test_flake8(self):
         """Ensure newcube built from skeleton is flake8-compliant"""
-        tmpdir = tempfile.mkdtemp(prefix="temp-cwctl-newcube-flake8")
-        try:
+        with TemporaryDirectory(prefix="temp-cwctl-newcube-flake8") as tmpdir:
             newcube(tmpdir, 'foo')
             cmd = [sys.executable, '-m', 'flake8',
                    osp.join(tmpdir, 'cubicweb-foo', 'cubicweb_foo')]
             proc = Popen(cmd, stdout=PIPE, stderr=STDOUT)
             retcode = proc.wait()
-        finally:
-            shutil.rmtree(tmpdir, ignore_errors=True)
         self.assertEqual(retcode, 0,
                          msg=to_unicode(proc.stdout.read()))
 
     def test_newcube_sdist(self):
         """Ensure sdist can be built from a new cube"""
-        tmpdir = tempfile.mkdtemp(prefix="temp-cwctl-newcube-sdist")
-        try:
+        with TemporaryDirectory(prefix="temp-cwctl-newcube-sdist") as tmpdir:
             newcube(tmpdir, 'foo')
             projectdir = osp.join(tmpdir, 'cubicweb-foo')
             cmd = [sys.executable, 'setup.py', 'sdist']
@@ -91,13 +84,10 @@
             self.assertEqual(retcode, 0, stdout)
             distfpath = osp.join(projectdir, 'dist', 'cubicweb-foo-0.1.0.tar.gz')
             self.assertTrue(osp.isfile(distfpath))
-        finally:
-            shutil.rmtree(tmpdir, ignore_errors=True)
 
     def test_newcube_install(self):
         """Ensure a new cube can be installed"""
-        tmpdir = tempfile.mkdtemp(prefix="temp-cwctl-newcube-install")
-        try:
+        with TemporaryDirectory(prefix="temp-cwctl-newcube-install") as tmpdir:
             newcube(tmpdir, 'foo')
             projectdir = osp.join(tmpdir, 'cubicweb-foo')
             env = os.environ.copy()
@@ -120,8 +110,6 @@
             self.assertItemsEqual(pkgcontent,
                                   [b'schema.py', b'entities.py', b'hooks.py', b'__init__.py',
                                    b'__pkginfo__.py', b'views.py'])
-        finally:
-            shutil.rmtree(tmpdir, ignore_errors=True)
 
 
 if __name__ == '__main__':
--- a/cubicweb/devtools/test/unittest_testlib.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/test/unittest_testlib.py	Mon Mar 20 10:28:01 2017 +0100
@@ -285,10 +285,6 @@
             self.assertTrue(rset)
             self.assertEqual('babar', req.form['elephant'])
 
-    def test_close(self):
-        acc = self.new_access('admin')
-        acc.close()
-
     def test_admin_access(self):
         with self.admin_access.client_cnx() as cnx:
             self.assertEqual('admin', cnx.user.login)
--- a/cubicweb/devtools/testlib.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/testlib.py	Mon Mar 20 10:28:01 2017 +0100
@@ -44,16 +44,15 @@
 
 from cubicweb import (ValidationError, NoSelectableObject, AuthenticationError,
                       BadConnectionId)
-from cubicweb import cwconfig, devtools, web, server
+from cubicweb import cwconfig, devtools, repoapi, server, web
 from cubicweb.utils import json
 from cubicweb.sobjects import notification
 from cubicweb.web import Redirect, application, eid_param
 from cubicweb.server.hook import SendMailOp
-from cubicweb.server.session import Session
 from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS
 from cubicweb.devtools import fake, htmlparser, DEFAULT_EMPTY_DB_ID
 from cubicweb.devtools.fill import insert_entity_queries, make_relations_queries
-
+from cubicweb.web.views.authentication import Session
 
 if sys.version_info[:2] < (3, 4):
     from unittest2 import TestCase
@@ -223,42 +222,20 @@
 
     .. automethod:: cubicweb.testlib.RepoAccess.cnx
     .. automethod:: cubicweb.testlib.RepoAccess.web_request
-
-    The RepoAccess need to be closed to destroy the associated Session.
-    TestCase usually take care of this aspect for the user.
-
-    .. automethod:: cubicweb.testlib.RepoAccess.close
     """
 
     def __init__(self, repo, login, requestcls):
         self._repo = repo
         self._login = login
         self.requestcls = requestcls
-        self._session = self._unsafe_connect(login)
-
-    def _unsafe_connect(self, login, **kwargs):
-        """ a completely unsafe connect method for the tests """
-        # use an internal connection
-        with self._repo.internal_cnx() as cnx:
-            # try to get a user object
-            user = cnx.find('CWUser', login=login).one()
-            user.groups
-            user.properties
-            user.login
-            session = Session(user, self._repo)
-            self._repo._sessions[session.sessionid] = session
-            user._cw = user.cw_rset.req = session
-        with session.new_cnx() as cnx:
-            self._repo.hm.call_hooks('session_open', cnx)
-            # commit connection at this point in case write operation has been
-            # done during `session_open` hooks
-            cnx.commit()
-        return session
+        with repo.internal_cnx() as cnx:
+            self._user = cnx.find('CWUser', login=login).one()
+            self._user.cw_attr_cache['login'] = login
 
     @contextmanager
     def cnx(self):
         """Context manager returning a server side connection for the user"""
-        with self._session.new_cnx() as cnx:
+        with repoapi.Connection(self._repo, self._user) as cnx:
             yield cnx
 
     # aliases for bw compat
@@ -273,20 +250,19 @@
             req.cnx.commit()
             req.cnx.rolback()
         """
+        session = kwargs.pop('session', Session(self._repo, self._user))
         req = self.requestcls(self._repo.vreg, url=url, headers=headers,
                               method=method, form=kwargs)
-        with self._session.new_cnx() as cnx:
+        with self.cnx() as cnx:
+            # web request expect a session attribute on cnx referencing the web session
+            cnx.session = session
             req.set_cnx(cnx)
             yield req
 
-    def close(self):
-        """Close the session associated to the RepoAccess"""
-        self._session.close()
-
     @contextmanager
     def shell(self):
         from cubicweb.server.migractions import ServerMigrationHelper
-        with self._session.new_cnx() as cnx:
+        with self.cnx() as cnx:
             mih = ServerMigrationHelper(None, repo=self._repo, cnx=cnx,
                                         interactive=False,
                                         # hack so it don't try to load fs schema
@@ -333,7 +309,6 @@
         cls.config.mode = 'test'
 
     def __init__(self, *args, **kwargs):
-        self._admin_session = None
         self.repo = None
         self._open_access = set()
         super(CubicWebTC, self).__init__(*args, **kwargs)
@@ -360,15 +335,10 @@
     def _close_access(self):
         while self._open_access:
             try:
-                self._open_access.pop().close()
+                self._open_access.pop()
             except BadConnectionId:
                 continue  # already closed
 
-    @property
-    def session(self):
-        """return admin session"""
-        return self._admin_session
-
     def _init_repo(self):
         """init the repository and connection to it.
         """
@@ -380,7 +350,6 @@
         # get an admin session (without actual login)
         login = text_type(db_handler.config.default_admin_config['login'])
         self.admin_access = self.new_access(login)
-        self._admin_session = self.admin_access._session
 
     # config management ########################################################
 
@@ -456,10 +425,6 @@
         MAILBOX[:] = []  # reset mailbox
 
     def tearDown(self):
-        # XXX hack until logilab.common.testlib is fixed
-        if self._admin_session is not None:
-            self._admin_session.close()
-            self._admin_session = None
         while self._cleanups:
             cleanup, args, kwargs = self._cleanups.pop(-1)
             cleanup(*args, **kwargs)
@@ -679,7 +644,8 @@
     def list_boxes_for(self, rset):
         """returns the list of boxes that can be applied on `rset`"""
         req = rset.req
-        for box in self.vreg['ctxcomponents'].possible_objects(req, rset=rset):
+        for box in self.vreg['ctxcomponents'].possible_objects(req, rset=rset,
+                                                               view=None):
             yield box
 
     def list_startup_views(self):
@@ -715,10 +681,10 @@
         return ctrl.publish(), req
 
     @contextmanager
-    def remote_calling(self, fname, *args):
+    def remote_calling(self, fname, *args, **kwargs):
         """remote json call simulation"""
         args = [json.dumps(arg) for arg in args]
-        with self.admin_access.web_request(fname=fname, pageid='123', arg=args) as req:
+        with self.admin_access.web_request(fname=fname, pageid='123', arg=args, **kwargs) as req:
             ctrl = self.vreg['controllers'].select('ajax', req)
             yield ctrl.publish(), req
 
@@ -900,15 +866,15 @@
         authm.anoninfo = authm.anoninfo[0], {'password': authm.anoninfo[1]}
         # not properly cleaned between tests
         self.open_sessions = sh.session_manager._sessions = {}
-        return req, self.session
+        return req
 
-    def assertAuthSuccess(self, req, origsession, nbsessions=1):
+    def assertAuthSuccess(self, req, nbsessions=1):
         session = self.app.get_session(req)
         cnx = session.new_cnx()
         with cnx:
             req.set_cnx(cnx)
         self.assertEqual(len(self.open_sessions), nbsessions, self.open_sessions)
-        self.assertEqual(session.login, origsession.login)
+        self.assertEqual(req.user.login, self.admlogin)
         self.assertEqual(session.anonymous_session, False)
 
     def assertAuthFailure(self, req, nbsessions=0):
--- a/cubicweb/entities/__init__.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/entities/__init__.py	Mon Mar 20 10:28:01 2017 +0100
@@ -98,57 +98,15 @@
 
     # meta data api ###########################################################
 
-    def dc_title(self):
-        """return a suitable *unicode* title for this entity"""
-        for rschema, attrschema in self.e_schema.attribute_definitions():
-            if rschema.meta:
-                continue
-            value = self.cw_attr_value(rschema.type)
-            if value is not None:
-                # make the value printable (dates, floats, bytes, etc.)
-                return self.printable_value(rschema.type, value, attrschema.type,
-                                            format='text/plain')
-        return u'%s #%s' % (self.dc_type(), self.eid)
-
-    def dc_long_title(self):
-        """return a more detailled title for this entity"""
-        return self.dc_title()
-
-    def dc_description(self, format='text/plain'):
-        """return a suitable description for this entity"""
-        if 'description' in self.e_schema.subjrels:
-            return self.printable_value('description', format=format)
-        return u''
-
-    def dc_authors(self):
-        """return a suitable description for the author(s) of the entity"""
-        try:
-            return ', '.join(u.name() for u in self.owned_by)
-        except Unauthorized:
-            return u''
-
-    def dc_creator(self):
-        """return a suitable description for the creator of the entity"""
-        if self.creator:
-            return self.creator.name()
-        return u''
-
-    def dc_date(self, date_format=None):# XXX default to ISO 8601 ?
-        """return latest modification date of this entity"""
-        return self._cw.format_date(self.modification_date, date_format=date_format)
-
-    def dc_type(self, form=''):
-        """return the display name for the type of this entity (translated)"""
-        return self.e_schema.display_name(self._cw, form)
-
-    def dc_language(self):
-        """return language used by this entity (translated)"""
-        # check if entities has internationalizable attributes
-        # XXX one is enough or check if all String attributes are internationalizable?
-        for rschema, attrschema in self.e_schema.attribute_definitions():
-            if rschema.rdef(self.e_schema, attrschema).internationalizable:
-                return self._cw._(self._cw.user.property_value('ui.language'))
-        return self._cw._(self._cw.vreg.property_value('ui.language'))
+    def __getattr__(self, name):
+        prefix = 'dc_'
+        if name.startswith(prefix):
+            # Proxy to IDublinCore adapter for bw compat.
+            adapted = self.cw_adapt_to('IDublinCore')
+            method = name[len(prefix):]
+            if hasattr(adapted, method):
+                return getattr(adapted, method)
+        raise AttributeError(name)
 
     @property
     def creator(self):
--- a/cubicweb/entities/adapters.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/entities/adapters.py	Mon Mar 20 10:28:01 2017 +0100
@@ -25,10 +25,71 @@
 from logilab.mtconverter import TransformError
 from logilab.common.decorators import cached
 
-from cubicweb import ValidationError, view, ViolatedConstraint, UniqueTogetherError
+from cubicweb import (Unauthorized, ValidationError, view, ViolatedConstraint,
+                      UniqueTogetherError)
 from cubicweb.predicates import is_instance, relation_possible, match_exception
 
 
+class IDublinCoreAdapter(view.EntityAdapter):
+    __regid__ = 'IDublinCore'
+    __select__ = is_instance('Any')
+
+    def title(self):
+        """Return a suitable *unicode* title for entity"""
+        entity = self.entity
+        for rschema, attrschema in entity.e_schema.attribute_definitions():
+            if rschema.meta:
+                continue
+            value = entity.cw_attr_value(rschema.type)
+            if value is not None:
+                # make the value printable (dates, floats, bytes, etc.)
+                return entity.printable_value(
+                    rschema.type, value, attrschema.type, format='text/plain')
+        return u'%s #%s' % (self.type(), entity.eid)
+
+    def long_title(self):
+        """Return a more detailled title for entity"""
+        return self.title()
+
+    def description(self, format='text/plain'):
+        """Return a suitable description for entity"""
+        if 'description' in self.entity.e_schema.subjrels:
+            return self.entity.printable_value('description', format=format)
+        return u''
+
+    def authors(self):
+        """Return a suitable description for the author(s) of the entity"""
+        try:
+            return u', '.join(u.name() for u in self.entity.owned_by)
+        except Unauthorized:
+            return u''
+
+    def creator(self):
+        """Return a suitable description for the creator of the entity"""
+        if self.entity.creator:
+            return self.entity.creator.name()
+        return u''
+
+    def date(self, date_format=None):  # XXX default to ISO 8601 ?
+        """Return latest modification date of entity"""
+        return self._cw.format_date(self.entity.modification_date,
+                                    date_format=date_format)
+
+    def type(self, form=''):
+        """Return the display name for the type of entity (translated)"""
+        return self.entity.e_schema.display_name(self._cw, form)
+
+    def language(self):
+        """Return language used by this entity (translated)"""
+        eschema = self.entity.e_schema
+        # check if entities has internationalizable attributes
+        # XXX one is enough or check if all String attributes are internationalizable?
+        for rschema, attrschema in eschema.attribute_definitions():
+            if rschema.rdef(eschema, attrschema).internationalizable:
+                return self._cw._(self._cw.user.property_value('ui.language'))
+        return self._cw._(self._cw.vreg.property_value('ui.language'))
+
+
 class IEmailableAdapter(view.EntityAdapter):
     __regid__ = 'IEmailable'
     __select__ = relation_possible('primary_email') | relation_possible('use_email')
--- a/cubicweb/entities/authobjs.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/entities/authobjs.py	Mon Mar 20 10:28:01 2017 +0100
@@ -17,8 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """entity classes user and group entities"""
 
-
-
 from six import string_types, text_type
 
 from logilab.common.decorators import cached
@@ -173,14 +171,3 @@
         return self.login
 
     dc_long_title = name
-
-    def __call__(self, *args, **kwargs):
-        """ugly hack for compatibility betweeb dbapi and repo api
-
-        In the dbapi, Connection and Session have a ``user`` method to
-        generated a user for a request In the repo api, Connection and Session
-        have a user attribute inherited from SessionRequestBase prototype. This
-        ugly hack allows to not break user of the user method.
-
-        XXX Deprecate me ASAP"""
-        return self
--- a/cubicweb/entity.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/entity.py	Mon Mar 20 10:28:01 2017 +0100
@@ -985,11 +985,10 @@
             if not safe:
                 raise
             rset = self._cw.empty_rset()
+        if cacheable:
+            self.cw_set_relation_cache(rtype, role, rset)
         if entities:
-            if cacheable:
-                self.cw_set_relation_cache(rtype, role, rset)
-                return self.related(rtype, role, entities=entities)
-            return list(rset.entities())
+            return tuple(rset.entities())
         else:
             return rset
 
@@ -1251,7 +1250,7 @@
     def cw_set_relation_cache(self, rtype, role, rset):
         """set cached values for the given relation"""
         if rset:
-            related = list(rset.entities(0))
+            related = tuple(rset.entities(0))
             rschema = self._cw.vreg.schema.rschema(rtype)
             if role == 'subject':
                 rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
--- a/cubicweb/etwist/http.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/http.py	Mon Mar 20 10:28:01 2017 +0100
@@ -8,6 +8,7 @@
 
 
 
+
 class HTTPResponse(object):
     """An object representing an HTTP Response to be sent to the client.
     """
@@ -29,9 +30,14 @@
         # add content-length if not present
         if (self._headers_out.getHeader('content-length') is None
             and self._stream is not None):
-           self._twreq.setHeader('content-length', len(self._stream))
+            self._twreq.setHeader('content-length', len(self._stream))
 
     def _finalize(self):
+        # cw_failed is set on errors such as "connection aborted by client". In
+        # such cases, req.finish() was already called and calling it a twice
+        # would crash
+        if getattr(self._twreq, 'cw_failed', False):
+            return
         # we must set code before writing anything, else it's too late
         if self._code is not None:
             self._twreq.setResponseCode(self._code)
--- a/cubicweb/etwist/request.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/request.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -17,8 +17,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Twisted request handler for CubicWeb"""
 
-
-
+from six import text_type
 
 from cubicweb.web.request import CubicWebRequestBase
 
@@ -27,19 +26,19 @@
     """ from twisted .req to cubicweb .form
     req.files are put into .form[<filefield>]
     """
-    def __init__(self, req, vreg, https):
+    def __init__(self, req, vreg):
         self._twreq = req
         super(CubicWebTwistedRequestAdapter, self).__init__(
-            vreg, https, req.args, headers=req.received_headers)
+            vreg, req.args, headers=req.received_headers)
         for key, name_stream_list in req.files.items():
             for name, stream in name_stream_list:
                 if name is not None:
-                    name = unicode(name, self.encoding)
+                    name = text_type(name, self.encoding)
                 self.form.setdefault(key, []).append((name, stream))
             # 3.16.4 backward compat
             if len(self.form[key]) == 1:
                 self.form[key] = self.form[key][0]
-        self.content = self._twreq.content # stream
+        self.content = self._twreq.content  # stream
 
     def http_method(self):
         """returns 'POST', 'GET', 'HEAD', etc."""
@@ -53,7 +52,7 @@
         :param includeparams:
            boolean indicating if GET form parameters should be kept in the path
         """
-        path = self._twreq.uri[1:] # remove the root '/'
+        path = self._twreq.uri[1:]  # remove the root '/'
         if not includeparams:
             path = path.split('?', 1)[0]
         return path
--- a/cubicweb/etwist/server.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/server.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -17,14 +17,12 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """twisted server for CubicWeb web instances"""
 
-
 import sys
-import select
 import traceback
 import threading
 from cgi import FieldStorage, parse_header
-
-from six.moves.urllib.parse import urlsplit, urlunsplit
+from functools import partial
+import warnings
 
 from cubicweb.statsd_logger import statsd_timeit
 
@@ -44,6 +42,7 @@
 from cubicweb.etwist.request import CubicWebTwistedRequestAdapter
 from cubicweb.etwist.http import HTTPResponse
 
+
 def start_task(interval, func):
     lc = task.LoopingCall(func)
     # wait until interval has expired to actually start the task, else we have
@@ -59,7 +58,6 @@
         # checks done before daemonization (eg versions consistency)
         self.appli = CubicWebPublisher(repo, config)
         self.base_url = config['base-url']
-        self.https_url = config['https-url']
         global MAX_POST_LENGTH
         MAX_POST_LENGTH = config['max-post-length']
 
@@ -71,7 +69,10 @@
             if config.mode != 'test':
                 reactor.addSystemEventTrigger('before', 'shutdown',
                                               self.shutdown_event)
-                self.appli.repo.start_looping_tasks()
+                warnings.warn(
+                    'twisted server does not start repository looping tasks anymore; '
+                    'use the standalone "scheduler" command if needed'
+                )
         self.set_url_rewriter()
         CW_EVENT_MANAGER.bind('after-registry-reload', self.set_url_rewriter)
 
@@ -92,13 +93,20 @@
         """Indicate which resource to use to process down the URL's path"""
         return self
 
+    def on_request_finished_ko(self, request, reason):
+        # annotate the twisted request so that we're able later to check for
+        # failure without having to dig into request's internal attributes such
+        # as _disconnected
+        request.cw_failed = True
+        self.warning('request finished abnormally: %s', reason)
+
     def render(self, request):
         """Render a page from the root resource"""
+        finish_deferred = request.notifyFinish()
+        finish_deferred.addErrback(partial(self.on_request_finished_ko, request))
         # reload modified files in debug mode
         if self.config.debugmode:
             self.config.uiprops.reload_if_needed()
-            if self.https_url:
-                self.config.https_uiprops.reload_if_needed()
             self.appli.vreg.reload_if_needed()
         if self.config['profile']: # default profiler don't trace threads
             return self.render_request(request)
@@ -123,18 +131,11 @@
     def _render_request(self, request):
         origpath = request.path
         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
         if self.url_rewriter is not None:
             # XXX should occur before authentication?
             path = self.url_rewriter.rewrite(host, origpath, request)
             request.uri.replace(origpath, path, 1)
-        req = CubicWebTwistedRequestAdapter(request, self.appli.vreg, https)
+        req = CubicWebTwistedRequestAdapter(request, self.appli.vreg)
         try:
             ### Try to generate the actual request content
             content = self.appli.handle_request(req)
--- a/cubicweb/etwist/service.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/service.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -38,15 +38,17 @@
 from cubicweb import set_log_methods
 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
 
+
 def _check_env(env):
     env_vars = ('CW_INSTANCES_DIR', 'CW_INSTANCES_DATA_DIR', 'CW_RUNTIME_DIR')
     for var in env_vars:
         if var not in env:
-            raise Exception('The environment variables %s must be set.' % \
+            raise Exception('The environment variables %s must be set.' %
                             ', '.join(env_vars))
     if not env.get('USERNAME'):
         env['USERNAME'] = 'cubicweb'
 
+
 class CWService(object, win32serviceutil.ServiceFramework):
     _svc_name_ = None
     _svc_display_name_ = None
@@ -80,8 +82,7 @@
             config.debugmode = False
             logger.info('starting cubicweb instance %s ', self.instance)
             config.info('clear ui caches')
-            for cachedir in ('uicache', 'uicachehttps'):
-                rm(join(config.appdatahome, cachedir, '*'))
+            rm(join(config.appdatahome, 'uicache', '*'))
             root_resource = CubicWebRootResource(config, config.repository())
             website = server.Site(root_resource)
             # serve it via standard HTTP on port set in the configuration
--- a/cubicweb/etwist/twconfig.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/twconfig.py	Mon Mar 20 10:28:01 2017 +0100
@@ -28,6 +28,7 @@
 from logilab.common.configuration import Method, merge_options
 
 from cubicweb.cwconfig import CONFIGURATIONS
+from cubicweb.server.serverconfig import ServerConfiguration
 from cubicweb.web.webconfig import WebConfiguration
 
 
@@ -96,20 +97,14 @@
         return 'http://%s:%s/' % (self['host'] or getfqdn().lower(), self['port'] or 8080)
 
 
-try:
-    from cubicweb.server.serverconfig import ServerConfiguration
+class AllInOneConfiguration(WebConfigurationBase, ServerConfiguration):
+    """repository and web instance in the same twisted process"""
+    name = 'all-in-one'
+    options = merge_options(WebConfigurationBase.options
+                            + ServerConfiguration.options)
 
-    class AllInOneConfiguration(WebConfigurationBase, ServerConfiguration):
-        """repository and web instance in the same twisted process"""
-        name = 'all-in-one'
-        options = merge_options(WebConfigurationBase.options
-                                + ServerConfiguration.options)
-
-        cubicweb_appobject_path = WebConfigurationBase.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path
-        cube_appobject_path = WebConfigurationBase.cube_appobject_path | ServerConfiguration.cube_appobject_path
+    cubicweb_appobject_path = WebConfigurationBase.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path
+    cube_appobject_path = WebConfigurationBase.cube_appobject_path | ServerConfiguration.cube_appobject_path
 
 
-    CONFIGURATIONS.append(AllInOneConfiguration)
-
-except ImportError:
-    pass
+CONFIGURATIONS.append(AllInOneConfiguration)
--- a/cubicweb/etwist/twctl.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/twctl.py	Mon Mar 20 10:28:01 2017 +0100
@@ -19,6 +19,7 @@
 
 from cubicweb.toolsutils import CommandHandler
 from cubicweb.web.webctl import WebCreateHandler, WebUpgradeHandler
+from cubicweb.server import serverctl
 
 # trigger configuration registration
 import cubicweb.etwist.twconfig # pylint: disable=W0611
@@ -45,35 +46,30 @@
     cfgname = 'twisted'
 
 
-try:
-    from cubicweb.server import serverctl
-    class AllInOneCreateHandler(serverctl.RepositoryCreateHandler,
-                                TWCreateHandler):
-        """configuration to get an instance running in a twisted web server
-        integrating a repository server in the same process
-        """
-        cfgname = 'all-in-one'
+class AllInOneCreateHandler(serverctl.RepositoryCreateHandler,
+                            TWCreateHandler):
+    """configuration to get an instance running in a twisted web server
+    integrating a repository server in the same process
+    """
+    cfgname = 'all-in-one'
 
-        def bootstrap(self, cubes, automatic=False, inputlevel=0):
-            """bootstrap this configuration"""
-            serverctl.RepositoryCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
-            TWCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
+    def bootstrap(self, cubes, automatic=False, inputlevel=0):
+        """bootstrap this configuration"""
+        serverctl.RepositoryCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
+        TWCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
 
-    class AllInOneStartHandler(TWStartHandler):
-        cmdname = 'start'
-        cfgname = 'all-in-one'
-        subcommand = 'cubicweb-twisted'
+class AllInOneStartHandler(TWStartHandler):
+    cmdname = 'start'
+    cfgname = 'all-in-one'
+    subcommand = 'cubicweb-twisted'
 
-    class AllInOneStopHandler(CommandHandler):
-        cmdname = 'stop'
-        cfgname = 'all-in-one'
-        subcommand = 'cubicweb-twisted'
+class AllInOneStopHandler(CommandHandler):
+    cmdname = 'stop'
+    cfgname = 'all-in-one'
+    subcommand = 'cubicweb-twisted'
 
-        def poststop(self):
-            pass
+    def poststop(self):
+        pass
 
-    class AllInOneUpgradeHandler(TWUpgradeHandler):
-        cfgname = 'all-in-one'
-
-except ImportError:
-    pass
+class AllInOneUpgradeHandler(TWUpgradeHandler):
+    cfgname = 'all-in-one'
--- a/cubicweb/hooks/__init__.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/hooks/__init__.py	Mon Mar 20 10:28:01 2017 +0100
@@ -29,6 +29,8 @@
     events = ('server_startup',)
 
     def __call__(self):
+        if self.repo._scheduler is None:
+            return
         # XXX use named args and inner functions to avoid referencing globals
         # which may cause reloading pb
         lifetime = timedelta(days=self.repo.config['keep-transaction-lifetime'])
@@ -49,6 +51,8 @@
     events = ('server_startup',)
 
     def __call__(self):
+        if self.repo._scheduler is None:
+            return
         def update_feeds(repo):
             # take a list to avoid iterating on a dictionary whose size may
             # change
@@ -71,6 +75,8 @@
     events = ('server_startup',)
 
     def __call__(self):
+        if self.repo._scheduler is None:
+            return
         def expire_dataimports(repo=self.repo):
             for uri, source in repo.sources_by_uri.items():
                 if (uri == 'system'
--- a/cubicweb/hooks/syncschema.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/hooks/syncschema.py	Mon Mar 20 10:28:01 2017 +0100
@@ -217,10 +217,6 @@
             repo.schema.rebuild_infered_relations()
             # trigger vreg reload
             repo.set_schema(repo.schema)
-            # CWUser class might have changed, update current session users
-            cwuser_cls = self.cnx.vreg['etypes'].etype_class('CWUser')
-            for session in repo._sessions.values():
-                session.user.__class__ = cwuser_cls
         except Exception:
             self.critical('error while setting schema', exc_info=True)
 
--- a/cubicweb/hooks/syncsession.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/hooks/syncsession.py	Mon Mar 20 10:28:01 2017 +0100
@@ -24,11 +24,9 @@
 from cubicweb.entities.authobjs import user_session_cache_key
 
 
-# take cnx and not repo because it's needed for other sessions implementation (e.g. pyramid)
-def get_user_sessions(cnx, ueid):
-    for session in cnx.repo._sessions.values():
-        if ueid == session.user.eid:
-            yield session
+def get_user_sessions(cnx, user_eid):
+    if cnx.user.eid == user_eid:
+        yield cnx
 
 
 class CachedValueMixin(object):
@@ -118,7 +116,6 @@
             # remove cached groups for the user
             key = user_session_cache_key(self.session.user.eid, 'groups')
             self.session.data.pop(key, None)
-            self.session.repo.close(self.session.sessionid)
         except BadConnectionId:
             pass  # already closed
 
--- a/cubicweb/hooks/test/unittest_security.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/hooks/test/unittest_security.py	Mon Mar 20 10:28:01 2017 +0100
@@ -51,6 +51,7 @@
                     cnx.commit()
                     self.assertEqual(email.sender[0].eid, self.add_eid)
 
+
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- a/cubicweb/i18n/de.po	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/i18n/de.po	Mon Mar 20 10:28:01 2017 +0100
@@ -198,6 +198,9 @@
 msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
 msgstr ""
 
+msgid "And more composite entities"
+msgstr ""
+
 msgid "Attributes permissions:"
 msgstr "Rechte der Attribute"
 
--- a/cubicweb/i18n/en.po	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/i18n/en.po	Mon Mar 20 10:28:01 2017 +0100
@@ -187,6 +187,9 @@
 msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
 msgstr ""
 
+msgid "And more composite entities"
+msgstr ""
+
 msgid "Attributes permissions:"
 msgstr ""
 
--- a/cubicweb/i18n/es.po	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/i18n/es.po	Mon Mar 20 10:28:01 2017 +0100
@@ -201,6 +201,9 @@
 msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
 msgstr "Relación agregada : %(entity_from)s %(rtype)s %(entity_to)s"
 
+msgid "And more composite entities"
+msgstr ""
+
 msgid "Attributes permissions:"
 msgstr "Permisos de atributos:"
 
--- a/cubicweb/i18n/fr.po	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/i18n/fr.po	Mon Mar 20 10:28:01 2017 +0100
@@ -195,6 +195,9 @@
 msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
 msgstr "Relation ajoutée : %(entity_from)s %(rtype)s %(entity_to)s"
 
+msgid "And more composite entities"
+msgstr "Et d'autres entités"
+
 msgid "Attributes permissions:"
 msgstr "Permissions des attributs"
 
--- a/cubicweb/predicates.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/predicates.py	Mon Mar 20 10:28:01 2017 +0100
@@ -868,7 +868,7 @@
 
 
 class partial_has_related_entities(PartialPredicateMixIn, has_related_entities):
-    """Same as :class:~`cubicweb.predicates.has_related_entity`, but will look
+    """Same as :class:~`cubicweb.predicates.has_related_entities`, but will look
     for attributes of the selected class to get information which is otherwise
     expected by the initializer.
 
--- a/cubicweb/pyramid/__init__.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/__init__.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,9 +1,34 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
+"""Pyramid interface to CubicWeb"""
+
+import atexit
 import os
 from warnings import warn
+
 import wsgicors
 
 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
 from pyramid.config import Configurator
+from pyramid.exceptions import ConfigurationError
 from pyramid.settings import asbool, aslist
 
 try:
@@ -12,11 +37,9 @@
     from ConfigParser import SafeConfigParser
 
 
-def make_cubicweb_application(cwconfig, settings=None):
-    """
-    Create a pyramid-based CubicWeb instance from a cubicweb configuration.
-
-    It is initialy meant to be used by the 'pyramid' command of cubicweb-ctl.
+def config_from_cwconfig(cwconfig, settings=None):
+    """Return a Pyramid Configurator instance built from a CubicWeb config and
+    Pyramid-specific configuration files (pyramid.ini).
 
     :param cwconfig: A CubicWeb configuration
     :returns: A Pyramid config object
@@ -74,7 +97,7 @@
 
     :returns: A fully operationnal WSGI application
     """
-    config = make_cubicweb_application(cwconfig)
+    config = config_from_cwconfig(cwconfig)
     profile = profile or asbool(config.registry.settings.get(
         'cubicweb.profile.enable', False))
     if profile:
@@ -146,11 +169,24 @@
     return wsgi_application_from_cwconfig(cwconfig)
 
 
+def pyramid_app(global_config, **settings):
+    """Return a Pyramid WSGI application bound to a CubicWeb repository."""
+    config = Configurator(settings=settings)
+    config.include('cubicweb.pyramid')
+    return config.make_wsgi_app()
+
+
 def includeme(config):
     """Set-up a CubicWeb instance.
 
     The CubicWeb instance can be set in several ways:
 
+    -   Provide an already loaded CubicWeb repository in the registry:
+
+        .. code-block:: python
+
+            config.registry['cubicweb.repository'] = your_repo_instance
+
     -   Provide an already loaded CubicWeb config instance in the registry:
 
         .. code-block:: python
@@ -160,8 +196,24 @@
     -   Provide an instance name in the pyramid settings with
         :confval:`cubicweb.instance`.
 
+    A CubicWeb repository is instantiated and attached in
+    'cubicweb.repository' registry key if not already present.
+
+    The CubicWeb instance registry is attached in 'cubicweb.registry' registry
+    key.
     """
     cwconfig = config.registry.get('cubicweb.config')
+    repo = config.registry.get('cubicweb.repository')
+
+    if repo is not None:
+        if cwconfig is None:
+            config.registry['cubicweb.config'] = cwconfig = repo.config
+        elif cwconfig is not repo.config:
+            raise ConfigurationError(
+                'CubicWeb config instance (found in "cubicweb.config" '
+                'registry key) mismatches with that of the repository '
+                '(registry["cubicweb.repository"])'
+            )
 
     if cwconfig is None:
         debugmode = asbool(
@@ -170,15 +222,11 @@
             config.registry.settings['cubicweb.instance'], debugmode=debugmode)
         config.registry['cubicweb.config'] = cwconfig
 
-    if cwconfig.debugmode:
-        try:
-            config.include('pyramid_debugtoolbar')
-        except ImportError:
-            warn('pyramid_debugtoolbar package not available, install it to '
-                 'get UI debug features', RuntimeWarning)
+    if repo is None:
+        repo = config.registry['cubicweb.repository'] = cwconfig.repository()
+    config.registry['cubicweb.registry'] = repo.vreg
 
-    config.registry['cubicweb.repository'] = repo = cwconfig.repository()
-    config.registry['cubicweb.registry'] = repo.vreg
+    atexit.register(repo.shutdown)
 
     if asbool(config.registry.settings.get('cubicweb.defaults', True)):
         config.include('cubicweb.pyramid.defaults')
@@ -186,10 +234,14 @@
     for name in aslist(config.registry.settings.get('cubicweb.includes', [])):
         config.include(name)
 
-    config.include('cubicweb.pyramid.tools')
-    config.include('cubicweb.pyramid.predicates')
     config.include('cubicweb.pyramid.core')
-    config.include('cubicweb.pyramid.syncsession')
 
     if asbool(config.registry.settings.get('cubicweb.bwcompat', True)):
         config.include('cubicweb.pyramid.bwcompat')
+
+    if cwconfig.debugmode:
+        try:
+            config.include('pyramid_debugtoolbar')
+        except ImportError:
+            warn('pyramid_debugtoolbar package not available, install it to '
+                 'get UI debug features', RuntimeWarning)
--- a/cubicweb/pyramid/auth.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/auth.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,25 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
+"""Authentication policies for cubicweb.pyramid."""
+
 import datetime
 import logging
 import warnings
@@ -106,20 +128,10 @@
         session_prefix = 'cubicweb.auth.authtkt.session.'
         persistent_prefix = 'cubicweb.auth.authtkt.persistent.'
 
-        try:
-            secret = config.registry['cubicweb.config']['pyramid-auth-secret']
-            warnings.warn(
-                "pyramid-auth-secret from all-in-one is now "
-                "cubicweb.auth.authtkt.[session|persistent].secret",
-                DeprecationWarning)
-        except:
-            secret = 'notsosecret'
-
         session_secret = settings.get(
-            session_prefix + 'secret', secret)
+            session_prefix + 'secret', 'notsosecret')
         persistent_secret = settings.get(
-            persistent_prefix + 'secret', secret)
-
+            persistent_prefix + 'secret', 'notsosecret')
         if 'notsosecret' in (session_secret, persistent_secret):
             warnings.warn('''
 
--- a/cubicweb/pyramid/bwcompat.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/bwcompat.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,25 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
+"""Backward compatibility layer for CubicWeb to run as a Pyramid application."""
+
 import sys
 import logging
 
@@ -53,10 +75,6 @@
         CubicWebPublisher.core_handle do
         """
 
-        # XXX The main handler of CW forbid anonymous https connections
-        # I guess we can drop this "feature" but in doubt I leave this comment
-        # so we don't forget about it. (cdevienne)
-
         req = request.cw_request
         vreg = request.registry['cubicweb.registry']
 
@@ -170,10 +188,6 @@
         self.cwhandler = registry['cubicweb.handler']
 
     def __call__(self, request):
-        if request.path.startswith('/https/'):
-            request.environ['PATH_INFO'] = request.environ['PATH_INFO'][6:]
-            assert not request.path.startswith('/https/')
-            request.scheme = 'https'
         try:
             response = self.handler(request)
         except httpexceptions.HTTPNotFound:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/config.py	Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,64 @@
+# copyright 2017 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/>.
+"""Configuration for CubicWeb instances on top of a Pyramid application"""
+
+from os import path
+import random
+import string
+
+from logilab.common.configuration import merge_options
+
+from cubicweb.cwconfig import CONFIGURATIONS
+from cubicweb.server.serverconfig import ServerConfiguration
+from cubicweb.toolsutils import fill_templated_file
+from cubicweb.web.webconfig import BaseWebConfiguration
+
+
+def get_random_secret_key():
+    """Return 50-character secret string"""
+    chars = string.ascii_letters + string.digits
+    return "".join([random.choice(chars) for i in range(50)])
+
+
+class CubicWebPyramidConfiguration(BaseWebConfiguration, ServerConfiguration):
+    """Pyramid application with a CubicWeb repository"""
+    name = 'pyramid'
+
+    cubicweb_appobject_path = (BaseWebConfiguration.cubicweb_appobject_path
+                               | ServerConfiguration.cubicweb_appobject_path)
+    cube_appobject_path = (BaseWebConfiguration.cube_appobject_path
+                           | ServerConfiguration.cube_appobject_path)
+
+    options = merge_options(ServerConfiguration.options +
+                            BaseWebConfiguration.options)
+
+    def write_development_ini(self, cubes):
+        """Write a 'development.ini' file into apphome."""
+        template_fpath = path.join(path.dirname(__file__), 'development.ini.tmpl')
+        target_fpath = path.join(self.apphome, 'development.ini')
+        context = {
+            'instance': self.appid,
+            'cubename': cubes[0],
+            'session-secret': get_random_secret_key(),
+            'auth-authtkt-persistent-secret': get_random_secret_key(),
+            'auth-authtkt-session-secret': get_random_secret_key(),
+        }
+        fill_templated_file(template_fpath, target_fpath, context)
+
+
+CONFIGURATIONS.append(CubicWebPyramidConfiguration)
--- a/cubicweb/pyramid/core.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/core.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,25 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
+"""Binding of CubicWeb connection to Pyramid request."""
+
 import itertools
 
 from contextlib import contextmanager
@@ -30,12 +52,12 @@
     """
 
     def __init__(self, session, *args, **kw):
-        super(Connection, self).__init__(session, *args, **kw)
-        self._session = session
+        super(Connection, self).__init__(session._repo, session._user, *args, **kw)
+        self.session = session
         self.lang = session._cached_lang
 
     def _get_session_data(self):
-        return self._session.data
+        return self.session.data
 
     def _set_session_data(self, data):
         pass
@@ -43,15 +65,26 @@
     _session_data = property(_get_session_data, _set_session_data)
 
 
-class Session(cwsession.Session):
+class Session(object):
     """ A Session that access the session data through a property.
 
     Along with :class:`Connection`, it avoid any load of the pyramid session
     data until it is actually accessed.
     """
     def __init__(self, pyramid_request, user, repo):
-        super(Session, self).__init__(user, repo)
         self._pyramid_request = pyramid_request
+        self._user = user
+        self._repo = repo
+
+    @property
+    def anonymous_session(self):
+        # XXX for now, anonymous_user only exists in webconfig (and testconfig).
+        # It will only be present inside all-in-one instance.
+        # there is plan to move it down to global config.
+        if not hasattr(self._repo.config, 'anonymous_user'):
+            # not a web or test config, no anonymous user
+            return False
+        return self._user.login == self._repo.config.anonymous_user()[0]
 
     def get_data(self):
         if not getattr(self, '_protect_data_access', False):
@@ -126,12 +159,11 @@
         self.path = request.upath_info
 
         vreg = request.registry['cubicweb.registry']
-        https = request.scheme == 'https'
 
         post = request.params.mixed()
         headers_in = request.headers
 
-        super(CubicWebPyramidRequest, self).__init__(vreg, https, post,
+        super(CubicWebPyramidRequest, self).__init__(vreg, post,
                                                      headers=headers_in)
 
         self.content = request.body_file_seekable
@@ -157,9 +189,6 @@
             else:
                 self.form[param] = val
 
-    def is_secure(self):
-        return self._request.scheme == 'https'
-
     def relative_path(self, includeparams=True):
         path = self._request.path_info[1:]
         if includeparams and self._request.query_string:
@@ -213,7 +242,7 @@
 
     :param request: A pyramid request
     :param vid: A CubicWeb view id
-    :param **kwargs: Keyword arguments to select and instanciate the view
+    :param kwargs: Keyword arguments to select and instanciate the view
     :returns: The rendered view content
     """
     vreg = request.registry['cubicweb.registry']
@@ -277,12 +306,6 @@
     session = Session(request, user, repo)
     session._cached_lang = lang
     tools.cnx_attach_entity(session, user)
-    # Calling the hooks should be done only once, disabling it completely for
-    # now
-    # with session.new_cnx() as cnx:
-    #     repo.hm.call_hooks('session_open', cnx)
-    #     cnx.commit()
-    # repo._sessions[session.sessionid] = session
     return session
 
 
--- a/cubicweb/pyramid/defaults.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/defaults.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,23 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
 """ Defaults for a classical CubicWeb instance. """
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/development.ini.tmpl	Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,41 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:cubicweb#main
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+    pyramid_debugtoolbar
+    cubicweb_%(cubename)s
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+##
+# CubicWeb instance settings
+# http://cubicweb.readthedocs.io/en/latest/book/pyramid/settings/
+##
+cubicweb.instance = %(instance)s
+cubicweb.bwcompat = false
+cubicweb.debug = true
+cubicweb.session.secret = %(session-secret)s
+cubicweb.auth.authtkt.persistent.secure = false
+cubicweb.auth.authtkt.persistent.secret = %(auth-authtkt-persistent-secret)s
+cubicweb.auth.authtkt.session.secure = false
+cubicweb.auth.authtkt.session.secret = %(auth-authtkt-session-secret)s
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = 127.0.0.1:6543 [::1]:6543
--- a/cubicweb/pyramid/init_instance.py	Mon Mar 20 09:40:24 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-from cubicweb.cwconfig import CubicWebConfiguration
-
-
-def includeme(config):
-    appid = config.registry.settings['cubicweb.instance']
-    cwconfig = CubicWebConfiguration.config_for(appid)
-
-    config.registry['cubicweb.config'] = cwconfig
-    config.registry['cubicweb.repository'] = repo = cwconfig.repository()
-    config.registry['cubicweb.registry'] = repo.vreg
--- a/cubicweb/pyramid/login.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/login.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,23 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
 """ Provide login views that reproduce a classical CubicWeb behavior"""
 from pyramid import security
 from pyramid.httpexceptions import HTTPSeeOther
--- a/cubicweb/pyramid/predicates.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/predicates.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,23 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
 """Contains predicates used in Pyramid views.
 """
 
--- a/cubicweb/pyramid/profile.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/profile.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,23 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
 """ Tools for profiling.
 
 See :ref:`profiling`."""
--- a/cubicweb/pyramid/pyramidctl.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/pyramidctl.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,9 +1,30 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
 """
 Provides a 'pyramid' command as a replacement to the 'start' command.
 
 The reloading strategy is heavily inspired by (and partially copied from)
 the pyramid script 'pserve'.
 """
+
 from __future__ import print_function
 
 import atexit
@@ -14,12 +35,14 @@
 import time
 import threading
 import subprocess
+import warnings
 
-from cubicweb import BadCommandUsage, ExecutionError
+from cubicweb import ExecutionError
 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
 from cubicweb.cwctl import CWCTL, InstanceCommand, init_cmdline_log_threshold
 from cubicweb.pyramid import wsgi_application_from_cwconfig
-from cubicweb.server import set_debug
+from cubicweb.server import serverctl, set_debug
+from cubicweb.web.webctl import WebCreateHandler
 
 import waitress
 
@@ -29,6 +52,17 @@
 LOG_LEVELS = ('debug', 'info', 'warning', 'error')
 
 
+class PyramidCreateHandler(serverctl.RepositoryCreateHandler,
+                           WebCreateHandler):
+    cfgname = 'pyramid'
+
+    def bootstrap(self, cubes, automatic=False, inputlevel=0):
+        serverctl.RepositoryCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
+        # Call WebCreateHandler.bootstrap to prompt about get anonymous-user.
+        WebCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
+        self.config.write_development_ini(cubes)
+
+
 class PyramidStartHandler(InstanceCommand):
     """Start an interactive pyramid server.
 
@@ -99,15 +133,6 @@
     def info(self, msg):
         print('INFO - %s' % msg)
 
-    def ordered_instances(self):
-        instances = super(PyramidStartHandler, self).ordered_instances()
-        if (self['debug-mode'] or self['debug'] or self['reload']) \
-                and len(instances) > 1:
-            raise BadCommandUsage(
-                '--debug-mode, --debug and --reload can be used on a single '
-                'instance only')
-        return instances
-
     def quote_first_command_arg(self, arg):
         """
         There's a bug in Windows when running an executable that's
@@ -326,8 +351,11 @@
         host = cwconfig['interface']
         port = cwconfig['port'] or 8080
         repo = app.application.registry['cubicweb.repository']
+        warnings.warn(
+            'the "pyramid" command does not start repository "looping tasks" '
+            'anymore; use the standalone "scheduler" command if needed'
+        )
         try:
-            repo.start_looping_tasks()
             waitress.serve(app, host=host, port=port)
         finally:
             repo.shutdown()
--- a/cubicweb/pyramid/resources.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/resources.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,5 +1,25 @@
-"""Contains resources classes.
-"""
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
+"""Pyramid resource definitions for CubicWeb."""
+
 from six import text_type
 
 from rql import TypeResolverException
--- a/cubicweb/pyramid/rest_api.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/rest_api.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,25 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
+"""Experimental REST API for CubicWeb using Pyramid."""
+
 from __future__ import absolute_import
 
 
@@ -16,6 +38,7 @@
 
 
 def includeme(config):
+    config.include('.predicates')
     config.add_route(
         'cwentities', '/{etype}/*traverse',
         factory=ETypeResource.from_match('etype'), match_is_etype='etype')
--- a/cubicweb/pyramid/session.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/session.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,25 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
+"""Pyramid session factories for CubicWeb."""
+
 import warnings
 import logging
 from contextlib import contextmanager
@@ -165,15 +187,9 @@
     See also :ref:`defaults_module`
     """
     settings = config.registry.settings
-    secret = settings.get('cubicweb.session.secret', '')
-    if not secret:
-        secret = config.registry['cubicweb.config'].get('pyramid-session-secret')
-        warnings.warn('''
-        Please migrate pyramid-session-secret from
-        all-in-one.conf to cubicweb.session.secret config entry in
-        your pyramid.ini file.
-        ''')
-    if not secret:
+    try:
+        secret = settings['cubicweb.session.secret']
+    except KeyError:
         secret = 'notsosecret'
         warnings.warn('''
 
--- a/cubicweb/pyramid/syncsession.py	Mon Mar 20 09:40:24 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-# copyright 2016 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/>.
-"""Override cubicweb's syncsession hooks to handle them in the pyramid's way"""
-
-from logilab.common.decorators import monkeypatch
-from cubicweb.hooks import syncsession
-
-
-def includeme(config):
-
-    @monkeypatch(syncsession)
-    def get_user_sessions(cnx, user_eid):
-        if cnx.user.eid == user_eid:
-            yield cnx
--- a/cubicweb/pyramid/test/__init__.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/test/__init__.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,9 +1,8 @@
 import webtest
 
+from pyramid.config import Configurator
 from cubicweb.devtools.webtest import CubicWebTestTC
 
-from cubicweb.pyramid import make_cubicweb_application
-
 
 class PyramidCWTest(CubicWebTestTC):
     settings = {}
@@ -11,15 +10,14 @@
     @classmethod
     def init_config(cls, config):
         super(PyramidCWTest, cls).init_config(config)
-        config.global_set_option('https-url', 'https://localhost.local/')
         config.global_set_option('anonymous-user', 'anon')
-        config.https_uiprops = None
-        config.https_datadir_url = None
 
     def setUp(self):
         # Skip CubicWebTestTC setUp
         super(CubicWebTestTC, self).setUp()
-        config = make_cubicweb_application(self.config, self.settings)
+        config = Configurator(settings=self.settings)
+        config.registry['cubicweb.repository'] = self.repo
+        config.include('cubicweb.pyramid')
         self.includeme(config)
         self.pyr_registry = config.registry
         self.webapp = webtest.TestApp(
--- a/cubicweb/pyramid/test/test_bw_request.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/test/test_bw_request.py	Mon Mar 20 10:28:01 2017 +0100
@@ -33,24 +33,6 @@
 
         self.assertEqual(b'some content', req.content.read())
 
-    def test_http_scheme(self):
-        req = CubicWebPyramidRequest(
-            self.make_request('/', {
-                'wsgi.url_scheme': 'http'}))
-
-        self.assertFalse(req.https)
-
-    def test_https_scheme(self):
-        req = CubicWebPyramidRequest(
-            self.make_request('/', {
-                'wsgi.url_scheme': 'https'}))
-
-        self.assertTrue(req.https)
-
-    def test_https_prefix(self):
-        r = self.webapp.get('/https/')
-        self.assertIn('https://', r.text)
-
     def test_big_content(self):
         content = b'x' * 100001
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_config.py	Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,64 @@
+# copyright 2017 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/>.
+"""Tests for cubicweb.pyramid.config module."""
+
+import os
+from os import path
+from unittest import TestCase
+
+from mock import patch
+
+from cubicweb.devtools.testlib import TemporaryDirectory
+
+from cubicweb.pyramid import config
+
+
+class PyramidConfigTC(TestCase):
+
+    def test_get_random_secret_key(self):
+        with patch('random.choice', return_value='0') as patched_choice:
+            secret = config.get_random_secret_key()
+        self.assertEqual(patched_choice.call_count, 50)
+        self.assertEqual(secret, '0' * 50)
+
+    def test_write_development_ini(self):
+        with TemporaryDirectory() as instancedir:
+            appid = 'pyramid-instance'
+            os.makedirs(path.join(instancedir, appid))
+            os.environ['CW_INSTANCES_DIR'] = instancedir
+            try:
+                cfg = config.CubicWebPyramidConfiguration(appid)
+                with patch('random.choice', return_value='0') as patched_choice:
+                    cfg.write_development_ini(['foo', 'bar'])
+            finally:
+                os.environ.pop('CW_INSTANCES_DIR')
+            with open(path.join(instancedir, appid, 'development.ini')) as f:
+                lines = f.readlines()
+        self.assertEqual(patched_choice.call_count, 50 * 3)
+        secret = '0' * 50
+        for option in ('cubicweb.session.secret',
+                       'cubicweb.auth.authtkt.persistent.secret',
+                       'cubicweb.auth.authtkt.session.secret'):
+            self.assertIn('{} = {}\n'.format(option, secret), lines)
+        self.assertIn('cubicweb.instance = {}\n'.format(appid), lines)
+        self.assertIn('    cubicweb_foo\n', lines)
+
+
+if __name__ == '__main__':
+    import unittest
+    unittest.main()
--- a/cubicweb/pyramid/test/test_login.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/test/test_login.py	Mon Mar 20 10:28:01 2017 +0100
@@ -3,6 +3,7 @@
 
 from cubicweb.pyramid.test import PyramidCWTest
 
+
 class LoginTestLangUrlPrefix(PyramidCWTest):
 
     @classmethod
@@ -19,7 +20,6 @@
         self.assertEqual(res.status_int, 303)
 
 
-
 class LoginTest(PyramidCWTest):
 
     def test_login_form(self):
--- a/cubicweb/pyramid/tools.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/tools.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,23 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, 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/>.
+
 """Various tools.
 
 .. warning::
@@ -6,11 +26,7 @@
     with caution, as the API may change without notice.
 """
 
-#: A short-term cache for user clones.
-#: used by cached_build_user to speed-up repetitive calls to build_user
-#: The expiration is handled in a dumb and brutal way: the whole cache is
-#: cleared every 5 minutes.
-_user_cache = {}
+from repoze.lru import lru_cache
 
 
 def clone_user(repo, user):
@@ -39,34 +55,13 @@
         entity.cw_rset.req = cnx
 
 
+@lru_cache(10)
 def cached_build_user(repo, eid):
     """Cached version of
     :meth:`cubicweb.server.repository.Repository._build_user`
     """
-    if eid in _user_cache:
-        user, lang = _user_cache[eid]
-        entity = clone_user(repo, user)
-        return entity, lang
-
     with repo.internal_cnx() as cnx:
         user = repo._build_user(cnx, eid)
         lang = user.prefered_language()
         user.cw_clear_relation_cache()
-        _user_cache[eid] = (clone_user(repo, user), lang)
-        return user, lang
-
-
-def clear_cache():
-    """Clear the user cache"""
-    _user_cache.clear()
-
-
-def includeme(config):
-    """Start the cache maintenance loop task.
-
-    Automatically included by :func:`cubicweb.pyramid.make_cubicweb_application`.
-    """
-    repo = config.registry['cubicweb.repository']
-    interval = int(config.registry.settings.get(
-        'cubicweb.usercache.expiration_time', 60 * 5))
-    repo.looping_task(interval, clear_cache)
+        return clone_user(repo, user), lang
--- a/cubicweb/repoapi.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/repoapi.py	Mon Mar 20 10:28:01 2017 +0100
@@ -15,21 +15,18 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Official API to access the content of a repository
-"""
+"""Official API to access the content of a repository."""
+
 from warnings import warn
 
 from six import add_metaclass
 
 from logilab.common.deprecation import class_deprecated
 
-from cubicweb.utils import parse_repo_uri
 from cubicweb import AuthenticationError
 from cubicweb.server.session import Connection
 
 
-### public API ######################################################
-
 def get_repository(uri=None, config=None, vreg=None):
     """get a repository for the given URI or config/vregistry (in case we're
     loading the repository for a client, eg web server, configuration).
@@ -43,11 +40,17 @@
     assert config is not None, 'get_repository(config=config)'
     return config.repository(vreg)
 
+
 def connect(repo, login, **kwargs):
     """Take credential and return associated Connection.
 
-    raise AuthenticationError if the credential are invalid."""
-    return repo.new_session(login, **kwargs).new_cnx()
+    raise AuthenticationError if the credential are invalid.
+    """
+    # use an internal connection to try to get a user object
+    with repo.internal_cnx() as cnx:
+        user = repo.authenticate_user(cnx, login, **kwargs)
+    return Connection(repo, user)
+
 
 def anonymous_cnx(repo):
     """return a Connection for Anonymous user.
@@ -55,7 +58,7 @@
     raises an AuthenticationError if anonymous usage is not allowed
     """
     anoninfo = getattr(repo.config, 'anonymous_user', lambda: None)()
-    if anoninfo is None: # no anonymous user
+    if anoninfo is None:  # no anonymous user
         raise AuthenticationError('anonymous access is not authorized')
     anon_login, anon_password = anoninfo
     # use vreg's repository cache
--- a/cubicweb/req.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/req.py	Mon Mar 20 10:28:01 2017 +0100
@@ -278,9 +278,6 @@
         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
@@ -295,8 +292,10 @@
         #     not try to process it and directly call req.build_url()
         base_url = kwargs.pop('base_url', None)
         if base_url is None:
-            secure = kwargs.pop('__secure__', None)
-            base_url = self.base_url(secure=secure)
+            if kwargs.pop('__secure__', None) is not None:
+                warn('[3.25] __secure__ argument is deprecated',
+                     DeprecationWarning, stacklevel=2)
+            base_url = self.base_url()
         path = self.build_url_path(method, kwargs)
         if not kwargs:
             return u'%s%s' % (base_url, path)
@@ -327,9 +326,9 @@
         necessary encoding / decoding. Also it's designed to quote each
         part of a url path and so the '/' character will be encoded as well.
         """
-        if PY2 and isinstance(value, unicode):
+        if PY2 and isinstance(value, text_type):
             quoted = urlquote(value.encode(self.encoding), safe=safe)
-            return unicode(quoted, self.encoding)
+            return text_type(quoted, self.encoding)
         return urlquote(str(value), safe=safe)
 
     def url_unquote(self, quoted):
@@ -340,12 +339,12 @@
         """
         if PY3:
             return urlunquote(quoted)
-        if isinstance(quoted, unicode):
+        if isinstance(quoted, text_type):
             quoted = quoted.encode(self.encoding)
         try:
-            return unicode(urlunquote(quoted), self.encoding)
+            return text_type(urlunquote(quoted), self.encoding)
         except UnicodeDecodeError:  # might occurs on manually typed URLs
-            return unicode(urlunquote(quoted), 'iso-8859-1')
+            return text_type(urlunquote(quoted), 'iso-8859-1')
 
     def url_parse_qsl(self, querystring):
         """return a list of (key, val) found in the url quoted query string"""
@@ -353,13 +352,13 @@
             for key, val in parse_qsl(querystring):
                 yield key, val
             return
-        if isinstance(querystring, unicode):
+        if isinstance(querystring, text_type):
             querystring = querystring.encode(self.encoding)
         for key, val in parse_qsl(querystring):
             try:
-                yield unicode(key, self.encoding), unicode(val, self.encoding)
+                yield text_type(key, self.encoding), text_type(val, self.encoding)
             except UnicodeDecodeError:  # might occurs on manually typed URLs
-                yield unicode(key, 'iso-8859-1'), unicode(val, 'iso-8859-1')
+                yield text_type(key, 'iso-8859-1'), text_type(val, 'iso-8859-1')
 
     def rebuild_url(self, url, **newparams):
         """return the given url with newparams inserted. If any new params
@@ -367,7 +366,7 @@
 
         newparams may only be mono-valued.
         """
-        if PY2 and isinstance(url, unicode):
+        if PY2 and isinstance(url, text_type):
             url = url.encode(self.encoding)
         schema, netloc, path, query, fragment = urlsplit(url)
         query = parse_qs(query)
@@ -439,7 +438,7 @@
             as_string = formatters[attrtype]
         except KeyError:
             self.error('given bad attrtype %s', attrtype)
-            return unicode(value)
+            return text_type(value)
         return as_string(value, self, props, displaytime)
 
     def format_date(self, date, date_format=None, time=False):
@@ -502,13 +501,12 @@
             raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
                              % {'value': value, 'format': format})
 
-    def _base_url(self, secure=None):
-        if secure:
-            return self.vreg.config.get('https-url') or self.vreg.config['base-url']
-        return self.vreg.config['base-url']
-
-    def base_url(self, secure=None):
-        """return the root url of the instance
-        """
-        url = self._base_url(secure=secure)
+    def base_url(self, **kwargs):
+        """Return the root url of the instance."""
+        secure = kwargs.pop('secure', None)
+        if secure is not None:
+            warn('[3.25] secure argument is deprecated', DeprecationWarning, stacklevel=2)
+        if kwargs:
+            raise TypeError('base_url got unexpected keyword arguments %s' % ', '.join(kwargs))
+        url = self.vreg.config['base-url']
         return url if url is None else url.rstrip('/') + '/'
--- a/cubicweb/rqlrewrite.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/rqlrewrite.py	Mon Mar 20 10:28:01 2017 +0100
@@ -580,6 +580,7 @@
                     done.add(rel)
                     rschema = get_rschema(rel.r_type)
                     if rschema.final or rschema.inlined:
+                        subselect_vrefs = []
                         rel.children[0].name = varname # XXX explain why
                         subselect.add_restriction(rel.copy(subselect))
                         for vref in rel.children[1].iget_nodes(n.VariableRef):
@@ -592,6 +593,7 @@
                                     "least uninline %s" % rel.r_type)
                             subselect.append_selected(vref.copy(subselect))
                             aliases.append(vref.name)
+                            subselect_vrefs.append(vref)
                         self.select.remove_node(rel)
                         # when some inlined relation has to be copied in the
                         # subquery and that relation is optional, we need to
@@ -602,14 +604,15 @@
                         # also, if some attributes or inlined relation of the
                         # object variable are accessed, we need to get all those
                         # from the subquery as well
-                        if vref.name not in done and rschema.inlined:
-                            # we can use vref here define in above for loop
-                            ostinfo = vref.variable.stinfo
-                            for orel in iter_relations(ostinfo):
-                                orschema = get_rschema(orel.r_type)
-                                if orschema.final or orschema.inlined:
-                                    todo.append( (vref.name, ostinfo) )
-                                    break
+                        for vref in subselect_vrefs:
+                            if vref.name not in done and rschema.inlined:
+                                # we can use vref here define in above for loop
+                                ostinfo = vref.variable.stinfo
+                                for orel in iter_relations(ostinfo):
+                                    orschema = get_rschema(orel.r_type)
+                                    if orschema.final or orschema.inlined:
+                                        todo.append( (vref.name, ostinfo) )
+                                        break
             if need_null_test:
                 snippetrqlst = n.Or(
                     n.make_relation(subselect.get_variable(selectvar), 'is',
--- a/cubicweb/rset.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/rset.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -20,14 +20,14 @@
 
 from warnings import warn
 
-from six import PY3
+from six import PY3, text_type
 from six.moves import range
 
 from logilab.common import nullobject
 from logilab.common.decorators import cached, clear_cache, copy_cache
 from rql import nodes, stmts
 
-from cubicweb import NotAnEntity, NoResultError, MultipleResultsError
+from cubicweb import NotAnEntity, NoResultError, MultipleResultsError, UnknownEid
 
 
 _MARKER = nullobject()
@@ -73,7 +73,7 @@
         # set by the cursor which returned this resultset
         self.req = None
         # actions cache
-        self._rsetactions = None
+        self._actions_cache = None
 
     def __str__(self):
         if not self.rows:
@@ -94,25 +94,26 @@
 
         if not self.description:
             return pattern % (self.rql, len(self.rows),
-                                                     '\n'.join(str(r) for r in rows))
+                              '\n'.join(str(r) for r in rows))
         return pattern % (self.rql, len(self.rows),
-                                                 '\n'.join('%s (%s)' % (r, d)
-                                                           for r, d in zip(rows, self.description)))
+                          '\n'.join('%s (%s)' % (r, d)
+                                    for r, d in zip(rows, self.description)))
 
     def possible_actions(self, **kwargs):
-        if self._rsetactions is None:
-            self._rsetactions = {}
-        if kwargs:
-            key = tuple(sorted(kwargs.items()))
-        else:
-            key = None
-        try:
-            return self._rsetactions[key]
-        except KeyError:
+        """Return possible actions on this result set. Should always be called with the same
+        arguments so it may be computed only once.
+        """
+        key = tuple(sorted(kwargs.items()))
+        if self._actions_cache is None:
             actions = self.req.vreg['actions'].poss_visible_objects(
                 self.req, rset=self, **kwargs)
-            self._rsetactions[key] = actions
+            self._actions_cache = (key, actions)
             return actions
+        else:
+            assert key == self._actions_cache[0], \
+                'unexpected new arguments for possible actions (%s vs %s)' % (
+                    key, self._actions_cache[0])
+            return self._actions_cache[1]
 
     def __len__(self):
         """returns the result set's size"""
@@ -120,7 +121,7 @@
 
     def __getitem__(self, i):
         """returns the ith element of the result set"""
-        return self.rows[i] #ResultSetRow(self.rows[i])
+        return self.rows[i]
 
     def __iter__(self):
         """Returns an iterator over rows"""
@@ -132,7 +133,7 @@
         # at least rql could be fixed now that we have union and sub-queries
         # but I tend to think that since we have that, we should not need this
         # method anymore (syt)
-        rset = ResultSet(self.rows+rset.rows, self.rql, self.args,
+        rset = ResultSet(self.rows + rset.rows, self.rql, self.args,
                          self.description + rset.description)
         rset.req = self.req
         return rset
@@ -163,7 +164,7 @@
         rset = self.copy(rows, descr)
         for row, desc in zip(self.rows, self.description):
             nrow, ndesc = transformcb(row, desc)
-            if ndesc: # transformcb returns None for ndesc to skip that row
+            if ndesc:  # transformcb returns None for ndesc to skip that row
                 rows.append(nrow)
                 descr.append(ndesc)
         rset.rowcount = len(rows)
@@ -192,7 +193,6 @@
         rset.rowcount = len(rows)
         return rset
 
-
     def sorted_rset(self, keyfunc, reverse=False, col=0):
         """sorts the result set according to a given keyfunc
 
@@ -308,7 +308,7 @@
             newselect = stmts.Select()
             newselect.limit = limit
             newselect.offset = offset
-            aliases = [nodes.VariableRef(newselect.get_variable(chr(65+i), i))
+            aliases = [nodes.VariableRef(newselect.get_variable(chr(65 + i), i))
                        for i in range(len(rqlst.children[0].selection))]
             for vref in aliases:
                 newselect.append_selected(nodes.VariableRef(vref.variable))
@@ -336,7 +336,7 @@
 
         :rtype: `ResultSet`
         """
-        stop = limit+offset
+        stop = limit + offset
         rows = self.rows[offset:stop]
         descr = self.description[offset:stop]
         if inplace:
@@ -375,11 +375,11 @@
             return rqlstr
         # sounds like we get encoded or unicode string due to a bug in as_string
         if not encoded:
-            if isinstance(rqlstr, unicode):
+            if isinstance(rqlstr, text_type):
                 return rqlstr
-            return unicode(rqlstr, encoding)
+            return text_type(rqlstr, encoding)
         else:
-            if isinstance(rqlstr, unicode):
+            if isinstance(rqlstr, text_type):
                 return rqlstr.encode(encoding)
             return rqlstr
 
@@ -560,11 +560,21 @@
 
     @cached
     def syntax_tree(self):
-        """return the syntax tree (:class:`rql.stmts.Union`) for the
-        originating query. You can expect it to have solutions
-        computed and it will be properly annotated.
+        """Return the **cached** syntax tree (:class:`rql.stmts.Union`) for the
+        originating query.
+
+        You can expect it to have solutions computed and it will be properly annotated.
+        Since this is a cached shared object, **you must not modify it**.
         """
-        return self.req.vreg.parse(self.req, self.rql, self.args)
+        cnx = getattr(self.req, 'cnx', self.req)
+        try:
+            rqlst = cnx.repo.querier.rql_cache.get(cnx, self.rql, self.args)[0]
+            if not rqlst.annotated:
+                self.req.vreg.rqlhelper.annotate(rqlst)
+            return rqlst
+        except UnknownEid:
+            # unknown eid in args prevent usage of rql cache, but we still need a rql st
+            return self.req.vreg.parse(self.req, self.rql, self.args)
 
     @cached
     def column_types(self, col):
@@ -592,7 +602,7 @@
             if row != last:
                 if last is not None:
                     result[-1][1] = i - 1
-                result.append( [i, None, row] )
+                result.append([i, None, row])
                 last = row
         if last is not None:
             result[-1][1] = i
@@ -665,7 +675,7 @@
                 try:
                     entity = self.get_entity(row, index)
                     return entity, rel.r_type
-                except NotAnEntity as exc:
+                except NotAnEntity:
                     return None, None
         return None, None
 
@@ -683,12 +693,14 @@
                 return rhs.eval(self.args)
         return None
 
+
 def _get_variable(term):
     # XXX rewritten const
     # use iget_nodes for (hack) case where we have things like MAX(V)
     for vref in term.iget_nodes(nodes.VariableRef):
         return vref.variable
 
+
 def attr_desc_iterator(select, selectidx, rootidx):
     """return an iterator on a list of 2-uple (index, attr_relation)
     localizing attribute relations of the main variable in a result's row
--- a/cubicweb/rtags.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/rtags.py	Mon Mar 20 10:28:01 2017 +0100
@@ -38,16 +38,26 @@
 
 
 import logging
-from warnings import warn
 
 from six import string_types
 
 from logilab.common.logging_ext import set_log_methods
 from logilab.common.registry import RegistrableInstance, yes
 
+
 def _ensure_str_key(key):
     return tuple(str(k) for k in key)
 
+
+def rtags_chain(rtag):
+    """Return the rtags chain, starting from the given one, and going back through each parent rtag
+    up to the root (i.e. which as no parent).
+    """
+    while rtag is not None:
+        yield rtag
+        rtag = rtag._parent
+
+
 class RegistrableRtags(RegistrableInstance):
     __registry__ = 'uicfg'
     __select__ = yes()
@@ -67,8 +77,13 @@
     # function given as __init__ argument and kept for bw compat
     _init = _initfunc = None
 
-    def __init__(self):
+    def __init__(self, parent=None, __module__=None):
+        super(RelationTags, self).__init__(__module__)
         self._tagdefs = {}
+        self._parent = parent
+        if parent is not None:
+            assert parent.__class__ is self.__class__, \
+                'inconsistent class for parent rtag {0}'.format(parent)
 
     def __repr__(self):
         # find a way to have more infos but keep it readable
@@ -99,12 +114,12 @@
         if check:
             for (stype, rtype, otype, tagged), value in list(self._tagdefs.items()):
                 for ertype in (stype, rtype, otype):
-                    if ertype != '*' and not ertype in schema:
+                    if ertype != '*' and ertype not in schema:
                         self.warning('removing rtag %s: %s, %s undefined in schema',
                                      (stype, rtype, otype, tagged), value, ertype)
                         self.del_rtag(stype, rtype, otype, tagged)
                         break
-        if self._init is not None:
+        if self._parent is None and self._init is not None:
             self.apply(schema, self._init)
 
     def apply(self, schema, func):
@@ -121,6 +136,19 @@
 
     # rtag declaration api ####################################################
 
+    def derive(self, module, select):
+        """Return a derivated of this relation tag, associated to given module and selector.
+
+        This derivated will hold a set of specific rules but delegate to its "parent" relation tags
+        for unfound keys.
+
+        >>> class_afs = uicfg.autoform_section.derive(__name__, is_instance('Class'))
+        """
+        copied = self.__class__(self, __module__=__name__)
+        copied.__module__ = module
+        copied.__select__ = select
+        return copied
+
     def tag_attribute(self, key, *args, **kwargs):
         key = list(key)
         key.append('*')
@@ -141,8 +169,8 @@
         assert len(key) == 4, 'bad key: %s' % list(key)
         if self._allowed_values is not None:
             assert tag in self._allowed_values, \
-                   '%r is not an allowed tag (should be in %s)' % (
-                tag, self._allowed_values)
+                '%r is not an allowed tag (should be in %s)' % (
+                    tag, self._allowed_values)
         self._tagdefs[_ensure_str_key(key)] = tag
         return tag
 
@@ -156,18 +184,21 @@
         else:
             self.tag_object_of((desttype, attr, etype), *args, **kwargs)
 
-
     # rtag runtime api ########################################################
 
     def del_rtag(self, *key):
         del self._tagdefs[key]
 
     def get(self, *key):
+        """Return value for the given key, by looking from the most specific key to the more
+        generic (using '*' wildcards). For each key, look into this rtag and its parent rtags.
+        """
         for key in reversed(self._get_keys(*key)):
-            try:
-                return self._tagdefs[key]
-            except KeyError:
-                continue
+            for rtag in rtags_chain(self):
+                try:
+                    return rtag._tagdefs[key]
+                except KeyError:
+                    continue
         return None
 
     def etype_get(self, etype, rtype, role, ttype='*'):
@@ -177,7 +208,7 @@
 
     # these are overridden by set_log_methods below
     # only defining here to prevent pylint from complaining
-    info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
+    info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
 
 
 class RelationTagsSet(RelationTags):
@@ -192,17 +223,23 @@
         return rtags
 
     def get(self, stype, rtype, otype, tagged):
+        """Return value for the given key, which is an union of the values found from the most
+        specific key to the more generic (using '*' wildcards). For each key, look into this rtag
+        and its parent rtags.
+        """
         rtags = self.tag_container_cls()
         for key in self._get_keys(stype, rtype, otype, tagged):
-            try:
-                rtags.update(self._tagdefs[key])
-            except KeyError:
-                continue
+            for rtag in rtags_chain(self):
+                try:
+                    rtags.update(rtag._tagdefs[key])
+                    break
+                except KeyError:
+                    continue
         return rtags
 
 
 class RelationTagsDict(RelationTagsSet):
-    """This class associates a set of tags to each key."""
+    """This class associates a dictionary to each key."""
     tag_container_cls = dict
 
     def tag_relation(self, key, tag):
--- a/cubicweb/schema.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/schema.py	Mon Mar 20 10:28:01 2017 +0100
@@ -19,6 +19,7 @@
 
 from __future__ import print_function
 
+import pkgutil
 import re
 from os.path import join, basename
 from hashlib import md5
@@ -325,6 +326,7 @@
                 keyarg = None
             rqlst.recover()
             return rql, found, keyarg
+        rqlst.where = nodes.Exists(rqlst.where)
         return rqlst.as_string(), None, None
 
     def _check(self, _cw, **kwargs):
@@ -347,7 +349,9 @@
         if keyarg is None:
             kwargs.setdefault('u', _cw.user.eid)
             try:
-                rset = _cw.execute(rql, kwargs, build_descr=True)
+                # ensure security is disabled
+                with getattr(_cw, 'cnx', _cw).security_enabled(read=False):
+                    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:
@@ -1366,19 +1370,12 @@
     """
     schemacls = CubicWebSchema
 
-    def load(self, config, path=(), **kwargs):
+    def load(self, config, modnames=(['cubicweb', 'cubicweb.schemas.bootstrap'],), **kwargs):
         """return a Schema instance from the schema definition read
         from <directory>
         """
         return super(BootstrapSchemaLoader, self).load(
-            path, config.appid, register_base_types=False, **kwargs)
-
-    def _load_definition_files(self, cubes=None):
-        # bootstraping, ignore cubes
-        filepath = join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py')
-        self.info('loading %s', filepath)
-        with tempattr(ybo, 'PACKAGE', 'cubicweb'):  # though we don't care here
-            self.handle_file(filepath)
+            modnames, name=config.appid, register_base_types=False, **kwargs)
 
     def unhandled_file(self, filepath):
         """called when a file without handler associated has been found"""
@@ -1399,30 +1396,12 @@
         from <directory>
         """
         self.info('loading %s schemas', ', '.join(config.cubes()))
-        self.extrapath = config.extrapath
-        if config.apphome:
-            path = tuple(reversed([config.apphome] + config.cubes_path()))
-        else:
-            path = tuple(reversed(config.cubes_path()))
         try:
-            return super(CubicWebSchemaLoader, self).load(config, path=path, **kwargs)
+            return super(CubicWebSchemaLoader, self).load(config, config.schema_modnames(), **kwargs)
         finally:
             # we've to cleanup modules imported from cubicweb.schemas as well
             cleanup_sys_modules([join(cubicweb.CW_SOFTWARE_ROOT, 'schemas')])
 
-    def _load_definition_files(self, cubes):
-        for filepath in (join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py'),
-                         join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'base.py'),
-                         join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'workflow.py'),
-                         join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'Bookmark.py')):
-            self.info('loading %s', filepath)
-            with tempattr(ybo, 'PACKAGE', 'cubicweb'):
-                self.handle_file(filepath)
-        for cube in cubes:
-            for filepath in self.get_schema_files(cube):
-                with tempattr(ybo, 'PACKAGE', basename(cube)):
-                    self.handle_file(filepath)
-
     # these are overridden by set_log_methods below
     # only defining here to prevent pylint from complaining
     info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
--- a/cubicweb/server/__init__.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/__init__.py	Mon Mar 20 10:28:01 2017 +0100
@@ -22,8 +22,6 @@
 """
 from __future__ import print_function
 
-
-
 from contextlib import contextmanager
 
 from six import text_type, string_types
@@ -39,12 +37,6 @@
 from cubicweb.appobject import AppObject
 
 
-class ShuttingDown(BaseException):
-    """raised when trying to access some resources while the repository is
-    shutting down. Inherit from BaseException so that `except Exception` won't
-    catch it.
-    """
-
 # server-side services #########################################################
 
 class Service(AppObject):
@@ -68,16 +60,16 @@
 
 # server debugging flags. They may be combined using binary operators.
 
-#:no debug information
+#: no debug information
 DBG_NONE = 0  #: no debug information
 #: rql execution information
-DBG_RQL  = 1
+DBG_RQL = 1
 #: executed sql
-DBG_SQL  = 2
+DBG_SQL = 2
 #: repository events
 DBG_REPO = 4
 #: multi-sources
-DBG_MS   = 8
+DBG_MS = 8
 #: hooks
 DBG_HOOKS = 16
 #: operations
@@ -206,7 +198,7 @@
 
 def init_repository(config, interactive=True, drop=False, vreg=None,
                     init_config=None):
-    """initialise a repository database by creating tables add filling them
+    """Initialise a repository database by creating tables and filling them
     with the minimal set of entities (ie at least the schema, base groups and
     a initial user)
     """
@@ -223,6 +215,7 @@
     config.cube_appobject_path = set(('hooks', 'entities'))
     # only enable the system source at initialization time
     repo = Repository(config, vreg=vreg)
+    repo.bootstrap()
     if init_config is not None:
         # further config initialization once it has been bootstrapped
         init_config(config)
--- a/cubicweb/server/checkintegrity.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/checkintegrity.py	Mon Mar 20 10:28:01 2017 +0100
@@ -140,6 +140,19 @@
         pb.finish()
 
 
+_CHECKERS = {}
+
+
+def _checker(func):
+    """Decorator to register a function as a checker for check()."""
+    fname = func.__name__
+    prefix = 'check_'
+    assert fname.startswith(prefix), 'cannot register %s as a checker' % func
+    _CHECKERS[fname[len(prefix):]] = func
+    return func
+
+
+@_checker
 def check_schema(schema, cnx, eids, fix=1):
     """check serialized schema"""
     print('Checking serialized schema')
@@ -157,10 +170,12 @@
                 print('dunno how to fix, do it yourself')
 
 
+@_checker
 def check_text_index(schema, cnx, eids, fix=1):
     """check all entities registered in the text index"""
     print('Checking text index')
-    msg = '  Entity with eid %s exists in the text index but in no source (autofix will remove from text index)'
+    msg = ('  Entity with eid %s exists in the text index but not in any '
+           'entity type table (autofix will remove from text index)')
     cursor = cnx.system_sql('SELECT uid FROM appears;')
     for row in cursor.fetchall():
         eid = row[0]
@@ -171,11 +186,13 @@
             notify_fixed(fix)
 
 
+@_checker
 def check_entities(schema, cnx, eids, fix=1):
     """check all entities registered in the repo system table"""
     print('Checking entities system table')
     # system table but no source
-    msg = '  Entity %s with eid %s exists in the system table but in no source (autofix will delete the entity)'
+    msg = ('  Entity %s with eid %s exists in "entities" table but not in any '
+           'entity type table (autofix will delete the entity)')
     cursor = cnx.system_sql('SELECT eid,type FROM entities;')
     for row in cursor.fetchall():
         eid, etype = row
@@ -228,7 +245,8 @@
                            '  WHERE cs.eid_from=e.eid AND cs.eid_to=s.cw_eid)')
         notify_fixed(True)
     print('Checking entities tables')
-    msg = '  Entity with eid %s exists in the %s table but not in the system table (autofix will delete the entity)'
+    msg = ('  Entity with eid %s exists in the %s table but not in "entities" '
+           'table (autofix will delete the entity)')
     for eschema in schema.entities():
         if eschema.final:
             continue
@@ -247,8 +265,9 @@
 
 
 def bad_related_msg(rtype, target, eid, fix):
-    msg = '  A relation %s with %s eid %s exists but no such entity in sources'
-    sys.stderr.write(msg % (rtype, target, eid))
+    msg = ('  A relation %(rtype)s with %(target)s eid %(eid)d exists but '
+           'entity #(eid)d does not exist')
+    sys.stderr.write(msg % {'rtype': rtype, 'target': target, 'eid': eid})
     notify_fixed(fix)
 
 
@@ -259,13 +278,14 @@
     notify_fixed(fix)
 
 
+@_checker
 def check_relations(schema, cnx, eids, fix=1):
     """check that eids referenced by relations are registered in the repo system
     table
     """
     print('Checking relations')
     for rschema in schema.relations():
-        if rschema.final or rschema.type in PURE_VIRTUAL_RTYPES:
+        if rschema.final or rschema.rule or rschema.type in PURE_VIRTUAL_RTYPES:
             continue
         if rschema.inlined:
             for subjtype in rschema.subjects():
@@ -308,6 +328,7 @@
                     cnx.system_sql(sql)
 
 
+@_checker
 def check_mandatory_relations(schema, cnx, eids, fix=1):
     """check entities missing some mandatory relation"""
     print('Checking mandatory relations')
@@ -335,6 +356,7 @@
                     notify_fixed(fix)
 
 
+@_checker
 def check_mandatory_attributes(schema, cnx, eids, fix=1):
     """check for entities stored in the system source missing some mandatory
     attribute
@@ -355,6 +377,7 @@
                     notify_fixed(fix)
 
 
+@_checker
 def check_metadata(schema, cnx, eids, fix=1):
     """check entities has required metadata
 
@@ -397,7 +420,7 @@
         eids_cache = {}
         with cnx.security_enabled(read=False, write=False): # ensure no read security
             for check in checks:
-                check_func = globals()['check_%s' % check]
+                check_func = _CHECKERS[check]
                 check_func(repo.schema, cnx, eids_cache, fix=fix)
         if fix:
             cnx.commit()
--- a/cubicweb/server/hook.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/hook.py	Mon Mar 20 10:28:01 2017 +0100
@@ -189,9 +189,6 @@
 `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.
-
 
 API
 ---
@@ -272,8 +269,7 @@
                        'before_delete_relation','after_delete_relation'))
 SYSTEM_HOOKS = set(('server_backup', 'server_restore',
                     'server_startup', 'server_maintenance',
-                    'server_shutdown', 'before_server_shutdown',
-                    'session_open', 'session_close'))
+                    'server_shutdown', 'before_server_shutdown',))
 
 ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
 
--- a/cubicweb/server/migractions.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/migractions.py	Mon Mar 20 10:28:01 2017 +0100
@@ -58,7 +58,7 @@
 from cubicweb.cwvreg import CW_EVENT_MANAGER
 from cubicweb import repoapi
 from cubicweb.migration import MigrationHelper, yes
-from cubicweb.server import hook, schemaserial as ss
+from cubicweb.server import hook, schemaserial as ss, repository
 from cubicweb.server.schema2sql import eschema2sql, rschema2sql, unique_index_name, sql_type
 from cubicweb.server.utils import manager_userpasswd
 from cubicweb.server.sqlutils import sqlexec, SQL_PREFIX
@@ -96,12 +96,9 @@
             assert repo
             self.cnx = cnx
             self.repo = repo
-            self.session = cnx.session
         elif connect:
             self.repo = config.repository()
             self.set_cnx()
-        else:
-            self.session = None
         # no config on shell to a remote instance
         if config is not None and (cnx or connect):
             repo = self.repo
@@ -154,7 +151,6 @@
             except (KeyboardInterrupt, EOFError):
                 print('aborting...')
                 sys.exit(0)
-        self.session = self.repo._get_session(self.cnx.sessionid)
 
     def cube_upgraded(self, cube, version):
         self.cmd_set_property('system.version.%s' % cube.lower(),
@@ -268,8 +264,7 @@
                 written_format = format_file.readline().strip()
                 if written_format in ('portable', 'native'):
                     format = written_format
-        self.config.init_cnxset_pool = False
-        repo = self.repo = self.config.repository()
+        repo = self.repo = repository.Repository(self.config)
         source = repo.system_source
         try:
             source.restore(osp.join(tmpdir, source.uri), self.confirm, drop, format)
@@ -277,9 +272,10 @@
             print('-> error trying to restore %s [%s]' % (source.uri, exc))
             if not self.confirm('Continue anyway?', default='n'):
                 raise SystemExit(1)
-        shutil.rmtree(tmpdir)
+        finally:
+            shutil.rmtree(tmpdir)
         # call hooks
-        repo.init_cnxset_pool()
+        repo.bootstrap()
         repo.hm.call_hooks('server_restore', repo=repo, timestamp=backupfile)
         print('-> database restored.')
 
--- a/cubicweb/server/querier.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/querier.py	Mon Mar 20 10:28:01 2017 +0100
@@ -23,14 +23,14 @@
 from itertools import repeat
 
 from six import text_type, string_types, integer_types
-from six.moves import range
+from six.moves import range, zip
 
 from rql import RQLSyntaxError, CoercionError
 from rql.stmts import Union
 from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj, Relation, Exists, Not
 from yams import BASE_TYPES
 
-from cubicweb import ValidationError, Unauthorized, UnknownEid
+from cubicweb import ValidationError, Unauthorized, UnknownEid, QueryError
 from cubicweb.rqlrewrite import RQLRelationRewriter
 from cubicweb import Binary, server
 from cubicweb.rset import ResultSet
@@ -477,33 +477,23 @@
 
     def set_schema(self, schema):
         self.schema = schema
-        repo = self._repo
-        # rql st and solution cache.
-        self._rql_cache = QueryCache(repo.config['rql-cache-size'])
-        # rql cache key cache. Don't bother using a Cache instance: we should
-        # have a limited number of queries in there, since there are no entries
-        # in this cache for user queries (which have no args)
-        self._rql_ck_cache = {}
-        # some cache usage stats
-        self.cache_hit, self.cache_miss = 0, 0
-        # rql parsing / analysing helper
-        self.solutions = repo.vreg.solutions
-        rqlhelper = repo.vreg.rqlhelper
-        # set backend on the rql helper, will be used for function checking
-        rqlhelper.backend = repo.config.system_source_config['db-driver']
-        self._parse = rqlhelper.parse
+        self.clear_caches()
+        rqlhelper = self._repo.vreg.rqlhelper
         self._annotate = rqlhelper.annotate
         # rql planner
         self._planner = SSPlanner(schema, rqlhelper)
         # sql generation annotator
         self.sqlgen_annotate = SQLGenAnnotator(schema).annotate
 
-    def parse(self, rql, annotate=False):
-        """return a rql syntax tree for the given rql"""
-        try:
-            return self._parse(text_type(rql), annotate=annotate)
-        except UnicodeError:
-            raise RQLSyntaxError(rql)
+    def clear_caches(self, eids=None, etypes=None):
+        if eids is None:
+            self.rql_cache = RQLCache(self._repo, self.schema)
+        else:
+            cache = self.rql_cache
+            for eid, etype in zip(eids, etypes):
+                cache.pop(('Any X WHERE X eid %s' % eid,), None)
+                if etype is not None:
+                    cache.pop(('%s X WHERE X eid %s' % (etype, eid),), None)
 
     def plan_factory(self, rqlst, args, cnx):
         """create an execution plan for an INSERT RQL query"""
@@ -535,46 +525,12 @@
             if server.DEBUG & (server.DBG_MORE | server.DBG_SQL):
                 print('*'*80)
             print('querier input', repr(rql), repr(args))
-        # parse the query and binds variables
-        cachekey = (rql,)
         try:
-            if args:
-                # search for named args in query which are eids (hence
-                # influencing query's solutions)
-                eidkeys = self._rql_ck_cache[rql]
-                if eidkeys:
-                    # if there are some, we need a better cache key, eg (rql +
-                    # entity type of each eid)
-                    try:
-                        cachekey = self._repo.querier_cache_key(cnx, rql,
-                                                                args, eidkeys)
-                    except UnknownEid:
-                        # we want queries such as "Any X WHERE X eid 9999"
-                        # return an empty result instead of raising UnknownEid
-                        return empty_rset(rql, args)
-            rqlst = self._rql_cache[cachekey]
-            self.cache_hit += 1
-            statsd_c('cache_hit')
-        except KeyError:
-            self.cache_miss += 1
-            statsd_c('cache_miss')
-            rqlst = self.parse(rql)
-            try:
-                # compute solutions for rqlst and return named args in query
-                # which are eids. Notice that if you may not need `eidkeys`, we
-                # have to compute solutions anyway (kept as annotation on the
-                # tree)
-                eidkeys = self.solutions(cnx, rqlst, args)
-            except UnknownEid:
-                # we want queries such as "Any X WHERE X eid 9999" return an
-                # empty result instead of raising UnknownEid
-                return empty_rset(rql, args)
-            if args and rql not in self._rql_ck_cache:
-                self._rql_ck_cache[rql] = eidkeys
-                if eidkeys:
-                    cachekey = self._repo.querier_cache_key(cnx, rql, args,
-                                                            eidkeys)
-            self._rql_cache[cachekey] = rqlst
+            rqlst, cachekey = self.rql_cache.get(cnx, rql, args)
+        except UnknownEid:
+            # we want queries such as "Any X WHERE X eid 9999"
+            # return an empty result instead of raising UnknownEid
+            return empty_rset(rql, args)
         if rqlst.TYPE != 'select':
             if cnx.read_security:
                 check_no_password_selected(rqlst)
@@ -646,6 +602,92 @@
     # only defining here to prevent pylint from complaining
     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
 
+
+class RQLCache(object):
+
+    def __init__(self, repo, schema):
+        # rql st and solution cache.
+        self._cache = QueryCache(repo.config['rql-cache-size'])
+        # rql cache key cache. Don't bother using a Cache instance: we should
+        # have a limited number of queries in there, since there are no entries
+        # in this cache for user queries (which have no args)
+        self._ck_cache = {}
+        # some cache usage stats
+        self.cache_hit, self.cache_miss = 0, 0
+        # rql parsing / analysing helper
+        self.solutions = repo.vreg.solutions
+        rqlhelper = repo.vreg.rqlhelper
+        # set backend on the rql helper, will be used for function checking
+        rqlhelper.backend = repo.config.system_source_config['db-driver']
+
+        def parse(rql, annotate=False, parse=rqlhelper.parse):
+            """Return a freshly parsed syntax tree for the given RQL."""
+            try:
+                return parse(text_type(rql), annotate=annotate)
+            except UnicodeError:
+                raise RQLSyntaxError(rql)
+        self._parse = parse
+
+    def __len__(self):
+        return len(self._cache)
+
+    def get(self, cnx, rql, args):
+        """Return syntax tree and cache key for the given RQL.
+
+        Returned syntax tree is cached and must not be modified
+        """
+        # parse the query and binds variables
+        cachekey = (rql,)
+        try:
+            if args:
+                # search for named args in query which are eids (hence
+                # influencing query's solutions)
+                eidkeys = self._ck_cache[rql]
+                if eidkeys:
+                    # if there are some, we need a better cache key, eg (rql +
+                    # entity type of each eid)
+                    cachekey = _rql_cache_key(cnx, rql, args, eidkeys)
+            rqlst = self._cache[cachekey]
+            self.cache_hit += 1
+            statsd_c('cache_hit')
+        except KeyError:
+            self.cache_miss += 1
+            statsd_c('cache_miss')
+            rqlst = self._parse(rql)
+            # compute solutions for rqlst and return named args in query
+            # which are eids. Notice that if you may not need `eidkeys`, we
+            # have to compute solutions anyway (kept as annotation on the
+            # tree)
+            eidkeys = self.solutions(cnx, rqlst, args)
+            if args and rql not in self._ck_cache:
+                self._ck_cache[rql] = eidkeys
+                if eidkeys:
+                    cachekey = _rql_cache_key(cnx, rql, args, eidkeys)
+            self._cache[cachekey] = rqlst
+        return rqlst, cachekey
+
+    def pop(self, key, *args):
+        """Pop a key from the cache."""
+        self._cache.pop(key, *args)
+
+
+def _rql_cache_key(cnx, rql, args, eidkeys):
+    cachekey = [rql]
+    type_from_eid = cnx.repo.type_from_eid
+    for key in sorted(eidkeys):
+        try:
+            etype = type_from_eid(args[key], cnx)
+        except KeyError:
+            raise QueryError('bad cache key %s (no value)' % key)
+        except TypeError:
+            raise QueryError('bad cache key %s (value: %r)' % (
+                key, args[key]))
+        cachekey.append(etype)
+        # ensure eid is correctly typed in args
+        args[key] = int(args[key])
+    return tuple(cachekey)
+
+
 from logging import getLogger
 from cubicweb import set_log_methods
 LOGGER = getLogger('cubicweb.querier')
--- a/cubicweb/server/repository.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/repository.py	Mon Mar 20 10:28:01 2017 +0100
@@ -30,7 +30,6 @@
 
 from warnings import warn
 from itertools import chain
-from time import time, localtime, strftime
 from contextlib import contextmanager
 from logging import getLogger
 
@@ -42,14 +41,13 @@
 from yams import BadSchemaDefinition
 from rql.utils import rqlvar_maker
 
-from cubicweb import (CW_MIGRATION_MAP, QueryError,
+from cubicweb import (CW_MIGRATION_MAP,
                       UnknownEid, AuthenticationError, ExecutionError,
-                      BadConnectionId, ValidationError, Unauthorized,
-                      UniqueTogetherError, onevent, ViolatedConstraint)
+                      UniqueTogetherError, ViolatedConstraint)
 from cubicweb import set_log_methods
 from cubicweb import cwvreg, schema, server
-from cubicweb.server import ShuttingDown, utils, hook, querier, sources
-from cubicweb.server.session import Session, InternalManager
+from cubicweb.server import utils, hook, querier, sources
+from cubicweb.server.session import InternalManager, Connection
 
 
 NO_CACHE_RELATIONS = set([
@@ -150,23 +148,76 @@
         pass
 
 
+class _CnxSetPool(object):
+
+    def __init__(self, source, size):
+        self._cnxsets = []
+        if size is not None:
+            self._queue = queue.Queue()
+            for i in range(size):
+                cnxset = source.wrapped_connection()
+                self._cnxsets.append(cnxset)
+                self._queue.put_nowait(cnxset)
+        else:
+            self._queue = None
+            self._source = source
+        super(_CnxSetPool, self).__init__()
+
+    def qsize(self):
+        q = self._queue
+        if q is None:
+            return None
+        return q.qsize()
+
+    def get(self):
+        q = self._queue
+        if q is None:
+            return self._source.wrapped_connection()
+        try:
+            return self._queue.get(True, timeout=5)
+        except queue.Empty:
+            raise Exception('no connections set available after 5 secs, probably either a '
+                            'bug in code (too many uncommited/rolled back '
+                            'connections) or too much load on the server (in '
+                            'which case you can try to set a bigger '
+                            'connections pool size)')
+
+    def release(self, cnxset):
+        q = self._queue
+        if q is None:
+            cnxset.close(True)
+        else:
+            self._queue.put_nowait(cnxset)
+
+    def __iter__(self):
+        for cnxset in self._cnxsets:
+            yield cnxset
+
+    def close(self):
+        q = self._queue
+        if q is not None:
+            while not q.empty():
+                cnxset = q.get_nowait()
+                try:
+                    cnxset.close(True)
+                except Exception:
+                    self.exception('error while closing %s' % cnxset)
+
+
 class Repository(object):
     """a repository provides access to a set of persistent storages for
     entities and relations
     """
 
-    def __init__(self, config, tasks_manager=None, vreg=None):
+    def __init__(self, config, scheduler=None, vreg=None):
         self.config = config
         self.sources_by_eid = {}
         if vreg is None:
             vreg = cwvreg.CWRegistryStore(config)
         self.vreg = vreg
-        self._tasks_manager = tasks_manager
+        self._scheduler = scheduler
 
         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
         # list of running threads
@@ -175,7 +226,7 @@
         self.schema = schema.CubicWebSchema(config.appid)
         self.vreg.schema = self.schema  # until actual schema is loaded...
         # shutdown flag
-        self.shutting_down = False
+        self.shutting_down = None
         # sources (additional sources info in the system database)
         self.system_source = self.get_source('native', 'system',
                                              config.system_source_config.copy())
@@ -184,34 +235,23 @@
         self.querier = querier.QuerierHelper(self, self.schema)
         # cache eid -> type
         self._type_cache = {}
-        # open some connection sets
-        if config.init_cnxset_pool:
-            self.init_cnxset_pool()
         # the hooks manager
         self.hm = hook.HooksManager(self.vreg)
 
-        # registry hook to fix user class on registry reload
-        @onevent('after-registry-reload', self)
-        def fix_user_classes(self):
-            # After registry reload the 'CWUser' class used for CWEtype
-            # changed.  So 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):
-                    session.user.__class__ = usercls
-
-    def init_cnxset_pool(self):
-        """should be called bootstrap_repository, as this is what it does"""
+    def bootstrap(self):
+        self.info('starting repository from %s', self.config.apphome)
+        self.shutting_down = False
         config = self.config
         # copy pool size here since config.init_cube() and config.load_schema()
         # reload configuration from file and could reset a manually set pool
         # size.
-        pool_size = config['connections-pool-size']
-        self._cnxsets_pool = queue.Queue()
+        if config['connections-pooler-enabled']:
+            pool_size, min_pool_size = config['connections-pool-size'], 1
+        else:
+            pool_size = min_pool_size = None
         # 0. init a cnxset that will be used to fetch bootstrap information from
         #    the database
-        self._cnxsets_pool.put_nowait(self.system_source.wrapped_connection())
+        self.cnxsets = _CnxSetPool(self.system_source, min_pool_size)
         # 1. set used cubes
         if config.creating or not config.read_instance_schema:
             config.bootstrap_cubes()
@@ -227,12 +267,12 @@
             # the registry
             config.cube_appobject_path = set(('hooks', 'entities'))
             config.cubicweb_appobject_path = set(('hooks', 'entities'))
-            # limit connections pool to 1
-            pool_size = 1
+            # limit connections pool size
+            pool_size = min_pool_size
         if config.quick_start or config.creating or not config.read_instance_schema:
             # load schema from the file system
             if not config.creating:
-                self.info("set fs instance'schema")
+                self.info("set fs instance's schema")
             self.set_schema(config.load_schema(expand_cubes=True))
             if not config.creating:
                 # set eids on entities schema
@@ -259,12 +299,10 @@
                 self.vreg.init_properties(self.properties())
         # 4. close initialization connection set and reopen fresh ones for
         #    proper initialization
-        self._get_cnxset().close(True)
-        # list of available cnxsets (can't iterate on a Queue)
-        self.cnxsets = []
-        for i in range(pool_size):
-            self.cnxsets.append(self.system_source.wrapped_connection())
-            self._cnxsets_pool.put_nowait(self.cnxsets[-1])
+        self.cnxsets.close()
+        self.cnxsets = _CnxSetPool(self.system_source, pool_size)
+        # 5. call instance level initialisation hooks
+        self.hm.call_hooks('server_startup', repo=self)
 
     # internals ###############################################################
 
@@ -284,9 +322,6 @@
                     continue
                 self.add_source(sourceent)
 
-    def _clear_planning_caches(self):
-        clear_cache(self, 'source_defs')
-
     def add_source(self, sourceent):
         try:
             source = self.get_source(sourceent.type, sourceent.name,
@@ -306,12 +341,12 @@
             source.init(True, sourceent)
         else:
             source.init(False, sourceent)
-        self._clear_planning_caches()
+        self._clear_source_defs_caches()
 
     def remove_source(self, uri):
         source = self.sources_by_uri.pop(uri)
         del self.sources_by_eid[source.eid]
-        self._clear_planning_caches()
+        self._clear_source_defs_caches()
 
     def get_source(self, type, uri, source_config, eid=None):
         # set uri and type in source config so it's available through
@@ -348,39 +383,21 @@
                 raise Exception('Is the database initialised ? (cause: %s)' % ex)
         return appschema
 
-    def _prepare_startup(self):
-        """Prepare "Repository as a server" for startup.
+    def run_scheduler(self):
+        """Start repository scheduler after preparing the repository for that.
 
         * 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
-            self.hm.call_hooks('server_startup', repo=self)
-            # register a task to cleanup expired session
-            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)
-            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.
+        * start the scheduler *and block*.
 
         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,\
+        assert self._scheduler is not None, \
             "This Repository is not intended to be used as a server"
-        self._tasks_manager.start()
+        self.info(
+            'starting repository scheduler with tasks: %s',
+            ', '.join(e.action.__name__ for e in self._scheduler.queue))
+        self._scheduler.run()
 
     def looping_task(self, interval, func, *args):
         """register a function to be called every `interval` seconds.
@@ -388,27 +405,17 @@
         looping tasks can only be registered during repository initialization,
         once done this method will fail.
         """
-        assert self._tasks_manager is not None,\
+        assert self._scheduler is not None, \
             "This Repository is not intended to be used as a server"
-        self._tasks_manager.add_looping_task(interval, func, *args)
+        event = utils.schedule_periodic_task(
+            self._scheduler, interval, func, *args)
+        self.info('scheduled periodic task %s (interval: %.2fs)',
+                  event.action.__name__, interval)
 
     def threaded_task(self, func):
         """start function in a separated thread"""
         utils.RepoThread(func, self._running_threads).start()
 
-    def _get_cnxset(self):
-        try:
-            return self._cnxsets_pool.get(True, timeout=5)
-        except queue.Empty:
-            raise Exception('no connections set available after 5 secs, probably either a '
-                            'bug in code (too many uncommited/rolled back '
-                            'connections) or too much load on the server (in '
-                            'which case you can try to set a bigger '
-                            'connections pool size)')
-
-    def _free_cnxset(self, cnxset):
-        self._cnxsets_pool.put_nowait(cnxset)
-
     def shutdown(self):
         """called on server stop event to properly close opened sessions and
         connections
@@ -419,9 +426,8 @@
             # then, the system source is still available
             self.hm.call_hooks('before_server_shutdown', repo=self)
         self.shutting_down = True
+        self.info('shutting down repository')
         self.system_source.shutdown()
-        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)
@@ -429,15 +435,8 @@
             self.info('waiting thread %s...', thread.getName())
             thread.join()
             self.info('thread %s finished', thread.getName())
-        self.close_sessions()
-        while not self._cnxsets_pool.empty():
-            cnxset = self._cnxsets_pool.get_nowait()
-            try:
-                cnxset.close(True)
-            except Exception:
-                self.exception('error while closing %s' % cnxset)
-                continue
-        hits, misses = self.querier.cache_hit, self.querier.cache_miss
+        self.cnxsets.close()
+        hits, misses = self.querier.rql_cache.cache_hit, self.querier.rql_cache.cache_miss
         try:
             self.info('rql st cache hit/miss: %s/%s (%s%% hits)', hits, misses,
                       (hits * 100) / (hits + misses))
@@ -588,6 +587,9 @@
             sources[uri] = source.public_config
         return sources
 
+    def _clear_source_defs_caches(self):
+        clear_cache(self, 'source_defs')
+
     def properties(self):
         """Return a result set containing system wide properties.
 
@@ -640,95 +642,39 @@
                                query_attrs)
             return rset.rows
 
-    def new_session(self, login, **kwargs):
-        """open a *new* session for a given user
-
-        raise `AuthenticationError` if the authentication failed
-        raise `ConnectionError` if we can't open a connection
-        """
-        # use an internal connection
-        with self.internal_cnx() as cnx:
-            # try to get a user object
-            user = self.authenticate_user(cnx, login, **kwargs)
-        session = Session(user, self)
-        user._cw = user.cw_rset.req = session
-        user.cw_clear_relation_cache()
-        self._sessions[session.sessionid] = session
-        self.info('opened session %s for user %s', session.sessionid, login)
-        with session.new_cnx() as cnx:
-            self.hm.call_hooks('session_open', cnx)
-            # commit connection at this point in case write operation has been
-            # done during `session_open` hooks
-            cnx.commit()
-        return session
-
-    @deprecated('[3.23] use .new_session instead (and get a plain session object)')
-    def connect(self, login, **kwargs):
-        return self.new_session(login, **kwargs).sessionid
-
-    @deprecated('[3.23] use session.close() directly')
-    def close(self, sessionid):
-        self._get_session(sessionid).close()
-
     # session handling ########################################################
 
-    def close_sessions(self):
-        """close every opened sessions"""
-        for session in list(self._sessions.values()):
-            session.close()
-
-    def clean_sessions(self):
-        """close sessions not used since an amount of time specified in the
-        configuration
-        """
-        mintime = time() - self.cleanup_session_time
-        self.debug('cleaning session unused since %s',
-                   strftime('%H:%M:%S', localtime(mintime)))
-        nbclosed = 0
-        for session in list(self._sessions.values()):
-            if session.timestamp < mintime:
-                session.close()
-                nbclosed += 1
-        return nbclosed
-
     @contextmanager
     def internal_cnx(self):
         """Context manager returning a Connection using internal user which have
         every access rights on the repository.
 
-        Beware that unlike the older :meth:`internal_session`, internal
-        connections have all hooks beside security enabled.
+        Internal connections have all hooks beside security enabled.
         """
-        with Session(InternalManager(), self).new_cnx() as cnx:
+        with Connection(self, InternalManager()) as cnx:
             cnx.user._cw = cnx  # XXX remove when "vreg = user._cw.vreg" hack in entity.py is gone
             with cnx.security_enabled(read=False, write=False):
                 yield cnx
 
-    def _get_session(self, sessionid, txid=None, checkshuttingdown=True):
-        """return the session associated with the given session identifier"""
-        if checkshuttingdown and self.shutting_down:
-            raise ShuttingDown('Repository is shutting down')
-        try:
-            session = self._sessions[sessionid]
-        except KeyError:
-            raise BadConnectionId('No such session %s' % sessionid)
-        return session
-
     # data sources handling ###################################################
     # * correspondance between eid and type
     # * correspondance between eid and local id (i.e. specific to a given source)
 
-    def clear_caches(self, eids):
-        etcache = self._type_cache
-        rqlcache = self.querier._rql_cache
-        for eid in eids:
-            try:
-                etype = etcache.pop(int(eid))  # may be a string in some cases
-                rqlcache.pop(('%s X WHERE X eid %s' % (etype, eid),), None)
-            except KeyError:
-                etype = None
-            rqlcache.pop(('Any X WHERE X eid %s' % eid,), None)
-            self.system_source.clear_eid_cache(eid, etype)
+    def clear_caches(self, eids=None):
+        if eids is None:
+            self._type_cache = {}
+            etypes = None
+        else:
+            etypes = []
+            etcache = self._type_cache
+            for eid in eids:
+                try:
+                    etype = etcache.pop(int(eid))  # may be a string in some cases
+                except KeyError:
+                    etype = None
+                etypes.append(etype)
+        self.querier.clear_caches(eids, etypes)
+        self.system_source.clear_caches(eids, etypes)
 
     def type_from_eid(self, eid, cnx):
         """Return the type of the entity with id `eid`"""
@@ -743,21 +689,6 @@
             self._type_cache[eid] = etype
             return etype
 
-    def querier_cache_key(self, cnx, rql, args, eidkeys):
-        cachekey = [rql]
-        for key in sorted(eidkeys):
-            try:
-                etype = self.type_from_eid(args[key], cnx)
-            except KeyError:
-                raise QueryError('bad cache key %s (no value)' % key)
-            except TypeError:
-                raise QueryError('bad cache key %s (value: %r)' % (
-                    key, args[key]))
-            cachekey.append(etype)
-            # ensure eid is correctly typed in args
-            args[key] = int(args[key])
-        return tuple(cachekey)
-
     def add_info(self, cnx, entity, source):
         """add type and source info for an eid into the system table,
         and index the entity with the full text index
@@ -788,21 +719,7 @@
                         rql = 'DELETE X %s Y WHERE X eid IN (%s)' % (rtype, in_eids)
                     else:
                         rql = 'DELETE Y %s X WHERE X eid IN (%s)' % (rtype, in_eids)
-                    try:
-                        cnx.execute(rql, build_descr=False)
-                    except ValidationError:
-                        raise
-                    except Unauthorized:
-                        self.exception(
-                            'Unauthorized exception while cascading delete for entity %s. '
-                            'RQL: %s.\nThis should not happen since security is disabled here.',
-                            entities, rql)
-                        raise
-                    except Exception:
-                        if self.config.mode == 'test':
-                            raise
-                        self.exception('error while cascading delete for entity %s. RQL: %s',
-                                       entities, rql)
+                    cnx.execute(rql, build_descr=False)
 
     def init_entity_caches(self, cnx, entity, source):
         """Add entity to connection entities cache and repo's cache."""
--- a/cubicweb/server/serverconfig.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/serverconfig.py	Mon Mar 20 10:28:01 2017 +0100
@@ -131,6 +131,11 @@
           'of inactivity. Default to 24h.',
           'group': 'main', 'level': 3,
           }),
+        ('connections-pooler-enabled',
+         {'type': 'yn', 'default': True,
+          'help': 'enable the connection pooler',
+          'group': 'main', 'level': 3,
+          }),
         ('connections-pool-size',
          {'type' : 'int',
           'default': 4,
@@ -215,10 +220,6 @@
            }),
         ) + CubicWebConfiguration.options)
 
-    # should we init the connections pool (eg connect to sources). This is
-    # usually necessary...
-    init_cnxset_pool = True
-
     # read the schema from the database
     read_instance_schema = True
     # set this to true to get a minimal repository, for instance to get cubes
--- a/cubicweb/server/serverctl.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/serverctl.py	Mon Mar 20 10:28:01 2017 +0100
@@ -37,6 +37,7 @@
 from cubicweb.toolsutils import Command, CommandHandler, underline_title
 from cubicweb.cwctl import CWCTL, check_options_consistency, ConfigureInstanceCommand
 from cubicweb.server import SOURCE_TYPES
+from cubicweb.server import checkintegrity
 from cubicweb.server.serverconfig import (
     USER_OPTIONS, ServerConfiguration, SourceConfiguration,
     ask_source_config, generate_source_config)
@@ -902,12 +903,8 @@
     options = (
         ('checks',
          {'short': 'c', 'type': 'csv', 'metavar': '<check list>',
-          'default': ('entities', 'relations',
-                      'mandatory_relations', 'mandatory_attributes',
-                      'metadata', 'schema', 'text_index'),
-          'help': 'Comma separated list of check to run. By default run all \
-checks, i.e. entities, relations, mandatory_relations, mandatory_attributes, \
-metadata, text_index and schema.'}
+          'default': sorted(checkintegrity._CHECKERS),
+          'help': 'Comma separated list of check to run. By default run all checks.'}
          ),
 
         ('autofix',
@@ -930,13 +927,12 @@
     )
 
     def run(self, args):
-        from cubicweb.server.checkintegrity import check
         appid = args[0]
         config = ServerConfiguration.config_for(appid)
         config.repairing = self.config.force
         repo, _cnx = repo_cnx(config)
         with repo.internal_cnx() as cnx:
-            check(repo, cnx,
+            checkintegrity.check(repo, cnx,
                   self.config.checks,
                   self.config.reindex,
                   self.config.autofix)
@@ -953,11 +949,10 @@
     min_args = 1
 
     def run(self, args):
-        from cubicweb.server.checkintegrity import check_indexes
         config = ServerConfiguration.config_for(args[0])
         repo, cnx = repo_cnx(config)
         with cnx:
-            status = check_indexes(cnx)
+            status = checkintegrity.check_indexes(cnx)
         sys.exit(status)
 
 
@@ -985,6 +980,45 @@
             cnx.commit()
 
 
+class RepositorySchedulerCommand(Command):
+    """Start a repository tasks scheduler.
+
+    Initialize a repository and start its tasks scheduler that would run
+    registered "looping tasks".
+
+    This is maintenance command that should be kept running along with a web
+    instance of a CubicWeb WSGI application (e.g. embeded into a Pyramid
+    application).
+
+    <instance>
+      the identifier of the instance
+    """
+    name = 'scheduler'
+    arguments = '<instance>'
+    min_args = max_args = 1
+    options = (
+        ('loglevel',
+         {'short': 'l', 'type': 'choice', 'metavar': '<log level>',
+          'default': 'info', 'choices': ('debug', 'info', 'warning', 'error')},
+         ),
+    )
+
+    def run(self, args):
+        from cubicweb.cwctl import init_cmdline_log_threshold
+        from cubicweb.server.repository import Repository
+        from cubicweb.server.utils import scheduler
+        config = ServerConfiguration.config_for(args[0])
+        # Log to stdout, since the this command runs in the foreground.
+        config.global_set_option('log-file', None)
+        init_cmdline_log_threshold(config, self['loglevel'])
+        repo = Repository(config, scheduler())
+        repo.bootstrap()
+        try:
+            repo.run_scheduler()
+        finally:
+            repo.shutdown()
+
+
 class SynchronizeSourceCommand(Command):
     """Force sources synchronization.
 
@@ -1095,6 +1129,7 @@
                  DBDumpCommand, DBRestoreCommand, DBCopyCommand, DBIndexSanityCheckCommand,
                  AddSourceCommand, CheckRepositoryCommand, RebuildFTICommand,
                  SynchronizeSourceCommand, SchemaDiffCommand,
+                 RepositorySchedulerCommand,
                  ):
     CWCTL.register(cmdclass)
 
--- a/cubicweb/server/session.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/session.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2017 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 @@
 
 import functools
 import sys
-from time import time
 from uuid import uuid4
 from warnings import warn
 from contextlib import contextmanager
@@ -30,13 +29,11 @@
 from six import text_type
 
 from logilab.common.deprecation import deprecated
-from logilab.common.textutils import unormalize
 from logilab.common.registry import objectify_predicate
 
 from cubicweb import QueryError, ProgrammingError, schema, server
 from cubicweb import set_log_methods
 from cubicweb.req import RequestSessionBase
-from cubicweb.utils import make_uid
 from cubicweb.rqlrewrite import RQLRewriter
 from cubicweb.server.edition import EditedEntity
 
@@ -111,27 +108,19 @@
         assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
         self.cnx = cnx
         self.mode = mode
-        self.categories = categories
-        self.oldmode = None
-        self.changes = ()
+        self.categories = set(categories)
+        self.old_mode = None
+        self.old_categories = None
 
     def __enter__(self):
-        self.oldmode = self.cnx.hooks_mode
-        self.cnx.hooks_mode = self.mode
-        if self.mode is HOOKS_DENY_ALL:
-            self.changes = self.cnx.enable_hook_categories(*self.categories)
-        else:
-            self.changes = self.cnx.disable_hook_categories(*self.categories)
+        self.old_mode = self.cnx._hooks_mode
+        self.old_categories = self.cnx._hooks_categories
+        self.cnx._hooks_mode = self.mode
+        self.cnx._hooks_categories = self.categories
 
     def __exit__(self, exctype, exc, traceback):
-        try:
-            if self.categories:
-                if self.mode is HOOKS_DENY_ALL:
-                    self.cnx.disable_hook_categories(*self.categories)
-                else:
-                    self.cnx.enable_hook_categories(*self.categories)
-        finally:
-            self.cnx.hooks_mode = self.oldmode
+        self.cnx._hooks_mode = self.old_mode
+        self.cnx._hooks_categories = self.old_categories
 
 
 @deprecated('[3.17] use <object>.security_enabled instead')
@@ -176,17 +165,12 @@
 DEFAULT_SECURITY = object()  # evaluated to true by design
 
 
-class SessionClosedError(RuntimeError):
-    pass
-
-
 def _open_only(func):
     """decorator for Connection method that check it is open"""
     @functools.wraps(func)
     def check_open(cnx, *args, **kwargs):
         if not cnx._open:
-            raise ProgrammingError('Closed Connection: %s'
-                                   % cnx.connectionid)
+            raise ProgrammingError('Closed Connection: %s' % cnx)
         return func(cnx, *args, **kwargs)
     return check_open
 
@@ -240,13 +224,8 @@
 
     Hooks controls:
 
-      :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`.
-
-      :attr:`enabled_hook_cats`, when :attr:`hooks_mode` is
-      `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled.
-
-      :attr:`disabled_hook_cats`, when :attr:`hooks_mode` is
-      `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled.
+    .. automethod:: cubicweb.server.session.Connection.deny_all_hooks_but
+    .. automethod:: cubicweb.server.session.Connection.allow_all_hooks_but
 
     Security level Management:
 
@@ -257,37 +236,34 @@
     is_request = False
     hooks_in_progress = False
 
-    def __init__(self, session):
-        super(Connection, self).__init__(session.repo.vreg)
+    def __init__(self, repo, user):
+        super(Connection, self).__init__(repo.vreg)
         #: connection unique id
         self._open = None
-        self.connectionid = '%s-%s' % (session.sessionid, uuid4().hex)
-        self.session = session
-        self.sessionid = session.sessionid
 
         #: server.Repository object
-        self.repo = session.repo
+        self.repo = repo
         self.vreg = self.repo.vreg
         self._execute = self.repo.querier.execute
 
-        # other session utility
-        self._session_timestamp = session._timestamp
-
         # internal (root) session
-        self.is_internal_session = isinstance(session.user, InternalManager)
+        self.is_internal_session = isinstance(user, InternalManager)
 
         #: dict containing arbitrary data cleared at the end of the transaction
         self.transaction_data = {}
-        self._session_data = session.data
         #: ordered list of operations to be processed on commit/rollback
         self.pending_operations = []
         #: (None, 'precommit', 'postcommit', 'uncommitable')
         self.commit_state = None
 
         # hook control attribute
-        self.hooks_mode = HOOKS_ALLOW_ALL
-        self.disabled_hook_cats = set()
-        self.enabled_hook_cats = set()
+        # `_hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`.
+        self._hooks_mode = HOOKS_ALLOW_ALL
+        # `_hooks_categories`, when :attr:`_hooks_mode` is `HOOKS_DENY_ALL`,
+        # this set contains hooks categories that are enabled ;
+        # when :attr:`_hooks_mode` is `HOOKS_ALLOW_ALL`, it contains hooks
+        # categories that are disabled.
+        self._hooks_categories = set()
         self.pruned_hooks_cache = {}
 
         # security control attributes
@@ -295,7 +271,7 @@
         self.write_security = DEFAULT_SECURITY
 
         # undo control
-        config = session.repo.config
+        config = repo.config
         if config.creating or config.repairing or self.is_internal_session:
             self.undo_actions = False
         else:
@@ -305,20 +281,20 @@
         self._rewriter = RQLRewriter(self)
 
         # other session utility
-        if session.user.login == '__internal_manager__':
-            self.user = session.user
+        if user.login == '__internal_manager__':
+            self.user = user
         else:
-            self._set_user(session.user)
+            self._set_user(user)
 
     @_open_only
     def get_schema(self):
         """Return the schema currently used by the repository."""
-        return self.session.repo.source_defs()
+        return self.repo.source_defs()
 
     @_open_only
     def get_option_value(self, option):
         """Return the value for `option` in the configuration."""
-        return self.session.repo.get_option_value(option)
+        return self.repo.get_option_value(option)
 
     # transaction api
 
@@ -385,7 +361,7 @@
     def __enter__(self):
         assert not self._open
         self._open = True
-        self.cnxset = self.repo._get_cnxset()
+        self.cnxset = self.repo.cnxsets.get()
         if self.lang is None:
             self.set_language(self.user.prefered_language())
         return self
@@ -395,7 +371,7 @@
         self.rollback()
         self._open = False
         self.cnxset.cnxset_freed()
-        self.repo._free_cnxset(self.cnxset)
+        self.repo.cnxsets.release(self.cnxset)
         self.cnxset = None
 
     @contextmanager
@@ -414,8 +390,9 @@
     # shared data handling ###################################################
 
     @property
+    @deprecated('[3.25] use transaction_data or req.session.data', stacklevel=3)
     def data(self):
-        return self._session_data
+        return self.transaction_data
 
     @property
     def rql_rewriter(self):
@@ -428,7 +405,7 @@
         if txdata:
             data = self.transaction_data
         else:
-            data = self._session_data
+            data = self.data
         if pop:
             return data.pop(key, default)
         else:
@@ -441,7 +418,7 @@
         if txdata:
             self.transaction_data[key] = value
         else:
-            self._session_data[key] = value
+            self.data[key] = value
 
     def clear(self):
         """reset internal data"""
@@ -472,10 +449,6 @@
     def ensure_cnx_set(self):
         yield
 
-    @property
-    def anonymous_connection(self):
-        return self.session.anonymous_session
-
     # Entity cache management #################################################
     #
     # The connection entity cache as held in cnx.transaction_data is removed at the
@@ -491,7 +464,7 @@
         # XXX not using _open_only because before at creation time. _set_user
         # call this function to cache the Connection user.
         if entity.cw_etype != 'CWUser' and not self._open:
-            raise ProgrammingError('Closed Connection: %s' % self.connectionid)
+            raise ProgrammingError('Closed Connection: %s' % self)
         ecache = self.transaction_data.setdefault('ecache', {})
         ecache.setdefault(entity.eid, entity)
 
@@ -506,14 +479,11 @@
         return self.transaction_data.get('ecache', {}).values()
 
     @_open_only
-    def drop_entity_cache(self, eid=None):
-        """drop entity from the cache
-
-        If eid is None, the whole cache is dropped"""
-        if eid is None:
-            self.transaction_data.pop('ecache', None)
-        else:
-            del self.transaction_data['ecache'][eid]
+    def drop_entity_cache(self):
+        """Drop the whole entity cache."""
+        for entity in self.cached_entities():
+            entity.cw_clear_all_caches()
+        self.transaction_data.pop('ecache', None)
 
     # relations handling #######################################################
 
@@ -679,60 +649,26 @@
 
     @_open_only
     def allow_all_hooks_but(self, *categories):
+        """Context manager to enable all hooks but those in the given
+        categories.
+        """
         return _hooks_control(self, HOOKS_ALLOW_ALL, *categories)
 
     @_open_only
     def deny_all_hooks_but(self, *categories):
-        return _hooks_control(self, HOOKS_DENY_ALL, *categories)
-
-    @_open_only
-    def disable_hook_categories(self, *categories):
-        """disable the given hook categories:
-
-        - on HOOKS_DENY_ALL mode, ensure those categories are not enabled
-        - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled
+        """Context manager to disable all hooks but those in the given
+        categories.
         """
-        changes = set()
-        self.pruned_hooks_cache.clear()
-        categories = set(categories)
-        if self.hooks_mode is HOOKS_DENY_ALL:
-            enabledcats = self.enabled_hook_cats
-            changes = enabledcats & categories
-            enabledcats -= changes  # changes is small hence faster
-        else:
-            disabledcats = self.disabled_hook_cats
-            changes = categories - disabledcats
-            disabledcats |= changes  # changes is small hence faster
-        return tuple(changes)
-
-    @_open_only
-    def enable_hook_categories(self, *categories):
-        """enable the given hook categories:
-
-        - on HOOKS_DENY_ALL mode, ensure those categories are enabled
-        - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled
-        """
-        changes = set()
-        self.pruned_hooks_cache.clear()
-        categories = set(categories)
-        if self.hooks_mode is HOOKS_DENY_ALL:
-            enabledcats = self.enabled_hook_cats
-            changes = categories - enabledcats
-            enabledcats |= changes  # changes is small hence faster
-        else:
-            disabledcats = self.disabled_hook_cats
-            changes = disabledcats & categories
-            disabledcats -= changes  # changes is small hence faster
-        return tuple(changes)
+        return _hooks_control(self, HOOKS_DENY_ALL, *categories)
 
     @_open_only
     def is_hook_category_activated(self, category):
         """return a boolean telling if the given category is currently activated
         or not
         """
-        if self.hooks_mode is HOOKS_DENY_ALL:
-            return category in self.enabled_hook_cats
-        return category not in self.disabled_hook_cats
+        if self._hooks_mode is HOOKS_DENY_ALL:
+            return category in self._hooks_categories
+        return category not in self._hooks_categories
 
     @_open_only
     def is_hook_activated(self, hook):
@@ -802,10 +738,8 @@
 
         See :meth:`cubicweb.dbapi.Cursor.execute` documentation.
         """
-        self._session_timestamp.touch()
         rset = self._execute(self, rql, kwargs, build_descr)
         rset.req = self
-        self._session_timestamp.touch()
         return rset
 
     @_open_only
@@ -830,9 +764,8 @@
                         self.critical('rollback error', exc_info=sys.exc_info())
                         continue
                 cnxset.rollback()
-                self.debug('rollback for transaction %s done', self.connectionid)
+                self.debug('rollback for transaction %s done', self)
         finally:
-            self._session_timestamp.touch()
             self.clear()
 
     @_open_only
@@ -877,7 +810,7 @@
                                 print(operation)
                             operation.handle_event('precommit_event')
                     self.pending_operations[:] = processed
-                    self.debug('precommit transaction %s done', self.connectionid)
+                    self.debug('precommit transaction %s done', self)
                 except BaseException:
                     # if error on [pre]commit:
                     #
@@ -921,10 +854,9 @@
                         except BaseException:
                             self.critical('error while postcommit',
                                           exc_info=sys.exc_info())
-                self.debug('postcommit transaction %s done', self.connectionid)
+                self.debug('postcommit transaction %s done', self)
                 return self.transaction_uuid(set=False)
         finally:
-            self._session_timestamp.touch()
             self.clear()
 
     # resource accessors ######################################################
@@ -955,129 +887,6 @@
         return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)]
 
 
-def cnx_attr(attr_name, writable=False):
-    """return a property to forward attribute access to connection.
-
-    This is to be used by session"""
-    args = {}
-
-    @deprecated('[3.19] use a Connection object instead')
-    def attr_from_cnx(session):
-        return getattr(session._cnx, attr_name)
-
-    args['fget'] = attr_from_cnx
-    if writable:
-        @deprecated('[3.19] use a Connection object instead')
-        def write_attr(session, value):
-            return setattr(session._cnx, attr_name, value)
-        args['fset'] = write_attr
-    return property(**args)
-
-
-class Timestamp(object):
-
-    def __init__(self):
-        self.value = time()
-
-    def touch(self):
-        self.value = time()
-
-    def __float__(self):
-        return float(self.value)
-
-
-class Session(object):
-    """Repository user session
-
-    This ties all together:
-     * session id,
-     * user,
-     * other session data.
-    """
-
-    def __init__(self, user, repo, _id=None):
-        self.sessionid = _id or make_uid(unormalize(user.login))
-        self.user = user  # XXX repoapi: deprecated and store only a login.
-        self.repo = repo
-        self._timestamp = Timestamp()
-        self.data = {}
-        self.closed = False
-
-    def close(self):
-        if self.closed:
-            self.warning('closing already closed session %s', self.sessionid)
-            return
-        with self.new_cnx() as cnx:
-            self.repo.hm.call_hooks('session_close', cnx)
-            cnx.commit()
-            del self.repo._sessions[self.sessionid]
-        self.closed = True
-        self.info('closed session %s for user %s', self.sessionid, self.user.login)
-
-    def __unicode__(self):
-        return '<session %s (%s 0x%x)>' % (
-            unicode(self.user.login), self.sessionid, id(self))
-
-    @property
-    def timestamp(self):
-        return float(self._timestamp)
-
-    @property
-    @deprecated('[3.19] session.id is deprecated, use session.sessionid')
-    def id(self):
-        return self.sessionid
-
-    @property
-    def login(self):
-        return self.user.login
-
-    def new_cnx(self):
-        """Return a new Connection object linked to the session
-
-        The returned Connection will *not* be managed by the Session.
-        """
-        return Connection(self)
-
-    @deprecated('[3.19] use a Connection object instead')
-    def get_option_value(self, option, foreid=None):
-        if foreid is not None:
-            warn('[3.19] foreid argument is deprecated', DeprecationWarning,
-                 stacklevel=2)
-        return self.repo.get_option_value(option)
-
-    def _touch(self):
-        """update latest session usage timestamp and reset mode to read"""
-        self._timestamp.touch()
-
-    local_perm_cache = cnx_attr('local_perm_cache')
-
-    @local_perm_cache.setter
-    def local_perm_cache(self, value):
-        # base class assign an empty dict:-(
-        assert value == {}
-        pass
-
-    # deprecated ###############################################################
-
-    @property
-    def anonymous_session(self):
-        # XXX for now, anonymous_user only exists in webconfig (and testconfig).
-        # It will only be present inside all-in-one instance.
-        # there is plan to move it down to global config.
-        if not hasattr(self.repo.config, 'anonymous_user'):
-            # not a web or test config, no anonymous user
-            return False
-        return self.user.login == self.repo.config.anonymous_user()[0]
-
-    @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
-    def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
-        return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop)
-
-    # these are overridden by set_log_methods below
-    # only defining here to prevent pylint from complaining
-    info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
-
-
 class InternalManager(object):
     """a manager user with all access rights used internally for task such as
     bootstrapping the repository or creating regular users according to
@@ -1125,5 +934,4 @@
         return None
 
 
-set_log_methods(Session, getLogger('cubicweb.session'))
 set_log_methods(Connection, getLogger('cubicweb.session'))
--- a/cubicweb/server/sources/__init__.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/sources/__init__.py	Mon Mar 20 10:28:01 2017 +0100
@@ -234,16 +234,6 @@
             cnxset.cnx = self.get_connection()
             cnxset.cu = cnxset.cnx.cursor()
 
-    # cache handling ###########################################################
-
-    def reset_caches(self):
-        """method called during test to reset potential source caches"""
-        pass
-
-    def clear_eid_cache(self, eid, etype):
-        """clear potential caches for the given eid"""
-        pass
-
     # user authentication api ##################################################
 
     def authenticate(self, cnx, login, **kwargs):
--- a/cubicweb/server/sources/native.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/sources/native.py	Mon Mar 20 10:28:01 2017 +0100
@@ -30,7 +30,7 @@
 import sys
 
 from six import PY2, text_type, string_types
-from six.moves import range, cPickle as pickle
+from six.moves import range, cPickle as pickle, zip
 
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.configuration import Method
@@ -361,15 +361,17 @@
         authentifier.source = self
         authentifier.set_schema(self.schema)
 
-    def reset_caches(self):
-        """method called during test to reset potential source caches"""
-        self._cache = QueryCache(self.repo.config['rql-cache-size'])
-
-    def clear_eid_cache(self, eid, etype):
-        """clear potential caches for the given eid"""
-        self._cache.pop('Any X WHERE X eid %s, X is %s' % (eid, etype), None)
-        self._cache.pop('Any X WHERE X eid %s' % eid, None)
-        self._cache.pop('Any %s' % eid, None)
+    def clear_caches(self, eids, etypes):
+        """Clear potential source caches."""
+        if eids is None:
+            self._cache = QueryCache(self.repo.config['rql-cache-size'])
+        else:
+            cache = self._cache
+            for eid, etype in zip(eids, etypes):
+                cache.pop('Any X WHERE X eid %s' % eid, None)
+                cache.pop('Any %s' % eid, None)
+                if etype is not None:
+                    cache.pop('Any X WHERE X eid %s, X is %s' % (eid, etype), None)
 
     @statsd_timeit
     def sqlexec(self, cnx, sql, args=None):
@@ -380,7 +382,7 @@
         # check full text index availibility
         if self.do_fti:
             if cnxset is None:
-                _cnxset = self.repo._get_cnxset()
+                _cnxset = self.repo.cnxsets.get()
             else:
                 _cnxset = cnxset
             if not self.dbhelper.has_fti_table(_cnxset.cu):
@@ -389,7 +391,7 @@
                 self.do_fti = False
             if cnxset is None:
                 _cnxset.cnxset_freed()
-                self.repo._free_cnxset(_cnxset)
+                self.repo.cnxsets.release(_cnxset)
 
     def backup(self, backupfile, confirm, format='native'):
         """method called to create a backup of the source's data"""
@@ -417,19 +419,13 @@
 
     def restore(self, backupfile, confirm, drop, format='native'):
         """method called to restore a backup of source's data"""
-        if self.repo.config.init_cnxset_pool:
-            self.close_source_connections()
-        try:
-            if format == 'portable':
-                helper = DatabaseIndependentBackupRestore(self)
-                helper.restore(backupfile)
-            elif format == 'native':
-                self.restore_from_file(backupfile, confirm, drop=drop)
-            else:
-                raise ValueError('Unknown format %r' % format)
-        finally:
-            if self.repo.config.init_cnxset_pool:
-                self.open_source_connections()
+        if format == 'portable':
+            helper = DatabaseIndependentBackupRestore(self)
+            helper.restore(backupfile)
+        elif format == 'native':
+            self.restore_from_file(backupfile, confirm, drop=drop)
+        else:
+            raise ValueError('Unknown format %r' % format)
 
     def init(self, activated, source_entity):
         super(NativeSQLSource, self).init(activated, source_entity)
--- a/cubicweb/server/sqlutils.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/sqlutils.py	Mon Mar 20 10:28:01 2017 +0100
@@ -173,8 +173,8 @@
 
 
 class ConnectionWrapper(object):
-    """handle connection to the system source, at some point associated to a
-    :class:`Session`
+    """Wrap a connection to the system source's database, attempting to handle
+    automatic reconnection.
     """
 
     # since 3.19, we only have to manage the system source connection
--- a/cubicweb/server/test/data/hooks.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/data/hooks.py	Mon Mar 20 10:28:01 2017 +0100
@@ -15,34 +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/>.
-"""
 
-"""
 from cubicweb.server.hook import Hook
 
 CALLED_EVENTS = {}
 
+
 class StartupHook(Hook):
     __regid__ = 'mystartup'
     events = ('server_startup',)
+
     def __call__(self):
         CALLED_EVENTS['server_startup'] = True
 
+
 class ShutdownHook(Hook):
     __regid__ = 'myshutdown'
     events = ('server_shutdown',)
+
     def __call__(self):
         CALLED_EVENTS['server_shutdown'] = True
-
-
-class LoginHook(Hook):
-    __regid__ = 'mylogin'
-    events = ('session_open',)
-    def __call__(self):
-        CALLED_EVENTS['session_open'] = self._cw.user.login
-
-class LogoutHook(Hook):
-    __regid__ = 'mylogout'
-    events = ('session_close',)
-    def __call__(self):
-        CALLED_EVENTS['session_close'] = self._cw.user.login
--- a/cubicweb/server/test/unittest_checkintegrity.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_checkintegrity.py	Mon Mar 20 10:28:01 2017 +0100
@@ -25,9 +25,9 @@
 else:
     from io import StringIO
 
-from cubicweb import devtools
-from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.server.checkintegrity import check, check_indexes, reindex_entities
+from cubicweb import devtools  # noqa: E402
+from cubicweb.devtools.testlib import CubicWebTC  # noqa: E402
+from cubicweb.server.checkintegrity import check, check_indexes, reindex_entities  # noqa: E402
 
 
 class CheckIntegrityTC(unittest.TestCase):
--- a/cubicweb/server/test/unittest_hook.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_hook.py	Mon Mar 20 10:28:01 2017 +0100
@@ -112,7 +112,7 @@
     def test_call_hook(self):
         self.o.register(AddAnyHook)
         dis = set()
-        cw = fake.FakeSession()
+        cw = fake.FakeConnection()
         cw.is_hook_activated = lambda cls: cls.category not in dis
         self.assertRaises(HookCalled,
                           self.o.call_hooks, 'before_add_entity', cw)
@@ -134,14 +134,6 @@
         self.repo.hm.call_hooks('server_shutdown', repo=self.repo)
         self.assertEqual(hooks.CALLED_EVENTS['server_shutdown'], True)
 
-    def test_session_open_close(self):
-        import hooks # cubicweb/server/test/data/hooks.py
-        anonaccess = self.new_access('anon')
-        with anonaccess.repo_cnx() as cnx:
-            self.assertEqual(hooks.CALLED_EVENTS['session_open'], 'anon')
-        anonaccess.close()
-        self.assertEqual(hooks.CALLED_EVENTS['session_close'], 'anon')
-
 
 if __name__ == '__main__':
     unittest.main()
--- a/cubicweb/server/test/unittest_ldapsource.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_ldapsource.py	Mon Mar 20 10:28:01 2017 +0100
@@ -246,9 +246,6 @@
             self.assertRaises(AuthenticationError,
                               source.authenticate, cnx, 'syt', 'toto')
             self.assertTrue(source.authenticate(cnx, 'syt', 'syt'))
-        session = self.repo.new_session('syt', password='syt')
-        self.assertTrue(session)
-        session.close()
 
     def test_base(self):
         with self.admin_access.repo_cnx() as cnx:
@@ -280,7 +277,6 @@
             eid = cnx.execute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
             cnx.execute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': eid})
             cnx.commit()
-            source.reset_caches()
             rset = cnx.execute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
             self.assertEqual(len(rset), 1)
             e = rset.get_entity(0, 0)
@@ -329,16 +325,18 @@
 
     def test_a_filter_inactivate(self):
         """ filtered out people should be deactivated, unable to authenticate """
+        repo_source = self.repo.sources_by_uri['ldap']
         with self.admin_access.repo_cnx() as cnx:
             source = cnx.execute('CWSource S WHERE S type="ldapfeed"').get_entity(0, 0)
-            config = source.repo_source.check_config(source)
+            config = repo_source.check_config(source)
             # filter with adim's phone number
             config['user-filter'] = u'(%s=%s)' % ('telephoneNumber', '109')
-            source.repo_source.update_config(source, config)
+            repo_source.update_config(source, config)
             cnx.commit()
         with self.repo.internal_cnx() as cnx:
             self.pull(cnx)
-        self.assertRaises(AuthenticationError, self.repo.new_session, 'syt', password='syt')
+            self.assertRaises(AuthenticationError,
+                              repo_source.authenticate, cnx, 'syt', 'syt')
         with self.admin_access.repo_cnx() as cnx:
             self.assertEqual(cnx.execute('Any N WHERE U login "syt", '
                                          'U in_state S, S name N').rows[0][0],
@@ -348,7 +346,7 @@
                              'activated')
             # unfilter, syt should be activated again
             config['user-filter'] = u''
-            source.repo_source.update_config(source, config)
+            repo_source.update_config(source, config)
             cnx.commit()
         with self.repo.internal_cnx() as cnx:
             self.pull(cnx)
@@ -367,7 +365,9 @@
         self.delete_ldap_entry('uid=syt,ou=People,dc=cubicweb,dc=test')
         with self.repo.internal_cnx() as cnx:
             self.pull(cnx)
-        self.assertRaises(AuthenticationError, self.repo.new_session, 'syt', password='syt')
+            source = self.repo.sources_by_uri['ldap']
+            self.assertRaises(AuthenticationError,
+                              source.authenticate, cnx, 'syt', 'syt')
         with self.admin_access.repo_cnx() as cnx:
             self.assertEqual(cnx.execute('Any N WHERE U login "syt", '
                                          'U in_state S, S name N').rows[0][0],
@@ -404,6 +404,7 @@
         # test reactivating BY HAND the user isn't enough to
         # authenticate, as the native source refuse to authenticate
         # user from other sources
+        repo_source = self.repo.sources_by_uri['ldap']
         self.delete_ldap_entry('uid=syt,ou=People,dc=cubicweb,dc=test')
         with self.repo.internal_cnx() as cnx:
             self.pull(cnx)
@@ -412,17 +413,16 @@
             user = cnx.execute('CWUser U WHERE U login "syt"').get_entity(0, 0)
             user.cw_adapt_to('IWorkflowable').fire_transition('activate')
             cnx.commit()
-            with self.assertRaises(AuthenticationError):
-                self.repo.new_session('syt', password='syt')
+            self.assertRaises(AuthenticationError,
+                              repo_source.authenticate, cnx, 'syt', 'syt')
 
             # ok now let's try to make it a system user
             cnx.execute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': user.eid})
             cnx.commit()
-        # and that we can now authenticate again
-        self.assertRaises(AuthenticationError, self.repo.new_session, 'syt', password='toto')
-        session = self.repo.new_session('syt', password='syt')
-        self.assertTrue(session)
-        session.close()
+            # and that we can now authenticate again
+            self.assertRaises(AuthenticationError,
+                              repo_source.authenticate, cnx, 'syt', 'toto')
+            self.assertTrue(self.repo.authenticate_user(cnx, 'syt', password='syt'))
 
 
 class LDAPFeedGroupTC(LDAPFeedTestBase):
--- a/cubicweb/server/test/unittest_migractions.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_migractions.py	Mon Mar 20 10:28:01 2017 +0100
@@ -19,6 +19,7 @@
 
 import os
 import os.path as osp
+import sys
 from datetime import date
 from contextlib import contextmanager
 import tempfile
@@ -77,13 +78,25 @@
         # we have to read schema from the database to get eid for schema entities
         self.repo.set_schema(self.repo.deserialize_schema(), resetvreg=False)
         # hack to read the schema from data/migrschema
-        config = self.config
-        config.appid = osp.join(self.appid, 'migratedapp')
-        config._apphome = osp.join(HERE, config.appid)
-        global migrschema
-        migrschema = config.load_schema()
-        config.appid = self.appid
-        config._apphome = osp.join(HERE, self.appid)
+
+        @contextmanager
+        def temp_app(config, appid, apphome):
+            old = config.apphome, config.appid
+            sys.path.remove(old[0])
+            sys.path.insert(0, apphome)
+            config._apphome, config.appid = apphome, appid
+            try:
+                yield config
+            finally:
+                sys.path.remove(apphome)
+                sys.path.insert(0, old[0])
+                config._apphome, config.appid = old
+
+        appid = osp.join(self.appid, 'migratedapp')
+        apphome = osp.join(HERE, appid)
+        with temp_app(self.config, appid, apphome) as config:
+            global migrschema
+            migrschema = config.load_schema()
 
     def setUp(self):
         self.configcls.cls_adjust_sys_path()
--- a/cubicweb/server/test/unittest_postgres.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_postgres.py	Mon Mar 20 10:28:01 2017 +0100
@@ -52,7 +52,7 @@
 
     def test_eid_range(self):
         # concurrent allocation of eid ranges
-        source = self.session.repo.sources_by_uri['system']
+        source = self.repo.sources_by_uri['system']
         range1 = []
         range2 = []
         def allocate_eid_ranges(session, target):
--- a/cubicweb/server/test/unittest_querier.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_querier.py	Mon Mar 20 10:28:01 2017 +0100
@@ -120,7 +120,7 @@
         pass
 
     def test_preprocess_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             reid = cnx.execute('Any X WHERE X is CWRType, X name "owned_by"')[0][0]
             rqlst = self._prepare(cnx, 'Any COUNT(RDEF) WHERE RDEF relation_type X, X eid %(x)s',
                                   {'x': reid})
@@ -128,7 +128,7 @@
                              rqlst.solutions)
 
     def test_preprocess_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             teid = cnx.execute("INSERT Tag X: X name 'tag'")[0][0]
             #geid = self.execute("CWGroup G WHERE G name 'users'")[0][0]
             #self.execute("SET X tags Y WHERE X eid %(t)s, Y eid %(g)s",
@@ -144,8 +144,7 @@
                                   text_type(parse(got)))
 
     def test_preprocess_security(self):
-        s = self.user_groups_session('users')
-        with s.new_cnx() as cnx:
+        with self.user_groups_session('users') as cnx:
             plan = self._prepare_plan(cnx, 'Any ETN,COUNT(X) GROUPBY ETN '
                                       'WHERE X is ET, ET name ETN')
             union = plan.rqlst
@@ -241,8 +240,7 @@
             self.assertEqual(solutions, [{'X': 'Basket', 'ET': 'CWEType', 'ETN': 'String'}])
 
     def test_preprocess_security_aggregat(self):
-        s = self.user_groups_session('users')
-        with s.new_cnx() as cnx:
+        with self.user_groups_session('users') as cnx:
             plan = self._prepare_plan(cnx, 'Any MAX(X)')
             union = plan.rqlst
             plan.preprocess(union)
@@ -254,13 +252,13 @@
                               ['MAX(X)'])
 
     def test_preprocess_nonregr(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any S ORDERBY SI WHERE NOT S ecrit_par O, S para SI')
             self.assertEqual(len(rqlst.solutions), 1)
 
     def test_build_description(self):
         # should return an empty result set
-        rset = self.qexecute('Any X WHERE X eid %(x)s', {'x': self.session.user.eid})
+        rset = self.qexecute('Any X WHERE X eid %(x)s', {'x': self.admin_access._user.eid})
         self.assertEqual(rset.description[0][0], 'CWUser')
         rset = self.qexecute('Any 1')
         self.assertEqual(rset.description[0][0], 'Int')
@@ -289,10 +287,9 @@
         self.assertEqual(rset.description[0][0], 'String')
 
     def test_build_descr1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rset = cnx.execute('(Any U,L WHERE U login L) UNION '
                                '(Any G,N WHERE G name N, G is CWGroup)')
-            # rset.req = self.session
             orig_length = len(rset)
             rset.rows[0][0] = 9999999
             description = manual_build_descr(cnx, rset.syntax_tree(), None, rset.rows)
@@ -493,7 +490,7 @@
         rset = self.qexecute('DISTINCT Any G WHERE U? in_group G')
         self.assertEqual(len(rset), 4)
         rset = self.qexecute('DISTINCT Any G WHERE U? in_group G, U eid %(x)s',
-                            {'x': self.session.user.eid})
+                            {'x': self.admin_access._user.eid})
         self.assertEqual(len(rset), 4)
 
     def test_select_ambigous_outer_join(self):
@@ -687,7 +684,7 @@
         self.assertEqual(rset.rows[0][0], 12)
 
 ##     def test_select_simplified(self):
-##         ueid = self.session.user.eid
+##         ueid = self.admin_access._user.eid
 ##         rset = self.qexecute('Any L WHERE %s login L'%ueid)
 ##         self.assertEqual(rset.rows[0][0], 'admin')
 ##         rset = self.qexecute('Any L WHERE %(x)s login L', {'x':ueid})
@@ -840,7 +837,7 @@
 
     def test_select_explicit_eid(self):
         rset = self.qexecute('Any X,E WHERE X owned_by U, X eid E, U eid %(u)s',
-                             {'u': self.session.user.eid})
+                             {'u': self.admin_access._user.eid})
         self.assertTrue(rset)
         self.assertEqual(rset.description[0][1], 'Int')
 
@@ -891,7 +888,7 @@
                                                            'Password', 'String',
                                                            'TZDatetime', 'TZTime',
                                                            'Time'])
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             cnx.create_entity('Personne', nom=u'louis', test=True)
             self.assertEqual(len(cnx.execute('Any X WHERE X test %(val)s', {'val': True})), 1)
             self.assertEqual(len(cnx.execute('Any X WHERE X test TRUE')), 1)
@@ -936,7 +933,7 @@
                      '(Any N,COUNT(X) GROUPBY N ORDERBY 2 WHERE X login N)')
 
     def test_select_union_aggregat_independant_group(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             cnx.execute('INSERT State X: X name "hop"')
             cnx.execute('INSERT State X: X name "hop"')
             cnx.execute('INSERT Transition X: X name "hop"')
@@ -1149,7 +1146,7 @@
 
         self.assertRaises(QueryError,
                           self.qexecute,
-                          "INSERT CWUser X: X login 'toto', X eid %s" % cnx.user(self.session).eid)
+                          "INSERT CWUser X: X login 'toto', X eid %s" % cnx.user.eid)
 
     def test_insertion_description_with_where(self):
         rset = self.qexecute('INSERT CWUser E, EmailAddress EM: E login "X", E upassword "X", '
@@ -1185,8 +1182,7 @@
         self.assertEqual(len(rset.rows), 0, rset.rows)
 
     def test_delete_3(self):
-        s = self.user_groups_session('users')
-        with s.new_cnx() as cnx:
+        with self.user_groups_session('users') as cnx:
             peid, = self.o.execute(cnx, "INSERT Personne P: P nom 'toto'")[0]
             seid, = self.o.execute(cnx, "INSERT Societe S: S nom 'logilab'")[0]
             self.o.execute(cnx, "SET P travaille S")
@@ -1224,7 +1220,7 @@
         eeid, = self.qexecute('INSERT Email X: X messageid "<1234>", X subject "test", '
                               'X sender Y, X recipients Y WHERE Y is EmailAddress')[0]
         self.qexecute("DELETE Email X")
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             sqlc = cnx.cnxset.cu
             sqlc.execute('SELECT * FROM recipients_relation')
             self.assertEqual(len(sqlc.fetchall()), 0)
@@ -1294,7 +1290,7 @@
         self.assertEqual(self.qexecute('Any X WHERE X nom "tutu"').rows, [[peid2]])
 
     def test_update_multiple2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             ueid = cnx.execute("INSERT CWUser X: X login 'bob', X upassword 'toto'")[0][0]
             peid1 = cnx.execute("INSERT Personne Y: Y nom 'turlu'")[0][0]
             peid2 = cnx.execute("INSERT Personne Y: Y nom 'tutu'")[0][0]
@@ -1342,7 +1338,7 @@
                           "WHERE X is Personne")
         self.assertRaises(QueryError,
                           self.qexecute,
-                          "SET X login 'tutu', X eid %s" % cnx.user(self.session).eid)
+                          "SET X login 'tutu', X eid %s" % cnx.user.eid)
 
 
     # HAVING on write queries test #############################################
@@ -1375,7 +1371,7 @@
         self.assertEqual(rset.description, [('CWUser',)])
         self.assertRaises(Unauthorized,
                           self.qexecute, "Any P WHERE X is CWUser, X login 'bob', X upassword P")
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             cursor = cnx.cnxset.cu
             cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
                            % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
@@ -1387,7 +1383,7 @@
         self.assertEqual(rset.description, [('CWUser',)])
 
     def test_update_upassword(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rset = cnx.execute("INSERT CWUser X: X login 'bob', X upassword %(pwd)s",
                                {'pwd': 'toto'})
             self.assertEqual(rset.description[0][0], 'CWUser')
@@ -1495,7 +1491,7 @@
                         'creation_date': '2000/07/03 11:00'})
         rset = self.qexecute('Any lower(N) ORDERBY LOWER(N) WHERE X is Tag, X name N,'
                             'X owned_by U, U eid %(x)s',
-                            {'x':self.session.user.eid})
+                            {'x':self.admin_access._user.eid})
         self.assertEqual(rset.rows, [[u'\xe9name0']])
 
     def test_nonregr_description(self):
@@ -1543,7 +1539,7 @@
         self.qexecute('Any X ORDERBY D DESC WHERE X creation_date D')
 
     def test_nonregr_extra_joins(self):
-        ueid = self.session.user.eid
+        ueid = self.admin_access._user.eid
         teid1 = self.qexecute("INSERT Folder X: X name 'folder1'")[0][0]
         teid2 = self.qexecute("INSERT Folder X: X name 'folder2'")[0][0]
         neid1 = self.qexecute("INSERT Note X: X para 'note1'")[0][0]
--- a/cubicweb/server/test/unittest_repository.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_repository.py	Mon Mar 20 10:28:01 2017 +0100
@@ -77,21 +77,21 @@
             self.assertFalse(cnx.execute('Any X WHERE NOT X cw_source S'))
 
     def test_connect(self):
-        session = self.repo.new_session(self.admlogin, password=self.admpassword)
-        self.assertTrue(session.sessionid)
-        session.close()
-        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)
-        self.assertRaises(AuthenticationError,
-                          self.repo.connect, self.admlogin)
-        self.assertRaises(AuthenticationError,
-                          self.repo.connect, None)
+        with self.repo.internal_cnx() as cnx:
+            self.assertTrue(
+                self.repo.authenticate_user(cnx, self.admlogin, password=self.admpassword))
+            self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+                              cnx, self.admlogin, password='nimportnawak')
+            self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+                              cnx, self.admlogin, password='')
+            self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+                              cnx, self.admlogin, password=None)
+            self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+                              cnx, None, password=None)
+            self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+                              cnx, self.admlogin)
+            self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+                              cnx, None)
 
     def test_login_upassword_accent(self):
         with self.admin_access.repo_cnx() as cnx:
@@ -99,10 +99,8 @@
                         'X in_group G WHERE G name "users"',
                         {'login': u"barnabé", 'passwd': u"héhéhé".encode('UTF8')})
             cnx.commit()
-        repo = self.repo
-        session = repo.new_session(u"barnabé", password=u"héhéhé".encode('UTF8'))
-        self.assertTrue(session.sessionid)
-        session.close()
+            repo = self.repo
+            self.assertTrue(repo.authenticate_user(cnx, u"barnabé", password=u"héhéhé".encode('UTF8')))
 
     def test_rollback_on_execute_validation_error(self):
         class ValidationErrorAfterHook(Hook):
@@ -142,14 +140,6 @@
                 cnx.rollback()
                 self.assertFalse(cnx.execute('Any X WHERE X is CWGroup, X name "toto"'))
 
-
-    def test_close(self):
-        repo = self.repo
-        session = repo.new_session(self.admlogin, password=self.admpassword)
-        self.assertTrue(session.sessionid)
-        session.close()
-
-
     def test_initial_schema(self):
         schema = self.repo.schema
         # check order of attributes is respected
@@ -193,13 +183,6 @@
         ownedby = schema.rschema('owned_by')
         self.assertEqual(ownedby.objects('CWEType'), ('CWUser',))
 
-    def test_internal_api(self):
-        repo = self.repo
-        session = repo.new_session(self.admlogin, password=self.admpassword)
-        with session.new_cnx() as cnx:
-            self.assertEqual(repo.type_from_eid(2, cnx), 'CWGroup')
-        session.close()
-
     def test_public_api(self):
         self.assertEqual(self.repo.get_schema(), self.repo.schema)
         self.assertEqual(self.repo.source_defs(), {'system': {'type': 'native',
--- a/cubicweb/server/test/unittest_rqlannotation.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_rqlannotation.py	Mon Mar 20 10:28:01 2017 +0100
@@ -40,7 +40,7 @@
         pass
 
     def test_0_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any SEN,RN,OEN WHERE X from_entity SE, '
                                   'SE eid 44, X relation_type R, R eid 139, '
                                   'X to_entity OE, OE eid 42, R name RN, SE name SEN, '
@@ -53,14 +53,14 @@
             self.assertEqual(rqlst.defined_vars['R'].stinfo['attrvar'], None)
 
     def test_0_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any O WHERE NOT S ecrit_par O, S eid 1, '
                                   'S inline1 P, O inline2 P')
             self.assertEqual(rqlst.defined_vars['P']._q_invariant, True)
             self.assertEqual(rqlst.defined_vars['O'].stinfo['attrvar'], None)
 
     def test_0_4(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any A,B,C WHERE A eid 12,A comment B, '
                                   'A ?wf_info_for C')
             self.assertEqual(rqlst.defined_vars['A']._q_invariant, False)
@@ -71,18 +71,18 @@
                                                {'A': 'TrInfo', 'B': 'String', 'C': 'Note'}])
 
     def test_0_5(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any P WHERE N ecrit_par P, N eid 0')
             self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['P']._q_invariant, True)
 
     def test_0_6(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any P WHERE NOT N ecrit_par P, N eid 512')
             self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
 
     def test_0_7(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Personne X,Y where X nom NX, '
                                   'Y nom NX, X eid XE, not Y eid XE')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
@@ -90,25 +90,25 @@
             self.assertTrue(rqlst.defined_vars['XE'].stinfo['attrvar'])
 
     def test_0_8(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any P WHERE X eid 0, NOT X connait P')
             self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
             self.assertEqual(len(rqlst.solutions), 1, rqlst.solutions)
 
     def test_0_10(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE X concerne Y, Y is Note')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_0_11(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE X todo_by Y, X is Affaire')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_0_12(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Personne P WHERE P concerne A, '
                                   'A concerne S, S nom "Logilab"')
             self.assertEqual(rqlst.defined_vars['P']._q_invariant, True)
@@ -116,31 +116,31 @@
             self.assertEqual(rqlst.defined_vars['S']._q_invariant, False)
 
     def test_1_0(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X,Y WHERE X created_by Y, '
                                   'X eid 5, NOT Y eid 6')
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_1_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X,Y WHERE X created_by Y, X eid 5, '
                                   'NOT Y eid IN (6,7)')
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE X identity Y, Y eid 1')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_7(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Personne X,Y where X nom NX, Y nom NX, '
                                   'X eid XE, not Y eid XE')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_8(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             # DISTINCT Any P WHERE P require_group %(g)s,
             # NOT %(u)s has_group_permission P, P is CWPermission
             rqlst = self._prepare(cnx, 'DISTINCT Any X WHERE A concerne X, '
@@ -149,69 +149,69 @@
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_diff_scope_identity_deamb(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE X concerne Y, Y is Note, '
                                   'EXISTS(Y identity Z, Z migrated_from N)')
             self.assertEqual(rqlst.defined_vars['Z']._q_invariant, True)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_optional_inlined(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X,S where X from_state S?')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['S']._q_invariant, True)
 
     def test_optional_inlined_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any N,A WHERE N? inline1 A')
             self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['A']._q_invariant, False)
 
     def test_optional_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X,S WHERE X travaille S?')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['S']._q_invariant, True)
 
     def test_greater_eid(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE X eid > 5')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_greater_eid_typed(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE X eid > 5, X is Note')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_max_eid(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any MAX(X)')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_max_eid_typed(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any MAX(X) WHERE X is Note')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_all_entities(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_all_typed_entity(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE X is Note')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_has_text_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE X has_text "toto tata"')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
             self.assertEqual(rqlst.defined_vars['X'].stinfo['principal'].r_type,
                              'has_text')
 
     def test_has_text_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE X is Personne, '
                                   'X has_text "coucou"')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
@@ -219,7 +219,7 @@
                              'has_text')
 
     def test_not_relation_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             # P can't be invariant since deambiguification caused by "NOT X require_permission P"
             # is not considered by generated sql (NOT EXISTS(...))
             rqlst = self._prepare(cnx, 'Any P,G WHERE P require_group G, '
@@ -229,134 +229,134 @@
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_not_relation_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'TrInfo X WHERE X eid 2, '
                                   'NOT X from_state Y, Y is State')
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_not_relation_3(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X, Y WHERE X eid 1, Y eid in (2, 3)')
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_relation_4_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Note X WHERE NOT Y evaluee X')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_not_relation_4_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE NOT Y evaluee X')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_not_relation_4_3(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any Y WHERE NOT Y evaluee X')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_relation_4_4(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE NOT Y evaluee X, Y is CWUser')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_relation_4_5(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE NOT Y evaluee X, '
                                   'Y eid %s, X is Note' % self.ueid)
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.solutions, [{'X': 'Note'}])
 
     def test_not_relation_5_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X,Y WHERE X name "CWGroup", '
                                   'Y eid IN(1, 2, 3), NOT X read_permission Y')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_relation_5_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'DISTINCT Any X,Y WHERE X name "CWGroup", '
                                   'Y eid IN(1, 2, 3), NOT X read_permission Y')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_relation_6(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Personne P where NOT P concerne A')
             self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['A']._q_invariant, True)
 
     def test_not_relation_7(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any K,V WHERE P is CWProperty, '
                                   'P pkey K, P value V, NOT P for_user U')
             self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['U']._q_invariant, True)
 
     def test_exists_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any U WHERE U eid IN (1,2), EXISTS(X owned_by U)')
             self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_exists_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any U WHERE EXISTS(U eid IN (1,2), X owned_by U)')
             self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_exists_3(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any U WHERE EXISTS(X owned_by U, X bookmarked_by U)')
             self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_exists_4(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X,Y WHERE X name "CWGroup", '
                                   'Y eid IN(1, 2, 3), EXISTS(X read_permission Y)')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_exists_5(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'DISTINCT Any X,Y WHERE X name "CWGroup", '
                                   'Y eid IN(1, 2, 3), EXISTS(X read_permission Y)')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_not_exists_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any U WHERE NOT EXISTS(X owned_by U, '
                                   'X bookmarked_by U)')
             self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_not_exists_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X,Y WHERE X name "CWGroup", '
                                   'Y eid IN(1, 2, 3), NOT EXISTS(X read_permission Y)')
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_not_exists_distinct_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'DISTINCT Any X,Y WHERE X name "CWGroup", '
                                   'Y eid IN(1, 2, 3), NOT EXISTS(X read_permission Y)')
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
 
     def test_or_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE X concerne B OR '
                                   'C concerne X, B eid 12, C eid 13')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
 
     def test_or_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any X WHERE X created_by U, X concerne B OR '
                                   'C concerne X, B eid 12, C eid 13')
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
@@ -364,14 +364,14 @@
             self.assertEqual(rqlst.defined_vars['X'].stinfo['principal'].r_type, 'created_by')
 
     def test_or_3(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any N WHERE A evaluee N or EXISTS(N todo_by U)')
             self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['A']._q_invariant, True)
             self.assertEqual(rqlst.defined_vars['U']._q_invariant, True)
 
     def test_or_exists_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             # query generated by security rewriting
             rqlst = self._prepare(cnx, 'DISTINCT Any A,S WHERE A is Affaire, S nom "chouette", '
                                   'S is IN(Division, Societe, SubDivision),'
@@ -388,7 +388,7 @@
             self.assertEqual(rqlst.defined_vars['S']._q_invariant, False)
 
     def test_or_exists_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any U WHERE EXISTS(U in_group G, G name "managers") OR '
                                   'EXISTS(X owned_by U, X bookmarked_by U)')
             self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
@@ -396,7 +396,7 @@
             self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
 
     def test_or_exists_3(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any COUNT(S),CS GROUPBY CS ORDERBY 1 DESC LIMIT 10 '
                                   'WHERE C is Societe, S concerne C, C nom CS, '
                                   '(EXISTS(S owned_by D)) '
@@ -409,14 +409,14 @@
             self.assertEqual(rqlst.defined_vars['S']._q_invariant, True)
 
     def test_nonregr_ambiguity(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Note N WHERE N attachment F')
             # N may be an image as well, not invariant
             self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
             self.assertEqual(rqlst.defined_vars['F']._q_invariant, True)
 
     def test_nonregr_ambiguity_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Any S,SN WHERE X has_text "tot", '
                                   'X in_state S, S name SN, X is CWUser')
             # X use has_text but should not be invariant as ambiguous, and has_text
@@ -425,19 +425,19 @@
             self.assertEqual(rqlst.defined_vars['S']._q_invariant, False)
 
     def test_remove_from_deleted_source_1(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Note X WHERE X eid 999998, NOT X cw_source Y')
             self.assertNotIn('X', rqlst.defined_vars)  # simplified
             self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
 
     def test_remove_from_deleted_source_2(self):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, 'Note X WHERE X eid IN (999998, 999999), NOT X cw_source Y')
             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):
-        with self.session.new_cnx() as cnx:
+        with self.admin_access.cnx() as cnx:
             rqlst = self._prepare(cnx, '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)
--- a/cubicweb/server/test/unittest_security.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_security.py	Mon Mar 20 10:28:01 2017 +0100
@@ -85,15 +85,13 @@
             oldhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser "
                                      "WHERE cw_login = 'oldpassword'").fetchone()[0]
             oldhash = self.repo.system_source.binary_to_str(oldhash)
-            session = self.repo.new_session('oldpassword', password='oldpassword')
-            session.close()
+            self.repo.authenticate_user(cnx, 'oldpassword', password='oldpassword')
             newhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser "
                                      "WHERE cw_login = 'oldpassword'").fetchone()[0]
             newhash = self.repo.system_source.binary_to_str(newhash)
             self.assertNotEqual(oldhash, newhash)
             self.assertTrue(newhash.startswith(b'$6$'))
-            session = self.repo.new_session('oldpassword', password='oldpassword')
-            session.close()
+            self.repo.authenticate_user(cnx, 'oldpassword', password='oldpassword')
             newnewhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE "
                                         "cw_login = 'oldpassword'").fetchone()[0]
             newnewhash = self.repo.system_source.binary_to_str(newnewhash)
@@ -305,8 +303,8 @@
             cnx.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
                        {'x': ueid, 'passwd': b'newpwd'})
             cnx.commit()
-        session = self.repo.new_session('user', password='newpwd')
-        session.close()
+        with self.repo.internal_cnx() as cnx:
+            self.repo.authenticate_user(cnx, 'user', password='newpwd')
 
     def test_user_cant_change_other_upassword(self):
         with self.admin_access.repo_cnx() as cnx:
@@ -523,9 +521,9 @@
         with self.temporary_permissions(Division={'read': ('managers',
                                                            ERQLExpression('X owned_by U'))}):
             with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+                rqlst = self.repo.vreg.rqlhelper.parse('Any X WHERE X is_instance_of Societe')
+                self.repo.vreg.solutions(cnx, rqlst, {})
                 querier = cnx.repo.querier
-                rqlst = querier.parse('Any X WHERE X is_instance_of Societe')
-                querier.solutions(cnx, rqlst, {})
                 querier._annotate(rqlst)
                 plan = querier.plan_factory(rqlst, {}, cnx)
                 plan.preprocess(rqlst)
--- a/cubicweb/server/test/unittest_serverctl.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_serverctl.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,16 +1,27 @@
 import os.path as osp
 import shutil
 
+from mock import patch
+
 from cubicweb import ExecutionError
 from cubicweb.devtools import testlib, ApptestConfiguration
-from cubicweb.server.serverctl import _local_dump, DBDumpCommand, SynchronizeSourceCommand
+from cubicweb.server.serverctl import (
+    DBDumpCommand,
+    RepositorySchedulerCommand,
+    SynchronizeSourceCommand,
+)
 from cubicweb.server.serverconfig import ServerConfiguration
 
+
 class ServerCTLTC(testlib.CubicWebTC):
+
     def setUp(self):
         super(ServerCTLTC, self).setUp()
         self.orig_config_for = ServerConfiguration.config_for
-        config_for = lambda appid: ApptestConfiguration(appid, __file__)
+
+        def config_for(appid):
+            return ApptestConfiguration(appid, __file__)
+
         ServerConfiguration.config_for = staticmethod(config_for)
 
     def tearDown(self):
@@ -21,6 +32,26 @@
         DBDumpCommand(None).run([self.appid])
         shutil.rmtree(osp.join(self.config.apphome, 'backup'))
 
+    def test_scheduler(self):
+        cmd = RepositorySchedulerCommand(None)
+        with patch('sched.scheduler.run',
+                   side_effect=RuntimeError('boom')) as patched_run:
+            with self.assertRaises(RuntimeError) as exc_cm:
+                with self.assertLogs('cubicweb.repository', level='INFO') as log_cm:
+                    cmd.run([self.appid])
+        # make sure repository scheduler started
+        scheduler_start_message = (
+            'INFO:cubicweb.repository:starting repository scheduler with '
+            'tasks: update_feeds, expire_dataimports'
+        )
+        self.assertIn(scheduler_start_message, log_cm.output)
+        # and that scheduler's run method got called
+        self.assertIn('boom', str(exc_cm.exception))
+        patched_run.assert_called_once_with()
+        # make sure repository's shutdown method got called
+        repo_shutdown_message = 'INFO:cubicweb.repository:shutting down repository'
+        self.assertIn(repo_shutdown_message, log_cm.output)
+
     def test_source_sync(self):
         with self.admin_access.repo_cnx() as cnx:
             cnx.create_entity('CWSource', name=u'success_feed', type=u'datafeed',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/unittest_session.py	Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,54 @@
+# copyright 2017 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 cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.server import session
+
+
+class HooksControlTC(CubicWebTC):
+
+    def test_hooks_control(self):
+        with self.admin_access.repo_cnx() as cnx:
+            self.assertEqual(cnx._hooks_mode, session.HOOKS_ALLOW_ALL)
+            self.assertEqual(cnx._hooks_categories, set())
+
+            with cnx.deny_all_hooks_but('metadata'):
+                self.assertEqual(cnx._hooks_mode, session.HOOKS_DENY_ALL)
+                self.assertEqual(cnx._hooks_categories, set(['metadata']))
+
+                with cnx.deny_all_hooks_but():
+                    self.assertEqual(cnx._hooks_categories, set())
+                self.assertEqual(cnx._hooks_categories, set(['metadata']))
+
+                with cnx.deny_all_hooks_but('integrity'):
+                    self.assertEqual(cnx._hooks_categories, set(['integrity']))
+                self.assertEqual(cnx._hooks_categories, set(['metadata']))
+
+                with cnx.allow_all_hooks_but('integrity'):
+                    self.assertEqual(cnx._hooks_mode, session.HOOKS_ALLOW_ALL)
+                    self.assertEqual(cnx._hooks_categories, set(['integrity']))
+                self.assertEqual(cnx._hooks_mode, session.HOOKS_DENY_ALL)
+                self.assertEqual(cnx._hooks_categories, set(['metadata']))
+
+            self.assertEqual(cnx._hooks_mode, session.HOOKS_ALLOW_ALL)
+            self.assertEqual(cnx._hooks_categories, set())
+
+
+if __name__ == '__main__':
+    import unittest
+    unittest.main()
--- a/cubicweb/server/test/unittest_utils.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_utils.py	Mon Mar 20 10:28:01 2017 +0100
@@ -15,29 +15,54 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
+"""Tests for cubicweb.server.utils module."""
 
-"""
-from logilab.common.testlib import TestCase, unittest_main
-
+from cubicweb.devtools import testlib
 from cubicweb.server import utils
 
-class UtilsTC(TestCase):
+
+class UtilsTC(testlib.BaseTestCase):
+
     def test_crypt(self):
         for hash in (
-            utils.crypt_password('xxx'), # default sha512
-            b'ab$5UsKFxRKKN.d8iBIFBnQ80', # custom md5
-            b'ab4Vlm81ZUHlg', # DES
-            ):
+            utils.crypt_password('xxx'),  # default sha512
+            b'ab$5UsKFxRKKN.d8iBIFBnQ80',  # custom md5
+            b'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', hash.decode('ascii')), hash.decode('ascii'))
+            self.assertEqual(utils.crypt_password(u'xxx', hash.decode('ascii')),
+                             hash.decode('ascii'))
             self.assertEqual(utils.crypt_password('yyy', hash), b'')
 
         # accept any password for empty hashes (is it a good idea?)
         self.assertEqual(utils.crypt_password('xxx', ''), '')
         self.assertEqual(utils.crypt_password('yyy', ''), '')
 
+    def test_schedule_periodic_task(self):
+        scheduler = utils.scheduler()
+        this = []
+
+        def fill_this(x):
+            this.append(x)
+            if len(this) > 2:
+                raise SystemExit()
+            elif len(this) > 1:
+                raise RuntimeError()
+
+        event = utils.schedule_periodic_task(scheduler, 0.01, fill_this, 1)
+        self.assertEqual(event.action.__name__, 'fill_this')
+        self.assertEqual(len(scheduler.queue), 1)
+
+        with self.assertLogs('cubicweb.scheduler', level='ERROR') as cm:
+            scheduler.run()
+        self.assertEqual(this, [1] * 3)
+        self.assertEqual(len(cm.output), 2)
+        self.assertIn('Unhandled exception in periodic task "fill_this"',
+                      cm.output[0])
+        self.assertIn('"fill_this" not re-scheduled', cm.output[1])
+
 
 if __name__ == '__main__':
-    unittest_main()
+    import unittest
+    unittest.main()
--- a/cubicweb/server/utils.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/utils.py	Mon Mar 20 10:28:01 2017 +0100
@@ -19,13 +19,14 @@
 from __future__ import print_function
 
 
-
+from functools import wraps
+import sched
 import sys
 import logging
-from threading import Timer, Thread
+from threading import Thread
 from getpass import getpass
 
-from six import PY2, text_type
+from six import PY2
 from six.moves import input
 
 from passlib.utils import handlers as uh, to_hash_str
@@ -58,26 +59,29 @@
         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 salt is None:
-        return _CRYPTO_CTX.encrypt(passwd).encode('ascii')
+        return _CRYPTO_CTX.hash(passwd).encode('ascii')
     # 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
+    except ValueError:  # e.g. couldn't identify hash
         pass
     # wrong password
     return b''
 
+
 @deprecated('[3.22] no more necessary, directly get eschema.eid')
 def eschema_eid(cnx, eschema):
     """get eid of the CWEType entity for the given yams type.
@@ -92,6 +96,7 @@
 DEFAULT_MSG = 'we need a manager connection on the repository \
 (the server doesn\'t have to run, even should better not)'
 
+
 def manager_userpasswd(user=None, msg=DEFAULT_MSG, confirm=False,
                        passwdmsg='password'):
     if not user:
@@ -113,7 +118,51 @@
     return user, passwd
 
 
+if PY2:
+    import time  # noqa
+
+    class scheduler(sched.scheduler):
+        """Python2 version of sched.scheduler that matches Python3 API."""
+
+        def __init__(self, **kwargs):
+            kwargs.setdefault('timefunc', time.time)
+            kwargs.setdefault('delayfunc', time.sleep)
+            # sched.scheduler is an old-style class.
+            sched.scheduler.__init__(self, **kwargs)
+
+else:
+    scheduler = sched.scheduler
+
+
+def schedule_periodic_task(scheduler, interval, func, *args):
+    """Enter a task with `func(*args)` as a periodic event in `scheduler`
+    executing at `interval` seconds. Once executed, the task would re-schedule
+    itself unless a BaseException got raised.
+    """
+    @wraps(func)
+    def task(*args):
+        restart = True
+        try:
+            func(*args)
+        except Exception:
+            logger = logging.getLogger('cubicweb.scheduler')
+            logger.exception('Unhandled exception in periodic task "%s"',
+                             func.__name__)
+        except BaseException as exc:
+            logger = logging.getLogger('cubicweb.scheduler')
+            logger.error('periodic task "%s" not re-scheduled due to %r',
+                         func.__name__, exc)
+            restart = False
+        finally:
+            if restart:
+                scheduler.enter(interval, 1, task, argument=args)
+
+    return scheduler.enter(interval, 1, task, argument=args)
+
+
 _MARKER = object()
+
+
 def func_name(func):
     name = getattr(func, '__name__', _MARKER)
     if name is _MARKER:
@@ -122,46 +171,6 @@
         name = repr(func)
     return name
 
-class LoopTask(object):
-    """threaded task restarting itself once executed"""
-    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._tasks_manager = tasks_manager
-        self.interval = interval
-        def auto_restart_func(self=self, func=func, args=args):
-            restart = True
-            try:
-                func(*args)
-            except Exception:
-                logger = logging.getLogger('cubicweb.repository')
-                logger.exception('Unhandled exception in LoopTask %s', self.name)
-                raise
-            except BaseException:
-                restart = False
-            finally:
-                if restart and tasks_manager.running:
-                    self.start()
-        self.func = auto_restart_func
-        self.name = func_name(func)
-
-    def __str__(self):
-        return '%s (%s seconds)' % (self.name, self.interval)
-
-    def start(self):
-        self._t = Timer(self.interval, self.func)
-        self._t.setName('%s-%s[%d]' % (self._t.getName(), self.name, self.interval))
-        self._t.start()
-
-    def cancel(self):
-        self._t.cancel()
-
-    def join(self):
-        if self._t.isAlive():
-            self._t.join()
-
 
 class RepoThread(Thread):
     """subclass of thread so it auto remove itself from a given list once
@@ -188,56 +197,3 @@
 
     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.
-
-        If interval is negative, no looping task is registered.
-        """
-        if interval < 0:
-            self.debug('looping task %s ignored due to interval %f < 0',
-                       func_name(func), interval)
-            return
-        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/cubicweb/skeleton/DISTNAME.spec.tmpl	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/skeleton/DISTNAME.spec.tmpl	Mon Mar 20 10:28:01 2017 +0100
@@ -35,13 +35,10 @@
 
 %%install
 %%{__python} setup.py --quiet install --no-compile --prefix=%%{_prefix} --root="$RPM_BUILD_ROOT"
-# remove generated .egg-info file
-rm -rf $RPM_BUILD_ROOT/usr/lib/python*
-
 
 %%clean
 rm -rf $RPM_BUILD_ROOT
 
 %%files
 %%defattr(-, root, root)
-%%{_prefix}/share/cubicweb/cubes/*
+%%{_python_sitelib}/*
--- a/cubicweb/skeleton/MANIFEST.in.tmpl	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/skeleton/MANIFEST.in.tmpl	Mon Mar 20 10:28:01 2017 +0100
@@ -5,6 +5,6 @@
 recursive-include cubicweb_%(cubename)s/i18n *.po
 recursive-include cubicweb_%(cubename)s/wdoc *
 recursive-include test/data bootstrap_cubes *.py
-include tox.ini
+include *.ini
 recursive-include debian changelog compat control copyright rules
 include cubicweb-%(cubename)s.spec
--- a/cubicweb/skeleton/cubicweb_CUBENAME/__init__.py.tmpl	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/skeleton/cubicweb_CUBENAME/__init__.py.tmpl	Mon Mar 20 10:28:01 2017 +0100
@@ -2,3 +2,7 @@
 
 %(longdesc)s
 """
+
+
+def includeme(config):
+    pass
--- a/cubicweb/sobjects/notification.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/sobjects/notification.py	Mon Mar 20 10:28:01 2017 +0100
@@ -25,14 +25,12 @@
 from six import text_type
 
 from logilab.common.textutils import normalize_text
-from logilab.common.deprecation import class_renamed, class_moved, deprecated
 from logilab.common.registry import yes
 
-from cubicweb.entity import Entity
 from cubicweb.view import Component, EntityView
 from cubicweb.server.hook import SendMailOp
 from cubicweb.mail import construct_message_id, format_mail
-from cubicweb.server.session import Session, InternalManager
+from cubicweb.server.session import Connection, InternalManager
 
 
 class RecipientsFinder(Component):
@@ -54,7 +52,7 @@
         elif mode == 'default-dest-addrs':
             lang = self._cw.vreg.property_value('ui.language')
             dests = zip(self._cw.vreg.config['default-dest-addrs'], repeat(lang))
-        else: # mode == 'none'
+        else:  # mode == 'none'
             dests = []
         return dests
 
@@ -75,8 +73,8 @@
     msgid_timestamp = True
 
     # to be defined on concrete sub-classes
-    content = None # body of the mail
-    message = None # action verb of the subject
+    content = None  # body of the mail
+    message = None  # action verb of the subject
 
     # this is usually the method to call
     def render_and_send(self, **kwargs):
@@ -120,8 +118,7 @@
                 emailaddr = something.cw_adapt_to('IEmailable').get_email()
                 user = something
             # hi-jack self._cw to get a session for the returned user
-            session = Session(user, self._cw.repo)
-            with session.new_cnx() as cnx:
+            with Connection(self._cw.repo, user) as cnx:
                 self._cw = cnx
                 try:
                     # since the same view (eg self) may be called multiple time and we
@@ -169,7 +166,7 @@
 
     def format_section(self, attr, value):
         return '%(attr)s\n%(ul)s\n%(value)s\n' % {
-            'attr': attr, 'ul': '-'*len(attr), 'value': value}
+            'attr': attr, 'ul': '-' * len(attr), 'value': value}
 
     def subject(self):
         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
@@ -183,12 +180,12 @@
         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
         for key, val in kwargs.items():
             if val and isinstance(val, text_type) and val.strip():
-               kwargs[key] = self._cw._(val)
+                kwargs[key] = self._cw._(val)
         kwargs.update({'user': self.user_data['login'],
                        'eid': entity.eid,
                        'etype': entity.dc_type(),
-                       'url': entity.absolute_url(__secure__=True),
-                       'title': entity.dc_long_title(),})
+                       'url': entity.absolute_url(),
+                       'title': entity.dc_long_title()})
         return kwargs
 
 
@@ -250,8 +247,8 @@
 
     def subject(self):
         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
-        return  u'%s #%s (%s)' % (self._cw.__('New %s' % entity.e_schema),
-                                  entity.eid, self.user_data['login'])
+        return u'%s #%s (%s)' % (self._cw.__('New %s' % entity.e_schema),
+                                 entity.eid, self.user_data['login'])
 
 
 def format_value(value):
@@ -316,5 +313,5 @@
 
     def subject(self):
         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
-        return  u'%s #%s (%s)' % (self._cw.__('Updated %s' % entity.e_schema),
-                                  entity.eid, self.user_data['login'])
+        return u'%s #%s (%s)' % (self._cw.__('Updated %s' % entity.e_schema),
+                                 entity.eid, self.user_data['login'])
--- a/cubicweb/sobjects/services.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/sobjects/services.py	Mon Mar 20 10:28:01 2017 +0100
@@ -30,31 +30,28 @@
     resources usage.
     """
 
-    __regid__  = 'repo_stats'
+    __regid__ = 'repo_stats'
     __select__ = match_user_groups('managers', 'users')
 
     def call(self):
-        repo = self._cw.repo # Service are repo side only.
+        repo = self._cw.repo  # Service are repo side only.
         results = {}
         querier = repo.querier
         source = repo.system_source
         for size, maxsize, hits, misses, title in (
-            (len(querier._rql_cache), repo.config['rql-cache-size'],
-            querier.cache_hit, querier.cache_miss, 'rqlt_st'),
+            (len(querier.rql_cache), repo.config['rql-cache-size'],
+             querier.rql_cache.cache_hit, querier.rql_cache.cache_miss, 'rqlt_st'),
             (len(source._cache), repo.config['rql-cache-size'],
-            source.cache_hit, source.cache_miss, 'sql'),
-            ):
+             source.cache_hit, source.cache_miss, 'sql'),
+        ):
             results['%s_cache_size' % title] = {'size': size, 'maxsize': maxsize}
             results['%s_cache_hit' % title] = hits
             results['%s_cache_miss' % title] = misses
             results['%s_cache_hit_percent' % title] = (hits * 100) / (hits + misses)
         results['type_cache_size'] = len(repo._type_cache)
         results['sql_no_cache'] = repo.system_source.no_cache
-        results['nb_open_sessions'] = len(repo._sessions)
         results['nb_active_threads'] = threading.activeCount()
-        looping_tasks = repo._tasks_manager._looping_tasks
-        results['looping_tasks'] = [(t.name, t.interval) for t in looping_tasks]
-        results['available_cnxsets'] = repo._cnxsets_pool.qsize()
+        results['available_cnxsets'] = repo.cnxsets.qsize()
         results['threads'] = [t.name for t in threading.enumerate()]
         return results
 
@@ -64,15 +61,13 @@
     resources usage.
     """
 
-    __regid__  = 'repo_gc_stats'
+    __regid__ = 'repo_gc_stats'
     __select__ = match_user_groups('managers')
 
     def call(self, nmax=20):
         """Return a dictionary containing some statistics about the repository
         memory usage.
 
-        This is a public method, not requiring a session id.
-
         nmax is the max number of (most) referenced object returned as
         the 'referenced' result
         """
@@ -86,11 +81,6 @@
         lookupclasses = (AppObject,
                          Union, ResultSet,
                          CubicWebRequestBase)
-        try:
-            from cubicweb.server.session import Session, InternalSession
-            lookupclasses += (InternalSession, Session)
-        except ImportError:
-            pass  # no server part installed
 
         results = {}
         counters, ocounters, garbage = gc_info(lookupclasses,
--- a/cubicweb/test/data/views.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/data/views.py	Mon Mar 20 10:28:01 2017 +0100
@@ -15,12 +15,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/>.
-from cubicweb.web.views import xmlrss
-xmlrss.RSSIconBox.visible = True
-
 
 from cubicweb.predicates import match_user_groups
 from cubicweb.server import Service
+from cubicweb.web.views import xmlrss
+
+
+xmlrss.RSSIconBox.visible = True
 
 
 class TestService(Service):
--- a/cubicweb/test/data_schemareader/schema.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/data_schemareader/schema.py	Mon Mar 20 10:28:01 2017 +0100
@@ -11,6 +11,7 @@
         'CWSource', inlined=True, cardinality='1*', composite='object',
         __permissions__=RELATION_MANAGERS_PERMISSIONS)
 
+
 cw_for_source = CWSourceSchemaConfig.get_relation('cw_for_source')
 cw_for_source.__permissions__ = {'read': ('managers', 'users'),
                                  'add': ('managers',),
--- a/cubicweb/test/unittest_cubes.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_cubes.py	Mon Mar 20 10:28:01 2017 +0100
@@ -20,36 +20,36 @@
 from contextlib import contextmanager
 import os
 from os import path
-import shutil
 import sys
-import tempfile
-import unittest
 
 from six import PY2
 
 from cubicweb import _CubesImporter
 from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.devtools.testlib import TemporaryDirectory, TestCase
 
 
 @contextmanager
 def temp_cube():
-    tempdir = tempfile.mkdtemp()
-    try:
-        libdir = path.join(tempdir, 'libpython')
-        cubedir = path.join(libdir, 'cubicweb_foo')
-        os.makedirs(cubedir)
-        with open(path.join(cubedir, '__init__.py'), 'w') as f:
-            f.write('"""cubicweb_foo application package"""')
-        with open(path.join(cubedir, 'bar.py'), 'w') as f:
-            f.write('baz = 1')
-        sys.path.append(libdir)
-        yield cubedir
-    finally:
-        shutil.rmtree(tempdir)
-        sys.path.remove(libdir)
+    with TemporaryDirectory() as tempdir:
+        try:
+            libdir = path.join(tempdir, 'libpython')
+            cubedir = path.join(libdir, 'cubicweb_foo')
+            os.makedirs(cubedir)
+            check_code = ("import logging\n"
+                          "logging.getLogger('cubicweb_foo')"
+                          ".warn('imported %s', __name__)\n")
+            with open(path.join(cubedir, '__init__.py'), 'w') as f:
+                f.write("'cubicweb_foo application package'\n" + check_code)
+            with open(path.join(cubedir, 'bar.py'), 'w') as f:
+                f.write(check_code + 'baz = 1\n')
+            sys.path.append(libdir)
+            yield cubedir
+        finally:
+            sys.path.remove(libdir)
 
 
-class CubesImporterTC(unittest.TestCase):
+class CubesImporterTC(TestCase):
 
     def setUp(self):
         # During discovery, CubicWebConfiguration.cls_adjust_sys_path may be
@@ -95,6 +95,38 @@
             from cubes.foo import bar
             self.assertEqual(bar.baz, 1)
 
+    def test_reload_cube(self):
+        """reloading cubes twice should return the same module"""
+        CubicWebConfiguration.cls_adjust_sys_path()
+        import cubes
+        if PY2:
+            new = reload(cubes)
+        else:
+            import importlib
+            new = importlib.reload(cubes)
+        self.assertIs(new, cubes)
+
+    def test_no_double_import(self):
+        """Check new and legacy import the same module once"""
+        with temp_cube():
+            CubicWebConfiguration.cls_adjust_sys_path()
+            with self.assertLogs('cubicweb_foo', 'WARNING') as cm:
+                from cubes.foo import bar
+                from cubicweb_foo import bar as bar2
+                self.assertIs(bar, bar2)
+                self.assertIs(sys.modules['cubes.foo'],
+                              sys.modules['cubicweb_foo'])
+            self.assertEqual(cm.output, [
+                'WARNING:cubicweb_foo:imported cubicweb_foo',
+                # module __name__ for subpackage differ along python version
+                # for PY2 it's based on how the module was imported "from
+                # cubes.foo import bar" and for PY3 based on __name__ of parent
+                # module "cubicweb_foo". Not sure if it's an issue, but PY3
+                # behavior looks better.
+                'WARNING:cubicweb_foo:imported ' + (
+                    'cubes.foo.bar' if PY2 else 'cubicweb_foo.bar')
+            ])
+
     def test_import_legacy_cube(self):
         """Check that importing a legacy cube works when sys.path got adjusted.
         """
--- a/cubicweb/test/unittest_cwconfig.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_cwconfig.py	Mon Mar 20 10:28:01 2017 +0100
@@ -17,8 +17,12 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """cubicweb.cwconfig unit tests"""
 
+import contextlib
+import compileall
+import functools
 import sys
 import os
+import pkgutil
 from os.path import dirname, join, abspath
 from pkg_resources import EntryPoint, Distribution
 import unittest
@@ -31,7 +35,8 @@
 
 from cubicweb.devtools import ApptestConfiguration
 from cubicweb.devtools.testlib import BaseTestCase, TemporaryDirectory
-from cubicweb.cwconfig import _find_prefix
+from cubicweb.cwconfig import (
+    CubicWebConfiguration, _find_prefix, _expand_modname)
 
 
 def unabsolutize(path):
@@ -44,6 +49,50 @@
     raise Exception('duh? %s' % path)
 
 
+def templibdir(func):
+    """create a temporary directory and insert it in sys.path"""
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        with TemporaryDirectory() as libdir:
+            sys.path.insert(0, libdir)
+            try:
+                args = args + (libdir,)
+                return func(*args, **kwargs)
+            finally:
+                sys.path.remove(libdir)
+    return wrapper
+
+
+def create_filepath(filepath):
+    filedir = dirname(filepath)
+    if not os.path.exists(filedir):
+        os.makedirs(filedir)
+    with open(filepath, 'a'):
+        pass
+
+
+@contextlib.contextmanager
+def temp_config(appid, instance_dir, cubes_dir, cubes):
+    """context manager that create a config object with specified appid,
+    instance_dir, cubes_dir and cubes"""
+    cls = CubicWebConfiguration
+    old = (cls._INSTANCES_DIR, cls.CUBES_DIR, cls.CUBES_PATH,
+           sys.path[:], sys.meta_path[:])
+    old_modules = set(sys.modules)
+    try:
+        cls._INSTANCES_DIR, cls.CUBES_DIR, cls.CUBES_PATH = (
+            instance_dir, cubes_dir, [])
+        config = cls(appid)
+        config._cubes = cubes
+        config.adjust_sys_path()
+        yield config
+    finally:
+        (cls._INSTANCES_DIR, cls.CUBES_DIR, cls.CUBES_PATH,
+         sys.path[:], sys.meta_path[:]) = old
+        for module in set(sys.modules) - old_modules:
+            del sys.modules[module]
+
+
 class CubicWebConfigurationTC(BaseTestCase):
 
     @classmethod
@@ -78,9 +127,9 @@
     @patch('pkg_resources.iter_entry_points', side_effect=iter_entry_points)
     def test_available_cubes(self, mock_iter_entry_points):
         expected_cubes = [
-            'card', 'comment', 'cubicweb_comment', 'cubicweb_email', 'file',
-            'cubicweb_file', 'cubicweb_forge', 'localperms',
-            'cubicweb_mycube', 'tag',
+            'card', 'comment', 'email', 'file',
+            'forge', 'localperms',
+            'mycube', 'tag',
         ]
         self._test_available_cubes(expected_cubes)
         mock_iter_entry_points.assert_called_once_with(
@@ -129,17 +178,6 @@
         self.assertEqual(self.config.expand_cubes(('email', 'comment')),
                           ['email', 'comment', 'file'])
 
-    def test_appobjects_path(self):
-        path = [unabsolutize(p) for p in self.config.appobjects_path()]
-        self.assertEqual(path[0], 'entities')
-        self.assertCountEqual(path[1:4], ['web/views', 'sobjects', 'hooks'])
-        self.assertEqual(path[4], 'file/entities')
-        self.assertCountEqual(path[5:7],
-                              ['file/views.py', 'file/hooks'])
-        self.assertEqual(path[7], 'email/entities.py')
-        self.assertCountEqual(path[8:10],
-                              ['email/views', 'email/hooks.py'])
-        self.assertEqual(path[10:], ['test/data/entities.py', 'test/data/views.py'])
 
     def test_init_cubes_ignore_pyramid_cube(self):
         warning_msg = 'cubicweb-pyramid got integrated into CubicWeb'
@@ -203,7 +241,8 @@
         from cubes import mycube
         self.assertEqual(mycube.__path__, [join(self.custom_cubes_dir, 'mycube')])
         # file cube should be overriden by the one found in data/cubes
-        if sys.modules.pop('cubes.file', None) and PY3:
+        sys.modules.pop('cubes.file')
+        if hasattr(cubes, 'file'):
             del cubes.file
         from cubes import file
         self.assertEqual(file.__path__, [join(self.custom_cubes_dir, 'file')])
@@ -331,5 +370,150 @@
                 os.environ['VIRTUAL_ENV'] = venv
 
 
+class ModnamesTC(unittest.TestCase):
+
+    @templibdir
+    def test_expand_modnames(self, libdir):
+        tempdir = join(libdir, 'lib')
+        filepaths = [
+            join(tempdir, '__init__.py'),
+            join(tempdir, 'a.py'),
+            join(tempdir, 'b.py'),
+            join(tempdir, 'c.py'),
+            join(tempdir, 'b', '__init__.py'),
+            join(tempdir, 'b', 'a.py'),
+            join(tempdir, 'b', 'c.py'),
+            join(tempdir, 'b', 'd', '__init__.py'),
+            join(tempdir, 'e', 'e.py'),
+        ]
+        for filepath in filepaths:
+            create_filepath(filepath)
+        # not importable
+        self.assertEqual(list(_expand_modname('isnotimportable')), [])
+        # not a python package
+        self.assertEqual(list(_expand_modname('lib.e')), [])
+        self.assertEqual(list(_expand_modname('lib.a')), [
+            ('lib.a', join(tempdir, 'a.py')),
+        ])
+        # lib.b.d (subpackage) not to be imported
+        self.assertEqual(list(_expand_modname('lib.b')), [
+            ('lib.b', join(tempdir, 'b', '__init__.py')),
+            ('lib.b.a', join(tempdir, 'b', 'a.py')),
+            ('lib.b.c', join(tempdir, 'b', 'c.py')),
+        ])
+        self.assertEqual(list(_expand_modname('lib')), [
+            ('lib', join(tempdir, '__init__.py')),
+            ('lib.a', join(tempdir, 'a.py')),
+            ('lib.c', join(tempdir, 'c.py')),
+        ])
+        for source in (
+            join(tempdir, 'c.py'),
+            join(tempdir, 'b', 'c.py'),
+        ):
+            if not PY3:
+                # ensure pyc file exists.
+                # Doesn't required for PY3 since it create __pycache__
+                # directory and will not import if source file doesn't
+                # exists.
+                compileall.compile_file(source, force=True)
+                self.assertTrue(os.path.exists(source + 'c'))
+            # remove source file
+            os.remove(source)
+        self.assertEqual(list(_expand_modname('lib.c')), [])
+        self.assertEqual(list(_expand_modname('lib.b')), [
+            ('lib.b', join(tempdir, 'b', '__init__.py')),
+            ('lib.b.a', join(tempdir, 'b', 'a.py')),
+        ])
+        self.assertEqual(list(_expand_modname('lib')), [
+            ('lib', join(tempdir, '__init__.py')),
+            ('lib.a', join(tempdir, 'a.py')),
+        ])
+
+    @templibdir
+    def test_schema_modnames(self, libdir):
+        for filepath in (
+            join(libdir, 'schema.py'),
+            join(libdir, 'cubicweb_foo', '__init__.py'),
+            join(libdir, 'cubicweb_foo', 'schema', '__init__.py'),
+            join(libdir, 'cubicweb_foo', 'schema', 'a.py'),
+            join(libdir, 'cubicweb_foo', 'schema', 'b.py'),
+            join(libdir, 'cubes', '__init__.py'),
+            join(libdir, 'cubes', 'bar', '__init__.py'),
+            join(libdir, 'cubes', 'bar', 'schema.py'),
+            join(libdir, '_instance_dir', 'data1', 'schema.py'),
+            join(libdir, '_instance_dir', 'data2', 'noschema.py'),
+        ):
+            create_filepath(filepath)
+        expected = [
+            ('cubicweb', 'cubicweb.schemas.bootstrap'),
+            ('cubicweb', 'cubicweb.schemas.base'),
+            ('cubicweb', 'cubicweb.schemas.workflow'),
+            ('cubicweb', 'cubicweb.schemas.Bookmark'),
+            ('bar', 'cubes.bar.schema'),
+            ('foo', 'cubes.foo.schema'),
+            ('foo', 'cubes.foo.schema.a'),
+            ('foo', 'cubes.foo.schema.b'),
+        ]
+        # app has schema file
+        instance_dir, cubes_dir = (
+            join(libdir, '_instance_dir'), join(libdir, 'cubes'))
+        with temp_config('data1', instance_dir, cubes_dir,
+                         ('foo', 'bar')) as config:
+            self.assertEqual(pkgutil.find_loader('schema').get_filename(),
+                             join(libdir, '_instance_dir',
+                                  'data1', 'schema.py'))
+            self.assertEqual(config.schema_modnames(),
+                             expected + [('data', 'schema')])
+        # app doesn't have schema file
+        with temp_config('data2', instance_dir, cubes_dir,
+                         ('foo', 'bar')) as config:
+            self.assertEqual(pkgutil.find_loader('schema').get_filename(),
+                             join(libdir, 'schema.py'))
+            self.assertEqual(config.schema_modnames(), expected)
+
+    @templibdir
+    def test_appobjects_modnames(self, libdir):
+        for filepath in (
+            join(libdir, 'entities.py'),
+            join(libdir, 'cubicweb_foo', '__init__.py'),
+            join(libdir, 'cubicweb_foo', 'entities', '__init__.py'),
+            join(libdir, 'cubicweb_foo', 'entities', 'a.py'),
+            join(libdir, 'cubicweb_foo', 'hooks.py'),
+            join(libdir, 'cubes', '__init__.py'),
+            join(libdir, 'cubes', 'bar', '__init__.py'),
+            join(libdir, 'cubes', 'bar', 'hooks.py'),
+            join(libdir, '_instance_dir', 'data1', 'entities.py'),
+            join(libdir, '_instance_dir', 'data2', 'hooks.py'),
+        ):
+            create_filepath(filepath)
+        instance_dir, cubes_dir = (
+            join(libdir, '_instance_dir'), join(libdir, 'cubes'))
+        expected = [
+            'cubicweb.entities',
+            'cubicweb.entities.adapters',
+            'cubicweb.entities.authobjs',
+            'cubicweb.entities.lib',
+            'cubicweb.entities.schemaobjs',
+            'cubicweb.entities.sources',
+            'cubicweb.entities.wfobjs',
+            'cubes.bar.hooks',
+            'cubes.foo.entities',
+            'cubes.foo.entities.a',
+            'cubes.foo.hooks',
+        ]
+        # data1 has entities
+        with temp_config('data1', instance_dir, cubes_dir,
+                         ('foo', 'bar')) as config:
+            config.cube_appobject_path = set(['entities', 'hooks'])
+            self.assertEqual(config.appobjects_modnames(),
+                             expected + ['entities'])
+        # data2 has hooks
+        with temp_config('data2', instance_dir, cubes_dir,
+                         ('foo', 'bar')) as config:
+            config.cube_appobject_path = set(['entities', 'hooks'])
+            self.assertEqual(config.appobjects_modnames(),
+                             expected + ['hooks'])
+
+
 if __name__ == '__main__':
     unittest.main()
--- a/cubicweb/test/unittest_cwctl.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_cwctl.py	Mon Mar 20 10:28:01 2017 +0100
@@ -24,11 +24,10 @@
 from six import PY2
 
 from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.cwctl import ListCommand
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.migractions import ServerMigrationHelper
 
-CubicWebConfiguration.load_cwctl_plugins() # XXX necessary?
-
 
 class CubicWebCtlTC(unittest.TestCase):
 
@@ -40,9 +39,15 @@
         sys.stdout = sys.__stdout__
 
     def test_list(self):
-        from cubicweb.cwctl import ListCommand
         ListCommand(None).run([])
 
+    def test_list_configurations(self):
+        ListCommand(None).run(['configurations'])
+        configs = [l[2:].strip() for l in self.stream.getvalue().splitlines()
+                   if l.startswith('* ')]
+        self.assertIn('all-in-one', configs)
+        self.assertIn('pyramid', configs)
+
 
 class CubicWebShellTC(CubicWebTC):
 
--- a/cubicweb/test/unittest_entity.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_entity.py	Mon Mar 20 10:28:01 2017 +0100
@@ -182,10 +182,15 @@
                 req.create_entity('Tag', name=tag)
             req.execute('SET X tags Y WHERE X is Tag, Y is Personne')
             self.assertEqual(len(p.related('tags', 'object', limit=2)), 2)
+            self.assertFalse(p.cw_relation_cached('tags', 'object'))
             self.assertEqual(len(p.related('tags', 'object')), 4)
+            self.assertTrue(p.cw_relation_cached('tags', 'object'))
             p.cw_clear_all_caches()
+            self.assertFalse(p.cw_relation_cached('tags', 'object'))
             self.assertEqual(len(p.related('tags', 'object', entities=True, limit=2)), 2)
+            self.assertFalse(p.cw_relation_cached('tags', 'object'))
             self.assertEqual(len(p.related('tags', 'object', entities=True)), 4)
+            self.assertTrue(p.cw_relation_cached('tags', 'object'))
 
     def test_related_targettypes(self):
         with self.admin_access.web_request() as req:
--- a/cubicweb/test/unittest_repoapi.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_repoapi.py	Mon Mar 20 10:28:01 2017 +0100
@@ -56,7 +56,7 @@
         """Check that ClientConnection requires explicit open and close
         """
         access = self.admin_access
-        cltcnx = Connection(access._session)
+        cltcnx = Connection(access._repo, access._user)
         # connection not open yet
         with self.assertRaises(ProgrammingError):
             cltcnx.execute('Any X WHERE X is CWUser')
--- a/cubicweb/test/unittest_req.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_req.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -19,25 +19,25 @@
 from logilab.common.testlib import TestCase, unittest_main
 from cubicweb import ObjectNotFound
 from cubicweb.req import RequestSessionBase, FindEntityError
-from cubicweb.devtools import ApptestConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import Unauthorized
 
+
 class RequestTC(TestCase):
     def test_rebuild_url(self):
         rebuild_url = RequestSessionBase(None).rebuild_url
         self.assertEqual(rebuild_url('http://logilab.fr?__message=pouet', __message='hop'),
-                          'http://logilab.fr?__message=hop')
+                         'http://logilab.fr?__message=hop')
         self.assertEqual(rebuild_url('http://logilab.fr', __message='hop'),
-                          'http://logilab.fr?__message=hop')
+                         'http://logilab.fr?__message=hop')
         self.assertEqual(rebuild_url('http://logilab.fr?vid=index', __message='hop'),
-                          'http://logilab.fr?__message=hop&vid=index')
+                         'http://logilab.fr?__message=hop&vid=index')
 
     def test_build_url(self):
         req = RequestSessionBase(None)
-        req.from_controller = lambda : 'view'
+        req.from_controller = lambda: 'view'
         req.relative_path = lambda includeparams=True: None
-        req.base_url = lambda secure=None: 'http://testing.fr/cubicweb/'
+        req.base_url = lambda: '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')
@@ -49,8 +49,10 @@
         req = RequestSessionBase(None)
         self.assertEqual(req.ensure_ro_rql('Any X WHERE X is CWUser'), None)
         self.assertEqual(req.ensure_ro_rql('  Any X WHERE X is CWUser  '), None)
-        self.assertRaises(Unauthorized, req.ensure_ro_rql, 'SET X login "toto" WHERE X is CWUser')
-        self.assertRaises(Unauthorized, req.ensure_ro_rql, '   SET X login "toto" WHERE X is CWUser   ')
+        self.assertRaises(Unauthorized, req.ensure_ro_rql,
+                          'SET X login "toto" WHERE X is CWUser')
+        self.assertRaises(Unauthorized, req.ensure_ro_rql,
+                          '   SET X login "toto" WHERE X is CWUser   ')
 
 
 class RequestCWTC(CubicWebTC):
@@ -59,11 +61,15 @@
         base_url = self.config['base-url']
         with self.admin_access.repo_cnx() as session:
             self.assertEqual(session.base_url(), base_url)
-            assert 'https-url' not in self.config
-            self.assertEqual(session.base_url(secure=True), base_url)
-            secure_base_url = base_url.replace('http', 'https')
-            self.config.global_set_option('https-url', secure_base_url)
-            self.assertEqual(session.base_url(secure=True), secure_base_url)
+
+    def test_secure_deprecated(self):
+        with self.admin_access.cnx() as cnx:
+            with self.assertWarns(DeprecationWarning):
+                cnx.base_url(secure=True)
+            with self.assertRaises(TypeError):
+                cnx.base_url(thing=42)
+            with self.assertWarns(DeprecationWarning):
+                cnx.build_url('ah', __secure__='whatever')
 
     def test_view_catch_ex(self):
         with self.admin_access.web_request() as req:
--- a/cubicweb/test/unittest_rqlrewrite.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_rqlrewrite.py	Mon Mar 20 10:28:01 2017 +0100
@@ -215,6 +215,12 @@
         self.assertEqual(rqlst.as_string(),
                          u'Any A,AR,X,CD WHERE A concerne X?, A ref AR, A eid %(a)s WITH X,CD BEING (Any X,CD WHERE X creation_date CD, EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))')
 
+    def test_ambiguous_optional_same_exprs_constant(self):
+        rqlst = parse(u'Any A,AR,X WHERE A concerne X?, A ref AR, A eid %(a)s, X creation_date TODAY')
+        rewrite(rqlst, {('X', 'X'): ('X created_by U',),}, {'a': 3})
+        self.assertEqual(rqlst.as_string(),
+                         u'Any A,AR,X WHERE A concerne X?, A ref AR, A eid %(a)s WITH X BEING (Any X WHERE X creation_date TODAY, EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))')
+
     def test_optional_var_inlined(self):
         c1 = ('X require_permission P')
         c2 = ('X inlined_card O, O require_permission P')
@@ -506,9 +512,9 @@
         if args is None:
             args = {}
         querier = self.repo.querier
-        union = querier.parse(rql)
+        union = parse(rql) # self.vreg.parse(rql, annotate=True)
         with self.admin_access.repo_cnx() as cnx:
-            querier.solutions(cnx, union, args)
+            self.vreg.solutions(cnx, union, args)
             querier._annotate(union)
             plan = querier.plan_factory(union, args, cnx)
             plan.preprocess(union)
--- a/cubicweb/test/unittest_rset.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_rset.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,5 +1,5 @@
 # coding: utf-8
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -44,12 +44,12 @@
     def test_relations_description(self):
         """tests relations_description() function"""
         queries = {
-            'Any U,L,M where U is CWUser, U login L, U mail M' : [(1, 'login', 'subject'), (2, 'mail', 'subject')],
-            'Any U,L,M where U is CWUser, L is Foo, U mail M' : [(2, 'mail', 'subject')],
-            'Any C,P where C is Company, C employs P' : [(1, 'employs', 'subject')],
-            'Any C,P where C is Company, P employed_by P' : [],
-            'Any C where C is Company, C employs P' : [],
-            }
+            'Any U,L,M where U is CWUser, U login L, U mail M': [(1, 'login', 'subject'), (2, 'mail', 'subject')],
+            'Any U,L,M where U is CWUser, L is Foo, U mail M': [(2, 'mail', 'subject')],
+            'Any C,P where C is Company, C employs P': [(1, 'employs', 'subject')],
+            'Any C,P where C is Company, P employed_by P': [],
+            'Any C where C is Company, C employs P': [],
+        }
         for rql, relations in queries.items():
             result = list(attr_desc_iterator(parse(rql).children[0], 0, 0))
             self.assertEqual((rql, result), (rql, relations))
@@ -57,9 +57,10 @@
     def test_relations_description_indexed(self):
         """tests relations_description() function"""
         queries = {
-            'Any C,U,P,L,M where C is Company, C employs P, U is CWUser, U login L, U mail M' :
-            {0: [(2,'employs', 'subject')], 1: [(3,'login', 'subject'), (4,'mail', 'subject')]},
-            }
+            'Any C,U,P,L,M where C is Company, C employs P, U is CWUser, U login L, U mail M':
+            {0: [(2, 'employs', 'subject')],
+             1: [(3, 'login', 'subject'), (4, 'mail', 'subject')]},
+        }
         for rql, results in queries.items():
             for idx, relations in results.items():
                 result = list(attr_desc_iterator(parse(rql).children[0], idx, idx))
@@ -88,7 +89,7 @@
         self.rset = ResultSet([[12, 'adim'], [13, 'syt']],
                               'Any U,L where U is CWUser, U login L',
                               description=[['CWUser', 'String'], ['Bar', 'String']])
-        self.rset.req = mock_object(vreg=self.vreg)
+        self.rset.req = mock_object(vreg=self.vreg, repo=self.repo)
 
     def compare_urls(self, url1, url2):
         info1 = urlsplit(url1)
@@ -118,17 +119,6 @@
             #                  '%stask/title/go' % baseurl)
             # empty _restpath should not crash
             self.compare_urls(req.build_url('view', _restpath=''), baseurl)
-            self.assertNotIn('https', req.build_url('view', vid='foo', rql='yo',
-                                                      __secure__=True))
-            try:
-                self.config.global_set_option('https-url', 'https://testing.fr/')
-                self.assertTrue('https', req.build_url('view', vid='foo', rql='yo',
-                                                         __secure__=True))
-                self.compare_urls(req.build_url('view', vid='foo', rql='yo',
-                                                __secure__=True),
-                                  '%sview?vid=foo&rql=yo' % req.base_url(secure=True))
-            finally:
-                self.config.global_set_option('https-url', None)
 
 
     def test_build(self):
@@ -155,8 +145,6 @@
 
     def test_limit_2(self):
         with self.admin_access.web_request() as req:
-            # drop user from cache for the sake of this test
-            req.drop_entity_cache(req.user.eid)
             rs = req.execute('Any E,U WHERE E is CWEType, E created_by U')
             # get entity on row 9. This will fill its created_by relation cache,
             # with cwuser on row 9 as well
@@ -284,7 +272,7 @@
     def test_get_entity_simple(self):
         with self.admin_access.web_request() as req:
             req.create_entity('CWUser', login=u'adim', upassword='adim',
-                                         surname=u'di mascio', firstname=u'adrien')
+                              surname=u'di mascio', firstname=u'adrien')
             req.drop_entity_cache()
             e = req.execute('Any X,T WHERE X login "adim", X surname T').get_entity(0, 0)
             self.assertEqual(e.cw_attr_cache['surname'], 'di mascio')
@@ -575,6 +563,13 @@
                               '(Any X,N WHERE X is CWGroup, X name N)'
                               ')')
 
+    def test_possible_actions_cache(self):
+        with self.admin_access.web_request() as req:
+            rset = req.execute('Any D, COUNT(U) GROUPBY D WHERE U is CWUser, U creation_date D')
+            rset.possible_actions(argument='Value')
+            self.assertRaises(AssertionError, rset.possible_actions, argument='OtherValue')
+            self.assertRaises(AssertionError, rset.possible_actions, other_argument='Value')
+
     def test_count_users_by_date(self):
         with self.admin_access.web_request() as req:
             rset = req.execute('Any D, COUNT(U) GROUPBY D WHERE U is CWUser, U creation_date D')
--- a/cubicweb/test/unittest_rtags.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_rtags.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,81 +15,140 @@
 #
 # 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.devtools.testlib import BaseTestCase
 from cubicweb.rtags import RelationTags, RelationTagsSet, RelationTagsDict
 
-class RelationTagsTC(TestCase):
+
+class RelationTagsTC(BaseTestCase):
+
+    def setUp(self):
+        self.rtags = RelationTags(__module__=__name__)
+        self.rtags.tag_subject_of(('Societe', 'travaille', '*'), 'primary')
+        self.rtags.tag_subject_of(('*', 'evaluee', '*'), 'secondary')
+        self.rtags.tag_object_of(('*', 'tags', '*'), 'generated')
 
-    def test_rtags_expansion(self):
-        rtags = RelationTags()
-        rtags.tag_subject_of(('Societe', 'travaille', '*'), 'primary')
-        rtags.tag_subject_of(('*', 'evaluee', '*'), 'secondary')
-        rtags.tag_object_of(('*', 'tags', '*'), 'generated')
-        self.assertEqual(rtags.get('Note', 'evaluee', '*', 'subject'),
-                          'secondary')
-        self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'),
-                          'primary')
-        self.assertEqual(rtags.get('Note', 'travaille', '*', 'subject'),
-                          None)
-        self.assertEqual(rtags.get('Note', 'tags', '*', 'subject'),
-                          None)
-        self.assertEqual(rtags.get('*', 'tags', 'Note', 'object'),
-                          'generated')
-        self.assertEqual(rtags.get('Tag', 'tags', '*', 'object'),
-                          'generated')
+    def test_expansion(self):
+        self.assertEqual(self.rtags.get('Note', 'evaluee', '*', 'subject'),
+                         'secondary')
+        self.assertEqual(self.rtags.get('Societe', 'travaille', '*', 'subject'),
+                         'primary')
+        self.assertEqual(self.rtags.get('Note', 'travaille', '*', 'subject'),
+                         None)
+        self.assertEqual(self.rtags.get('Note', 'tags', '*', 'subject'),
+                         None)
+        self.assertEqual(self.rtags.get('*', 'tags', 'Note', 'object'),
+                         'generated')
+        self.assertEqual(self.rtags.get('Tag', 'tags', '*', 'object'),
+                         'generated')
 
-#         self.assertEqual(rtags.rtag('evaluee', 'Note', 'subject'), set(('secondary', 'link')))
-#         self.assertEqual(rtags.is_inlined('evaluee', 'Note', 'subject'), False)
-#         self.assertEqual(rtags.rtag('evaluee', 'Personne', 'subject'), set(('secondary', 'link')))
-#         self.assertEqual(rtags.is_inlined('evaluee', 'Personne', 'subject'), False)
-#         self.assertEqual(rtags.rtag('ecrit_par', 'Note', 'object'), set(('inlineview', 'link')))
-#         self.assertEqual(rtags.is_inlined('ecrit_par', 'Note', 'object'), True)
-#         class Personne2(Personne):
-#             id = 'Personne'
-#             __rtags__ = {
-#                 ('evaluee', 'Note', 'subject') : set(('inlineview',)),
-#                 }
-#         self.vreg.register(Personne2)
-#         rtags = Personne2.rtags
-#         self.assertEqual(rtags.rtag('evaluee', 'Note', 'subject'), set(('inlineview', 'link')))
-#         self.assertEqual(rtags.is_inlined('evaluee', 'Note', 'subject'), True)
-#         self.assertEqual(rtags.rtag('evaluee', 'Personne', 'subject'), set(('secondary', 'link')))
-#         self.assertEqual(rtags.is_inlined('evaluee', 'Personne', 'subject'), False)
+    def test_expansion_with_parent(self):
+        derived_rtags = self.rtags.derive(__name__, None)
+        derived_rtags.tag_subject_of(('Societe', 'travaille', '*'), 'secondary')
+        derived_rtags.tag_subject_of(('Note', 'evaluee', '*'), 'primary')
+        self.rtags.tag_object_of(('*', 'tags', '*'), 'hidden')
+
+        self.assertEqual(derived_rtags.get('Note', 'evaluee', '*', 'subject'),
+                         'primary')
+        self.assertEqual(derived_rtags.get('Societe', 'evaluee', '*', 'subject'),
+                         'secondary')
+        self.assertEqual(derived_rtags.get('Societe', 'travaille', '*', 'subject'),
+                         'secondary')
+        self.assertEqual(derived_rtags.get('Note', 'travaille', '*', 'subject'),
+                         None)
+        self.assertEqual(derived_rtags.get('*', 'tags', 'Note', 'object'),
+                         'hidden')
 
 
-    def test_rtagset_expansion(self):
-        rtags = RelationTagsSet()
-        rtags.tag_subject_of(('Societe', 'travaille', '*'), 'primary')
-        rtags.tag_subject_of(('*', 'travaille', '*'), 'secondary')
-        self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'),
-                          set(('primary', 'secondary')))
-        self.assertEqual(rtags.get('Note', 'travaille', '*', 'subject'),
-                          set(('secondary',)))
-        self.assertEqual(rtags.get('Note', 'tags', "*", 'subject'),
-                          set())
+class RelationTagsSetTC(BaseTestCase):
+
+    def setUp(self):
+        self.rtags = RelationTagsSet(__module__=__name__)
+        self.rtags.tag_subject_of(('Societe', 'travaille', '*'), 'primary')
+        self.rtags.tag_subject_of(('*', 'travaille', '*'), 'secondary')
+
+    def test_expansion(self):
+        self.assertEqual(self.rtags.get('Societe', 'travaille', '*', 'subject'),
+                         set(('primary', 'secondary')))
+        self.assertEqual(self.rtags.get('Note', 'travaille', '*', 'subject'),
+                         set(('secondary',)))
+        self.assertEqual(self.rtags.get('Note', 'tags', "*", 'subject'),
+                         set())
+
+    def test_expansion_with_parent(self):
+        derived_rtags = self.rtags.derive(__name__, None)
+        derived_rtags.tag_subject_of(('Societe', 'travaille', '*'), 'derived_primary')
+        self.assertEqual(derived_rtags.get('Societe', 'travaille', '*', 'subject'),
+                         set(('derived_primary', 'secondary')))
+        self.assertEqual(derived_rtags.get('Note', 'travaille', '*', 'subject'),
+                         set(('secondary',)))
+
+        derived_rtags.tag_subject_of(('*', 'travaille', '*'), 'derived_secondary')
+        self.assertEqual(derived_rtags.get('Societe', 'travaille', '*', 'subject'),
+                         set(('derived_primary', 'derived_secondary')))
+        self.assertEqual(derived_rtags.get('Note', 'travaille', '*', 'subject'),
+                         set(('derived_secondary',)))
+
+        self.assertEqual(derived_rtags.get('Note', 'tags', "*", 'subject'),
+                         set())
+
+
+class RelationTagsDictTC(BaseTestCase):
+
+    def setUp(self):
+        self.rtags = RelationTagsDict(__module__=__name__)
+        self.rtags.tag_subject_of(('Societe', 'travaille', '*'),
+                                  {'key1': 'val1', 'key2': 'val1'})
+        self.rtags.tag_subject_of(('*', 'travaille', '*'),
+                                  {'key1': 'val0', 'key3': 'val0'})
+        self.rtags.tag_subject_of(('Societe', 'travaille', '*'),
+                                  {'key2': 'val2'})
 
-    def test_rtagdict_expansion(self):
-        rtags = RelationTagsDict()
-        rtags.tag_subject_of(('Societe', 'travaille', '*'),
-                             {'key1': 'val1', 'key2': 'val1'})
-        rtags.tag_subject_of(('*', 'travaille', '*'),
-                             {'key1': 'val0', 'key3': 'val0'})
-        rtags.tag_subject_of(('Societe', 'travaille', '*'),
-                             {'key2': 'val2'})
-        self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'),
-                          {'key1': 'val1', 'key2': 'val2', 'key3': 'val0'})
-        self.assertEqual(rtags.get('Note', 'travaille', '*', 'subject'),
-                          {'key1': 'val0', 'key3': 'val0'})
-        self.assertEqual(rtags.get('Note', 'tags', "*", 'subject'),
-                          {})
+    def test_expansion(self):
+        self.assertEqual(self.rtags.get('Societe', 'travaille', '*', 'subject'),
+                         {'key1': 'val1', 'key2': 'val2', 'key3': 'val0'})
+        self.assertEqual(self.rtags.get('Note', 'travaille', '*', 'subject'),
+                         {'key1': 'val0', 'key3': 'val0'})
+        self.assertEqual(self.rtags.get('Note', 'tags', "*", 'subject'),
+                         {})
+
+        self.rtags.setdefault(('Societe', 'travaille', '*', 'subject'), 'key1', 'val4')
+        self.rtags.setdefault(('Societe', 'travaille', '*', 'subject'), 'key4', 'val4')
+        self.assertEqual(self.rtags.get('Societe', 'travaille', '*', 'subject'),
+                         {'key1': 'val1', 'key2': 'val2', 'key3': 'val0', 'key4': 'val4'})
+
+    def test_expansion_with_parent(self):
+        derived_rtags = self.rtags.derive(__name__, None)
 
-        rtags.setdefault(('Societe', 'travaille', '*', 'subject'), 'key1', 'val4')
-        rtags.setdefault(('Societe', 'travaille', '*', 'subject'), 'key4', 'val4')
-        self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'),
-                          {'key1': 'val1', 'key2': 'val2', 'key3': 'val0', 'key4': 'val4'})
+        derived_rtags.tag_subject_of(('Societe', 'travaille', '*'),
+                                     {'key0': 'val0'})
+        self.assertEqual(derived_rtags.get('Societe', 'travaille', '*', 'subject'),
+                         {'key0': 'val0', 'key1': 'val0', 'key3': 'val0'})
+        self.assertEqual(derived_rtags.get('Note', 'travaille', '*', 'subject'),
+                         {'key1': 'val0', 'key3': 'val0'})
+        self.assertEqual(derived_rtags.get('Note', 'tags', "*", 'subject'),
+                         {})
+
+        derived_rtags.tag_subject_of(('*', 'travaille', '*'),
+                                     {'key0': 'val00', 'key4': 'val4'})
+        self.assertEqual(derived_rtags.get('Societe', 'travaille', '*', 'subject'),
+                         {'key0': 'val0', 'key4': 'val4'})
+        self.assertEqual(derived_rtags.get('Note', 'travaille', '*', 'subject'),
+                         {'key0': 'val00', 'key4': 'val4'})
+
+
+class DeprecatedInstanceWithoutModule(BaseTestCase):
+
+    def test_deprecated_instance_without_module(self):
+        class SubRelationTags(RelationTags):
+            pass
+        with self.assertWarnsRegex(
+            DeprecationWarning,
+            'instantiate SubRelationTags with __module__=__name__',
+        ):
+            SubRelationTags()
+
 
 if __name__ == '__main__':
-    unittest_main()
+    import unittest
+    unittest.main()
--- a/cubicweb/test/unittest_schema.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_schema.py	Mon Mar 20 10:28:01 2017 +0100
@@ -17,7 +17,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unit tests for module cubicweb.schema"""
 
-from os.path import join, dirname
+from os.path import join, dirname, splitext
 
 from logilab.common.testlib import TestCase, unittest_main
 
@@ -402,7 +402,8 @@
         self.loader.post_build_callbacks = []
 
     def _test(self, schemafile, msg):
-        self.loader.handle_file(join(DATADIR, schemafile))
+        self.loader.handle_file(join(DATADIR, schemafile),
+                                splitext(schemafile)[0])
         sch = self.loader.schemacls('toto')
         with self.assertRaises(BadSchemaDefinition) as cm:
             fill_schema(sch, self.loader.defined, False)
@@ -575,5 +576,32 @@
                                          for r, role in schema[etype].composite_rdef_roles]))
 
 
+class CWShemaTC(CubicWebTC):
+
+    def test_transform_has_permission_match(self):
+        with self.admin_access.repo_cnx() as cnx:
+            eschema = cnx.vreg.schema['EmailAddress']
+            rql_exprs = eschema.get_rqlexprs('update')
+            self.assertEqual(len(rql_exprs), 1)
+            self.assertEqual(rql_exprs[0].expression,
+                             'P use_email X, U has_update_permission P')
+            rql, found, keyarg = rql_exprs[0].transform_has_permission()
+            self.assertEqual(rql, 'Any X,P WHERE P use_email X, X eid %(x)s')
+            self.assertEqual(found, [(u'update', 1)])
+            self.assertEqual(keyarg, None)
+
+    def test_transform_has_permission_no_match(self):
+        with self.admin_access.repo_cnx() as cnx:
+            eschema = cnx.vreg.schema['EmailAddress']
+            rql_exprs = eschema.get_rqlexprs('read')
+            self.assertEqual(len(rql_exprs), 1)
+            self.assertEqual(rql_exprs[0].expression,
+                             'U use_email X')
+            rql, found, keyarg = rql_exprs[0].transform_has_permission()
+            self.assertEqual(rql, 'Any X WHERE EXISTS(U use_email X, X eid %(x)s, U eid %(u)s)')
+            self.assertEqual(found, None)
+            self.assertEqual(keyarg, None)
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/cubicweb/test/unittest_utils.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_utils.py	Mon Mar 20 10:28:01 2017 +0100
@@ -33,7 +33,7 @@
 from cubicweb import Binary, Unauthorized
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.utils import (make_uid, UStringIO, RepeatList, HTMLHead,
-                            QueryCache, parse_repo_uri)
+                            QueryCache)
 from cubicweb.entity import Entity
 
 try:
@@ -59,18 +59,6 @@
             d.add(uid)
 
 
-class TestParseRepoUri(TestCase):
-
-    def test_parse_repo_uri(self):
-        self.assertEqual(('inmemory', None, 'myapp'),
-                         parse_repo_uri('myapp'))
-        self.assertEqual(('inmemory', None, 'myapp'),
-                         parse_repo_uri('inmemory://myapp'))
-        with self.assertRaises(NotImplementedError):
-            parse_repo_uri('foo://bar')
-
-
-
 class TestQueryCache(TestCase):
     def test_querycache(self):
         c = QueryCache(ceiling=20)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/unittest_wfutils.py	Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,115 @@
+# copyright 2017 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/>.
+
+import copy
+
+from cubicweb.devtools import testlib
+from cubicweb.wfutils import setup_workflow
+
+
+class TestWFUtils(testlib.CubicWebTC):
+
+    defs = {
+        'group': {
+            'etypes': 'CWGroup',
+            'default': True,
+            'initial_state': u'draft',
+            'states': [u'draft', u'published'],
+            'transitions': {
+                u'publish': {
+                    'fromstates': u'draft',
+                    'tostate': u'published',
+                    'requiredgroups': u'managers'
+                }
+            }
+        }
+    }
+
+    def test_create_workflow(self):
+        with self.admin_access.cnx() as cnx:
+            wf = setup_workflow(cnx, 'group', self.defs['group'])
+            self.assertEqual(wf.name, 'group')
+            self.assertEqual(wf.initial.name, u'draft')
+
+            draft = wf.state_by_name(u'draft')
+            self.assertIsNotNone(draft)
+
+            published = wf.state_by_name(u'published')
+            self.assertIsNotNone(published)
+
+            publish = wf.transition_by_name(u'publish')
+            self.assertIsNotNone(publish)
+
+            self.assertEqual(publish.destination_state, (published, ))
+            self.assertEqual(draft.allowed_transition, (publish, ))
+
+            self.assertEqual(
+                {g.name for g in publish.require_group},
+                {'managers'})
+
+    def test_update(self):
+        with self.admin_access.cnx() as cnx:
+            wf = setup_workflow(cnx, 'group', self.defs['group'])
+            eid = wf.eid
+
+        with self.admin_access.cnx() as cnx:
+            wfdef = copy.deepcopy(self.defs['group'])
+            wfdef['states'].append('new')
+            wfdef['initial_state'] = 'new'
+            wfdef['transitions'][u'publish']['fromstates'] = ('draft', 'new')
+            wfdef['transitions'][u'publish']['requiredgroups'] = (
+                u'managers', u'users')
+            wfdef['transitions'][u'todraft'] = {
+                'fromstates': ('new', 'published'),
+                'tostate': 'draft',
+            }
+
+            wf = setup_workflow(cnx, 'group', wfdef)
+            self.assertEqual(wf.eid, eid)
+            self.assertEqual(wf.name, 'group')
+            self.assertEqual(wf.initial.name, u'new')
+
+            new = wf.state_by_name(u'new')
+            self.assertIsNotNone(new)
+
+            draft = wf.state_by_name(u'draft')
+            self.assertIsNotNone(draft)
+
+            published = wf.state_by_name(u'published')
+            self.assertIsNotNone(published)
+
+            publish = wf.transition_by_name(u'publish')
+            self.assertIsNotNone(publish)
+
+            todraft = wf.transition_by_name(u'todraft')
+            self.assertIsNotNone(todraft)
+
+            self.assertEqual(
+                {g.name for g in publish.require_group},
+                {'managers', 'users'})
+
+            self.assertEqual(publish.destination_state, (published, ))
+            self.assertEqual(draft.allowed_transition, (publish, ))
+            self.assertEqual(todraft.destination_state, (draft, ))
+            self.assertEqual(published.allowed_transition, (todraft, ))
+            self.assertCountEqual(new.allowed_transition, (publish, todraft))
+
+
+if __name__ == '__main__':
+    import unittest
+    unittest.main()
--- a/cubicweb/utils.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/utils.py	Mon Mar 20 10:28:01 2017 +0100
@@ -19,8 +19,6 @@
 
 from __future__ import division
 
-
-
 import base64
 import decimal
 import datetime
@@ -37,7 +35,6 @@
 from logging import getLogger
 
 from six import text_type
-from six.moves.urllib.parse import urlparse
 
 from logilab.mtconverter import xml_escape
 from logilab.common.deprecation import deprecated
@@ -51,18 +48,19 @@
 # initialize random seed from current time
 random.seed()
 
+
 def admincnx(appid):
+    from cubicweb import repoapi
     from cubicweb.cwconfig import CubicWebConfiguration
     from cubicweb.server.repository import Repository
-    from cubicweb.server.utils import TasksManager
     config = CubicWebConfiguration.config_for(appid)
 
     login = config.default_admin_config['login']
     password = config.default_admin_config['password']
 
-    repo = Repository(config, TasksManager())
-    session = repo.new_session(login, password=password)
-    return session.new_cnx()
+    repo = Repository(config)
+    repo.bootstrap()
+    return repoapi.connect(repo, login, password=password)
 
 
 def make_uid(key=None):
@@ -585,21 +583,6 @@
     return 'javascript: ' + PERCENT_IN_URLQUOTE_RE.sub(r'%25', javascript_code)
 
 
-def parse_repo_uri(uri):
-    """ transform a command line uri into a (protocol, hostport, appid), e.g:
-    <myapp>                      -> 'inmemory', None, '<myapp>'
-    inmemory://<myapp>           -> 'inmemory', None, '<myapp>'
-    """
-    parseduri = urlparse(uri)
-    scheme = parseduri.scheme
-    if scheme == '':
-        return ('inmemory', None, parseduri.path)
-    if scheme == 'inmemory':
-        return (scheme, None, parseduri.netloc)
-    raise NotImplementedError('URI protocol not implemented for `%s`' % uri)
-
-
-
 logger = getLogger('cubicweb.utils')
 
 class QueryCache(object):
--- a/cubicweb/web/application.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/application.py	Mon Mar 20 10:28:01 2017 +0100
@@ -18,7 +18,6 @@
 """CubicWeb web client application object"""
 
 
-
 import contextlib
 from functools import wraps
 import json
@@ -77,10 +76,14 @@
 
 @contextmanager
 def anonymized_request(req):
+    from cubicweb.web.views.authentication import Session
+
     orig_cnx = req.cnx
     anon_cnx = anonymous_cnx(orig_cnx.session.repo)
     try:
         with anon_cnx:
+            # web request expect a session attribute on cnx referencing the web session
+            anon_cnx.session = Session(orig_cnx.session.repo, anon_cnx.user)
             req.set_cnx(anon_cnx)
             yield req
     finally:
@@ -125,8 +128,6 @@
         """return a string giving the name of the cookie used to store the
         session identifier.
         """
-        if req.https:
-            return '__%s_https_session' % self.vreg.config.appid
         return '__%s_session' % self.vreg.config.appid
 
     def get_session(self, req):
@@ -158,7 +159,7 @@
     def open_session(self, req):
         session = self.session_manager.open_session(req)
         sessioncookie = self.session_cookie(req)
-        secure = req.https and req.base_url().startswith('https://')
+        secure = req.base_url().startswith('https://')
         req.set_cookie(sessioncookie, session.sessionid,
                        maxage=None, secure=secure, httponly=True)
         if not session.anonymous_session:
@@ -252,15 +253,18 @@
             return set_cnx
 
         req.set_cnx = wrap_set_cnx(req.set_cnx)
+        tstart, cstart = time(), clock()
         try:
             return self.main_handle_request(req)
         finally:
             cnx = req.cnx
-            if cnx:
+            if cnx and cnx.executed_queries:
                 with self._logfile_lock:
+                    tend, cend = time(), clock()
                     try:
                         result = ['\n' + '*' * 80]
-                        result.append(req.url())
+                        result.append('%s -- (%.3f sec, %.3f CPU sec)' % (
+                            req.url(), tend - tstart, cend - cstart))
                         result += ['%s %s -- (%.3f sec, %.3f CPU sec)' % q
                                    for q in cnx.executed_queries]
                         cnx.executed_queries = []
@@ -331,27 +335,20 @@
             content = self.redirect_handler(req, ex)
         # Wrong, absent or Reseted credential
         except AuthenticationError:
-            # If there is an https url configured and
-            # the request does not use https, redirect to login form
-            https_url = self.vreg.config['https-url']
-            if https_url and req.base_url() != https_url:
-                req.status_out = http_client.SEE_OTHER
-                req.headers_out.setHeader('location', https_url + 'login')
+            # 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 = http_client.UNAUTHORIZED
+            # In the other case (coky auth) we assume that there is no way
+            # for the user to provide them...
+            # XXX But WHY ?
             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 = http_client.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 = http_client.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)
+                req.status_out = http_client.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)
         assert isinstance(content, binary_type)
         return content
 
--- a/cubicweb/web/data/cubicweb.edition.js	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/data/cubicweb.edition.js	Mon Mar 20 10:28:01 2017 +0100
@@ -9,7 +9,7 @@
 
 //============= Eproperty form functions =====================================//
 /**
- * .. function:: setPropValueWidget(varname, tabindex)
+ * .. function:: setPropValueWidget(varname)
  *
  * called on CWProperty key selection:
  * - get the selected value
@@ -17,16 +17,15 @@
  * - fill associated div with the returned html
  *
  * * `varname`, the name of the variable as used in the original creation form
- * * `tabindex`, the tabindex that should be set on the widget
  */
 
-function setPropValueWidget(varname, tabindex) {
+function setPropValueWidget(varname) {
     var key = firstSelected(document.getElementById('pkey-subject:' + varname));
     if (key) {
         var args = {
             fname: 'prop_widget',
             pageid: pageid,
-            arg: $.map([key.value, varname, tabindex], JSON.stringify)
+            arg: $.map([key.value, varname], JSON.stringify)
         };
         cw.jqNode('div:value-subject:' + varname).loadxhtml(AJAX_BASE_URL, args, 'post');
     }
--- a/cubicweb/web/formfields.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/formfields.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -111,6 +111,7 @@
     result += sorted(partresult)
     return result
 
+
 _MARKER = nullobject()
 
 
@@ -361,7 +362,6 @@
             if callable(self.value):
                 return self.value(form, self)
             return self.value
-        formattr = '%s_%s_default' % (self.role, self.name)
         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)
@@ -1237,8 +1237,19 @@
         kwargs.setdefault('label', (eschema.type, rschema.type))
     kwargs.setdefault('help', rdef.description)
     if rschema.final:
-        fieldclass = FIELDS[targetschema]
-        if fieldclass is StringField:
+        fieldclass = kwargs.pop('fieldclass', FIELDS[targetschema])
+        if issubclass(fieldclass, FileField):
+            if req:
+                aff_kwargs = req.vreg['uicfg'].select('autoform_field_kwargs', req)
+            else:
+                aff_kwargs = _AFF_KWARGS
+            for metadata in KNOWN_METAATTRIBUTES:
+                metaschema = eschema.has_metadata(rschema, metadata)
+                if metaschema is not None:
+                    metakwargs = aff_kwargs.etype_get(eschema, metaschema, 'subject')
+                    kwargs['%s_field' % metadata] = guess_field(eschema, metaschema,
+                                                                req=req, **metakwargs)
+        elif issubclass(fieldclass, StringField):
             if eschema.has_metadata(rschema, 'format'):
                 # use RichTextField instead of StringField if the attribute has
                 # a "format" metadata. But getting information from constraints
@@ -1255,18 +1266,6 @@
             for cstr in rdef.constraints:
                 if isinstance(cstr, SizeConstraint) and cstr.max is not None:
                     kwargs['max_length'] = cstr.max
-            return StringField(**kwargs)
-        if fieldclass is FileField:
-            if req:
-                aff_kwargs = req.vreg['uicfg'].select('autoform_field_kwargs', req)
-            else:
-                aff_kwargs = _AFF_KWARGS
-            for metadata in KNOWN_METAATTRIBUTES:
-                metaschema = eschema.has_metadata(rschema, metadata)
-                if metaschema is not None:
-                    metakwargs = aff_kwargs.etype_get(eschema, metaschema, 'subject')
-                    kwargs['%s_field' % metadata] = guess_field(eschema, metaschema,
-                                                                req=req, **metakwargs)
         return fieldclass(**kwargs)
     return RelationField.fromcardinality(card, **kwargs)
 
--- a/cubicweb/web/formwidgets.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/formwidgets.py	Mon Mar 20 10:28:01 2017 +0100
@@ -125,9 +125,6 @@
     :attr:`setdomid`
        flag telling if HTML DOM identifier should be set on input.
 
-    :attr:`settabindex`
-       flag telling if HTML tabindex attribute of inputs should be set.
-
     :attr:`suffix`
        string to use a suffix when generating input, to ease usage as a
        sub-widgets (eg widget used by another widget)
@@ -157,21 +154,17 @@
     needs_js = ()
     needs_css = ()
     setdomid = True
-    settabindex = True
     suffix = None
     # does this widget expect a vocabulary
     vocabulary_widget = False
 
-    def __init__(self, attrs=None, setdomid=None, settabindex=None, suffix=None):
+    def __init__(self, attrs=None, setdomid=None, suffix=None):
         if attrs is None:
             attrs = {}
         self.attrs = attrs
         if setdomid is not None:
             # override class's default value
             self.setdomid = setdomid
-        if settabindex is not None:
-            # override class's default value
-            self.settabindex = settabindex
         if suffix is not None:
             self.suffix = suffix
 
@@ -202,14 +195,11 @@
 
     def attributes(self, form, field):
         """Return HTML attributes for the widget, automatically setting DOM
-        identifier and tabindex when desired (see :attr:`setdomid` and
-        :attr:`settabindex` attributes)
+        identifier when desired (see :attr:`setdomid` attribute)
         """
         attrs = dict(self.attrs)
         if self.setdomid:
             attrs['id'] = field.dom_id(form, self.suffix)
-        if self.settabindex and 'tabindex' not in attrs:
-            attrs['tabindex'] = form._cw.next_tabindex()
         if 'placeholder' in attrs:
             attrs['placeholder'] = form._cw._(attrs['placeholder'])
         return attrs
@@ -386,7 +376,6 @@
     """
     type = 'hidden'
     setdomid = False  # by default, don't set id attribute on hidden input
-    settabindex = False
 
 
 class ButtonInput(Input):
@@ -682,9 +671,12 @@
     choose a date anterior(/posterior) to this DatePicker.
 
     example:
-    start and end are two JQueryDatePicker and start must always be before end
+
+    start and end are two JQueryDatePicker and start must always be before end::
+
         affk.set_field_kwargs(etype, 'start_date', widget=JQueryDatePicker(min_of='end_date'))
         affk.set_field_kwargs(etype, 'end_date', widget=JQueryDatePicker(max_of='start_date'))
+
     That way, on change of end(/start) value a new max(/min) will be set for start(/end)
     The invalid dates will be gray colored in the datepicker
     """
@@ -803,17 +795,17 @@
         req = form._cw
         datestr = req.form.get(field.input_name(form, 'date'))
         if datestr:
-            datestr = datestr.strip() or None
-        timestr = req.form.get(field.input_name(form, 'time'))
-        if timestr:
-            timestr = timestr.strip() or None
-        if datestr is None:
+            datestr = datestr.strip()
+        if not datestr:
             return None
         try:
             date = todatetime(req.parse_datetime(datestr, 'Date'))
         except ValueError as exc:
             raise ProcessFormError(text_type(exc))
-        if timestr is None:
+        timestr = req.form.get(field.input_name(form, 'time'))
+        if timestr:
+            timestr = timestr.strip()
+        if not timestr:
             return date
         try:
             time = req.parse_datetime(timestr, 'Time')
@@ -1000,8 +992,6 @@
         attrs = dict(self.attrs)
         if self.setdomid:
             attrs['id'] = field.dom_id(form)
-        if self.settabindex and 'tabindex' not in attrs:
-            attrs['tabindex'] = req.next_tabindex()
         # ensure something is rendered
         inputs = [u'<table><tr><th>',
                   req._('i18n_bookmark_url_path'),
@@ -1012,8 +1002,6 @@
                   u'</th><td>']
         if self.setdomid:
             attrs['id'] = field.dom_id(form, 'fqs')
-        if self.settabindex:
-            attrs['tabindex'] = req.next_tabindex()
         attrs.setdefault('cols', 60)
         attrs.setdefault('onkeyup', 'autogrow(this)')
         inputs += [tags.textarea(fqs, name=fqsqname, **attrs),
@@ -1061,9 +1049,9 @@
     css_class = 'validateButton'
 
     def __init__(self, label=stdmsgs.BUTTON_OK, attrs=None,
-                 setdomid=None, settabindex=None,
+                 setdomid=None,
                  name='', value='', onclick=None, cwaction=None):
-        super(Button, self).__init__(attrs, setdomid, settabindex)
+        super(Button, self).__init__(attrs, setdomid)
         if isinstance(label, tuple):
             self.label = label[0]
             self.icon = label[1]
@@ -1089,8 +1077,6 @@
             attrs['name'] = self.name
             if self.setdomid:
                 attrs['id'] = self.name
-        if self.settabindex and 'tabindex' not in attrs:
-            attrs['tabindex'] = form._cw.next_tabindex()
         if self.icon:
             img = tags.img(src=form._cw.uiprops[self.icon], alt=self.icon)
         else:
--- a/cubicweb/web/propertysheet.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/propertysheet.py	Mon Mar 20 10:28:01 2017 +0100
@@ -19,6 +19,7 @@
 
 
 
+import errno
 import re
 import os
 import os.path as osp
@@ -109,7 +110,9 @@
                 stream.write(content)
             try:
                 os.rename(tmpfile, cachefile)
-            except IOError:
+            except OSError as err:
+                if err.errno != errno.EEXIST:
+                    raise
                 # Under windows, os.rename won't overwrite an existing file
                 os.unlink(cachefile)
                 os.rename(tmpfile, cachefile)
--- a/cubicweb/web/request.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/request.py	Mon Mar 20 10:28:01 2017 +0100
@@ -105,28 +105,21 @@
     """
     ajax_request = False # to be set to True by ajax controllers
 
-    def __init__(self, vreg, https=False, form=None, headers=None):
+    def __init__(self, vreg, form=None, headers=None):
         """
         :vreg: Vregistry,
-        :https: boolean, s this a https request
         :form: Forms value
         :headers: dict, request header
         """
         super(_CubicWebRequestBase, self).__init__(vreg)
-        #: (Boolean) Is this an https request.
-        self.https = https
-        #: User interface property (vary with https) (see :ref:`uiprops`)
+        #: User interface property (see :ref:`uiprops`)
         self.uiprops = None
-        #: url for serving datadir (vary with https) (see :ref:`resources`)
+        #: url for serving datadir (see :ref:`resources`)
         self.datadir_url = None
-        if https and vreg.config.https_uiprops is not None:
-            self.uiprops = vreg.config.https_uiprops
-        else:
-            self.uiprops = vreg.config.uiprops
-        if https and vreg.config.https_datadir_url is not None:
-            self.datadir_url = vreg.config.https_datadir_url
-        else:
-            self.datadir_url = vreg.config.datadir_url
+        # some config (i.e. "pyramid") do not have "uiprops" nor "datadir_url"
+        # attributes)
+        self.uiprops = getattr(vreg.config, 'uiprops', None)
+        self.datadir_url = getattr(vreg.config, 'datadir_url', None)
         #: enable UStringIO's write tracing
         self.tracehtml = False
         if vreg.config.debugmode:
@@ -179,22 +172,6 @@
         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
@@ -215,13 +192,6 @@
         """
         return self.set_varmaker()
 
-    def next_tabindex(self):
-        nextfunc = self.get_page_data('nexttabfunc')
-        if nextfunc is None:
-            nextfunc = Counter(1)
-            self.set_page_data('nexttabfunc', nextfunc)
-        return nextfunc()
-
     def set_varmaker(self):
         varmaker = self.get_page_data('rql_varmaker')
         if varmaker is None:
@@ -959,7 +929,7 @@
     cnx = None
     session = None
 
-    def __init__(self, vreg, https=False, form=None, headers={}):
+    def __init__(self, vreg, form=None, headers={}):
         """"""
         self.vreg = vreg
         try:
@@ -967,8 +937,7 @@
             self.translations = vreg.config.translations
         except AttributeError:
             self.translations = {}
-        super(ConnectionCubicWebRequestBase, self).__init__(vreg, https=https,
-                                                       form=form, headers=headers)
+        super(ConnectionCubicWebRequestBase, self).__init__(vreg, form=form, headers=headers)
         self.session = _MockAnonymousSession()
         self.cnx = self.user = _NeedAuthAccessMock()
 
@@ -1016,11 +985,8 @@
     def cached_entities(self):
         return self.transaction_data.get('req_ecache', {}).values()
 
-    def drop_entity_cache(self, eid=None):
-        if eid is None:
-            self.transaction_data.pop('req_ecache', None)
-        else:
-            del self.transaction_data['req_ecache'][eid]
+    def drop_entity_cache(self):
+        self.transaction_data.pop('req_ecache', None)
 
 
 CubicWebRequestBase = ConnectionCubicWebRequestBase
--- a/cubicweb/web/test/unittest_application.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_application.py	Mon Mar 20 10:28:01 2017 +0100
@@ -24,7 +24,7 @@
 from six.moves.http_cookies import SimpleCookie
 
 from logilab.common.testlib import TestCase, unittest_main
-from logilab.common.decorators import clear_cache, classproperty
+from logilab.common.decorators import clear_cache
 
 from cubicweb import view
 from cubicweb.devtools.testlib import CubicWebTC, real_error_handling
@@ -32,7 +32,6 @@
 from cubicweb.web import LogOut, Redirect, INTERNAL_FIELD_VALUE
 from cubicweb.web.views.basecontrollers import ViewController
 from cubicweb.web.application import anonymized_request
-from cubicweb import repoapi
 
 
 class FakeMapping:
@@ -219,7 +218,7 @@
     def test_handle_request_with_lang_fromurl(self):
         """No language negociation, get language from URL."""
         self.config.global_set_option('language-mode', 'url-prefix')
-        req, origsession = self.init_authentication('http')
+        req = self.init_authentication('http')
         self.assertEqual(req.url(), 'http://testing.fr/cubicweb/login')
         self.assertEqual(req.lang, 'en')
         self.app.handle_request(req)
@@ -503,7 +502,7 @@
             self.assertTrue(cnx.find('Directory', eid=subd.eid))
             self.assertTrue(cnx.find('Filesystem', eid=fs.eid))
             self.assertEqual(cnx.find('Directory', eid=subd.eid).one().parent,
-                             [topd,])
+                             (topd,))
 
     def test_subject_mixed_composite_subentity_removal_2(self):
         """Editcontroller: detaching several subentities respects each rdef's
@@ -542,7 +541,7 @@
             self.assertTrue(cnx.find('Directory', eid=subd.eid))
             self.assertTrue(cnx.find('Filesystem', eid=fs.eid))
             self.assertEqual(cnx.find('Directory', eid=subd.eid).one().parent,
-                             [topd,])
+                             (topd,))
 
     def test_object_mixed_composite_subentity_removal_2(self):
         """Editcontroller: detaching several subentities respects each rdef's
@@ -574,13 +573,13 @@
             perm_eid = text_type(perm.eid)
             req.form = {
                 'eid': [dir_eid, perm_eid],
-                '__maineid' : dir_eid,
+                '__maineid': dir_eid,
                 '__type:%s' % dir_eid: 'Directory',
                 '__type:%s' % perm_eid: 'DirectoryPermission',
                 '_cw_entity_fields:%s' % dir_eid: '',
                 '_cw_entity_fields:%s' % perm_eid: 'has_permission-object',
                 'has_permission-object:%s' % perm_eid: '',
-                }
+            }
             path, _params = self.expect_redirect_handle_request(req, 'edit')
             self.assertTrue(req.find('Directory', eid=mydir.eid))
             self.assertFalse(req.find('DirectoryPermission', eid=perm.eid))
@@ -638,20 +637,20 @@
     # authentication tests ####################################################
 
     def test_http_auth_no_anon(self):
-        req, origsession = self.init_authentication('http')
+        req = self.init_authentication('http')
         self.assertAuthFailure(req)
         self.app.handle_request(req)
         self.assertEqual(401, req.status_out)
         clear_cache(req, 'get_authorization')
         authstr = base64.encodestring(('%s:%s' % (self.admlogin, self.admpassword)).encode('ascii'))
         req.set_request_header('Authorization', 'basic %s' % authstr.decode('ascii'))
-        self.assertAuthSuccess(req, origsession)
+        self.assertAuthSuccess(req)
         req._url = 'logout'
         self.assertRaises(LogOut, self.app_handle_request, req)
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_cookie_auth_no_anon(self):
-        req, origsession = self.init_authentication('cookie')
+        req = self.init_authentication('cookie')
         self.assertAuthFailure(req)
         try:
             form = self.app.handle_request(req)
@@ -663,7 +662,7 @@
         self.assertFalse(req.cnx)  # Mock cnx are False
         req.form['__login'] = self.admlogin
         req.form['__password'] = self.admpassword
-        self.assertAuthSuccess(req, origsession)
+        self.assertAuthSuccess(req)
         req._url = 'logout'
         self.assertRaises(LogOut, self.app_handle_request, req)
         self.assertEqual(len(self.open_sessions), 0)
@@ -675,11 +674,11 @@
             cnx.execute('INSERT EmailAddress X: X address %(address)s, U primary_email X '
                         'WHERE U login %(login)s', {'address': address, 'login': login})
             cnx.commit()
-        req, origsession = self.init_authentication('cookie')
+        req = self.init_authentication('cookie')
         self.set_option('allow-email-login', True)
         req.form['__login'] = address
         req.form['__password'] = self.admpassword
-        self.assertAuthSuccess(req, origsession)
+        self.assertAuthSuccess(req)
         req._url = 'logout'
         self.assertRaises(LogOut, self.app_handle_request, req)
         self.assertEqual(len(self.open_sessions), 0)
@@ -701,7 +700,7 @@
         with cnx:
             req.set_cnx(cnx)
         self.assertEqual(len(self.open_sessions), 1)
-        self.assertEqual(asession.login, 'anon')
+        self.assertEqual(asession.user.login, 'anon')
         self.assertTrue(asession.anonymous_session)
         self._reset_cookie(req)
 
@@ -718,27 +717,27 @@
         self._reset_cookie(req)
 
     def test_http_auth_anon_allowed(self):
-        req, origsession = self.init_authentication('http', 'anon')
+        req = self.init_authentication('http', 'anon')
         self._test_auth_anon(req)
         authstr = base64.encodestring(b'toto:pouet')
         req.set_request_header('Authorization', 'basic %s' % authstr.decode('ascii'))
         self._test_anon_auth_fail(req)
         authstr = base64.encodestring(('%s:%s' % (self.admlogin, self.admpassword)).encode('ascii'))
         req.set_request_header('Authorization', 'basic %s' % authstr.decode('ascii'))
-        self.assertAuthSuccess(req, origsession)
+        self.assertAuthSuccess(req)
         req._url = 'logout'
         self.assertRaises(LogOut, self.app_handle_request, req)
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_cookie_auth_anon_allowed(self):
-        req, origsession = self.init_authentication('cookie', 'anon')
+        req = self.init_authentication('cookie', 'anon')
         self._test_auth_anon(req)
         req.form['__login'] = 'toto'
         req.form['__password'] = 'pouet'
         self._test_anon_auth_fail(req)
         req.form['__login'] = self.admlogin
         req.form['__password'] = self.admpassword
-        self.assertAuthSuccess(req, origsession)
+        self.assertAuthSuccess(req)
         req._url = 'logout'
         self.assertRaises(LogOut, self.app_handle_request, req)
         self.assertEqual(0, len(self.open_sessions))
@@ -749,10 +748,10 @@
             # admin should see anon + admin
             self.assertEqual(2, len(list(req.find('CWUser'))))
             with anonymized_request(req):
-                self.assertEqual('anon', req.session.login, 'anon')
+                self.assertEqual('anon', req.session.user.login)
                 # anon should only see anon user
                 self.assertEqual(1, len(list(req.find('CWUser'))))
-            self.assertEqual(self.admlogin, req.session.login)
+            self.assertEqual(self.admlogin, req.session.user.login)
             self.assertEqual(2, len(list(req.find('CWUser'))))
 
     def test_non_regr_optional_first_var(self):
--- a/cubicweb/web/test/unittest_form.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_form.py	Mon Mar 20 10:28:01 2017 +0100
@@ -18,14 +18,12 @@
 
 import time
 from datetime import datetime
-import pytz
-
-from xml.etree.ElementTree import fromstring
-from lxml import html
 
 from six import text_type
 
-from logilab.common.testlib import unittest_main
+import pytz
+
+from lxml import html
 
 from cubicweb import Binary, ValidationError
 from cubicweb.mttransforms import HAS_TAL
@@ -34,7 +32,7 @@
                                      PasswordField, DateTimeField,
                                      FileField, EditableFileField,
                                      TZDatetimeField)
-from cubicweb.web.formwidgets import PasswordInput, Input, DateTimePicker
+from cubicweb.web.formwidgets import DateTimePicker, JQueryDateTimePicker
 from cubicweb.web.views.forms import EntityFieldsForm, FieldsForm
 from cubicweb.web.views.workflow import ChangeStateForm
 from cubicweb.web.views.formrenderers import FormRenderer
@@ -64,6 +62,15 @@
             form = AForm(req)
             self.assertRaises(ValidationError, form.process_posted)
 
+    def test_jqdt_process_data(self):
+        widget = JQueryDateTimePicker()
+        field = DateTimeField('jqdt')
+        with self.admin_access.web_request(**{'jqdt-date': '', 'jqdt-time': '00:00'}) as req:
+            self._cw = req
+            self.formvalues = {}
+            date = widget.process_field_data(self, field)
+            self.assertIsNone(date)
+
 
 class EntityFieldsFormTC(CubicWebTC):
 
@@ -200,20 +207,20 @@
     def test_richtextfield_1(self):
         with self.admin_access.web_request() as req:
             req.use_fckeditor = lambda: False
-            self._test_richtextfield(req, '''<select id="description_format-subject:%(eid)s" name="description_format-subject:%(eid)s" size="1" style="display: block" tabindex="1">
+            self._test_richtextfield(req, '''<select id="description_format-subject:%(eid)s" name="description_format-subject:%(eid)s" size="1" style="display: block">
 ''' + ('<option value="text/cubicweb-page-template">text/cubicweb-page-template</option>\n'
 if HAS_TAL else '') +
 '''<option selected="selected" value="text/html">text/html</option>
 <option value="text/markdown">text/markdown</option>
 <option value="text/plain">text/plain</option>
 <option value="text/rest">text/rest</option>
-</select><textarea cols="80" id="description-subject:%(eid)s" name="description-subject:%(eid)s" onkeyup="autogrow(this)" rows="2" tabindex="2"></textarea>''')
+</select><textarea cols="80" id="description-subject:%(eid)s" name="description-subject:%(eid)s" onkeyup="autogrow(this)" rows="2"></textarea>''')
 
 
     def test_richtextfield_2(self):
         with self.admin_access.web_request() as req:
             req.use_fckeditor = lambda: True
-            self._test_richtextfield(req, '<input name="description_format-subject:%(eid)s" type="hidden" value="text/html" /><textarea cols="80" cubicweb:type="wysiwyg" id="description-subject:%(eid)s" name="description-subject:%(eid)s" onkeyup="autogrow(this)" rows="2" tabindex="1"></textarea>')
+            self._test_richtextfield(req, '<input name="description_format-subject:%(eid)s" type="hidden" value="text/html" /><textarea cols="80" cubicweb:type="wysiwyg" id="description-subject:%(eid)s" name="description-subject:%(eid)s" onkeyup="autogrow(this)" rows="2"></textarea>')
 
 
     def test_filefield(self):
@@ -229,11 +236,11 @@
                                      data=Binary(b'new widgets system'))
             form = FFForm(req, redirect_path='perdu.com', entity=file)
             self.assertMultiLineEqual(self._render_entity_field(req, 'data', form),
-                              '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" tabindex="1" type="file" value="" />
+                              '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" type="file" value="" />
 <a href="javascript: toggleVisibility(&#39;data-subject:%(eid)s-advanced&#39;)" title="show advanced fields"><img src="http://testing.fr/cubicweb/data/puce_down.png" alt="show advanced fields"/></a>
 <div id="data-subject:%(eid)s-advanced" class="hidden">
-<label for="data_format-subject:%(eid)s">data_format</label><input id="data_format-subject:%(eid)s" maxlength="50" name="data_format-subject:%(eid)s" size="45" tabindex="2" type="text" value="text/plain" /><br/>
-<label for="data_encoding-subject:%(eid)s">data_encoding</label><input id="data_encoding-subject:%(eid)s" maxlength="20" name="data_encoding-subject:%(eid)s" size="20" tabindex="3" type="text" value="UTF-8" /><br/>
+<label for="data_format-subject:%(eid)s">data_format</label><input id="data_format-subject:%(eid)s" maxlength="50" name="data_format-subject:%(eid)s" size="45" type="text" value="text/plain" /><br/>
+<label for="data_encoding-subject:%(eid)s">data_encoding</label><input id="data_encoding-subject:%(eid)s" maxlength="20" name="data_encoding-subject:%(eid)s" size="20" type="text" value="UTF-8" /><br/>
 </div>
 <br/>
 <input name="data-subject__detach:%(eid)s" type="checkbox" />
@@ -253,17 +260,17 @@
                                      data=Binary(b'new widgets system'))
             form = EFFForm(req, redirect_path='perdu.com', entity=file)
             self.assertMultiLineEqual(self._render_entity_field(req, 'data', form),
-                              '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" tabindex="1" type="file" value="" />
+                              '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" type="file" value="" />
 <a href="javascript: toggleVisibility(&#39;data-subject:%(eid)s-advanced&#39;)" title="show advanced fields"><img src="http://testing.fr/cubicweb/data/puce_down.png" alt="show advanced fields"/></a>
 <div id="data-subject:%(eid)s-advanced" class="hidden">
-<label for="data_format-subject:%(eid)s">data_format</label><input id="data_format-subject:%(eid)s" maxlength="50" name="data_format-subject:%(eid)s" size="45" tabindex="2" type="text" value="text/plain" /><br/>
-<label for="data_encoding-subject:%(eid)s">data_encoding</label><input id="data_encoding-subject:%(eid)s" maxlength="20" name="data_encoding-subject:%(eid)s" size="20" tabindex="3" type="text" value="UTF-8" /><br/>
+<label for="data_format-subject:%(eid)s">data_format</label><input id="data_format-subject:%(eid)s" maxlength="50" name="data_format-subject:%(eid)s" size="45" type="text" value="text/plain" /><br/>
+<label for="data_encoding-subject:%(eid)s">data_encoding</label><input id="data_encoding-subject:%(eid)s" maxlength="20" name="data_encoding-subject:%(eid)s" size="20" type="text" value="UTF-8" /><br/>
 </div>
 <br/>
 <input name="data-subject__detach:%(eid)s" type="checkbox" />
 detach attached file
 <p><b>You can either submit a new file using the browse button above, or choose to remove already uploaded file by checking the "detach attached file" check-box, or edit file content online with the widget below.</b></p>
-<textarea cols="80" name="data-subject:%(eid)s" onkeyup="autogrow(this)" rows="3" tabindex="4">new widgets system</textarea>''' % {'eid': file.eid})
+<textarea cols="80" name="data-subject:%(eid)s" onkeyup="autogrow(this)" rows="3">new widgets system</textarea>''' % {'eid': file.eid})
 
     def _modified_tzdatenaiss(self, eid, date_and_time_str=None):
         ctx = {}
@@ -303,9 +310,9 @@
         with self.admin_access.web_request() as req:
             form = PFForm(req, redirect_path='perdu.com', entity=req.user)
             self.assertMultiLineEqual(self._render_entity_field(req, 'upassword', form),
-                                  '''<input id="upassword-subject:%(eid)s" name="upassword-subject:%(eid)s" tabindex="1" type="password" value="" />
+                                  '''<input id="upassword-subject:%(eid)s" name="upassword-subject:%(eid)s" type="password" value="" />
 <br/>
-<input name="upassword-subject-confirm:%(eid)s" tabindex="1" type="password" value="" />
+<input name="upassword-subject-confirm:%(eid)s" type="password" value="" />
 &#160;
 <span class="emphasis">confirm password</span>''' % {'eid': req.user.eid})
 
@@ -319,4 +326,5 @@
     #     self.assertEqual(init, cur)
 
 if __name__ == '__main__':
-    unittest_main()
+    import unittest
+    unittest.main()
--- a/cubicweb/web/test/unittest_formwidgets.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_formwidgets.py	Mon Mar 20 10:28:01 2017 +0100
@@ -38,7 +38,7 @@
     def test_bitselect_widget(self):
         field = formfields.guess_field(self.schema['CWAttribute'], self.schema['ordernum'])
         field.choices = [('un', '1',), ('deux', '2',)]
-        widget = formwidgets.BitSelect(settabindex=False)
+        widget = formwidgets.BitSelect()
         req = fake.FakeRequest(form={'ordernum-subject:A': ['1', '2']})
         form = mock(_cw=req, formvalues={}, edited_entity=mock(eid='A'),
                     form_previous_values=())
@@ -62,7 +62,7 @@
             field = form.field_by_name('bool')
             widget = field.widget
             self.assertMultiLineEqual(widget._render(form, field, None),
-                '<label><input id="bool" name="bool" tabindex="1" '
+                '<label><input id="bool" name="bool" '
                 'type="checkbox" value="1" />&#160;'
                 'python &gt;&gt; others</label>')
 
--- a/cubicweb/web/test/unittest_http_headers.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_http_headers.py	Mon Mar 20 10:28:01 2017 +0100
@@ -13,6 +13,7 @@
         with self.assertRaises(ValueError):
             http_headers.generateTrueFalse('any value')
 
+
 if __name__ == '__main__':
     from unittest import main
     main()
--- a/cubicweb/web/test/unittest_reledit.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_reledit.py	Mon Mar 20 10:28:01 2017 +0100
@@ -73,13 +73,13 @@
 <table class="">
 <tr class="title_subject_row">
 <td>
-<input id="title-subject:%(eid)s" maxlength="32" name="title-subject:%(eid)s" size="32" tabindex="1" type="text" value="cubicweb-world-domination" />
+<input id="title-subject:%(eid)s" maxlength="32" name="title-subject:%(eid)s" size="32" type="text" value="cubicweb-world-domination" />
 </td></tr>
 </table></fieldset>
 <table class="buttonbar">
 <tr>
-<td><button class="validateButton" tabindex="2" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
-<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;title-subject-%(eid)s&#39;)" tabindex="3" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
+<td><button class="validateButton" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;title-subject-%(eid)s&#39;)" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
 </tr></table>
 </fieldset>
 <iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
@@ -108,23 +108,23 @@
 <tr class="title_subject_row">
 <th class="labelCol"><label class="required" for="title-subject:A">title</label></th>
 <td>
-<input id="title-subject:A" maxlength="50" name="title-subject:A" size="45" tabindex="4" type="text" value="" />
+<input id="title-subject:A" maxlength="50" name="title-subject:A" size="45" type="text" value="" />
 </td></tr>
 <tr class="description_subject_row">
 <th class="labelCol"><label for="description-subject:A">description</label></th>
 <td>
-<input name="description_format-subject:A" type="hidden" value="text/html" /><textarea cols="80" cubicweb:type="wysiwyg" id="description-subject:A" name="description-subject:A" onkeyup="autogrow(this)" rows="2" tabindex="5"></textarea>
+<input name="description_format-subject:A" type="hidden" value="text/html" /><textarea cols="80" cubicweb:type="wysiwyg" id="description-subject:A" name="description-subject:A" onkeyup="autogrow(this)" rows="2"></textarea>
 </td></tr>
 <tr class="rss_url_subject_row">
 <th class="labelCol"><label for="rss_url-subject:A">rss_url</label></th>
 <td>
-<input id="rss_url-subject:A" maxlength="128" name="rss_url-subject:A" size="45" tabindex="6" type="text" value="" />
+<input id="rss_url-subject:A" maxlength="128" name="rss_url-subject:A" size="45" type="text" value="" />
 </td></tr>
 </table></fieldset>
 <table class="buttonbar">
 <tr>
-<td><button class="validateButton" tabindex="7" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
-<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;long_desc-subject-%(eid)s&#39;)" tabindex="8" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
+<td><button class="validateButton" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;long_desc-subject-%(eid)s&#39;)" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
 </tr></table>
 </fieldset>
 <iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
@@ -152,7 +152,7 @@
 <table class="">
 <tr class="manager_subject_row">
 <td>
-<select id="manager-subject:%(eid)s" name="manager-subject:%(eid)s" size="1" tabindex="9">
+<select id="manager-subject:%(eid)s" name="manager-subject:%(eid)s" size="1">
 <option value="__cubicweb_internal_field__"></option>
 <option value="%(toto)s">Toto</option>
 </select>
@@ -160,8 +160,8 @@
 </table></fieldset>
 <table class="buttonbar">
 <tr>
-<td><button class="validateButton" tabindex="10" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
-<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;manager-subject-%(eid)s&#39;)" tabindex="11" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
+<td><button class="validateButton" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;manager-subject-%(eid)s&#39;)" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
 </tr></table>
 </fieldset>
 <iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
--- a/cubicweb/web/test/unittest_request.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_request.py	Mon Mar 20 10:28:01 2017 +0100
@@ -71,28 +71,13 @@
 
 class WebRequestTC(unittest.TestCase):
 
-    def test_base_url(self):
-        dummy_vreg = FakeCWRegistryStore(FakeConfig(), initlog=False)
-        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))
-
     def test_negotiated_language(self):
         vreg = FakeCWRegistryStore(FakeConfig(), initlog=False)
         vreg.config.translations = {'fr': (None, None), 'en': (None, None)}
         headers = {
             'Accept-Language': 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3',
         }
-        req = CubicWebRequestBase(vreg, https=False, headers=headers)
+        req = CubicWebRequestBase(vreg, headers=headers)
         self.assertEqual(req.negotiated_language(), 'fr')
 
     def test_build_url_language_from_url(self):
@@ -100,7 +85,7 @@
         vreg.config['base-url'] = 'http://testing.fr/cubicweb/'
         vreg.config['language-mode'] = 'url-prefix'
         vreg.config.translations['fr'] = text_type, text_type
-        req = CubicWebRequestBase(vreg, https=False)
+        req = CubicWebRequestBase(vreg)
         # Override from_controller to avoid getting into relative_path method,
         # which is not implemented in CubicWebRequestBase.
         req.from_controller = lambda : 'not view'
--- a/cubicweb/web/test/unittest_uicfg.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_uicfg.py	Mon Mar 20 10:28:01 2017 +0100
@@ -16,6 +16,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/>.
 import copy
+import warnings
+
 from logilab.common.testlib import tag
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web import uihelper, formwidgets as fwdgs
@@ -70,7 +72,10 @@
     def test_uihelper_set_fields_order(self):
         afk_get = uicfg.autoform_field_kwargs.get
         self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {})
-        uihelper.set_fields_order('CWUser', ('login', 'firstname', 'surname'))
+        with warnings.catch_warnings(record=True) as w:
+            uihelper.set_fields_order('CWUser', ('login', 'firstname', 'surname'))
+            self.assertEqual(len(w), 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
         self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {'order': 1})
 
     @tag('uicfg', 'order', 'func')
@@ -86,7 +91,10 @@
         afk_get = uicfg.autoform_field_kwargs.get
         self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {})
         wdg = fwdgs.TextInput({'size': 30})
-        uihelper.set_field_kwargs('CWUser', 'firstname', widget=wdg)
+        with warnings.catch_warnings(record=True) as w:
+            uihelper.set_field_kwargs('CWUser', 'firstname', widget=wdg)
+            self.assertEqual(len(w), 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
         self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {'widget': wdg})
 
     @tag('uihelper', 'hidden', 'func')
@@ -95,11 +103,17 @@
         section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject')
         self.assertCountEqual(section_conf, ['main_attributes', 'muledit_attributes'])
         # hide field in main form
-        uihelper.hide_fields('CWUser', ('login', 'in_group'))
+        with warnings.catch_warnings(record=True) as w:
+            uihelper.hide_fields('CWUser', ('login', 'in_group'))
+            self.assertEqual(len(w), 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
         section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject')
         self.assertCountEqual(section_conf, ['main_hidden', 'muledit_attributes'])
         # hide field in muledit form
-        uihelper.hide_fields('CWUser', ('login', 'in_group'), formtype='muledit')
+        with warnings.catch_warnings(record=True) as w:
+            uihelper.hide_fields('CWUser', ('login', 'in_group'), formtype='muledit')
+            self.assertEqual(len(w), 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
         section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject')
         self.assertCountEqual(section_conf, ['main_hidden', 'muledit_hidden'])
 
--- a/cubicweb/web/test/unittest_views_basecontrollers.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_views_basecontrollers.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -27,22 +27,20 @@
 from logilab.common.testlib import unittest_main
 from logilab.common.decorators import monkeypatch
 
-from cubicweb import Binary, NoSelectableObject, ValidationError, AuthenticationError
+from cubicweb import Binary, NoSelectableObject, ValidationError, transaction as tx
 from cubicweb.schema import RRQLExpression
+from cubicweb.predicates import is_instance
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.webtest import CubicWebTestTC
 from cubicweb.devtools.httptest import CubicWebServerTC
 from cubicweb.utils import json_dumps
 from cubicweb.uilib import rql_for_eid
-from cubicweb.web import Redirect, RemoteCallFailed, http_headers
-import cubicweb.server.session
-from cubicweb.server.session import Connection
+from cubicweb.web import Redirect, RemoteCallFailed, http_headers, formfields as ff
 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
+from cubicweb.server.session import Connection
 from cubicweb.server.hook import Hook, Operation
-from cubicweb.predicates import is_instance
 
 
 class ViewControllerTC(CubicWebTestTC):
@@ -617,6 +615,58 @@
             finally:
                 blogentry.__class__.cw_skip_copy_for = []
 
+    def test_avoid_multiple_process_posted(self):
+        # test that when some entity is being created and data include non-inlined relations, the
+        # values for this relation are stored for later usage, without calling twice field's
+        # process_form method, which may be unexpected for custom fields
+
+        orig_process_posted = ff.RelationField.process_posted
+
+        def count_process_posted(self, form):
+            res = list(orig_process_posted(self, form))
+            nb_process_posted_calls[0] += 1
+            return res
+
+        ff.RelationField.process_posted = count_process_posted
+
+        try:
+            with self.admin_access.web_request() as req:
+                gueid = req.execute('CWGroup G WHERE G name "users"')[0][0]
+                req.form = {
+                    'eid': 'X',
+                    '__type:X': 'CWUser',
+                    '_cw_entity_fields:X': 'login-subject,upassword-subject,in_group-subject',
+                    'login-subject:X': u'adim',
+                    'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto',
+                    'in_group-subject:X': repr(gueid),
+                }
+                nb_process_posted_calls = [0]
+                self.expect_redirect_handle_request(req, 'edit')
+                self.assertEqual(nb_process_posted_calls[0], 1)
+                user = req.find('CWUser', login=u'adim').one()
+                self.assertEqual(set(g.eid for g in user.in_group), set([gueid]))
+                req.form = {
+                    'eid': ['X', 'Y'],
+                    '__type:X': 'CWUser',
+                    '_cw_entity_fields:X': 'login-subject,upassword-subject,in_group-subject',
+                    'login-subject:X': u'dlax',
+                    'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto',
+                    'in_group-subject:X': repr(gueid),
+
+                    '__type:Y': 'EmailAddress',
+                    '_cw_entity_fields:Y': 'address-subject,use_email-object',
+                    'address-subject:Y': u'dlax@cw.org',
+                    'use_email-object:Y': 'X',
+                }
+                nb_process_posted_calls = [0]
+                self.expect_redirect_handle_request(req, 'edit')
+                self.assertEqual(nb_process_posted_calls[0], 3)  # 3 = 1 (in_group) + 2 (use_email)
+                user = req.find('CWUser', login=u'dlax').one()
+                self.assertEqual(set(e.address for e in user.use_email), set(['dlax@cw.org']))
+
+        finally:
+            ff.RelationField.process_posted = orig_process_posted
+
     def test_nonregr_eetype_etype_editing(self):
         """non-regression test checking that a manager user can edit a CWEType entity
         """
@@ -669,7 +719,6 @@
             self.assertEqual(e.title, '"13:03:40"')
             self.assertEqual(e.content, '"13:03:43"')
 
-
     def test_nonregr_multiple_empty_email_addr(self):
         with self.admin_access.web_request() as req:
             gueid = req.execute('CWGroup G WHERE G name "users"')[0][0]
@@ -835,7 +884,8 @@
                 (self.schema['tags'].rdefs['Tag', 'CWUser'],
                  {'delete': (RRQLExpression('S owned_by U'), )}, )):
             with self.admin_access.web_request(rql='CWUser P WHERE P login "John"',
-                                   pageid='123', fname='view') as req:
+                                               pageid='123', fname='view',
+                                               session=req.session) as req:
                 ctrl = self.ctrl(req)
                 rset = self.john.as_rset()
                 rset.req = req
@@ -849,7 +899,8 @@
             self.assertEqual(deletes, [])
             inserts = get_pending_inserts(req)
             self.assertEqual(inserts, ['12:tags:13'])
-        with self.remote_calling('add_pending_inserts', [['12', 'tags', '14']]) as (_, req):
+        with self.remote_calling('add_pending_inserts', [['12', 'tags', '14']],
+                                 session=req.session) as (_, req):
             deletes = get_pending_deletes(req)
             self.assertEqual(deletes, [])
             inserts = get_pending_inserts(req)
@@ -868,7 +919,8 @@
             self.assertEqual(inserts, [])
             deletes = get_pending_deletes(req)
             self.assertEqual(deletes, ['12:tags:13'])
-        with self.remote_calling('add_pending_delete', ['12', 'tags', '14']) as (_, req):
+        with self.remote_calling('add_pending_delete', ['12', 'tags', '14'],
+                                 session=req.session) as (_, req):
             inserts = get_pending_inserts(req)
             self.assertEqual(inserts, [])
             deletes = get_pending_deletes(req)
@@ -882,9 +934,10 @@
             req.remove_pending_operations()
 
     def test_remove_pending_operations(self):
-        with self.remote_calling('add_pending_delete', ['12', 'tags', '13']):
+        with self.remote_calling('add_pending_delete', ['12', 'tags', '13']) as (_, req):
             pass
-        with self.remote_calling('add_pending_inserts', [['12', 'tags', '14']]) as (_, req):
+        with self.remote_calling('add_pending_inserts', [['12', 'tags', '14']],
+                                 session=req.session) as (_, req):
             inserts = get_pending_inserts(req)
             self.assertEqual(inserts, ['12:tags:14'])
             deletes = get_pending_deletes(req)
--- a/cubicweb/web/test/unittest_views_baseviews.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_views_baseviews.py	Mon Mar 20 10:28:01 2017 +0100
@@ -156,5 +156,138 @@
                                       b'<head>'],
                                      source_lines[:3])
 
+class BaseViewsTC(CubicWebTC):
+
+    def test_null(self):
+        with self.admin_access.web_request() as req:
+            rset = req.execute('Any X WHERE X login "admin"')
+            result = req.view('null', rset)
+            self.assertEqual(result, u'')
+
+    def test_final(self):
+        with self.admin_access.web_request() as req:
+            rset = req.execute('Any "<script></script>"')
+            result = req.view('final', rset)
+            self.assertEqual(result, u'&lt;script&gt;&lt;/script&gt;')
+
+    def test_incontext(self):
+        with self.admin_access.web_request() as req:
+            entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+            result = entity.view('incontext')
+            expected = (u'<a href="http://testing.fr/cubicweb/%d" title="">'
+                        u'&lt;script&gt;&lt;/script&gt;</a>' % entity.eid)
+            self.assertEqual(result, expected)
+
+    def test_outofcontext(self):
+        with self.admin_access.web_request() as req:
+            entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+            result = entity.view('outofcontext')
+            expect = (u'<a href="http://testing.fr/cubicweb/%d" title="">'
+                      u'&lt;script&gt;&lt;/script&gt;</a>' % entity.eid)
+            self.assertEqual(result, expect)
+
+    def test_outofcontext(self):
+        with self.admin_access.web_request() as req:
+            entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+            result = entity.view('oneline')
+            expect = (u'<a href="http://testing.fr/cubicweb/%d" title="">'
+                      u'&lt;script&gt;&lt;/script&gt;</a>' % entity.eid)
+            self.assertEqual(result, expect)
+
+    def test_text(self):
+        with self.admin_access.web_request() as req:
+            entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+            result = entity.view('text')
+            self.assertEqual(result, u'<script></script>')
+
+    def test_textincontext(self):
+        with self.admin_access.web_request() as req:
+            entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+            result = entity.view('textincontext')
+            self.assertEqual(result, u'<script></script>')
+
+    def test_textoutofcontext(self):
+        with self.admin_access.web_request() as req:
+            entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+            result = entity.view('textoutofcontext')
+            self.assertEqual(result, u'<script></script>')
+
+    def test_list(self):
+        with self.admin_access.web_request() as req:
+            entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+            rset = req.execute('Any X WHERE X is CWUser')
+            result = req.view('list', rset)
+            expected = u'''<ul class="section">
+<li><a href="http://testing.fr/cubicweb/%d" title="">&lt;script&gt;&lt;/script&gt;</a></li>
+<li><a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></li>
+<li><a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></li>
+</ul>
+''' % entity.eid
+            self.assertEqual(result, expected)
+
+    def test_simplelist(self):
+        with self.admin_access.web_request() as req:
+            entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+            rset = req.execute('Any X WHERE X is CWUser')
+            result = req.view('simplelist', rset)
+            expected = (
+                u'<div class="section">'
+                u'<a href="http://testing.fr/cubicweb/%d" title="">'
+                u'&lt;script&gt;&lt;/script&gt;</a></div>'
+                u'<div class="section">'
+                u'<a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div>'
+                u'<div class="section">'
+                u'<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div>'
+                % entity.eid
+            )
+            self.assertEqual(result, expected)
+
+    def test_sameetypelist(self):
+        with self.admin_access.web_request() as req:
+            entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+            rset = req.execute('Any X WHERE X is CWUser')
+            result = req.view('sameetypelist', rset)
+            expected = (
+                u'<h1>CWUser_plural</h1>'
+                u'<div class="section">'
+                u'<a href="http://testing.fr/cubicweb/%d" title="">'
+                u'&lt;script&gt;&lt;/script&gt;</a></div>'
+                u'<div class="section">'
+                u'<a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div>'
+                u'<div class="section">'
+                u'<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div>'
+                % entity.eid
+            )
+            self.assertEqual(expected, result)
+
+    def test_sameetypelist(self):
+        with self.admin_access.web_request() as req:
+            entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+            rset = req.execute('Any X WHERE X is CWUser')
+            result = req.view('csv', rset)
+            expected = (
+                u'<a href="http://testing.fr/cubicweb/%d" title="">&lt;script&gt;&lt;/script&gt;</a>, '
+                u'<a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a>, '
+                u'<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a>'
+                % entity.eid
+            )
+            self.assertEqual(result, expected)
+
+    def test_metadata(self):
+        with self.admin_access.web_request() as req:
+            entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+            entity.cw_set(creation_date=u'2000-01-01 00:00:00')
+            entity.cw_set(modification_date=u'2015-01-01 00:00:00')
+            result = entity.view('metadata')
+            expected = (
+                u'<div>CWUser #%d - <span>latest update on</span>'
+                u' <span class="value">2015/01/01</span>,'
+                u' <span>created on</span>'
+                u' <span class="value">2000/01/01</span></div>'
+                % entity.eid
+            )
+            self.assertEqual(result, expected)
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/cubicweb/web/test/unittest_views_editforms.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_views_editforms.py	Mon Mar 20 10:28:01 2017 +0100
@@ -20,7 +20,9 @@
 from logilab.common import tempattr
 
 from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.predicates import is_instance
 from cubicweb.web.views import uicfg
+from cubicweb.web.views.editforms import DeleteConfFormView
 from cubicweb.web.formwidgets import AutoCompletionWidget
 from cubicweb.schema import RRQLExpression
 
@@ -239,6 +241,59 @@
             rset = req.execute('CWGroup X')
             self.view('deleteconf', rset, template=None, req=req).source
 
+    def test_delete_conf_formview_composite(self):
+        with self.admin_access.cnx() as cnx:
+            d1 = cnx.create_entity('Directory', name=u'dtest1')
+            d2 = cnx.create_entity('Directory', name=u'dtest2', parent=d1)
+            d3 = cnx.create_entity('Directory', name=u'dtest3', parent=d2)
+            d4 = cnx.create_entity('Directory', name=u'dtest4', parent=d1)
+            for i in range(3):
+                cnx.create_entity('Directory', name=u'child%s' % (i,),
+                                  parent=d3)
+            cnx.commit()
+
+        class DirectoryDeleteView(DeleteConfFormView):
+            __select__ = (DeleteConfFormView.__select__ &
+                          is_instance('Directory'))
+            show_composite = True
+
+        self.vreg['propertyvalues']['navigation.page-size'] = 3
+        with self.admin_access.web_request() as req, \
+                self.temporary_appobjects(DirectoryDeleteView):
+            rset = req.execute('Directory X WHERE X name "dtest1"')
+            source = self.view('deleteconf', rset,
+                               template=None, req=req).source.decode('utf-8')
+            # Show composite object at depth 1
+            # Don't display "And more composite entities" since their are equal
+            # to page size
+            expected = (
+                '<li>'
+                '<a href="http://testing.fr/cubicweb/directory/%s">dtest1</a>'
+                '<ul class="treeview"><li>'
+                '<a href="http://testing.fr/cubicweb/directory/%s">dtest4</a>'
+                '</li><li class="last">'
+                '<a href="http://testing.fr/cubicweb/directory/%s">dtest2</a>'
+                '</li></ul></li>') % (d1.eid, d4.eid, d2.eid)
+            self.assertIn(expected, source)
+
+            # Page size is reached, show "And more composite entities"
+            rset = req.execute('Directory X WHERE X name "dtest3"')
+            source = self.view('deleteconf', rset,
+                               template=None, req=req).source.decode('utf-8')
+            expected = (
+                '<li>'
+                '<a href="http://testing.fr/cubicweb/directory/%s">dtest3</a>'
+                '<ul class="treeview"><li>'
+                '<a href="http://testing.fr/cubicweb/directory/%s">child2</a>'
+                '</li><li>'
+                '<a href="http://testing.fr/cubicweb/directory/%s">child1</a>'
+                '</li><li class="last">And more composite entities</li>'
+                '</ul></li>') % (
+                    d3.eid,
+                    req.find('Directory', name='child2').one().eid,
+                    req.find('Directory', name='child1').one().eid)
+            self.assertIn(expected, source)
+
     def test_automatic_edition_formview(self):
         with self.admin_access.web_request() as req:
             rset = req.execute('CWUser X')
--- a/cubicweb/web/test/unittest_views_forms.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_views_forms.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -20,6 +20,8 @@
 
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web.views.autoform import InlinedFormField
+from cubicweb.web.views.forms import EntityFieldsForm
+
 
 class InlinedFormTC(CubicWebTC):
 
@@ -68,7 +70,21 @@
                                                   InlinedFormField(view=formview)])
                 self.assertTrue(formview._get_removejs())
 
+    def test_field_by_name_consider_aff(self):
+        class MyField(object):
+            def __init__(self, *args, **kwargs):
+                pass
+
+        EntityFieldsForm.uicfg_aff.tag_attribute(('CWUser', 'firstname'), MyField)
+        try:
+            with self.admin_access.web_request() as req:
+                form = req.vreg['forms'].select('base', req, entity=req.user)
+                self.assertIsInstance(form.field_by_name('firstname', 'subject', req.user.e_schema),
+                                      MyField)
+        finally:
+            EntityFieldsForm.uicfg_aff.del_rtag('CWUser', 'firstname', '*', 'subject')
+
 
 if __name__ == '__main__':
-    from logilab.common.testlib import unittest_main
-    unittest_main()
+    import unittest
+    unittest.main()
--- a/cubicweb/web/test/unittest_views_searchrestriction.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_views_searchrestriction.py	Mon Mar 20 10:28:01 2017 +0100
@@ -23,8 +23,9 @@
 class InsertAttrRelationTC(CubicWebTC):
 
     def parse(self, query):
-        rqlst = self.vreg.parse(self.session, query)
-        select = rqlst.children[0]
+        with self.admin_access.cnx() as cnx:
+            rqlst = self.vreg.parse(cnx, query)
+            rqlst.children[0]
         return rqlst
 
     def _generate(self, rqlst, rel, role, attr):
--- a/cubicweb/web/views/actions.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/actions.py	Mon Mar 20 10:28:01 2017 +0100
@@ -48,7 +48,7 @@
         # display action anyway
         form = entity._cw.vreg['forms'].select('edition', entity._cw,
                                                entity=entity, mainform=False)
-        for dummy in form.editable_relations():
+        for dummy in form.iter_editable_relations():
             return 1
         for dummy in form.inlined_form_views():
             return 1
--- a/cubicweb/web/views/authentication.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/authentication.py	Mon Mar 20 10:28:01 2017 +0100
@@ -17,16 +17,18 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """user authentication component"""
 
-
-
 from logilab.common.deprecation import class_renamed
+from logilab.common.textutils import unormalize
 
 from cubicweb import AuthenticationError
+from cubicweb.utils import make_uid
 from cubicweb.view import Component
 from cubicweb.web import InvalidSession
+from cubicweb.server.session import Connection
 
 
-class NoAuthInfo(Exception): pass
+class NoAuthInfo(Exception):
+    pass
 
 
 class WebAuthInfoRetriever(Component):
@@ -67,6 +69,7 @@
         """
         pass
 
+
 WebAuthInfoRetreiver = class_renamed(
     'WebAuthInfoRetreiver', WebAuthInfoRetriever,
     '[3.17] WebAuthInfoRetreiver had been renamed into WebAuthInfoRetriever '
@@ -92,12 +95,45 @@
     def revalidate_login(self, req):
         return req.get_authorization()[0]
 
+
 LoginPasswordRetreiver = class_renamed(
     'LoginPasswordRetreiver', LoginPasswordRetriever,
     '[3.17] LoginPasswordRetreiver had been renamed into LoginPasswordRetriever '
     '("ie" instead of "ei")')
 
 
+class Session(object):
+    """In-memory user session
+    """
+
+    def __init__(self, repo, user):
+        self.user = user  # XXX deprecate and store only a login.
+        self.repo = repo
+        self.sessionid = make_uid(unormalize(user.login))
+        self.data = {}
+
+    def __unicode__(self):
+        return '<session %s (0x%x)>' % (unicode(self.user.login), id(self))
+
+    @property
+    def anonymous_session(self):
+        # XXX for now, anonymous_user only exists in webconfig (and testconfig).
+        # It will only be present inside all-in-one instance.
+        # there is plan to move it down to global config.
+        if not hasattr(self.repo.config, 'anonymous_user'):
+            # not a web or test config, no anonymous user
+            return False
+        return self.user.login == self.repo.config.anonymous_user()[0]
+
+    def new_cnx(self):
+        """Return a new Connection object linked to the session
+
+        The returned Connection will *not* be managed by the Session.
+        """
+        cnx = Connection(self.repo, self.user)
+        cnx.session = self
+        return cnx
+
 
 class RepositoryAuthenticationManager(object):
     """authenticate user associated to a request and check session validity"""
@@ -133,7 +169,7 @@
         # check session.login and not user.login, since in case of login by
         # email, login and cnx.login are the email while user.login is the
         # actual user login
-        if login and session.login != login:
+        if login and session.user.login != login:
             raise InvalidSession('login mismatch')
 
     def authenticate(self, req):
@@ -155,7 +191,7 @@
                 session = self._authenticate(login, authinfo)
             except AuthenticationError:
                 retriever.cleanup_authentication_information(req)
-                continue # the next one may succeed
+                continue  # the next one may succeed
             for retriever_ in self.authinforetrievers:
                 retriever_.authenticated(retriever, req, session, login, authinfo)
             return session, login
@@ -170,4 +206,6 @@
         raise AuthenticationError()
 
     def _authenticate(self, login, authinfo):
-        return self.repo.new_session(login, **authinfo)
+        with self.repo.internal_cnx() as cnx:
+            user = self.repo.authenticate_user(cnx, login, **authinfo)
+        return Session(self.repo, user)
--- a/cubicweb/web/views/autoform.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/autoform.py	Mon Mar 20 10:28:01 2017 +0100
@@ -565,9 +565,9 @@
                 w(u'</tr>')
         w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid)
         w(u'<th class="labelCol">')
-        w(u'<select id="relationSelector_%s" tabindex="%s" '
+        w(u'<select id="relationSelector_%s" '
           'onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">'
-          % (eid, req.next_tabindex(), xml_escape(json_dumps(eid))))
+          % (eid, xml_escape(json_dumps(eid))))
         w(u'<option value="">%s</option>' % _('select a relation'))
         for i18nrtype, rschema, role in field.relations:
             # more entities to link to
@@ -861,24 +861,25 @@
         """return a list of (relation schema, role) to edit for the entity"""
         if self.display_fields is not None:
             schema = self._cw.vreg.schema
-            return [(schema[rtype], role) for rtype, role in self.display_fields]
+            for rtype, role in self.display_fields:
+                yield (schema[rtype], role)
         if self.edited_entity.has_eid() and not self.edited_entity.cw_has_perm('update'):
-            return []
+            return
         action = 'update' if self.edited_entity.has_eid() else 'add'
-        return [(rtype, role) for rtype, _, role in self._relations_by_section(
-            'attributes', action, strict)]
+        for rtype, _, role in self._relations_by_section('attributes', action, strict):
+            yield (rtype, role)
 
     def editable_relations(self):
         """return a sorted list of (relation's label, relation'schema, role) for
         relations in the 'relations' section
         """
-        result = []
-        for rschema, _, role in self._relations_by_section('relations',
-                                                           strict=True):
-            result.append( (rschema.display_name(self.edited_entity._cw, role,
-                                                 self.edited_entity.cw_etype),
-                            rschema, role) )
-        return sorted(result)
+        return sorted(self.iter_editable_relations())
+
+    def iter_editable_relations(self):
+        for rschema, _, role in self._relations_by_section('relations', strict=True):
+            yield (rschema.display_name(self.edited_entity._cw, role,
+                                        self.edited_entity.cw_etype),
+                   rschema, role)
 
     def inlined_relations(self):
         """return a list of (relation schema, target schemas, role) matching
@@ -889,10 +890,8 @@
     # inlined forms control ####################################################
 
     def inlined_form_views(self):
-        """compute and return list of inlined form views (hosting the inlined
-        form object)
+        """Yield inlined form views (hosting the inlined form object)
         """
-        allformviews = []
         entity = self.edited_entity
         for rschema, ttypes, role in self.inlined_relations():
             # show inline forms only if there's one possible target type
@@ -904,11 +903,15 @@
                 continue
             tschema = ttypes[0]
             ttype = tschema.type
-            formviews = list(self.inline_edition_form_view(rschema, ttype, role))
+            existing = bool(entity.related(rschema, role)) if entity.has_eid() else False
+            for formview in self.inline_edition_form_view(rschema, ttype, role):
+                yield formview
+                existing = True
             card = rschema.role_rdef(entity.e_schema, ttype, role).role_cardinality(role)
-            existing = entity.related(rschema, role) if entity.has_eid() else formviews
             if self.should_display_inline_creation_form(rschema, existing, card):
-                formviews += self.inline_creation_form_view(rschema, ttype, role)
+                for formview in self.inline_creation_form_view(rschema, ttype, role):
+                    yield formview
+                    existing = True
             # we can create more than one related entity, we thus display a link
             # to add new related entities
             if self.must_display_add_new_relation_link(rschema, role, tschema,
@@ -918,9 +921,7 @@
                     etype=ttype, rtype=rschema, role=role, card=card,
                     peid=self.edited_entity.eid,
                     petype=self.edited_entity.e_schema, pform=self)
-                formviews.append(addnewlink)
-            allformviews += formviews
-        return allformviews
+                yield addnewlink
 
     def should_display_inline_creation_form(self, rschema, existing, card):
         """return true if a creation form should be inlined
--- a/cubicweb/web/views/basecomponents.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/basecomponents.py	Mon Mar 20 10:28:01 2017 +0100
@@ -66,9 +66,9 @@
             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" />
+<input type="text" id="rql" name="rql" value="%s"  title="%s" accesskey="q" class="searchField" />
 ''' % (not self.cw_propval('visible') and 'hidden' or '',
-       req.build_url('view'), xml_escape(rql), req._('full text or RQL query'), req.next_tabindex()))
+       req.build_url('view'), xml_escape(rql), req._('full text or RQL query')))
         if req.search_state[0] != 'normal':
             self.w(u'<input type="hidden" name="__mode" value="%s"/>'
                    % ':'.join(req.search_state[1]))
@@ -169,7 +169,8 @@
     def render(self, w):
         # display useractions and siteactions
         self._cw.add_css('cubicweb.pictograms.css')
-        actions = self._cw.vreg['actions'].possible_actions(self._cw, rset=self.cw_rset)
+        actions = self._cw.vreg['actions'].possible_actions(self._cw, rset=self.cw_rset,
+                                                            view=self.cw_extra_kwargs['view'])
         box = MenuWidget('', 'userActionsBox', _class='', islist=False)
         menu = PopupBoxMenu(self._cw.user.login, isitem=False, link_class='icon-user')
         box.append(menu)
--- a/cubicweb/web/views/basetemplates.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/basetemplates.py	Mon Mar 20 10:28:01 2017 +0100
@@ -18,20 +18,20 @@
 """default templates for CubicWeb web client"""
 
 
-from cubicweb import _
-
 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 import _
+from cubicweb.predicates import match_kwargs, anonymous_user
 from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW, StartupView
 from cubicweb.utils import UStringIO
 from cubicweb.schema import display_name
-from cubicweb.web import component, formfields as ff, formwidgets as fw
+from cubicweb.web import formfields as ff, formwidgets as fw
 from cubicweb.web.views import forms
 
+
 # main templates ##############################################################
 
 class LogInOutTemplate(MainTemplate):
@@ -92,6 +92,7 @@
     if req.form.get('__modal', None):
         return 1
 
+
 @objectify_predicate
 def templatable_view(cls, req, rset, *args, **kwargs):
     view = kwargs.pop('view', None)
@@ -176,7 +177,6 @@
 
     def template_html_header(self, content_type, page_title, additional_headers=()):
         w = self.whead
-        lang = self._cw.lang
         self.write_doctype()
         self._cw.html_headers.define_var('BASE_URL', self._cw.base_url())
         self._cw.html_headers.define_var('DATA_URL', self._cw.datadir_url)
@@ -208,14 +208,13 @@
         self.w(u'</td>\n')
         self.nav_column(view, 'right')
         self.w(u'</tr></table></div>\n')
-        self.wview('footer', rset=self.cw_rset)
+        self.wview('footer', rset=self.cw_rset, view=view)
         self.w(u'</body>')
 
     def nav_column(self, view, context):
         boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects(
             self._cw, rset=self.cw_rset, view=view, context=context))
         if boxes:
-            getlayout = self._cw.vreg['components'].select
             self.w(u'<td id="navColumn%s"><div class="navboxes">\n' % context.capitalize())
             for box in boxes:
                 box.render(w=self.w, view=view)
@@ -248,7 +247,6 @@
 
     def template_header(self, content_type, view=None, page_title='', additional_headers=()):
         w = self.whead
-        lang = self._cw.lang
         self.write_doctype()
         w(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n'
           % (content_type, self._cw.encoding))
@@ -269,7 +267,6 @@
         page_title = page_title or view.page_title()
         additional_headers = additional_headers or view.html_headers()
         whead = self.whead
-        lang = self._cw.lang
         self.write_doctype()
         whead(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n'
               % (content_type, self._cw.encoding))
@@ -337,10 +334,10 @@
 
     def alternates(self):
         urlgetter = self._cw.vreg['components'].select_or_none('rss_feed_url',
-                                                           self._cw, rset=self.cw_rset)
+                                                               self._cw, rset=self.cw_rset)
         if urlgetter is not None:
             self.whead(u'<link rel="alternate" type="application/rss+xml" title="RSS feed" href="%s"/>\n'
-                       %  xml_escape(urlgetter.feed_url()))
+                       % xml_escape(urlgetter.feed_url()))
 
 
 class HTMLPageHeader(View):
@@ -398,7 +395,8 @@
 
     def footer_content(self):
         actions = self._cw.vreg['actions'].possible_actions(self._cw,
-                                                            rset=self.cw_rset)
+                                                            rset=self.cw_rset,
+                                                            view=self.cw_extra_kwargs['view'])
         footeractions = actions.get('footer', ())
         for i, action in enumerate(footeractions):
             self.w(u'<a href="%s">%s</a>' % (action.url(),
@@ -406,10 +404,9 @@
             if i < (len(footeractions) - 1):
                 self.w(u' | ')
 
+
 class HTMLContentHeader(View):
-    """default html page content header:
-    * include message component if selectable for this request
-    * include selectable content navigation components
+    """default html page content header: include selectable content navigation components
     """
     __regid__ = 'contentheader'
 
@@ -439,6 +436,7 @@
                 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
     """
@@ -461,7 +459,7 @@
                         fw.ResetButton(label=_('cancel'),
                                        attrs={'class': 'loginButton',
                                               'onclick': onclick}),]
-        ## Can't shortcut next access because __dict__ is a "dictproxy" which 
+        ## 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
@@ -474,9 +472,10 @@
             url_args = {}
             if target and target != '/':
                 url_args['postlogin_path'] = target
-            return self._cw.build_url('login', __secure__=True, **url_args)
+            return self._cw.build_url('login', **url_args)
         return super(BaseLogForm, self).form_action()
 
+
 class LogForm(BaseLogForm):
     """Simple login form that send username and password
     """
@@ -488,7 +487,7 @@
     __password = ff.StringField('__password', label=_('password'),
                                 widget=fw.PasswordSingleInput({'class': 'data'}))
 
-    onclick_args =  ('popupLoginBox', '__login')
+    onclick_args = ('popupLoginBox', '__login')
 
 
 class LogFormView(View):
@@ -531,4 +530,5 @@
         form.render(w=self.w, table_class='', display_progress_div=False)
         cw.html_headers.add_onload('jQuery("#__login:visible").focus()')
 
+
 LogFormTemplate = class_renamed('LogFormTemplate', LogFormView)
--- a/cubicweb/web/views/boxes.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/boxes.py	Mon Mar 20 10:28:01 2017 +0100
@@ -67,7 +67,7 @@
         self._menus_by_id = {}
         # build list of actions
         actions = self._cw.vreg['actions'].possible_actions(self._cw, self.cw_rset,
-                                                            **self.cw_extra_kwargs)
+                                                            view=self.cw_extra_kwargs['view'])
         other_menu = self._get_menu('moreactions', _('more actions'))
         for category, defaultmenu in (('mainactions', self),
                                       ('moreactions', other_menu),
@@ -138,11 +138,11 @@
     order = 0
     formdef = u"""<form action="%(action)s">
 <table id="%(id)s"><tr><td>
-<input class="norql" type="text" accesskey="q" tabindex="%(tabindex1)s" title="search text" value="%(value)s" name="rql" />
+<input class="norql" type="text" accesskey="q" title="search text" value="%(value)s" name="rql" />
 <input type="hidden" name="__fromsearchbox" value="1" />
 <input type="hidden" name="subvid" value="tsearch" />
 </td><td>
-<input tabindex="%(tabindex2)s" type="submit" class="rqlsubmit" value="" />
+<input type="submit" class="rqlsubmit" value="" />
  </td></tr></table>
  </form>"""
 
@@ -155,13 +155,10 @@
             rql = self._cw.form.get('rql', '')
         else:
             rql = ''
-        tabidx1 = self._cw.next_tabindex()
-        tabidx2 = self._cw.next_tabindex()
         w(self.formdef % {'action': self._cw.build_url('view'),
                           'value': xml_escape(rql),
-                          'id': self.cw_extra_kwargs.get('domid', 'tsearch'),
-                          'tabindex1': tabidx1,
-                          'tabindex2': tabidx2})
+                          'id': self.cw_extra_kwargs.get('domid', 'tsearch')
+                          })
 
 
 # boxes disabled by default ###################################################
--- a/cubicweb/web/views/calendar.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/calendar.py	Mon Mar 20 10:28:01 2017 +0100
@@ -41,12 +41,17 @@
                _('november'), _('december')
                )
 
+ICAL_EVENT = "event"
+ICAL_TODO = "todo"
 
 class ICalendarableAdapter(EntityAdapter):
     __needs_bw_compat__ = True
     __regid__ = 'ICalendarable'
     __abstract__ = True
 
+    # component type
+    component = ICAL_EVENT
+
     @property
     def start(self):
         """return start date"""
@@ -64,7 +69,7 @@
     from vobject import iCalendar
 
     class iCalView(EntityView):
-        """A calendar view that generates a iCalendar file (RFC 2445)
+        """A calendar view that generates a iCalendar file (RFC 5545)
 
         Does apply to ICalendarable compatible entities
         """
@@ -79,14 +84,21 @@
             ical = iCalendar()
             for i in range(len(self.cw_rset.rows)):
                 task = self.cw_rset.complete_entity(i, 0)
-                event = ical.add('vevent')
-                event.add('summary').value = task.dc_title()
-                event.add('description').value = task.dc_description()
-                icalendarable = task.cw_adapt_to('ICalendarable')
-                if icalendarable.start:
-                    event.add('dtstart').value = icalendarable.start
-                if icalendarable.stop:
-                    event.add('dtend').value = icalendarable.stop
+                ical_task = task.cw_adapt_to('ICalendarable')
+                if ical_task.component == ICAL_TODO:
+                    elt = ical.add('vtodo')
+                    stop_kw = "due"
+                else:
+                    elt = ical.add('vevent')
+                    stop_kw = "dtend"
+                elt.add('uid').value = task.absolute_url() # unique stable id
+                elt.add('url').value = task.absolute_url()
+                elt.add('summary').value = task.dc_title()
+                elt.add('description').value = task.dc_description()
+                if ical_task.start:
+                    elt.add('dtstart').value = ical_task.start
+                if ical_task.stop:
+                    elt.add(stop_kw).value = ical_task.stop
 
             buff = ical.serialize()
             if not isinstance(buff, unicode):
--- a/cubicweb/web/views/cwproperties.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/cwproperties.py	Mon Mar 20 10:28:01 2017 +0100
@@ -315,9 +315,8 @@
     def render(self, form, renderer):
         wdg = self.get_widget(form)
         # pylint: disable=E1101
-        wdg.attrs['tabindex'] = form._cw.next_tabindex()
-        wdg.attrs['onchange'] = "javascript:setPropValueWidget('%s', %s)" % (
-            form.edited_entity.eid, form._cw.next_tabindex())
+        wdg.attrs['onchange'] = "javascript:setPropValueWidget('%s')" % (
+            form.edited_entity.eid)
         return wdg.render(form, self, renderer)
 
     def vocabulary(self, form):
@@ -335,10 +334,8 @@
     """
     widget = PlaceHolderWidget
 
-    def render(self, form, renderer=None, tabindex=None):
+    def render(self, form, renderer=None):
         wdg = self.get_widget(form)
-        if tabindex is not None:
-            wdg.attrs['tabindex'] = tabindex
         return wdg.render(form, self, renderer)
 
     def form_init(self, form):
@@ -422,7 +419,7 @@
 
 
 @ajaxfunc(output_type='xhtml')
-def prop_widget(self, propkey, varname, tabindex=None):
+def prop_widget(self, propkey, varname):
     """specific method for CWProperty handling"""
     entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
     entity.eid = varname
@@ -431,7 +428,7 @@
     form.build_context()
     vfield = form.field_by_name('value', 'subject')
     renderer = formrenderers.FormRenderer(self._cw)
-    return vfield.render(form, renderer, tabindex=tabindex) \
+    return vfield.render(form, renderer) \
            + renderer.render_help(form, vfield)
 
 _afs = uicfg.autoform_section
--- a/cubicweb/web/views/debug.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/debug.py	Mon Mar 20 10:28:01 2017 +0100
@@ -91,7 +91,6 @@
         w(u'<h2>%s</h2>' % _('Repository'))
         w(u'<h3>%s</h3>' % _('resources usage'))
         stats = self._cw.call_service('repo_stats')
-        stats['looping_tasks'] = ', '.join('%s (%s seconds)' % (n, i) for n, i in stats['looping_tasks'])
         stats['threads'] = ', '.join(sorted(stats['threads']))
         for k in stats:
             if k == 'type_cache_size':
@@ -104,22 +103,6 @@
         pyvalue = [(sname, format_stat(sname, sval))
                     for sname, sval in sorted(stats.items())]
         self.wview('pyvaltable', pyvalue=pyvalue, header_column_idx=0)
-        # open repo sessions
-        if req.user.is_in_group('managers'):
-            w(u'<h3>%s</h3>' % _('opened sessions'))
-            sessions = repo._sessions.values()
-            if sessions:
-                w(u'<ul>')
-                for session in sessions:
-                    w(u'<li>%s (%s: %s)<br/>' % (
-                        xml_escape(text_type(session)),
-                        _('last usage'),
-                        strftime(dtformat, localtime(session.timestamp))))
-                    dict_to_html(w, session.data)
-                    w(u'</li>')
-                w(u'</ul>')
-            else:
-                w(u'<p>%s</p>' % _('no repository sessions found'))
         # web server information
         w(u'<h2>%s</h2>' % _('Web server'))
         pyvalue = ((_('base url'), req.base_url()),
--- a/cubicweb/web/views/editcontroller.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/editcontroller.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -17,8 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """The edit controller, automatically handling entity form submitting"""
 
-
-
 from warnings import warn
 from collections import defaultdict
 
@@ -26,16 +24,14 @@
 
 from six import text_type
 
-from logilab.common.deprecation import deprecated
 from logilab.common.graph import ordered_nodes
 
 from rql.utils import rqlvar_maker
 
-from cubicweb import _, Binary, ValidationError, UnknownEid
+from cubicweb import _, ValidationError, UnknownEid
 from cubicweb.view import EntityAdapter
 from cubicweb.predicates import is_instance
-from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit,
-                          ProcessFormError)
+from cubicweb.web import RequestError, NothingToEdit, ProcessFormError
 from cubicweb.web.views import basecontrollers, autoform
 
 
@@ -74,6 +70,7 @@
     except (ValueError, TypeError):
         return eid
 
+
 class RqlQuery(object):
     def __init__(self):
         self.edited = []
@@ -190,15 +187,16 @@
             req.transaction_data['__maineid'] = form['__maineid']
         # no specific action, generic edition
         self._to_create = req.data['eidmap'] = {}
-        # those two data variables are used to handle relation from/to entities
+        # those three data variables are used to handle relation from/to entities
         # which doesn't exist at time where the entity is edited and that
         # deserves special treatment
         req.data['pending_inlined'] = defaultdict(set)
         req.data['pending_others'] = set()
         req.data['pending_composite_delete'] = set()
+        req.data['pending_values'] = dict()
         try:
             for formparams in self._ordered_formparams():
-                eid = self.edit_entity(formparams)
+                self.edit_entity(formparams)
         except (RequestError, NothingToEdit) as ex:
             if '__linkto' in req.form and 'eid' in req.form:
                 self.execute_linkto()
@@ -208,10 +206,20 @@
         # treated now (pop to ensure there are no attempt to add new ones)
         pending_inlined = req.data.pop('pending_inlined')
         assert not pending_inlined, pending_inlined
+        pending_values = req.data.pop('pending_values')
         # handle all other remaining relations now
         while req.data['pending_others']:
             form_, field = req.data['pending_others'].pop()
-            self.handle_formfield(form_, field)
+            # attempt to retrieve values and original values if they have already gone through
+            # handle_formfield (may not if there has been some not yet known eid at the first
+            # processing round). In the later case we've to go back through handle_formfield.
+            try:
+                values, origvalues = pending_values.pop((form_, field))
+            except KeyError:
+                self.handle_formfield(form_, field)
+            else:
+                self.handle_relation(form_, field, values, origvalues)
+        assert not pending_values, 'unexpected remaining pending values %s' % pending_values
         del req.data['pending_others']
         # then execute rql to set all relations
         for querydef in self.relations_rql:
@@ -236,7 +244,7 @@
             neweid = entity.eid
         except ValidationError as ex:
             self._to_create[eid] = ex.entity
-            if self._cw.ajax_request: # XXX (syt) why?
+            if self._cw.ajax_request:  # XXX (syt) why?
                 ex.entity = eid
             raise
         self._to_create[eid] = neweid
@@ -268,7 +276,7 @@
         form = req.vreg['forms'].select(formid, req, entity=entity)
         eid = form.actual_eid(entity.eid)
         editedfields = formparams['_cw_entity_fields']
-        form.formvalues = {} # init fields value cache
+        form.formvalues = {}  # init fields value cache
         for field in form.iter_modified_fields(editedfields, entity):
             self.handle_formfield(form, field, rqlquery)
         # if there are some inlined field which were waiting for this entity's
@@ -279,9 +287,9 @@
             if self.errors:
                 errors = dict((f.role_name(), text_type(ex)) for f, ex in self.errors)
                 raise ValidationError(valerror_eid(entity.eid), errors)
-            if eid is None: # creation or copy
+            if eid is None:  # creation or copy
                 entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery)
-            elif rqlquery.edited: # edition of an existant entity
+            elif rqlquery.edited:  # edition of an existant entity
                 self.check_concurrent_edition(formparams, eid)
                 self._update_entity(eid, rqlquery)
         else:
@@ -294,7 +302,7 @@
             autoform.delete_relations(req, todelete)
         if '__cloned_eid' in formparams:
             entity.copy_relations(int(formparams['__cloned_eid']))
-        if is_main_entity: # only execute linkto for the main entity
+        if is_main_entity:  # only execute linkto for the main entity
             self.execute_linkto(entity.eid)
         return eid
 
@@ -303,10 +311,9 @@
         eschema = entity.e_schema
         try:
             for field, value in field.process_posted(form):
-                if not (
-                    (field.role == 'subject' and field.name in eschema.subjrels)
-                    or
-                    (field.role == 'object' and field.name in eschema.objrels)):
+                if not ((field.role == 'subject' and field.name in eschema.subjrels)
+                        or
+                        (field.role == 'object' and field.name in eschema.objrels)):
                     continue
 
                 rschema = self._cw.vreg.schema.rschema(field.name)
@@ -315,11 +322,11 @@
                     continue
 
                 if entity.has_eid():
-                    origvalues = set(data[0] for data in entity.related(field.name, field.role).rows)
+                    origvalues = set(row[0] for row in entity.related(field.name, field.role).rows)
                 else:
                     origvalues = set()
                 if value is None or value == origvalues:
-                    continue # not edited / not modified / to do later
+                    continue  # not edited / not modified / to do later
 
                 unlinked_eids = origvalues - value
 
@@ -333,7 +340,8 @@
                 elif form.edited_entity.has_eid():
                     self.handle_relation(form, field, value, origvalues)
                 else:
-                    form._cw.data['pending_others'].add( (form, field) )
+                    form._cw.data['pending_others'].add((form, field))
+                    form._cw.data['pending_values'][(form, field)] = (value, origvalues)
 
         except ProcessFormError as exc:
             self.errors.append((field, exc))
@@ -387,15 +395,10 @@
     def handle_relation(self, form, field, values, origvalues):
         """handle edition for the (rschema, x) relation of the given entity
         """
-        etype = form.edited_entity.e_schema
         rschema = self._cw.vreg.schema.rschema(field.name)
         if field.role == 'subject':
-            desttype = rschema.objects(etype)[0]
-            card = rschema.rdef(etype, desttype).cardinality[0]
             subjvar, objvar = 'X', 'Y'
         else:
-            desttype = rschema.subjects(etype)[0]
-            card = rschema.rdef(desttype, etype).cardinality[1]
             subjvar, objvar = 'Y', 'X'
         eid = form.edited_entity.eid
         if field.role == 'object' or not rschema.inlined or not values:
@@ -419,7 +422,7 @@
         for eid, etype in eidtypes:
             entity = self._cw.entity_from_eid(eid, etype)
             path, params = entity.cw_adapt_to('IEditControl').after_deletion_path()
-            redirect_info.add( (path, tuple(params.items())) )
+            redirect_info.add((path, tuple(params.items())))
             entity.cw_delete()
         if len(redirect_info) > 1:
             # In the face of ambiguity, refuse the temptation to guess.
@@ -431,7 +434,6 @@
         else:
             self._cw.set_message(self._cw._('entity deleted'))
 
-
     def check_concurrent_edition(self, formparams, eid):
         req = self._cw
         try:
@@ -446,7 +448,7 @@
             msg = _("Entity %(eid)s has changed since you started to edit it."
                     " Reload the page and reapply your changes.")
             # ... this is why we pass the formats' dict as a third argument.
-            raise ValidationError(eid, {None: msg}, {'eid' : eid})
+            raise ValidationError(eid, {None: msg}, {'eid': eid})
 
     def _action_apply(self):
         self._default_publish()
--- a/cubicweb/web/views/editforms.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/editforms.py	Mon Mar 20 10:28:01 2017 +0100
@@ -71,11 +71,25 @@
     # don't use navigation, all entities asked to be deleted should be displayed
     # else we will only delete the displayed page
     paginable = False
+    # show first level of composite relations in a treeview
+    show_composite = False
+    show_composite_skip_rtypes = set('wf_info_for',)
+
+    def _iter_composite_entities(self, entity, limit=None):
+        for rdef, role in entity.e_schema.composite_rdef_roles:
+            if rdef.rtype in self.show_composite_skip_rtypes:
+                continue
+            for centity in entity.related(
+                rdef.rtype, role, limit=limit
+            ).entities():
+                yield centity
 
     def call(self, onsubmit=None):
         """ask for confirmation before real deletion"""
         req, w = self._cw, self.w
         _ = req._
+        if self.show_composite:
+            req.add_css(('jquery-treeview/jquery.treeview.css', 'cubicweb.treeview.css'))
         w(u'<script type="text/javascript">updateMessage(\'%s\');</script>\n'
           % _('this action is not reversible!'))
         # XXX above message should have style of a warning
@@ -84,11 +98,30 @@
                                              rset=self.cw_rset,
                                              onsubmit=onsubmit)
         w(u'<ul>\n')
+        page_size = req.property_value('navigation.page-size')
         for entity in self.cw_rset.entities():
             # don't use outofcontext view or any other that may contain inline
             # edition form
-            w(u'<li>%s</li>' % tags.a(entity.view('textoutofcontext'),
-                                      href=entity.absolute_url()))
+            w(u'<li>%s' % tags.a(entity.view('textoutofcontext'),
+                                 href=entity.absolute_url()))
+            if self.show_composite:
+                content = None
+                for count, centity in enumerate(self._iter_composite_entities(
+                    entity, limit=page_size,
+                )):
+                    if count == 0:
+                        w(u'<ul class="treeview">')
+                    if content is not None:
+                        w(u'<li>%s</li>' % content)
+                    if count == page_size - 1:
+                        w(u'<li class="last">%s</li></ul>' % _(
+                            'And more composite entities'))
+                        break
+                    content = tags.a(centity.view('textoutofcontext'),
+                                     href=centity.absolute_url())
+                else:
+                    w(u'<li class="last">%s</li></ul>' % content)
+            w(u'</li>\n')
         w(u'</ul>\n')
         form.render(w=self.w)
 
--- a/cubicweb/web/views/forms.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/forms.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -42,9 +42,6 @@
 but you'll use this one rarely.
 """
 
-
-
-
 import time
 import inspect
 
@@ -177,8 +174,10 @@
             return self._onsubmit
         except AttributeError:
             return "return freezeFormButtons('%(domid)s');" % dictattr(self)
+
     def _set_onsubmit(self, value):
         self._onsubmit = value
+
     onsubmit = property(_get_onsubmit, _set_onsubmit)
 
     def add_media(self):
@@ -210,6 +209,7 @@
             rset=self.cw_rset, row=self.cw_row, col=self.cw_col or 0)
 
     formvalues = None
+
     def build_context(self, formvalues=None):
         """build form context values (the .context attribute which is a
         dictionary with field instance as key associated to a dictionary
@@ -217,7 +217,7 @@
         a string).
         """
         if self.formvalues is not None:
-            return # already built
+            return  # already built
         self.formvalues = formvalues or {}
         # use a copy in case fields are modified while context is built (eg
         # __linkto handling for instance)
@@ -239,6 +239,7 @@
                             eidparam=True)
 
     _default_form_action_path = 'edit'
+
     def form_action(self):
         action = self.action
         if action is None:
@@ -256,7 +257,7 @@
                 editedfields = self._cw.form['_cw_fields']
             except KeyError:
                 raise RequestError(self._cw._('no edited fields specified'))
-        entityform = entity and len(inspect.getargspec(self.field_by_name)) == 4 # XXX
+        entityform = entity and len(inspect.getargspec(self.field_by_name)) == 4  # XXX
         for editedfield in splitstrip(editedfields):
             try:
                 name, role = editedfield.split('-')
@@ -275,7 +276,7 @@
         will return a dictionary with field names as key and typed value as
         associated value.
         """
-        with tempattr(self, 'formvalues', {}): # init fields value cache
+        with tempattr(self, 'formvalues', {}):  # init fields value cache
             errors = []
             processed = {}
             for field in self.iter_modified_fields():
@@ -317,16 +318,16 @@
             rschema = eschema.schema.rschema(name)
             # XXX use a sample target type. Document this.
             tschemas = rschema.targets(eschema, role)
-            fieldcls = cls_or_self.uicfg_aff.etype_get(
+            fieldclass = cls_or_self.uicfg_aff.etype_get(
                 eschema, rschema, role, tschemas[0])
             kwargs = cls_or_self.uicfg_affk.etype_get(
                 eschema, rschema, role, tschemas[0])
             if kwargs is None:
                 kwargs = {}
-            if fieldcls:
-                if not isinstance(fieldcls, type):
-                    return fieldcls  # already and instance
-                return fieldcls(name=name, role=role, eidparam=True, **kwargs)
+            if fieldclass:
+                if not isinstance(fieldclass, type):
+                    return fieldclass  # already an instance
+                kwargs['fieldclass'] = fieldclass
             if isinstance(cls_or_self, type):
                 req = None
             else:
@@ -441,7 +442,7 @@
     def actual_eid(self, eid):
         # should be either an int (existant entity) or a variable (to be
         # created entity)
-        assert eid or eid == 0, repr(eid) # 0 is a valid eid
+        assert eid or eid == 0, repr(eid)  # 0 is a valid eid
         try:
             return int(eid)
         except ValueError:
@@ -470,8 +471,8 @@
 
     def build_context(self, formvalues=None):
         super(CompositeFormMixIn, self).build_context(formvalues)
-        for form in self.forms:
-            form.build_context(formvalues)
+        for form_ in self.forms:
+            form_.build_context(formvalues)
 
 
 class CompositeForm(CompositeFormMixIn, FieldsForm):
@@ -479,5 +480,6 @@
     at once.
     """
 
+
 class CompositeEntityForm(CompositeFormMixIn, EntityFieldsForm):
-    pass # XXX why is this class necessary?
+    pass  # XXX why is this class necessary?
--- a/cubicweb/web/views/management.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/management.py	Mon Mar 20 10:28:01 2017 +0100
@@ -188,7 +188,6 @@
 
     def call(self):
         stats = self._cw.call_service('repo_stats')
-        stats['looping_tasks'] = ', '.join('%s (%s seconds)' % (n, i) for n, i in stats['looping_tasks'])
         stats['threads'] = ', '.join(sorted(stats['threads']))
         for k in stats:
             if k in ('extid_cache_size', 'type_source_cache_size'):
--- a/cubicweb/web/views/sessions.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/sessions.py	Mon Mar 20 10:28:01 2017 +0100
@@ -127,9 +127,6 @@
         except InvalidSession:
             self.close_session(session)
             raise
-        if session.closed:
-            self.close_session(session)
-            raise InvalidSession()
         return session
 
     def open_session(self, req):
@@ -176,4 +173,3 @@
         """
         self.info('closing http session %s' % session.sessionid)
         self._sessions.pop(session.sessionid, None)
-        session.close()
--- a/cubicweb/web/views/staticcontrollers.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/staticcontrollers.py	Mon Mar 20 10:28:01 2017 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -39,7 +39,6 @@
 from cubicweb.web.views.urlrewrite import URLRewriter
 
 
-
 class StaticFileController(Controller):
     """an abtract class to serve static file
 
@@ -49,7 +48,7 @@
 
     def max_age(self, path):
         """max cache TTL"""
-        return 60*60*24*7
+        return 60 * 60 * 24 * 7
 
     def static_file(self, path):
         """Return full content of a static file.
@@ -81,7 +80,6 @@
         self._cw.set_header('last-modified', generateDateTime(os.stat(path).st_mtime))
         if self._cw.is_client_cache_valid():
             return ''
-        # XXX elif uri.startswith('/https/'): uri = uri[6:]
         mimetype, encoding = mimetypes.guess_type(path)
         if mimetype is None:
             mimetype = 'application/octet-stream'
@@ -226,11 +224,7 @@
     __regid__ = 'fckeditor'
 
     def publish(self, rset=None):
-        config = self._cw.vreg.config
-        if self._cw.https:
-            uiprops = config.https_uiprops
-        else:
-            uiprops = config.uiprops
+        uiprops = self._cw.vreg.config.uiprops
         relpath = self.relpath
         if relpath.startswith('fckeditor/'):
             relpath = relpath[len('fckeditor/'):]
@@ -248,9 +242,11 @@
         relpath = self.relpath[len(self.__regid__) + 1:]
         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.
 
@@ -267,6 +263,5 @@
         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/cubicweb/web/views/uicfg.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/uicfg.py	Mon Mar 20 10:28:01 2017 +0100
@@ -54,9 +54,6 @@
    uicfg.actionbox_appearsin_addmenu.tag_object_of(('*', 'entry_of', 'Blog'), True)
 """
 
-
-from warnings import warn
-
 from six import string_types
 
 from cubicweb import neg_role
@@ -93,7 +90,8 @@
                     section = 'sideboxes'
             self.tag_relation((sschema, rschema, oschema, role), section)
 
-primaryview_section = PrimaryViewSectionRelationTags()
+
+primaryview_section = PrimaryViewSectionRelationTags(__module__=__name__)
 
 
 class DisplayCtrlRelationTags(NoTargetRelationTagsDict):
@@ -144,7 +142,7 @@
                 self.tag_object_of(('*', rtype, etype), {'order': index})
 
 
-primaryview_display_ctrl = DisplayCtrlRelationTags()
+primaryview_display_ctrl = DisplayCtrlRelationTags(__module__=__name__)
 
 
 # index view configuration ####################################################
@@ -155,7 +153,7 @@
 # * 'hidden'
 # * 'subobject' (not displayed by default)
 
-class InitializableDict(dict): # XXX not a rtag. Turn into an appobject?
+class InitializableDict(dict):  # XXX not a rtag. Turn into an appobject?
     def __init__(self, *args, **kwargs):
         super(InitializableDict, self).__init__(*args, **kwargs)
         self.__defaults = dict(self)
@@ -174,12 +172,13 @@
             else:
                 self.setdefault(eschema, 'application')
 
+
 indexview_etype_section = InitializableDict(
     EmailAddress='subobject',
     Bookmark='system',
     # entity types in the 'system' table by default (managers only)
     CWUser='system', CWGroup='system',
-    )
+)
 
 
 # autoform.AutomaticEntityForm configuration ##################################
@@ -191,6 +190,7 @@
         result[formtype] = section
     return result
 
+
 def _card_and_comp(sschema, rschema, oschema, role):
     rdef = rschema.rdef(sschema, oschema)
     if role == 'subject':
@@ -201,6 +201,7 @@
         composed = not rschema.final and rdef.composite == 'subject'
     return card, composed
 
+
 class AutoformSectionRelationTags(RelationTagsSet):
     """autoform relations'section"""
     __regid__ = 'autoform_section'
@@ -220,12 +221,7 @@
         formsections = self.init_get(sschema, rschema, oschema, role)
         if formsections is None:
             formsections = self.tag_container_cls()
-        if not any(tag.startswith('inlined') for tag in formsections):
-            if not rschema.final:
-                negsects = self.init_get(sschema, rschema, oschema, neg_role(role))
-                if 'main_inlined' in negsects:
-                    formsections.add('inlined_hidden')
-        key = _ensure_str_key( (sschema, rschema, oschema, role) )
+        key = _ensure_str_key((sschema, rschema, oschema, role))
         self._tagdefs[key] = formsections
 
     def _initfunc_step2(self, sschema, rschema, oschema, role):
@@ -242,31 +238,26 @@
             sectdict.setdefault('muledit', 'hidden')
             sectdict.setdefault('inlined', 'hidden')
         # ensure we have a tag for each form type
-        if not 'main' in sectdict:
-            if not rschema.final and (
-                sectdict.get('inlined') == 'attributes' or
-                'inlined_attributes' in self.init_get(sschema, rschema, oschema,
-                                                      neg_role(role))):
-                sectdict['main'] = 'hidden'
-            elif sschema.is_metadata(rschema):
+        if 'main' not in sectdict:
+            if sschema.is_metadata(rschema):
                 sectdict['main'] = 'metadata'
             else:
                 card, composed = _card_and_comp(sschema, rschema, oschema, role)
                 if card in '1+':
                     sectdict['main'] = 'attributes'
-                    if not 'muledit' in sectdict:
+                    if 'muledit' not in sectdict:
                         sectdict['muledit'] = 'attributes'
                 elif rschema.final:
                     sectdict['main'] = 'attributes'
                 else:
                     sectdict['main'] = 'relations'
-        if not 'muledit' in sectdict:
+        if 'muledit' not in sectdict:
             sectdict['muledit'] = 'hidden'
             if sectdict['main'] == 'attributes':
                 card, composed = _card_and_comp(sschema, rschema, oschema, role)
                 if card in '1+' and not composed:
                     sectdict['muledit'] = 'attributes'
-        if not 'inlined' in sectdict:
+        if 'inlined' not in sectdict:
             sectdict['inlined'] = sectdict['main']
         # recompute formsections and set it to avoid recomputing
         for formtype, section in sectdict.items():
@@ -278,11 +269,11 @@
                 self.tag_relation(key, ftype, section)
             return
         assert formtype in self._allowed_form_types, \
-               'formtype should be in (%s), not %s' % (
-            ','.join(self._allowed_form_types), formtype)
+            'formtype should be in (%s), not %s' % (
+                ','.join(self._allowed_form_types), formtype)
         assert section in self._allowed_values[formtype], \
-               'section for %s should be in (%s), not %s' % (
-            formtype, ','.join(self._allowed_values[formtype]), section)
+            'section for %s should be in (%s), not %s' % (
+                formtype, ','.join(self._allowed_values[formtype]), section)
         rtags = self._tagdefs.setdefault(_ensure_str_key(key),
                                          self.tag_container_cls())
         # remove previous section for this form type if any
@@ -303,8 +294,8 @@
                 section, value = tag.split('_', 1)
                 rtags[section] = value
         cls = self.tag_container_cls
-        rtags = cls('_'.join([section,value])
-                    for section,value in rtags.items())
+        rtags = cls('_'.join([section, value])
+                    for section, value in rtags.items())
         return rtags
 
     def get(self, *key):
@@ -320,9 +311,10 @@
           bool telling if having local role is enough (strict = False) or not
         """
         tag = '%s_%s' % (formtype, section)
-        eschema  = entity.e_schema
+        eschema = entity.e_schema
         cw = entity._cw
-        permsoverrides = cw.vreg['uicfg'].select('autoform_permissions_overrides', cw, entity=entity)
+        permsoverrides = cw.vreg['uicfg'].select('autoform_permissions_overrides', cw,
+                                                 entity=entity)
         if entity.has_eid():
             eid = entity.eid
         else:
@@ -339,7 +331,7 @@
             for tschema in targetschemas:
                 # check section's tag first, potentially lower cost than
                 # checking permission which may imply rql queries
-                if not tag in self.etype_get(eschema, rschema, role, tschema):
+                if tag not in self.etype_get(eschema, rschema, role, tschema):
                     continue
                 rdef = rschema.role_rdef(eschema, tschema, role)
                 if rschema.final:
@@ -361,7 +353,8 @@
             # XXX tag allowing to hijack the permission machinery when
             # permission is not verifiable until the entity is actually
             # created...
-            if eid is None and '%s_on_new' % permission in permsoverrides.etype_get(eschema, rschema, role):
+            if eid is None and '%s_on_new' % permission in permsoverrides.etype_get(
+                    eschema, rschema, role):
                 yield (rschema, targetschemas, role)
                 continue
             if not rschema.final and role == 'subject':
@@ -491,7 +484,8 @@
         for attr in attrs:
             self.edit_as_attr(self, etype, attr, formtype='muledit')
 
-autoform_section = AutoformSectionRelationTags()
+
+autoform_section = AutoformSectionRelationTags(__module__=__name__)
 
 
 # relations'field class
@@ -510,7 +504,8 @@
         """
         self._tag_etype_attr(etype, attr, '*', field)
 
-autoform_field = AutoformFieldTags()
+
+autoform_field = AutoformFieldTags(__module__=__name__)
 
 
 # relations'field explicit kwargs (given to field's __init__)
@@ -562,7 +557,7 @@
         self._tag_etype_attr(etype, attr, '*', kwargs)
 
 
-autoform_field_kwargs = AutoformFieldKwargsTags()
+autoform_field_kwargs = AutoformFieldKwargsTags(__module__=__name__)
 
 
 # set of tags of the form <action>_on_new on relations. <action> is a
@@ -571,7 +566,8 @@
 class AutoFormPermissionsOverrides(RelationTagsSet):
     __regid__ = 'autoform_permissions_overrides'
 
-autoform_permissions_overrides = AutoFormPermissionsOverrides()
+
+autoform_permissions_overrides = AutoFormPermissionsOverrides(__module__=__name__)
 
 
 class ReleditTags(NoTargetRelationTagsDict):
@@ -626,13 +622,14 @@
                 edittarget = 'related' if composite else 'rtype'
                 self.tag_relation((sschema, rschema, oschema, role),
                                   {'edit_target': edittarget})
-        if not 'novalue_include_rtype' in values:
+        if 'novalue_include_rtype' not in values:
             showlabel = primaryview_display_ctrl.get(
                 sschema, rschema, oschema, role).get('showlabel', True)
             self.tag_relation((sschema, rschema, oschema, role),
                               {'novalue_include_rtype': not showlabel})
 
-reledit_ctrl = ReleditTags()
+
+reledit_ctrl = ReleditTags(__module__=__name__)
 
 
 # boxes.EditBox configuration #################################################
@@ -666,7 +663,8 @@
 
         :param etype: the entity type as a string
         :param attr: the name of the attribute or relation to hide
-        :param createdtype: the target type of the relation (optional, defaults to '*' (all possible types))
+        :param createdtype: the target type of the relation
+                            (optional, defaults to '*' (all possible types))
 
         `attr` can be a string or 2-tuple (relname, role_of_etype_in_the_relation)
 
@@ -678,14 +676,15 @@
 
         :param etype: the entity type as a string
         :param attr: the name of the attribute or relation to hide
-        :param createdtype: the target type of the relation (optional, defaults to '*' (all possible types))
+        :param createdtype: the target type of the relation
+                            (optional, defaults to '*' (all possible types))
 
         `attr` can be a string or 2-tuple (relname, role_of_etype_in_the_relation)
         """
         self._tag_etype_attr(etype, attr, createdtype, False)
 
-actionbox_appearsin_addmenu = ActionBoxUicfg()
 
+actionbox_appearsin_addmenu = ActionBoxUicfg(__module__=__name__)
 
 
 def registration_callback(vreg):
--- a/cubicweb/web/views/workflow.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/workflow.py	Mon Mar 20 10:28:01 2017 +0100
@@ -152,7 +152,8 @@
         else:
             sel += ',WF'
             headers = (_('from_state'), _('to_state'), _('comment'), _('date'))
-        rql = '%s %s, X eid %%(x)s' % (sel, rql)
+        sel += ',FSN,TSN,CF'
+        rql = '%s %s, FS name FSN, TS name TSN, WF comment_format CF, X eid %%(x)s' % (sel, rql)
         try:
             rset = self._cw.execute(rql, {'x': eid})
         except Unauthorized:
--- a/cubicweb/web/webconfig.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/webconfig.py	Mon Mar 20 10:28:01 2017 +0100
@@ -78,11 +78,9 @@
     ))
 
 
-class WebConfiguration(CubicWebConfiguration):
-    """the WebConfiguration is a singleton object handling instance's
-    configuration and preferences
-    """
-    cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set([join('web', 'views')])
+class BaseWebConfiguration(CubicWebConfiguration):
+    """Base class for web configurations"""
+    cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set(['web.views'])
     cube_appobject_path = CubicWebConfiguration.cube_appobject_path | set(['views'])
 
     options = merge_options(CubicWebConfiguration.options + (
@@ -92,7 +90,11 @@
           'help': 'see `cubicweb.dbapi.connect` documentation for possible value',
           'group': 'web', 'level': 2,
           }),
-
+        ('use-uicache',
+         {'type': 'yn', 'default': True,
+          'help': _('should css be compiled and store in uicache'),
+          'group': 'ui', 'level': 2,
+          }),
         ('anonymous-user',
          {'type' : 'string',
           'default': None,
@@ -112,20 +114,41 @@
           'help': 'web instance query log file',
           'group': 'web', 'level': 3,
           }),
+        ('cleanup-anonymous-session-time',
+         {'type' : 'time',
+          'default': '5min',
+          'help': 'Same as cleanup-session-time but specific to anonymous '
+          'sessions. You can have a much smaller timeout here since it will be '
+          'transparent to the user. Default to 5min.',
+          'group': 'web', 'level': 3,
+          }),
+    ))
+
+    def anonymous_user(self):
+        """return a login and password to use for anonymous users.
+
+        None may be returned for both if anonymous connection is not
+        allowed or if an empty login is used in configuration
+        """
+        try:
+            user   = self['anonymous-user'] or None
+            passwd = self['anonymous-password']
+            if user:
+                user = text_type(user)
+        except KeyError:
+            user, passwd = None, None
+        except UnicodeDecodeError:
+            raise ConfigurationError("anonymous information should only contains ascii")
+        return user, passwd
+
+
+
+class WebConfiguration(BaseWebConfiguration):
+    """the WebConfiguration is a singleton object handling instance's
+    configuration and preferences
+    """
+    options = merge_options(BaseWebConfiguration.options + (
         # web configuration
-        ('https-url',
-         {'type' : 'string',
-          'default': None,
-          'help': 'web server root url on https. By specifying this option your '\
-          'site can be available as an http and https site. Authenticated users '\
-          'will in this case be authenticated and once done navigate through the '\
-          'https site. IMPORTANTE NOTE: to do this work, you should have your '\
-          'apache redirection include "https" as base url path so cubicweb can '\
-          'differentiate between http vs https access. For instance: \n'\
-          'RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P]\n'\
-          'where the cubicweb web server is listening on port 8080.',
-          'group': 'main', 'level': 3,
-          }),
         ('datadir-url',
          {'type': 'string', 'default': None,
           'help': ('base url for static data, if different from "${base-url}/data/".  '
@@ -154,14 +177,6 @@
           "Should be 0 or greater than repository\'s session-time.",
           'group': 'web', 'level': 2,
           }),
-        ('cleanup-anonymous-session-time',
-         {'type' : 'time',
-          'default': '5min',
-          'help': 'Same as cleanup-session-time but specific to anonymous '
-          'sessions. You can have a much smaller timeout here since it will be '
-          'transparent to the user. Default to 5min.',
-          'group': 'web', 'level': 3,
-          }),
         ('submit-mail',
          {'type' : 'string',
           'default': None,
@@ -261,9 +276,7 @@
     def __init__(self, *args, **kwargs):
         super(WebConfiguration, self).__init__(*args, **kwargs)
         self.uiprops = None
-        self.https_uiprops = None
         self.datadir_url = None
-        self.https_datadir_url = None
 
     def fckeditor_installed(self):
         if self.uiprops is None:
@@ -280,23 +293,6 @@
     def vc_config(self):
         return self.repository().get_versions()
 
-    def anonymous_user(self):
-        """return a login and password to use for anonymous users.
-
-        None may be returned for both if anonymous connection is not
-        allowed or if an empty login is used in configuration
-        """
-        try:
-            user   = self['anonymous-user'] or None
-            passwd = self['anonymous-password']
-            if user:
-                user = text_type(user)
-        except KeyError:
-            user, passwd = None, None
-        except UnicodeDecodeError:
-            raise ConfigurationError("anonymous information should only contains ascii")
-        return user, passwd
-
     @cachedproperty
     def _instance_salt(self):
         """This random key/salt is used to sign content to be sent back by
@@ -343,7 +339,7 @@
         directory = self._fs_path_locate(rid, rdirectory)
         if directory is None:
             return None, None
-        if rdirectory == 'data' and rid.endswith('.css'):
+        if self['use-uicache'] and rdirectory == 'data' and rid.endswith('.css'):
             if rid == 'cubicweb.old.css':
                 # @import('cubicweb.css') in css
                 warn('[3.20] cubicweb.old.css has been renamed back to cubicweb.css',
@@ -382,16 +378,8 @@
                 self.datadir_url += '/'
             if self.mode != 'test':
                 self.datadir_url += '%s/' % self.instance_md5_version()
-            self.https_datadir_url = self.datadir_url
             return
-        httpsurl = self['https-url']
         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 + data_relpath
         self.datadir_url = baseurl + data_relpath
 
     def data_relpath(self):
@@ -409,14 +397,6 @@
             data=lambda x: self.datadir_url + x,
             datadir_url=self.datadir_url[:-1])
         self._init_uiprops(self.uiprops)
-        if self['https-url']:
-            cachedir = join(self.appdatahome, 'uicachehttps')
-            self.check_writeable_uid_directory(cachedir)
-            self.https_uiprops = PropertySheet(
-                cachedir,
-                data=lambda x: self.https_datadir_url + x,
-                datadir_url=self.https_datadir_url[:-1])
-            self._init_uiprops(self.https_uiprops)
 
     def _init_uiprops(self, uiprops):
         libuiprops = join(self.shared_dir(), 'data', 'uiprops.py')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/wfutils.py	Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,149 @@
+# copyright 2017 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/>.
+"""Workflow setup utilities.
+
+These functions work with a declarative workflow definition:
+
+.. code-block:: python
+
+        {
+            'etypes': 'CWGroup',
+            'default': True,
+            'initial_state': u'draft',
+            'states': [u'draft', u'published'],
+            'transitions': {
+                u'publish': {
+                    'fromstates': u'draft',
+                    'tostate': u'published',
+                    'requiredgroups': u'managers'
+                    'conditions': (
+                        'U in_group X',
+                        'X owned_by U'
+                    )
+                }
+            }
+        }
+
+.. autofunction:: setup_workflow
+.. autofunction:: cleanupworkflow
+"""
+
+import collections
+
+from six import text_type
+
+from cubicweb import NoResultError
+
+
+def get_tuple_or_list(value):
+    if value is None:
+        return None
+    if not isinstance(value, (tuple, list)):
+        value = (value,)
+    return value
+
+
+def cleanupworkflow(cnx, wf, wfdef):
+    """Cleanup an existing workflow by removing the states and transitions that
+    do not exist in the given definition.
+
+    :param cnx: A connexion with enough permissions to define a workflow
+    :param wf: A `Workflow` entity
+    :param wfdef: A workflow definition
+    """
+    cnx.execute(
+        'DELETE State S WHERE S state_of WF, WF eid %%(wf)s, '
+        'NOT S name IN (%s)' % (
+            ', '.join('"%s"' % s for s in wfdef['states'])),
+        {'wf': wf.eid})
+
+    cnx.execute(
+        'DELETE Transition T WHERE T transition_of WF, WF eid %%(wf)s, '
+        'NOT T name IN (%s)' % (
+            ', '.join('"%s"' % s for s in wfdef['transitions'])),
+        {'wf': wf.eid})
+
+
+def setup_workflow(cnx, name, wfdef, cleanup=True):
+    """Create or update a workflow definition so it matches the given
+    definition.
+
+    :param cnx: A connexion with enough permissions to define a workflow
+    :param name: The workflow name. Used to create the `Workflow` entity, or
+                 to find an existing one.
+    :param wfdef: A workflow definition.
+    :param cleanup: Remove extra states and transitions. Can be done separatly
+                    by calling :func:`cleanupworkflow`.
+    :return: The created/updated workflow entity
+    """
+    name = text_type(name)
+    try:
+        wf = cnx.find('Workflow', name=name).one()
+    except NoResultError:
+        wf = cnx.create_entity('Workflow', name=name)
+
+    etypes = get_tuple_or_list(wfdef['etypes'])
+    cnx.execute('DELETE WF workflow_of ETYPE WHERE WF eid %%(wf)s, '
+                'NOT ETYPE name IN (%s)' % ','.join('"%s"' for e in etypes),
+                {'wf': wf.eid})
+    cnx.execute('SET WF workflow_of ETYPE WHERE'
+                ' NOT WF workflow_of ETYPE, WF eid %%(wf)s, ETYPE name IN (%s)'
+                % ','.join('"%s"' % e for e in etypes),
+                {'wf': wf.eid})
+    if wfdef['default']:
+        cnx.execute(
+            'SET ETYPE default_workflow X '
+            'WHERE '
+            'NOT ETYPE default_workflow X, '
+            'X eid %%(x)s, ETYPE name IN (%s)' % ','.join(
+                '"%s"' % e for e in etypes),
+            {'x': wf.eid})
+
+    states = {}
+    states_transitions = collections.defaultdict(list)
+    for state in wfdef['states']:
+        st = wf.state_by_name(state) or wf.add_state(state)
+        states[state] = st
+
+    if 'initial_state' in wfdef:
+        wf.cw_set(initial_state=states[wfdef['initial_state']])
+
+    for trname, trdef in wfdef['transitions'].items():
+        tr = (wf.transition_by_name(trname) or
+              cnx.create_entity('Transition', name=trname))
+        tr.cw_set(transition_of=wf)
+        if trdef.get('tostate'):
+            tr.cw_set(destination_state=states[trdef['tostate']])
+        fromstates = get_tuple_or_list(trdef.get('fromstates', ()))
+        for stname in fromstates:
+            states_transitions[stname].append(tr)
+
+        requiredgroups = get_tuple_or_list(trdef.get('requiredgroups', ()))
+        conditions = get_tuple_or_list(trdef.get('conditions', ()))
+
+        tr.set_permissions(requiredgroups, conditions, reset=True)
+
+    for stname, transitions in states_transitions.items():
+        state = states[stname]
+        state.cw_set(allowed_transition=None)
+        state.cw_set(allowed_transition=transitions)
+
+    if cleanup:
+        cleanupworkflow(cnx, wf, wfdef)
+
+    return wf
--- a/cubicweb/wsgi/request.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/wsgi/request.py	Mon Mar 20 10:28:01 2017 +0100
@@ -69,15 +69,10 @@
                           if k.startswith('HTTP_'))
         if 'CONTENT_TYPE' in environ:
             headers_in['Content-Type'] = environ['CONTENT_TYPE']
-        https = self.is_secure()
-        if self.path.startswith('/https/'):
-            self.path = self.path[6:]
-            self.environ['PATH_INFO'] = self.path
-            https = True
 
         post, files = self.get_posted_data()
 
-        super(CubicWebWsgiRequest, self).__init__(vreg, https, post,
+        super(CubicWebWsgiRequest, self).__init__(vreg, post,
                                                   headers= headers_in)
         self.content = environ['wsgi.input']
         if files is not None:
@@ -121,9 +116,6 @@
 
     ## wsgi request helpers ###################################################
 
-    def is_secure(self):
-        return self.environ['wsgi.url_scheme'] == 'https'
-
     def get_posted_data(self):
         # The WSGI spec says 'QUERY_STRING' may be absent.
         post = parse_qs(self.environ.get('QUERY_STRING', ''))
--- a/cubicweb/wsgi/server.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/wsgi/server.py	Mon Mar 20 10:28:01 2017 +0100
@@ -39,7 +39,6 @@
     httpd.set_app(app)
     repo = app.appli.repo
     try:
-        repo.start_looping_tasks()
         LOGGER.info('starting http server on %s', config['base-url'])
         httpd.serve_forever()
     finally:
--- a/cubicweb/wsgi/test/unittest_wsgi.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/wsgi/test/unittest_wsgi.py	Mon Mar 20 10:28:01 2017 +0100
@@ -27,30 +27,6 @@
 
         self.assertEqual(b'some content', req.content.read())
 
-    def test_http_scheme(self):
-        r = webtest.app.TestRequest.blank('/', {
-            'wsgi.url_scheme': 'http'})
-
-        req = CubicWebWsgiRequest(r.environ, self.vreg)
-
-        self.assertFalse(req.https)
-
-    def test_https_scheme(self):
-        r = webtest.app.TestRequest.blank('/', {
-            'wsgi.url_scheme': 'https'})
-
-        req = CubicWebWsgiRequest(r.environ, self.vreg)
-
-        self.assertTrue(req.https)
-
-    def test_https_prefix(self):
-        r = webtest.app.TestRequest.blank('/https/', {
-            'wsgi.url_scheme': 'http'})
-
-        req = CubicWebWsgiRequest(r.environ, self.vreg)
-
-        self.assertTrue(req.https)
-
     def test_big_content(self):
         content = b'x'*100001
         r = webtest.app.TestRequest.blank('/', {
--- a/cubicweb/wsgi/tnd.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/wsgi/tnd.py	Mon Mar 20 10:28:01 2017 +0100
@@ -40,7 +40,6 @@
     http_server.listen(port, interface)
     repo = app.appli.repo
     try:
-        repo.start_looping_tasks()
         LOGGER.info('starting http server on %s', config['base-url'])
         ioloop.IOLoop.instance().start()
     finally:
--- a/cubicweb/wsgi/wz.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/wsgi/wz.py	Mon Mar 20 10:28:01 2017 +0100
@@ -38,7 +38,6 @@
     app = CubicWebWSGIApplication(config)
     repo = app.appli.repo
     try:
-        repo.start_looping_tasks()
         LOGGER.info('starting http server on %s', config['base-url'])
         run_simple(interface, port, app,
                    threaded=True,
--- a/debian/control	Mon Mar 20 09:40:24 2017 +0100
+++ b/debian/control	Mon Mar 20 10:28:01 2017 +0100
@@ -25,7 +25,8 @@
  python-pyramid,
  python-pyramid-multiauth,
  python-waitress,
- python-passlib (<< 2.0),
+ python-passlib (>= 1.7.0),
+ python-repoze.lru,
  python-wsgicors,
  sphinx-common,
 Standards-Version: 3.9.6
@@ -157,6 +158,7 @@
  python-pyramid-multiauth,
  python-waitress (>= 0.8.9),
  python-wsgicors,
+ python-repoze.lru,
 Recommends:
  python-pyramid-debugtoolbar
 Conflicts:
--- a/doc/api/pyramid/authplugin.rst	Mon Mar 20 09:40:24 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-.. _authplugin_module:
-
-:mod:`cubicweb.pyramid.authplugin`
-----------------------------------
-
-.. automodule:: cubicweb.pyramid.authplugin
-
-    .. autoclass:: DirectAuthentifier
-        :show-inheritance:
-        :members:
--- a/doc/api/pyramid/tools.rst	Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/api/pyramid/tools.rst	Mon Mar 20 10:28:01 2017 +0100
@@ -1,7 +1,7 @@
 .. _tools_module:
 
 :mod:`cubicweb.pyramid.tools`
-----------------------------
+-----------------------------
 
 .. automodule:: cubicweb.pyramid.tools
 
--- a/doc/book/admin/instance-config.rst	Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/book/admin/instance-config.rst	Mon Mar 20 10:28:01 2017 +0100
@@ -42,12 +42,9 @@
 :`main.base-url`:
     url base site to be used to generate the urls of web pages
 
-Https configuration
-```````````````````
-It is possible to make a site accessible for anonymous http connections
-and https for authenticated users. This requires to
-use apache (for example) for redirection and the variable `main.https-url`
-of configuration file.
+Apache configuration
+````````````````````
+It is possible to use apache (for example) as proxy.
 
 For this to work you have to activate the following apache modules :
 
@@ -62,9 +59,8 @@
 
 :Example:
 
-   For an apache redirection of a site accessible via `http://localhost/demo`
-   and `https://localhost/demo` and actually running on port 8080, it
-   takes to the http:::
+   For an apache redirection of a site accessible via `http://localhost/demo` while cubicweb is
+   actually running on port 8080:::
 
      ProxyPreserveHost On
      RewriteEngine On
@@ -72,24 +68,11 @@
      RewriteRule ^/demo$ /demo/
      RewriteRule ^/demo/(.*) http://127.0.0.1:8080/$1 [L,P]
 
-   and for the https:::
-
-     ProxyPreserveHost On
-     RewriteEngine On
-     RewriteCond %{REQUEST_URI} ^/ demo
-     RewriteRule ^/demo$/demo/
-     RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P]
-
 
    and we will file in the all-in-one.conf of the instance:::
 
      base-url = http://localhost/demo
-     https-url = https://localhost/demo
 
-Notice that if you simply want a site accessible through https, not *both* http
-and https, simply set `base-url` to the https url and the first section into your
-apache configuration (as you would have to do for an http configuration with an
-apache front-end).
 
 Setting up the web client
 -------------------------
--- a/doc/book/devrepo/datamodel/define-workflows.rst	Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/book/devrepo/datamodel/define-workflows.rst	Mon Mar 20 10:28:01 2017 +0100
@@ -158,3 +158,11 @@
 re-create all the workflow entities. The user interface should only be
 a reference for you to view the states and transitions, but is not the
 appropriate interface to define your application workflow.
+
+
+Alternative way to declare workflows
+------------------------------------
+
+.. automodule:: cubicweb.wfutils
+
+
--- a/doc/book/devweb/edition/dissection.rst	Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/book/devweb/edition/dissection.rst	Mon Mar 20 10:28:01 2017 +0100
@@ -107,14 +107,14 @@
             <th class="labelCol"><label class="required" for="title-subject:763">title</label></th>
             <td>
               <input id="title-subject:763" maxlength="128" name="title-subject:763" size="45"
-                     tabindex="1" type="text" value="let us write more doc" />
+                     type="text" value="let us write more doc" />
             </td>
           </tr>
           ... (description field omitted) ...
           <tr class="priority_subject_row">
             <th class="labelCol"><label class="required" for="priority-subject:763">priority</label></th>
             <td>
-              <select id="priority-subject:763" name="priority-subject:763" size="1" tabindex="4">
+              <select id="priority-subject:763" name="priority-subject:763" size="1">
                 <option value="important">important</option>
                 <option selected="selected" value="normal">normal</option>
                 <option value="minor">minor</option>
@@ -126,7 +126,7 @@
           <tr class="concerns_subject_row">
             <th class="labelCol"><label class="required" for="concerns-subject:763">concerns</label></th>
             <td>
-              <select id="concerns-subject:763" name="concerns-subject:763" size="1" tabindex="6">
+              <select id="concerns-subject:763" name="concerns-subject:763" size="1">
                 <option selected="selected" value="760">Foo</option>
               </select>
             </td>
@@ -134,7 +134,7 @@
           <tr class="done_in_subject_row">
             <th class="labelCol"><label for="done_in-subject:763">done in</label></th>
             <td>
-              <select id="done_in-subject:763" name="done_in-subject:763" size="1" tabindex="7">
+              <select id="done_in-subject:763" name="done_in-subject:763" size="1">
                 <option value="__cubicweb_internal_field__"></option>
                 <option selected="selected" value="761">Foo 0.1.0</option>
                 <option value="762">Foo 0.2.0</option>
@@ -180,7 +180,7 @@
                 <tr><th>&#160;</th><td>&#160;</td></tr>
                 <tr id="relationSelectorRow_763" class="separator">
                   <th class="labelCol">
-                    <select id="relationSelector_763" tabindex="8"
+                    <select id="relationSelector_763"
                             onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,763);">
                       <option value="">select a relation</option>
                       <option value="appeared_in_subject">appeared in</option>
@@ -228,7 +228,7 @@
         <tbody>
           <tr>
             <td align="center">
-              <button class="validateButton" tabindex="9" type="submit" value="validate">
+              <button class="validateButton" type="submit" value="validate">
                 <img alt="OK_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/ok.png" />
                 validate
               </button>
@@ -236,13 +236,13 @@
             <td style="align: right; width: 50%;">
               <button class="validateButton"
                       onclick="postForm(&#39;__action_apply&#39;, &#39;button_apply&#39;, &#39;entityForm&#39;)"
-                      tabindex="10" type="button" value="apply">
+                      type="button" value="apply">
                 <img alt="APPLY_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/plus.png" />
                 apply
               </button>
               <button class="validateButton"
                       onclick="postForm(&#39;__action_cancel&#39;, &#39;button_cancel&#39;, &#39;entityForm&#39;)"
-                      tabindex="11" type="button" value="cancel">
+                      type="button" value="cancel">
                 <img alt="CANCEL_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/cancel.png" />
                 cancel
               </button>
--- a/doc/book/devweb/request.rst	Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/book/devweb/request.rst	Mon Mar 20 10:28:01 2017 +0100
@@ -48,8 +48,6 @@
   * etype_rset
   * `form`, dictionary containing the values of a web form
   * `encoding`, character encoding to use in the response
-  * `next_tabindex()`: returns a monotonically growing integer used to
-    build the html tab index of forms
 
 * `HTTP`
 
--- a/doc/book/pyramid/quickstart.rst	Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/book/pyramid/quickstart.rst	Mon Mar 20 10:28:01 2017 +0100
@@ -57,3 +57,17 @@
 
 -   Configure the base-url and https-url in all-in-one.conf to match the ones
     of the pyramid configuration (this is a temporary limitation).
+
+
+Usage with pserve
+-----------------
+
+To run a Pyramid application using pserve_:
+
+::
+
+    pserve /path/to/development.ini instance=<appid>
+
+
+.. _pserve: \
+    http://docs.pylonsproject.org/projects/pyramid/en/latest/pscripts/pserve.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.25.rst	Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,16 @@
+3.25 (UNRELEASED)
+=================
+
+New features
+------------
+
+* A new option `connections-pooler-enabled` (default yes) has been added. This
+  allow to switch off internal connection pooling for use with others poolers
+  such as pgbouncer_.
+
+.. _pgbouncer: https://pgbouncer.github.io/
+
+
+* A new way to declare workflows as simple data structure (dict/list) has been
+  introduced. Respective utility functions live in ``cubicweb.wfutils``
+  module. This handles both the creation and migration of workflows.
--- a/doc/changes/changelog.rst	Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/changes/changelog.rst	Mon Mar 20 10:28:01 2017 +0100
@@ -2,6 +2,7 @@
  Changelog history
 ===================
 
+.. include:: 3.25.rst
 .. include:: 3.24.rst
 .. include:: 3.23.rst
 .. include:: 3.22.rst
--- a/doc/changes/index.rst	Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/changes/index.rst	Mon Mar 20 10:28:01 2017 +0100
@@ -4,6 +4,7 @@
 .. toctree::
     :maxdepth: 1
 
+    3.25
     3.24
     3.23
     3.22
--- a/doc/index.rst	Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/index.rst	Mon Mar 20 10:28:01 2017 +0100
@@ -108,7 +108,7 @@
 .. toctree::
     :maxdepth: 1
 
-    js_api/index
+    book/en/devweb/js_api/index
 
 Developpers
 ~~~~~~~~~~~
--- a/flake8-ok-files.txt	Mon Mar 20 09:40:24 2017 +0100
+++ b/flake8-ok-files.txt	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,5 @@
+cubicweb/__init__.py
+cubicweb/__main__.py
 cubicweb/dataimport/csv.py
 cubicweb/dataimport/importer.py
 cubicweb/dataimport/massive_store.py
@@ -18,24 +20,31 @@
 cubicweb/devtools/test/unittest_webtest.py
 cubicweb/devtools/webtest.py
 cubicweb/entities/adapters.py
+cubicweb/entities/authobjs.py
 cubicweb/entities/test/unittest_base.py
 cubicweb/etwist/__init__.py
+cubicweb/etwist/request.py
+cubicweb/etwist/service.py
 cubicweb/ext/__init__.py
 cubicweb/hooks/test/data/hooks.py
-cubicweb/hooks/test/unittest_notification.py
+cubicweb/hooks/test/unittest_notificationhooks.py
 cubicweb/hooks/test/unittest_security.py
 cubicweb/hooks/test/unittest_syncsession.py
-cubicweb/__init__.py
-cubicweb/__main__.py
 cubicweb/pylintext.py
+cubicweb/repoapi.py
+cubicweb/rset.py
+cubicweb/rtags.py
+cubicweb/server/__init__.py
 cubicweb/server/repository.py
 cubicweb/server/rqlannotation.py
 cubicweb/server/schema2sql.py
 cubicweb/server/session.py
 cubicweb/server/sqlutils.py
+cubicweb/server/utils.py
 cubicweb/server/test/datacomputed/migratedapp/schema.py
 cubicweb/server/test/datacomputed/schema.py
 cubicweb/server/test/data/entities.py
+cubicweb/server/test/data/hooks.py
 cubicweb/server/test/data-migractions/cubes/fakecustomtype/__init__.py
 cubicweb/server/test/data-migractions/cubes/fakeemail/__init__.py
 cubicweb/server/test/data-migractions/cubes/__init__.py
@@ -43,27 +52,31 @@
 cubicweb/server/test/data-schema2sql/__init__.py
 cubicweb/server/test/unittest_checkintegrity.py
 cubicweb/server/test/unittest_ldapsource.py
+cubicweb/server/test/unittest_serverctl.py
+cubicweb/server/test/unittest_session.py
 cubicweb/server/test/unittest_rqlannotation.py
-cubicweb/skeleton/test/pytestconf.py
+cubicweb/server/test/unittest_utils.py
+cubicweb/sobjects/notification.py
+cubicweb/sobjects/services.py
 cubicweb/sobjects/test/unittest_notification.py
 cubicweb/sobjects/test/unittest_register_user.py
 cubicweb/sobjects/textparsers.py
-cubicweb/test/data/cubes/comment/__init__.py
-cubicweb/test/data/cubes/comment/__pkginfo__.py
-cubicweb/test/data/cubes/email/entities.py
-cubicweb/test/data/cubes/email/hooks.py
-cubicweb/test/data/cubes/email/__init__.py
-cubicweb/test/data/cubes/email/__pkginfo__.py
-cubicweb/test/data/cubes/email/views/__init__.py
-cubicweb/test/data/cubes/file/entities/__init__.py
-cubicweb/test/data/cubes/file/hooks/__init__.py
-cubicweb/test/data/cubes/file/__init__.py
-cubicweb/test/data/cubes/file/__pkginfo__.py
-cubicweb/test/data/cubes/file/views.py
-cubicweb/test/data/cubes/forge/__init__.py
-cubicweb/test/data/cubes/forge/__pkginfo__.py
-cubicweb/test/data/cubes/mycube/__init__.py
-cubicweb/test/data/cubes/mycube/__pkginfo__.py
+cubicweb/test/data/libpython/cubicweb_comment/__init__.py
+cubicweb/test/data/libpython/cubicweb_comment/__pkginfo__.py
+cubicweb/test/data/libpython/cubicweb_email/entities.py
+cubicweb/test/data/libpython/cubicweb_email/hooks.py
+cubicweb/test/data/libpython/cubicweb_email/__init__.py
+cubicweb/test/data/libpython/cubicweb_email/__pkginfo__.py
+cubicweb/test/data/libpython/cubicweb_email/views/__init__.py
+cubicweb/test/data/libpython/cubicweb_file/entities/__init__.py
+cubicweb/test/data/libpython/cubicweb_file/hooks/__init__.py
+cubicweb/test/data/libpython/cubicweb_file/__init__.py
+cubicweb/test/data/libpython/cubicweb_file/__pkginfo__.py
+cubicweb/test/data/libpython/cubicweb_file/views.py
+cubicweb/test/data/libpython/cubicweb_forge/__init__.py
+cubicweb/test/data/libpython/cubicweb_forge/__pkginfo__.py
+cubicweb/test/data/libpython/cubicweb_mycube/__init__.py
+cubicweb/test/data/libpython/cubicweb_mycube/__pkginfo__.py
 cubicweb/test/data/migration/0.1.0_common.py
 cubicweb/test/data/migration/0.1.0_repository.py
 cubicweb/test/data_schemareader/schema.py
@@ -72,23 +85,31 @@
 cubicweb/test/unittest_binary.py
 cubicweb/test/unittest_mail.py
 cubicweb/test/unittest_repoapi.py
+cubicweb/test/unittest_req.py
+cubicweb/test/unittest_rtags.py
 cubicweb/test/unittest_schema.py
 cubicweb/test/unittest_toolsutils.py
-cubicweb/test/unittest_utils.py
+cubicweb/test/unittest_wfutils.py
+cubicweb/web/application.py
 cubicweb/web/formwidgets.py
 cubicweb/web/test/data/entities.py
+cubicweb/web/test/unittest_application.py
 cubicweb/web/test/unittest_http_headers.py
 cubicweb/web/test/unittest_views_basetemplates.py
 cubicweb/web/test/unittest_views_cwsources.py
 cubicweb/web/test/unittest_views_json.py
+cubicweb/web/views/authentication.py
+cubicweb/web/views/editcontroller.py
 cubicweb/web/views/json.py
 cubicweb/web/views/searchrestriction.py
+cubicweb/web/views/staticcontrollers.py
+cubicweb/web/views/uicfg.py
 cubicweb/xy.py
 cubicweb/pyramid/auth.py
 cubicweb/pyramid/bwcompat.py
+cubicweb/pyramid/config.py
 cubicweb/pyramid/core.py
 cubicweb/pyramid/defaults.py
-cubicweb/pyramid/init_instance.py
 cubicweb/pyramid/__init__.py
 cubicweb/pyramid/login.py
 cubicweb/pyramid/predicates.py
@@ -99,8 +120,10 @@
 cubicweb/pyramid/tools.py
 cubicweb/pyramid/test/__init__.py
 cubicweb/pyramid/test/test_bw_request.py
+cubicweb/pyramid/test/test_config.py
 cubicweb/pyramid/test/test_core.py
 cubicweb/pyramid/test/test_login.py
 cubicweb/pyramid/test/test_rest_api.py
 cubicweb/pyramid/test/test_tools.py
 cubicweb/pyramid/pyramidctl.py
+cubicweb/wfutils.py
--- a/requirements/dev.txt	Mon Mar 20 09:40:24 2017 +0100
+++ b/requirements/dev.txt	Mon Mar 20 10:28:01 2017 +0100
@@ -1,1 +1,3 @@
 pytest
+http://hg.logilab.org/master/logilab/common/archive/default.tar.bz2#egg=logilab-common
+http://hg.logilab.org/master/yams/archive/default.tar.bz2#egg=yams
--- a/requirements/test-server.txt	Mon Mar 20 09:40:24 2017 +0100
+++ b/requirements/test-server.txt	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,4 @@
+mock
 psycopg2
 ldap3 < 2
 cubicweb-basket
--- a/setup.cfg	Mon Mar 20 09:40:24 2017 +0100
+++ b/setup.cfg	Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,6 @@
+[bdist_wheel]
+universal = 1
+
 [check-manifest]
 ignore =
   debian
--- a/setup.py	Mon Mar 20 09:40:24 2017 +0100
+++ b/setup.py	Mon Mar 20 10:28:01 2017 +0100
@@ -149,40 +149,16 @@
                 dest = join(self.install_dir, src)
                 export(src, dest, verbose=self.verbose)
 
-# write required share/cubicweb/cubes/__init__.py
-class MyInstallData(install_data.install_data):
-    """A class That manages data files installation"""
-    def run(self):
-        """overridden from install_data class"""
-        install_data.install_data.run(self)
-        path = join(self.install_dir, 'share', 'cubicweb', 'cubes', '__init__.py')
-        ini = open(path, 'w')
-        ini.write('# Cubicweb cubes directory\n')
-        ini.close()
-
-
-class CWDevelop(develop.develop):
-    """Custom "develop" command warning about (legacy) cubes directory not
-    installed.
-    """
-
-    def run(self):
-        cubespath = join(sys.prefix, 'share', 'cubicweb', 'cubes')
-        self.warn('develop command does not install (legacy) cubes directory (%s)'
-                  % cubespath)
-        return develop.develop.run(self)
-
 
 # re-enable copying data files in sys.prefix
-# overwrite MyInstallData to use sys.prefix instead of the egg directory
-MyInstallMoreData = MyInstallData
-class MyInstallData(MyInstallMoreData): # pylint: disable=E0102
+# overwrite install_data to use sys.prefix instead of the egg directory
+class MyInstallData(install_data.install_data):
     """A class that manages data files installation"""
     def run(self):
         _old_install_dir = self.install_dir
         if self.install_dir.endswith('egg'):
             self.install_dir = sys.prefix
-        MyInstallMoreData.run(self)
+        install_data.install_data.run(self)
         self.install_dir = _old_install_dir
 try:
     import setuptools.command.easy_install # only if easy_install available
@@ -223,11 +199,16 @@
         'yams >= 0.44.0',
         'lxml',
         'logilab-database >= 1.15.0',
-        'passlib < 2.0',
+        'passlib >= 1.7.0',
         'pytz',
         'Markdown',
         'unittest2 >= 0.7.0',
     ],
+    entry_points={
+        'paste.app_factory': [
+            'main=cubicweb.pyramid:pyramid_app',
+        ],
+    },
     extras_require={
         'captcha': [
             'Pillow',
@@ -249,6 +230,7 @@
             'waitress >= 0.8.9',
             'wsgicors >= 0.3',
             'pyramid_multiauth',
+            'repoze.lru',
         ],
         'rdf': [
             'rdflib',
@@ -263,7 +245,6 @@
     cmdclass={
         'install_lib': MyInstallLib,
         'install_data': MyInstallData,
-        'develop': CWDevelop,
     },
     zip_safe=False,
 )
--- a/tox.ini	Mon Mar 20 09:40:24 2017 +0100
+++ b/tox.ini	Mon Mar 20 10:28:01 2017 +0100
@@ -5,8 +5,6 @@
 
 [testenv]
 sitepackages = True
-whitelist_externals =
-  /usr/bin/touch
 deps =
   -r{toxinidir}/requirements/dev.txt
   py27: backports.tempfile
@@ -14,7 +12,6 @@
   server: -r{toxinidir}/requirements/test-server.txt
   web: -r{toxinidir}/requirements/test-web.txt
 commands =
-  py34: touch {envdir}/share/cubicweb/cubes/__init__.py
   misc: {envpython} -m pip install --upgrade --no-deps --quiet git+git://github.com/logilab/yapps@master#egg=yapps
   misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/test {toxinidir}/cubicweb/dataimport/test {toxinidir}/cubicweb/devtools/test {toxinidir}/cubicweb/entities/test {toxinidir}/cubicweb/ext/test {toxinidir}/cubicweb/hooks/test {toxinidir}/cubicweb/sobjects/test {toxinidir}/cubicweb/wsgi/test {toxinidir}/cubicweb/pyramid/test
   py27-misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/etwist/test
@@ -53,7 +50,7 @@
 format = pylint
 ignore = W503
 max-line-length = 100
-exclude = setup.py,doc/*,cubicweb/misc/*,cubicweb/test/*,cubicweb/*/test/*,.tox/*
+exclude = doc/*,.tox/*
 
 
 # vim: wrap sts=2 sw=2