merge 3.17.11
authorJulien Cristau <julien.cristau@logilab.fr>
Mon, 09 Dec 2013 16:13:10 +0100
changeset 9340 b1e933b0e850
parent 9322 2dae5bf5ea68 (diff)
parent 9339 cf27006ce813 (current diff)
child 9341 099a3a33eaaa
merge 3.17.11
__pkginfo__.py
cubicweb.spec
cwctl.py
devtools/__init__.py
entity.py
server/__init__.py
server/hook.py
server/sqlutils.py
test/unittest_entity.py
web/webconfig.py
--- a/__init__.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/__init__.py	Mon Dec 09 16:13:10 2013 +0100
@@ -22,6 +22,8 @@
 
 # ignore the pygments UserWarnings
 import warnings
+import cPickle
+import zlib
 warnings.filterwarnings('ignore', category=UserWarning,
                         message='.*was already imported',
                         module='.*pygments')
@@ -120,6 +122,26 @@
         binary.seek(0)
         return binary
 
+    def __eq__(self, other):
+        if not isinstance(other, Binary):
+            return False
+        return self.getvalue(), other.getvalue()
+
+
+    # Binary helpers to store/fetch python objects
+
+    @classmethod
+    def zpickle(cls, obj):
+        """ return a Binary containing a gzipped pickle of obj """
+        retval = cls()
+        retval.write(zlib.compress(cPickle.dumps(obj, protocol=2)))
+        return retval
+
+    def unzpickle(self):
+        """ decompress and loads the stream before returning it """
+        return cPickle.loads(zlib.decompress(self.getvalue()))
+
+
 def str_or_binary(value):
     if isinstance(value, Binary):
         return value
@@ -127,7 +149,6 @@
 BASE_CONVERTERS['Password'] = str_or_binary
 
 
-
 # use this dictionary to rename entity types while keeping bw compat
 ETYPE_NAME_MAP = {}
 
--- a/__pkginfo__.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/__pkginfo__.py	Mon Dec 09 16:13:10 2013 +0100
@@ -40,10 +40,10 @@
 ]
 
 __depends__ = {
-    'logilab-common': '>= 0.59.0',
+    'logilab-common': '>= 0.60.0',
     'logilab-mtconverter': '>= 0.8.0',
     'rql': '>= 0.31.2',
-    'yams': '>= 0.37.0',
+    'yams': '>= 0.39.0',
     #gettext                    # for xgettext, msgcat, etc...
     # web dependancies
     'simplejson': '>= 2.0.9',
--- a/cubicweb.spec	Fri Dec 06 17:20:59 2013 +0100
+++ b/cubicweb.spec	Mon Dec 09 16:13:10 2013 +0100
@@ -23,7 +23,7 @@
 Requires:       %{python}-logilab-common >= 0.59.0
 Requires:       %{python}-logilab-mtconverter >= 0.8.0
 Requires:       %{python}-rql >= 0.31.2
-Requires:       %{python}-yams >= 0.37.0
+Requires:       %{python}-yams >= 0.39.0
 Requires:       %{python}-logilab-database >= 1.10.0
 Requires:       %{python}-passlib
 Requires:       %{python}-lxml
--- a/cwconfig.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/cwconfig.py	Mon Dec 09 16:13:10 2013 +0100
@@ -53,8 +53,7 @@
 
 If you are not administrator of you machine or if you need to play with some
 specific version of |cubicweb| you can use `virtualenv`_ a tool to create
-isolated Python environments.  Since version 3.9 |cubicweb| is **`virtualenv`
-friendly** and won't write any file outside the virtualenv directory.
+isolated Python environments.
 
 - instances are stored in :file:`<VIRTUAL_ENV>/etc/cubicweb.d`
 - temporary files (such as pid file) in :file:`<VIRTUAL_ENV>/var/run/cubicweb`
@@ -206,7 +205,7 @@
     """return a list of installed configurations in a directory
     according to \*-ctl files
     """
-    return [name for name in ('repository', 'twisted', 'all-in-one')
+    return [name for name in ('repository', 'all-in-one')
             if exists(join(directory, '%s.conf' % name))]
 
 def guess_configuration(directory):
@@ -328,7 +327,7 @@
     # the format below can be useful to debug multi thread issues:
     # log_format = '%(asctime)s - [%(threadName)s] (%(name)s) %(levelname)s: %(message)s'
     # nor remove appobjects based on unused interface [???]
-    cleanup_interface_sobjects = True
+    cleanup_unused_appobjects = True
 
     if (CWDEV and _forced_mode != 'system'):
         mode = 'user'
@@ -499,21 +498,11 @@
             try:
                 gendeps = getattr(pkginfo, key.replace('_cubes', ''))
             except AttributeError:
-                # bw compat
-                if hasattr(pkginfo, oldkey):
-                    warn('[3.8] cube %s: %s is deprecated, use %s dict'
-                         % (cube, oldkey, key), DeprecationWarning)
-                    deps = getattr(pkginfo, oldkey)
-                else:
-                    deps = {}
+                deps = {}
             else:
                 deps = dict( (x[len('cubicweb-'):], v)
                              for x, v in gendeps.iteritems()
                              if x.startswith('cubicweb-'))
-        if not isinstance(deps, dict):
-            deps = dict((key, None) for key in deps)
-            warn('[3.8] cube %s should define %s as a dict' % (cube, key),
-                 DeprecationWarning)
         for depcube in deps:
             try:
                 newname = CW_MIGRATION_MAP[depcube]
@@ -940,10 +929,9 @@
                                      ' "cubicweb-ctl list")' % appid)
         return home
 
-    MODES = ('common', 'repository', 'Any', 'web')
+    MODES = ('common', 'repository', 'Any')
     MCOMPAT = {'all-in-one': MODES,
-               'repository': ('common', 'repository', 'Any'),
-               'twisted'   : ('common', 'web'),}
+               'repository': ('common', 'repository', 'Any')}
     @classmethod
     def accept_mode(cls, mode):
         #assert mode in cls.MODES, mode
--- a/cwctl.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/cwctl.py	Mon Dec 09 16:13:10 2013 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -266,12 +266,7 @@
                         if tinfo:
                             descr = getattr(tinfo, 'description', '')
                             if not descr:
-                                descr = getattr(tinfo, 'short_desc', '')
-                                if descr:
-                                    warn('[3.8] short_desc is deprecated, update %s'
-                                         ' pkginfo' % cube, DeprecationWarning)
-                                else:
-                                    descr = tinfo.__doc__
+                                descr = tinfo.__doc__
                             if descr:
                                 print '    '+ '    \n'.join(descr.splitlines())
                         modes = detect_available_modes(cwcfg.cube_dir(cube))
@@ -357,7 +352,7 @@
           }),
         ('config',
          {'short': 'c', 'type' : 'choice', 'metavar': '<install type>',
-          'choices': ('all-in-one', 'repository', 'twisted'),
+          'choices': ('all-in-one', 'repository'),
           'default': 'all-in-one',
           'help': 'installation type, telling which part of an instance '
           'should be installed. You can list available configurations using the'
@@ -1039,9 +1034,56 @@
                     raise ConfigurationError('unknown configuration key "%s" for mode %s' % (key, appcfg.name))
             appcfg.save()
 
+
+# WSGI #########
+
+def wsgichoices():
+    try:
+        from werkzeug import serving
+    except ImportError:
+        return ('stdlib',)
+    return ('stdlib', 'werkzeug')
+
+class WSGIDebugStartHandler(InstanceCommand):
+    """Start an interactive wsgi server """
+    name = 'wsgi'
+    actionverb = 'started'
+    arguments = '<instance>'
+    options = (
+        ('method',
+         {'short': 'm',
+          'type': 'choice',
+          'metavar': '<method>',
+          'default': 'stdlib',
+          'choices': wsgichoices(),
+          'help': 'wsgi utility/method'}),
+        ('loglevel',
+         {'short': 'l',
+          'type' : 'choice',
+          'metavar': '<log level>',
+          'default': 'debug',
+          'choices': ('debug', 'info', 'warning', 'error'),
+          'help': 'debug if -D is set, error otherwise',
+          }),
+        )
+
+    def wsgi_instance(self, appid):
+        config = cwcfg.config_for(appid, debugmode=1)
+        init_cmdline_log_threshold(config, self['loglevel'])
+        assert config.name == 'all-in-one'
+        meth = self['method']
+        if meth == 'stdlib':
+            from cubicweb.wsgi import server
+        else:
+            from cubicweb.wsgi import wz as server
+        return server.run(config)
+
+
+
 for cmdcls in (ListCommand,
                CreateInstanceCommand, DeleteInstanceCommand,
                StartInstanceCommand, StopInstanceCommand, RestartInstanceCommand,
+               WSGIDebugStartHandler,
                ReloadConfigurationCommand, StatusCommand,
                UpgradeInstanceCommand,
                ListVersionsInstanceCommand,
@@ -1052,6 +1094,8 @@
                ):
     CWCTL.register(cmdcls)
 
+
+
 def run(args):
     """command line tool"""
     import os
--- a/cwvreg.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/cwvreg.py	Mon Dec 09 16:13:10 2013 +0100
@@ -211,8 +211,7 @@
 
 from cubicweb import (CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, CW_EVENT_MANAGER,
                       onevent, Binary, UnknownProperty, UnknownEid)
-from cubicweb.predicates import (implements, appobject_selectable,
-                                 _reset_is_instance_cache)
+from cubicweb.predicates import appobject_selectable, _reset_is_instance_cache
 
 
 @onevent('before-registry-reload')
@@ -230,15 +229,6 @@
     sys.modules.pop('cubicweb.web.uicfg', None)
     sys.modules.pop('cubicweb.web.uihelper', None)
 
-def use_interfaces(obj):
-    """return interfaces required by the given object by searching for
-    `implements` predicate
-    """
-    impl = obj.__select__.search_selector(implements)
-    if impl:
-        return sorted(impl.expected_ifaces)
-    return ()
-
 def require_appobject(obj):
     """return appobjects required by the given object by searching for
     `appobject_selectable` predicate
@@ -568,7 +558,6 @@
     def reset(self):
         CW_EVENT_MANAGER.emit('before-registry-reset', self)
         super(CWRegistryStore, self).reset()
-        self._needs_iface = {}
         self._needs_appobject = {}
         # two special registries, propertydefs which care all the property
         # definitions, and propertyvals which contains values for those
@@ -641,20 +630,6 @@
                 for obj in objects:
                     obj.schema = schema
 
-    @deprecated('[3.9] use .register instead')
-    def register_if_interface_found(self, obj, ifaces, **kwargs):
-        """register `obj` but remove it if no entity class implements one of
-        the given `ifaces` interfaces at the end of the registration process.
-
-        Extra keyword arguments are given to the
-        :meth:`~cubicweb.cwvreg.CWRegistryStore.register` function.
-        """
-        self.register(obj, **kwargs)
-        if not isinstance(ifaces,  (tuple, list)):
-            self._needs_iface[obj] = (ifaces,)
-        else:
-            self._needs_iface[obj] = ifaces
-
     def register(self, obj, *args, **kwargs):
         """register `obj` application object into `registryname` or
         `obj.__registry__` if not specified, with identifier `oid` or
@@ -665,15 +640,6 @@
         """
         obj = related_appobject(obj)
         super(CWRegistryStore, self).register(obj, *args, **kwargs)
-        # XXX bw compat
-        ifaces = use_interfaces(obj)
-        if ifaces:
-            if not obj.__name__.endswith('Adapter') and \
-                   any(iface for iface in ifaces if not isinstance(iface, basestring)):
-                warn('[3.9] %s: interfaces in implements selector are '
-                     'deprecated in favor of adapters / adaptable '
-                     'selector' % obj.__name__, DeprecationWarning)
-            self._needs_iface[obj] = ifaces
         depends_on = require_appobject(obj)
         if depends_on is not None:
             self._needs_appobject[obj] = depends_on
@@ -687,41 +653,14 @@
     def initialization_completed(self):
         """cw specific code once vreg initialization is completed:
 
-        * remove objects requiring a missing interface, unless
-          config.cleanup_interface_sobjects is false
+        * remove objects requiring a missing appobject, unless
+          config.cleanup_unused_appobjects is false
         * init rtags
         """
         # we may want to keep interface dependent objects (e.g.for i18n
         # catalog generation)
-        if self.config.cleanup_interface_sobjects:
-            # XXX deprecated with cw 3.9: remove appobjects that don't support
-            # any available interface
-            implemented_interfaces = set()
-            if 'Any' in self.get('etypes', ()):
-                for etype in self.schema.entities():
-                    if etype.final:
-                        continue
-                    cls = self['etypes'].etype_class(etype)
-                    if cls.__implements__:
-                        warn('[3.9] %s: using __implements__/interfaces are '
-                             'deprecated in favor of adapters' % cls.__name__,
-                             DeprecationWarning)
-                    for iface in cls.__implements__:
-                        implemented_interfaces.update(iface.__mro__)
-                    implemented_interfaces.update(cls.__mro__)
-            for obj, ifaces in self._needs_iface.items():
-                ifaces = frozenset(isinstance(iface, basestring)
-                                   and iface in self.schema
-                                   and self['etypes'].etype_class(iface)
-                                   or iface
-                                   for iface in ifaces)
-                if not ('Any' in ifaces or ifaces & implemented_interfaces):
-                    reg = self[obj_registries(obj)[0]]
-                    self.debug('unregister %s (no implemented '
-                               'interface among %s)', reg.objid(obj), ifaces)
-                    self.unregister(obj)
-            # since 3.9: remove appobjects which depending on other, unexistant
-            # appobjects
+        if self.config.cleanup_unused_appobjects:
+            # remove appobjects which depend on other, unexistant appobjects
             for obj, (regname, regids) in self._needs_appobject.items():
                 try:
                     registry = self[regname]
@@ -740,8 +679,8 @@
         if 'uicfg' in self: # 'uicfg' is not loaded in a pure repository mode
             for rtags in self['uicfg'].itervalues():
                 for rtag in rtags:
-                    # don't check rtags if we don't want to cleanup_interface_sobjects
-                    rtag.init(self.schema, check=self.config.cleanup_interface_sobjects)
+                    # don't check rtags if we don't want to cleanup_unused_appobjects
+                    rtag.init(self.schema, check=self.config.cleanup_unused_appobjects)
 
     # rql parsing utilities ####################################################
 
--- a/dbapi.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/dbapi.py	Mon Dec 09 16:13:10 2013 +0100
@@ -422,25 +422,6 @@
         req.set_session(self.session, user)
         return req
 
-    @deprecated('[3.8] use direct access to req.session.data dictionary')
-    def session_data(self):
-        """return a dictionary containing session data"""
-        return self.session.data
-
-    @deprecated('[3.8] use direct access to req.session.data dictionary')
-    def get_session_data(self, key, default=None, pop=False):
-        if pop:
-            return self.session.data.pop(key, default)
-        return self.session.data.get(key, default)
-
-    @deprecated('[3.8] use direct access to req.session.data dictionary')
-    def set_session_data(self, key, value):
-        self.session.data[key] = value
-
-    @deprecated('[3.8] use direct access to req.session.data dictionary')
-    def del_session_data(self, key):
-        self.session.data.pop(key, None)
-
     # 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
@@ -486,7 +467,7 @@
     def _txid(self):
         return self.connection._txid(self)
 
-    def execute(self, rql, args=None, eid_key=None, build_descr=True):
+    def execute(self, rql, args=None, build_descr=True):
         """execute a rql query, return resulting rows and their description in
         a :class:`~cubicweb.rset.ResultSet` object
 
@@ -517,10 +498,6 @@
 
             execute('Any X WHERE X eid %(x)s', {'x': 123})
         """
-        if eid_key is not None:
-            warn('[3.8] eid_key is deprecated, you can safely remove this argument',
-                 DeprecationWarning, stacklevel=2)
-        # XXX use named argument for build_descr in case repo is < 3.8
         rset = self._repo.execute(self._sessid, rql, args,
                                   build_descr=build_descr, **self._txid())
         rset.req = self.req
--- a/debian/control	Fri Dec 06 17:20:59 2013 +0100
+++ b/debian/control	Mon Dec 09 16:13:10 2013 +0100
@@ -15,7 +15,7 @@
  python-unittest2 | python (>= 2.7),
  python-logilab-mtconverter,
  python-rql,
- python-yams (>= 0.37),
+ python-yams (>= 0.39),
  python-lxml,
 Standards-Version: 3.9.1
 Homepage: http://www.cubicweb.org
@@ -131,6 +131,8 @@
  python-fyzz,
  python-imaging,
  python-rdflib
+Breaks:
+ cubicweb-inlinedit (<< 1.1.1),
 Description: web interface library for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -150,7 +152,7 @@
  gettext,
  python-logilab-mtconverter (>= 0.8.0),
  python-logilab-common (>= 0.59.0),
- python-yams (>= 0.37.0),
+ python-yams (>= 0.39.0),
  python-rql (>= 0.31.2),
  python-lxml
 Recommends:
@@ -158,6 +160,10 @@
  python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
+Breaks:
+ cubicweb-comment (<< 1.9.1),
+ cubicweb-person (<< 1.8.0),
+ cubicweb-geocoding (<< 0.2.0),
 Description: common library for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
--- a/devtools/__init__.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/devtools/__init__.py	Mon Dec 09 16:13:10 2013 +0100
@@ -38,7 +38,7 @@
 from cubicweb import ExecutionError, BadConnectionId
 from cubicweb import schema, cwconfig
 from cubicweb.server.serverconfig import ServerConfiguration
-from cubicweb.etwist.twconfig import TwistedConfiguration
+from cubicweb.etwist.twconfig import WebConfigurationBase
 
 cwconfig.CubicWebConfiguration.cls_adjust_sys_path()
 
@@ -214,12 +214,12 @@
         return BASE_URL
 
 
-class BaseApptestConfiguration(TestServerConfiguration, TwistedConfiguration):
+class BaseApptestConfiguration(TestServerConfiguration, WebConfigurationBase):
     name = 'all-in-one' # so it search for all-in-one.conf, not repository.conf
     options = cwconfig.merge_options(TestServerConfiguration.options
-                                     + TwistedConfiguration.options)
-    cubicweb_appobject_path = TestServerConfiguration.cubicweb_appobject_path | TwistedConfiguration.cubicweb_appobject_path
-    cube_appobject_path = TestServerConfiguration.cube_appobject_path | TwistedConfiguration.cube_appobject_path
+                                     + WebConfigurationBase.options)
+    cubicweb_appobject_path = TestServerConfiguration.cubicweb_appobject_path | WebConfigurationBase.cubicweb_appobject_path
+    cube_appobject_path = TestServerConfiguration.cube_appobject_path | WebConfigurationBase.cube_appobject_path
 
     def available_languages(self, *args):
         return self.cw_languages()
--- a/devtools/devctl.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/devtools/devctl.py	Mon Dec 09 16:13:10 2013 +0100
@@ -46,7 +46,7 @@
     a cube or for cubicweb (without a home)
     """
     creating = True
-    cleanup_interface_sobjects = False
+    cleanup_unused_appobjects = False
 
     cubicweb_appobject_path = (ServerConfiguration.cubicweb_appobject_path
                                | WebConfiguration.cubicweb_appobject_path)
--- a/devtools/testlib.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/devtools/testlib.py	Mon Dec 09 16:13:10 2013 +0100
@@ -419,13 +419,10 @@
         return self.cnx.cursor(req or self.request())
 
     @nocoverage
-    def execute(self, rql, args=None, eidkey=None, req=None):
+    def execute(self, rql, args=None, req=None):
         """executes <rql>, builds a resultset, and returns a couple (rset, req)
         where req is a FakeRequest
         """
-        if eidkey is not None:
-            warn('[3.8] eidkey is deprecated, you can safely remove this argument',
-                 DeprecationWarning, stacklevel=2)
         req = req or self.request(rql=rql)
         return req.execute(unicode(rql), args)
 
@@ -447,10 +444,7 @@
 
     # server side db api #######################################################
 
-    def sexecute(self, rql, args=None, eid_key=None):
-        if eid_key is not None:
-            warn('[3.8] eid_key is deprecated, you can safely remove this argument',
-                 DeprecationWarning, stacklevel=2)
+    def sexecute(self, rql, args=None):
         self.session.set_cnxset()
         return self.session.execute(rql, args)
 
@@ -986,15 +980,6 @@
             self.assertEqual(len(MAILBOX), nb_msgs)
         return messages
 
-    # deprecated ###############################################################
-
-    @deprecated('[3.8] use self.execute(...).get_entity(0, 0)')
-    def entity(self, rql, args=None, eidkey=None, req=None):
-        if eidkey is not None:
-            warn('[3.8] eidkey is deprecated, you can safely remove this argument',
-                 DeprecationWarning, stacklevel=2)
-        return self.execute(rql, args, req=req).get_entity(0, 0)
-
 
 # auto-populating test classes and utilities ###################################
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/3.18.rst	Mon Dec 09 16:13:10 2013 +0100
@@ -0,0 +1,41 @@
+What's new in CubicWeb 3.18?
+============================
+
+New functionalities
+--------------------
+
+* add a security debugging tool
+  (see `#2920304 <http://www.cubicweb.org/2920304>`_)
+
+
+API changes
+-----------
+
+* not really an API change, but the entity permission checks are now
+  systematically deferred to an operation, instead of a) trying in a
+  hook and b) if it failed, retrying later in an operation
+
+* The default value storage for attributes is no longer String, but
+  Bytes.  This opens the road to storing arbitrary python objects, e.g.
+  numpy arrays, and fixes a bug where default values whose truth value
+  was False were not properly migrated.
+
+Deprecation
+---------------------
+
+* the old multi-source system
+
+
+Deprecated Code Drops
+----------------------
+
+* ``ldapuser`` have been dropped; use ``ldapfeed`` now
+  (see `#2936496 <http://www.cubicweb.org/2936496>`_)
+
+* action ``GotRhythm`` was removed, make sure you do not
+  import it in your cubes (even to unregister it)
+  (see `#3093362 <http://www.cubicweb.org/3093362>`_)
+
+* all 3.8 backward compat is gone
+
+* all 3.9 backward compat (including the javascript side) is gone
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/4.0.rst	Mon Dec 09 16:13:10 2013 +0100
@@ -0,0 +1,8 @@
+What's new in CubicWeb 4.0?
+============================
+
+Deprecated Code Drops
+----------------------
+
+* The ldapuser source has been dropped. ldapfeed is the only official source
+  remaining for ldap.
--- a/doc/book/en/admin/config.rst	Fri Dec 06 17:20:59 2013 +0100
+++ b/doc/book/en/admin/config.rst	Mon Dec 09 16:13:10 2013 +0100
@@ -57,19 +57,28 @@
 PostgreSQL
 ~~~~~~~~~~
 
-For installation, please refer to the `PostgreSQL project online documentation`_.
-
-.. _`PostgreSQL project online documentation`: http://www.postgresql.org/
+Many Linux distributions ship with the appropriate PostgreSQL packages.
+Basically, you need to install the following packages:
 
-You need to install the three following packages: `postgresql-8.X`,
-`postgresql-client-8.X`, and `postgresql-plpython-8.X`. If you run postgres
-version prior to 8.3, you'll also need the `postgresql-contrib-8.X` package for
-full-text search extension.
+* `postgresql` and `postgresql-client`, which will pull the respective
+  versioned packages (e.g. `postgresql-9.1` and `postgresql-client-9.1`) and,
+  optionally,
+* a `postgresql-plpython-X.Y` package with a version corresponding to that of
+  the aforementioned packages (e.g. `postgresql-plpython-9.1`).
+
+If you run postgres version prior to 8.3, you'll also need the
+`postgresql-contrib-8.X` package for full-text search extension.
 
 If you run postgres on another host than the |cubicweb| repository, you should
 install the `postgresql-client` package on the |cubicweb| host, and others on the
 database host.
 
+For extra details concerning installation, please refer to the `PostgreSQL
+project online documentation`_.
+
+.. _`PostgreSQL project online documentation`: http://www.postgresql.org/docs
+
+
 Database cluster
 ++++++++++++++++
 
--- a/doc/book/en/admin/setup.rst	Fri Dec 06 17:20:59 2013 +0100
+++ b/doc/book/en/admin/setup.rst	Mon Dec 09 16:13:10 2013 +0100
@@ -121,10 +121,10 @@
 `Virtualenv` install
 --------------------
 
-Since version 3.9, |cubicweb| can be safely installed, used and contained inside
-a `virtualenv`_. You can use either :ref:`pip <PipInstallation>` or
-:ref:`easy_install <EasyInstallInstallation>` to install |cubicweb| inside an
-activated virtual environment.
+|cubicweb| can be safely installed, used and contained inside a
+`virtualenv`_. You can use either :ref:`pip <PipInstallation>` or
+:ref:`easy_install <EasyInstallInstallation>` to install |cubicweb|
+inside an activated virtual environment.
 
 .. _PipInstallation:
 
--- a/doc/book/en/devrepo/datamodel/definition.rst	Fri Dec 06 17:20:59 2013 +0100
+++ b/doc/book/en/devrepo/datamodel/definition.rst	Mon Dec 09 16:13:10 2013 +0100
@@ -226,13 +226,13 @@
 * `SizeConstraint`: allows to specify a minimum and/or maximum size on
   string (generic case of `maxsize`)
 
-* `BoundConstraint`: allows to specify a minimum and/or maximum value
+* `BoundaryConstraint`: allows to specify a minimum and/or maximum value
   on numeric types and date
 
 .. sourcecode:: python
 
-   from yams.constraints import BoundConstraint, TODAY
-   BoundConstraint('<=', TODAY())
+   from yams.constraints import BoundaryConstraint, TODAY
+   BoundaryConstraint('<=', TODAY())
 
 * `IntervalBoundConstraint`: allows to specify an interval with
   included values
@@ -476,13 +476,8 @@
 
 Here are the current rules:
 
-1. permission to add/update entity and its attributes are checked:
-
-   - on commit if the entity has been added
-
-   - in an 'after_update_entity' hook if the entity has been updated. If it fails
-     at this time, it will be retried on commit (hence you get the permission if
-     you have it just after the modification or *at* commit time)
+1. permission to add/update entity and its attributes are checked on
+   commit
 
 2. permission to delete an entity is checked in 'before_delete_entity' hook
 
--- a/doc/book/en/devrepo/entityclasses/adapters.rst	Fri Dec 06 17:20:59 2013 +0100
+++ b/doc/book/en/devrepo/entityclasses/adapters.rst	Mon Dec 09 16:13:10 2013 +0100
@@ -10,13 +10,7 @@
 .. _`interfaces`: http://java.sun.com/docs/books/tutorial/java/concepts/interface.html
 .. _`adapter`: http://en.wikipedia.org/wiki/Adapter_pattern
 
-In |cubicweb| adapters provide logical functionalities to entity types. They
-are introduced in version `3.9`. Before that one had to implement Interfaces in
-entity classes to achieve a similar goal. However, the problem with this
-approach is that is clutters the entity class's namespace, exposing name
-collision risks with schema attributes/relations or even methods names
-(different interfaces may define the same method with not necessarily the same
-behaviour expected).
+In |cubicweb| adapters provide logical functionalities to entity types.
 
 Definition of an adapter is quite trivial. An excerpt from cubicweb
 itself (found in :mod:`cubicweb.entities.adapters`):
--- a/doc/book/en/devrepo/migration.rst	Fri Dec 06 17:20:59 2013 +0100
+++ b/doc/book/en/devrepo/migration.rst	Mon Dec 09 16:13:10 2013 +0100
@@ -46,7 +46,7 @@
 Again in the directory `migration`, the file `depends.map` allows to indicate
 that for the migration to a particular model version, you always have to first
 migrate to a particular *CubicWeb* version. This file can contain comments (lines
-starting by `#`) and a dependancy is listed as follows: ::
+starting with `#`) and a dependency is listed as follows: ::
 
   <model version n° X.Y.Z> : <cubicweb version n° X.Y.Z>
 
@@ -170,9 +170,9 @@
 * `rql(rql, kwargs=None, cachekey=None, ask_confirm=True)`, executes an arbitrary RQL
   query, either to interrogate or update. A result set object is returned.
 
-* `add_entity(etype, *args, **kwargs)`, adds a nes entity type of the given
-  type. The attribute and relation values are specified using the named and
-  positionned parameters.
+* `add_entity(etype, *args, **kwargs)`, adds a new entity of the given type.
+  The attribute and relation values are specified as named positional
+  arguments.
 
 Workflow creation
 -----------------
--- a/doc/book/en/devrepo/testing.rst	Fri Dec 06 17:20:59 2013 +0100
+++ b/doc/book/en/devrepo/testing.rst	Mon Dec 09 16:13:10 2013 +0100
@@ -18,12 +18,7 @@
 convenience methods to help test all of this.
 
 In the realm of views, automatic tests check that views are valid
-XHTML. See :ref:`automatic_views_tests` for details. Since 3.9, bases
-for web functional testing using `windmill
-<http://www.getwindmill.com/>`_ are set. See test cases in
-cubicweb/web/test/windmill and python wrapper in
-cubicweb/web/test_windmill/ if you want to use this in your own cube.
-
+XHTML. See :ref:`automatic_views_tests` for details.
 
 Most unit tests need a live database to work against. This is achieved
 by CubicWeb using automatically sqlite (bundled with Python, see
--- a/doc/book/en/tutorials/advanced/part04_ui-base.rst	Fri Dec 06 17:20:59 2013 +0100
+++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst	Mon Dec 09 16:13:10 2013 +0100
@@ -194,8 +194,6 @@
 
 .. Note::
 
-    * Adapters have been introduced in CubicWeb 3.9 / cubicweb-folder 1.8.
-
     * As seen earlier, we want to **replace** the folder's `ITree` adapter by our
       implementation, hence the custom `registration_callback` method.
 
@@ -241,12 +239,6 @@
 ascendant/descendant ordering and a strict comparison with current file's name
 (the "X" variable representing the current file).
 
-.. Note::
-
-    * Former `implements` selector should be replaced by one of `is_instance` /
-      `adaptable` selector with CubicWeb >= 3.9. In our case, `is_instance` to
-      tell our adapter is able to adapt `File` entities.
-
 Notice that this query supposes we wont have two files of the same name in the
 same folder, else things may go wrong. Fixing this is out of the scope of this
 blog. And as I would like to have at some point a smarter, context sensitive
@@ -358,7 +350,7 @@
 You'll have to answer some questions, as we've seen in `an earlier post`_.
 
 Now that everything is tested, I can transfer the new code to the production
-server, `apt-get upgrade` cubicweb 3.9 and its dependencies, and eventually
+server, `apt-get upgrade` cubicweb and its dependencies, and eventually
 upgrade the production instance.
 
 
--- a/doc/book/en/tutorials/advanced/part05_ui-advanced.rst	Fri Dec 06 17:20:59 2013 +0100
+++ b/doc/book/en/tutorials/advanced/part05_ui-advanced.rst	Mon Dec 09 16:13:10 2013 +0100
@@ -1,8 +1,6 @@
 Building my photos web site with |cubicweb| part V: let's make it even more user friendly
 =========================================================================================
 
-We'll now see how to benefit from features introduced in 3.9 and 3.10 releases of CubicWeb
-
 .. _uiprops:
 
 Step 1: tired of the default look?
@@ -29,9 +27,9 @@
 
    LOGO = data('logo.jpg')
 
-The uiprops machinery has been introduced in `CubicWeb 3.9`_. It is used to define
-some static file resources, such as the logo, default Javascript / CSS files, as
-well as CSS properties (we'll see that later).
+The uiprops machinery is used to define some static file resources,
+such as the logo, default Javascript / CSS files, as well as CSS
+properties (we'll see that later).
 
 .. Note::
    This file is imported specifically by |cubicweb|, with a predefined name space,
@@ -373,5 +371,4 @@
 
 
 .. _`CubicWeb 3.10`: http://www.cubicweb.org/blogentry/1330518
-.. _`CubicWeb 3.9`: http://www.cubicweb.org/blogentry/1179899
 .. _`here`: http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
--- a/doc/tools/pyjsrest.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/doc/tools/pyjsrest.py	Mon Dec 09 16:13:10 2013 +0100
@@ -136,7 +136,6 @@
     'cubicweb.preferences',
     'cubicweb.edition',
     'cubicweb.reledit',
-    'cubicweb.rhythm',
     'cubicweb.timeline-ext',
 ]
 
--- a/entities/adapters.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/entities/adapters.py	Mon Dec 09 16:13:10 2013 +0100
@@ -28,9 +28,7 @@
 from logilab.common.decorators import cached
 
 from cubicweb import ValidationError, view
-from cubicweb.predicates import (implements, is_instance, relation_possible,
-                                match_exception)
-from cubicweb.interfaces import IDownloadable, ITree
+from cubicweb.predicates import is_instance, relation_possible, match_exception
 
 
 class IEmailableAdapter(view.EntityAdapter):
@@ -67,11 +65,9 @@
 
 
 class INotifiableAdapter(view.EntityAdapter):
-    __needs_bw_compat__ = True
     __regid__ = 'INotifiable'
     __select__ = is_instance('Any')
 
-    @view.implements_adapter_compat('INotifiableAdapter')
     def notification_references(self, view):
         """used to control References field of email send on notification
         for this entity. `view` is the notification view.
@@ -167,27 +163,25 @@
 
 class IDownloadableAdapter(view.EntityAdapter):
     """interface for downloadable entities"""
-    __needs_bw_compat__ = True
     __regid__ = 'IDownloadable'
-    __select__ = implements(IDownloadable, warn=False) # XXX for bw compat, else should be abstract
+    __abstract__ = True
 
-    @view.implements_adapter_compat('IDownloadable')
     def download_url(self, **kwargs): # XXX not really part of this interface
         """return an url to download entity's content"""
         raise NotImplementedError
-    @view.implements_adapter_compat('IDownloadable')
+
     def download_content_type(self):
         """return MIME type of the downloadable content"""
         raise NotImplementedError
-    @view.implements_adapter_compat('IDownloadable')
+
     def download_encoding(self):
         """return encoding of the downloadable content"""
         raise NotImplementedError
-    @view.implements_adapter_compat('IDownloadable')
+
     def download_file_name(self):
         """return file name of the downloadable content"""
         raise NotImplementedError
-    @view.implements_adapter_compat('IDownloadable')
+
     def download_data(self):
         """return actual data of the downloadable content"""
         raise NotImplementedError
@@ -219,27 +213,16 @@
     .. automethod: children_rql
     .. automethod: path
     """
-    __needs_bw_compat__ = True
     __regid__ = 'ITree'
-    __select__ = implements(ITree, warn=False) # XXX for bw compat, else should be abstract
+    __abstract__ = True
 
     child_role = 'subject'
     parent_role = 'object'
 
-    @property
-    def tree_relation(self):
-        warn('[3.9] tree_attribute is deprecated, define tree_relation on a custom '
-             'ITree for %s instead' % (self.entity.__class__),
-             DeprecationWarning)
-        return self.entity.tree_attribute
-
-    # XXX should be removed from the public interface
-    @view.implements_adapter_compat('ITree')
     def children_rql(self):
         """Returns RQL to get the children of the entity."""
         return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
 
-    @view.implements_adapter_compat('ITree')
     def different_type_children(self, entities=True):
         """Return children entities of different type as this entity.
 
@@ -253,7 +236,6 @@
             return [e for e in res if e.e_schema != eschema]
         return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col)
 
-    @view.implements_adapter_compat('ITree')
     def same_type_children(self, entities=True):
         """Return children entities of the same type as this entity.
 
@@ -267,23 +249,19 @@
             return [e for e in res if e.e_schema == eschema]
         return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col)
 
-    @view.implements_adapter_compat('ITree')
     def is_leaf(self):
         """Returns True if the entity does not have any children."""
         return len(self.children()) == 0
 
-    @view.implements_adapter_compat('ITree')
     def is_root(self):
         """Returns true if the entity is root of the tree (e.g. has no parent).
         """
         return self.parent() is None
 
-    @view.implements_adapter_compat('ITree')
     def root(self):
         """Return the root entity of the tree."""
         return self._cw.entity_from_eid(self.path()[0])
 
-    @view.implements_adapter_compat('ITree')
     def parent(self):
         """Returns the parent entity if any, else None (e.g. if we are on the
         root).
@@ -294,7 +272,6 @@
         except (KeyError, IndexError):
             return None
 
-    @view.implements_adapter_compat('ITree')
     def children(self, entities=True, sametype=False):
         """Return children entities.
 
@@ -307,7 +284,6 @@
             return self.entity.related(self.tree_relation, self.parent_role,
                                        entities=entities)
 
-    @view.implements_adapter_compat('ITree')
     def iterparents(self, strict=True):
         """Return an iterator on the parents of the entity."""
         def _uptoroot(self):
@@ -322,7 +298,6 @@
             return chain([self.entity], _uptoroot(self))
         return _uptoroot(self)
 
-    @view.implements_adapter_compat('ITree')
     def iterchildren(self, _done=None):
         """Return an iterator over the item's children."""
         if _done is None:
@@ -334,7 +309,6 @@
             yield child
             _done.add(child.eid)
 
-    @view.implements_adapter_compat('ITree')
     def prefixiter(self, _done=None):
         """Return an iterator over the item's descendants in a prefixed order."""
         if _done is None:
@@ -347,7 +321,6 @@
             for entity in child.cw_adapt_to('ITree').prefixiter(_done):
                 yield entity
 
-    @view.implements_adapter_compat('ITree')
     @cached
     def path(self):
         """Returns the list of eids from the root object to this object."""
--- a/entities/test/unittest_base.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/entities/test/unittest_base.py	Mon Dec 09 16:13:10 2013 +0100
@@ -25,7 +25,6 @@
 
 from cubicweb.devtools.testlib import CubicWebTC
 
-from cubicweb.interfaces import IMileStone, ICalendarable
 from cubicweb.entities import AnyEntity
 
 
@@ -134,27 +133,6 @@
         self.request().create_entity('CWGroup', name=u'logilab', reverse_in_group=e)
 
 
-class InterfaceTC(CubicWebTC):
-
-    def test_nonregr_subclasses_and_mixins_interfaces(self):
-        from cubicweb.entities.wfobjs import WorkflowableMixIn
-        WorkflowableMixIn.__implements__ = (ICalendarable,)
-        CWUser = self.vreg['etypes'].etype_class('CWUser')
-        class MyUser(CWUser):
-            __implements__ = (IMileStone,)
-        self.vreg._loadedmods[__name__] = {}
-        self.vreg.register(MyUser)
-        self.vreg['etypes'].initialization_completed()
-        MyUser_ = self.vreg['etypes'].etype_class('CWUser')
-        # a copy is done systematically
-        self.assertTrue(issubclass(MyUser_, MyUser))
-        self.assertTrue(implements(MyUser_, IMileStone))
-        self.assertTrue(implements(MyUser_, ICalendarable))
-        # original class should not have beed modified, only the copy
-        self.assertTrue(implements(MyUser, IMileStone))
-        self.assertFalse(implements(MyUser, ICalendarable))
-
-
 class SpecializedEntityClassesTC(CubicWebTC):
 
     def select_eclass(self, etype):
--- a/entities/wfobjs.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/entities/wfobjs.py	Mon Dec 09 16:13:10 2013 +0100
@@ -32,7 +32,6 @@
 from cubicweb.entities import AnyEntity, fetch_config
 from cubicweb.view import EntityAdapter
 from cubicweb.predicates import relation_possible
-from cubicweb.mixins import MI_REL_TRIGGERS
 
 class WorkflowException(Exception): pass
 
@@ -379,65 +378,8 @@
         return self.by_transition and self.by_transition[0] or None
 
 
-class WorkflowableMixIn(object):
-    """base mixin providing workflow helper methods for workflowable entities.
-    This mixin will be automatically set on class supporting the 'in_state'
-    relation (which implies supporting 'wf_info_for' as well)
-    """
 
-    @property
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').main_workflow")
-    def main_workflow(self):
-        return self.cw_adapt_to('IWorkflowable').main_workflow
-    @property
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_workflow")
-    def current_workflow(self):
-        return self.cw_adapt_to('IWorkflowable').current_workflow
-    @property
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_state")
-    def current_state(self):
-        return self.cw_adapt_to('IWorkflowable').current_state
-    @property
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').state")
-    def state(self):
-        return self.cw_adapt_to('IWorkflowable').state
-    @property
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').printable_state")
-    def printable_state(self):
-        return self.cw_adapt_to('IWorkflowable').printable_state
-    @property
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').workflow_history")
-    def workflow_history(self):
-        return self.cw_adapt_to('IWorkflowable').workflow_history
-
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').cwetype_workflow()")
-    def cwetype_workflow(self):
-        return self.cw_adapt_to('IWorkflowable').main_workflow()
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').latest_trinfo()")
-    def latest_trinfo(self):
-        return self.cw_adapt_to('IWorkflowable').latest_trinfo()
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').possible_transitions()")
-    def possible_transitions(self, type='normal'):
-        return self.cw_adapt_to('IWorkflowable').possible_transitions(type)
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').fire_transition()")
-    def fire_transition(self, tr, comment=None, commentformat=None):
-        return self.cw_adapt_to('IWorkflowable').fire_transition(tr, comment, commentformat)
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').change_state()")
-    def change_state(self, statename, comment=None, commentformat=None, tr=None):
-        return self.cw_adapt_to('IWorkflowable').change_state(statename, comment, commentformat, tr)
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo()")
-    def subworkflow_input_trinfo(self):
-        return self.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo()
-    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_transition()")
-    def subworkflow_input_transition(self):
-        return self.cw_adapt_to('IWorkflowable').subworkflow_input_transition()
-
-
-MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn
-
-
-
-class IWorkflowableAdapter(WorkflowableMixIn, EntityAdapter):
+class IWorkflowableAdapter(EntityAdapter):
     """base adapter providing workflow helper methods for workflowable entities.
     """
     __regid__ = 'IWorkflowable'
--- a/entity.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/entity.py	Mon Dec 09 16:13:10 2013 +0100
@@ -42,7 +42,6 @@
 from cubicweb.rqlrewrite import RQLRewriter
 
 from cubicweb.uilib import soup2xhtml
-from cubicweb.mixins import MI_REL_TRIGGERS
 from cubicweb.mttransforms import ENGINE
 
 _marker = object()
@@ -194,31 +193,11 @@
             setattr(cls, rschema.type, Attribute(rschema.type))
         mixins = []
         for rschema, _, role in eschema.relation_definitions():
-            if (rschema, role) in MI_REL_TRIGGERS:
-                mixin = MI_REL_TRIGGERS[(rschema, role)]
-                if not (issubclass(cls, mixin) or mixin in mixins): # already mixed ?
-                    mixins.append(mixin)
-                for iface in getattr(mixin, '__implements__', ()):
-                    if not interface.implements(cls, iface):
-                        interface.extend(cls, iface)
             if role == 'subject':
                 attr = rschema.type
             else:
                 attr = 'reverse_%s' % rschema.type
             setattr(cls, attr, Relation(rschema, role))
-        if mixins:
-            # see etype class instantation in cwvreg.ETypeRegistry.etype_class method:
-            # due to class dumping, cls is the generated top level class with actual
-            # user class as (only) parent. Since we want to be able to override mixins
-            # method from this user class, we have to take care to insert mixins after that
-            # class
-            #
-            # note that we don't plug mixins as user class parent since it causes pb
-            # with some cases of entity classes inheritance.
-            mixins.insert(0, cls.__bases__[0])
-            mixins += cls.__bases__[1:]
-            cls.__bases__ = tuple(mixins)
-            cls.info('plugged %s mixins on %s', mixins, cls)
 
     fetch_attrs = ('modification_date',)
 
@@ -558,7 +537,14 @@
         raise NotImplementedError('comparison not implemented for %s' % self.__class__)
 
     def __eq__(self, other):
-        raise NotImplementedError('comparison not implemented for %s' % self.__class__)
+        if isinstance(self.eid, (int, long)):
+            return self.eid == other.eid
+        return self is other
+
+    def __hash__(self):
+        if isinstance(self.eid, (int, long)):
+            return self.eid
+        return super(Entity, self).__hash__()
 
     def _cw_update_attr_cache(self, attrcache):
         # if context is a repository session, don't consider dont-cache-attrs as
@@ -983,7 +969,7 @@
             return value
 
     def related(self, rtype, role='subject', limit=None, entities=False, # XXX .cw_related
-                safe=False):
+                safe=False, targettypes=None):
         """returns a resultset of related entities
 
         :param rtype:
@@ -997,10 +983,13 @@
         :param safe:
           if True, an empty rset/list of entities will be returned in case of
           :exc:`Unauthorized`, else (the default), the exception is propagated
+        :param targettypes:
+          a tuple of target entity types to restrict the query
         """
         rtype = str(rtype)
-        if limit is None:
-            # we cannot do much wrt cache on limited queries
+        # Caching restricted/limited results is best avoided.
+        cacheable = limit is None and targettypes is None
+        if cacheable:
             cache_key = '%s_%s' % (rtype, role)
             if cache_key in self._cw_related_cache:
                 return self._cw_related_cache[cache_key][entities]
@@ -1008,7 +997,7 @@
             if entities:
                 return []
             return self._cw.empty_rset()
-        rql = self.cw_related_rql(rtype, role, limit=limit)
+        rql = self.cw_related_rql(rtype, role, limit=limit, targettypes=targettypes)
         try:
             rset = self._cw.execute(rql, {'x': self.eid})
         except Unauthorized:
@@ -1016,9 +1005,9 @@
                 raise
             rset = self._cw.empty_rset()
         if entities:
-            if limit is None:
+            if cacheable:
                 self.cw_set_relation_cache(rtype, role, rset)
-                return self.related(rtype, role, limit, entities)
+                return self.related(rtype, role, entities=entities)
             return list(rset.entities())
         else:
             return rset
@@ -1340,34 +1329,6 @@
     def clear_all_caches(self):
         return self.cw_clear_all_caches()
 
-    @deprecated('[3.9] use entity.cw_attr_value(attr)')
-    def get_value(self, name):
-        return self.cw_attr_value(name)
-
-    @deprecated('[3.9] use entity.cw_delete()')
-    def delete(self, **kwargs):
-        return self.cw_delete(**kwargs)
-
-    @deprecated('[3.9] use entity.cw_attr_metadata(attr, metadata)')
-    def attr_metadata(self, attr, metadata):
-        return self.cw_attr_metadata(attr, metadata)
-
-    @deprecated('[3.9] use entity.cw_has_perm(action)')
-    def has_perm(self, action):
-        return self.cw_has_perm(action)
-
-    @deprecated('[3.9] use entity.cw_set_relation_cache(rtype, role, rset)')
-    def set_related_cache(self, rtype, role, rset):
-        self.cw_set_relation_cache(rtype, role, rset)
-
-    @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role)')
-    def clear_related_cache(self, rtype=None, role=None):
-        self.cw_clear_relation_cache(rtype, role)
-
-    @deprecated('[3.9] use entity.cw_related_rql(rtype, [role, [targettypes]])')
-    def related_rql(self, rtype, role='subject', targettypes=None):
-        return self.cw_related_rql(rtype, role, targettypes)
-
     @property
     @deprecated('[3.10] use entity.cw_edited')
     def edited_attributes(self):
--- a/etwist/request.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/etwist/request.py	Mon Dec 09 16:13:10 2013 +0100
@@ -24,15 +24,21 @@
 
 
 class CubicWebTwistedRequestAdapter(CubicWebRequestBase):
+    """ from twisted .req to cubicweb .form
+    req.files are put into .form[<filefield>]
+    """
     def __init__(self, req, vreg, https):
         self._twreq = req
         super(CubicWebTwistedRequestAdapter, self).__init__(
             vreg, https, req.args, headers=req.received_headers)
-        for key, (name, stream) in req.files.iteritems():
-            if name is None:
-                self.form[key] = (name, stream)
-            else:
-                self.form[key] = (unicode(name, self.encoding), stream)
+        for key, name_stream_list in req.files.iteritems():
+            for name, stream in name_stream_list:
+                if name is not None:
+                    name = unicode(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
 
     def http_method(self):
--- a/etwist/server.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/etwist/server.py	Mon Dec 09 16:13:10 2013 +0100
@@ -244,7 +244,6 @@
             self._do_process_multipart = True
     self.process()
 
-
 @monkeypatch(http.Request)
 def process_multipart(self):
     if not self._do_process_multipart:
@@ -254,16 +253,17 @@
                         keep_blank_values=1,
                         strict_parsing=1)
     for key in form:
-        value = form[key]
-        if isinstance(value, list):
-            self.args[key] = [v.value for v in value]
-        elif value.filename:
-            if value.done != -1: # -1 is transfer has been interrupted
-                self.files[key] = (value.filename, value.file)
+        values = form[key]
+        if not isinstance(values, list):
+            values = [values]
+        for value in values:
+            if value.filename:
+                if value.done != -1: # -1 is transfer has been interrupted
+                    self.files.setdefault(key, []).append((value.filename, value.file))
+                else:
+                    self.files.setdefault(key, []).append((None, None))
             else:
-                self.files[key] = (None, None)
-        else:
-            self.args[key] = value.value
+                self.args.setdefault(key, []).append(value.value)
 
 from logging import getLogger
 from cubicweb import set_log_methods
--- a/etwist/twconfig.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/etwist/twconfig.py	Mon Dec 09 16:13:10 2013 +0100
@@ -34,9 +34,8 @@
 from cubicweb.web.webconfig import WebConfiguration
 
 
-class TwistedConfiguration(WebConfiguration):
+class WebConfigurationBase(WebConfiguration):
     """web instance (in a twisted web server) client of a RQL server"""
-    name = 'twisted'
 
     options = merge_options((
         # ctl configuration
@@ -107,19 +106,17 @@
         return 'http://%s:%s/' % (self['host'] or getfqdn(), self['port'] or 8080)
 
 
-CONFIGURATIONS.append(TwistedConfiguration)
-
 try:
     from cubicweb.server.serverconfig import ServerConfiguration
 
-    class AllInOneConfiguration(TwistedConfiguration, ServerConfiguration):
+    class AllInOneConfiguration(WebConfigurationBase, ServerConfiguration):
         """repository and web instance in the same twisted process"""
         name = 'all-in-one'
-        options = merge_options(TwistedConfiguration.options
+        options = merge_options(WebConfigurationBase.options
                                 + ServerConfiguration.options)
 
-        cubicweb_appobject_path = TwistedConfiguration.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path
-        cube_appobject_path = TwistedConfiguration.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
         def pyro_enabled(self):
             """tell if pyro is activated for the in memory repository"""
             return self['pyro-server']
--- a/ext/rest.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/ext/rest.py	Mon Dec 09 16:13:10 2013 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -29,6 +29,8 @@
 
 * `sourcecode` (if pygments is installed), source code colorization
 
+* `rql-table`, create a table from a RQL query
+
 """
 __docformat__ = "restructuredtext en"
 
@@ -40,7 +42,7 @@
 
 from docutils import statemachine, nodes, utils, io
 from docutils.core import Publisher
-from docutils.parsers.rst import Parser, states, directives
+from docutils.parsers.rst import Parser, states, directives, Directive
 from docutils.parsers.rst.roles import register_canonical_role, set_classes
 
 from logilab.mtconverter import ESC_UCAR_TABLE, ESC_CAR_TABLE, xml_escape
@@ -251,6 +253,76 @@
 winclude_directive.options = {'literal': directives.flag,
                               'encoding': directives.encoding}
 
+class RQLTableDirective(Directive):
+    """rql-table directive
+
+    Example:
+
+        .. rql-table::
+           :vid: mytable
+           :headers: , , progress
+           :colvids: 2=progress
+
+            Any X,U,X WHERE X is Project, X url U
+
+    All fields but the RQL string are optionnal. The ``:headers:`` option can
+    contain empty column names.
+    """
+
+    required_arguments = 0
+    optional_arguments = 0
+    has_content= True
+    final_argument_whitespace = True
+    option_spec = {'vid': directives.unchanged,
+                   'headers': directives.unchanged,
+                   'colvids': directives.unchanged}
+
+    def run(self):
+        errid = "rql-table directive"
+        self.assert_has_content()
+        if self.arguments:
+            raise self.warning('%s does not accept arguments' % errid)
+        rql = ' '.join([l.strip() for l in self.content])
+        _cw = self.state.document.settings.context._cw
+        _cw.ensure_ro_rql(rql)
+        try:
+            rset = _cw.execute(rql)
+        except Exception as exc:
+            raise self.error("fail to execute RQL query in %s: %r" %
+                             (errid, exc))
+        if not rset:
+            raise self.warning("empty result set")
+        vid = self.options.get('vid', 'table')
+        try:
+            view = _cw.vreg['views'].select(vid, _cw, rset=rset)
+        except Exception as exc:
+            raise self.error("fail to select '%s' view in %s: %r" %
+                             (vid, errid, exc))
+        headers = None
+        if 'headers' in self.options:
+            headers = [h.strip() for h in self.options['headers'].split(',')]
+            while headers.count(''):
+                headers[headers.index('')] = None
+            if len(headers) != len(rset[0]):
+                raise self.error("the number of 'headers' does not match the "
+                                 "number of columns in %s" % errid)
+        cellvids = None
+        if 'colvids' in self.options:
+            cellvids = {}
+            for f in self.options['colvids'].split(','):
+                try:
+                    idx, vid = f.strip().split('=')
+                except ValueError:
+                    raise self.error("malformatted 'colvids' option in %s" %
+                                     errid)
+                cellvids[int(idx.strip())] = vid.strip()
+        try:
+            content = view.render(headers=headers, cellvids=cellvids)
+        except Exception as exc:
+            raise self.error("Error rendering %s (%s)" % (errid, exc))
+        return [nodes.raw('', content, format='html')]
+
+
 try:
     from pygments import highlight
     from pygments.lexers import get_lexer_by_name
@@ -385,3 +457,4 @@
     directives.register_directive('winclude', winclude_directive)
     if pygments_directive is not None:
         directives.register_directive('sourcecode', pygments_directive)
+    directives.register_directive('rql-table', RQLTableDirective)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/test/data/views.py	Mon Dec 09 16:13:10 2013 +0100
@@ -0,0 +1,24 @@
+# copyright 2013 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.web.views import tableview
+
+class CustomRsetTableView(tableview.RsetTableView):
+    __regid__ = 'mytable'
+
--- a/ext/test/unittest_rest.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/ext/test/unittest_rest.py	Mon Dec 09 16:13:10 2013 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -82,5 +82,133 @@
         out = rest_publish(context, ':bookmark:`%s`' % eid)
         self.assertEqual(out, u'<p><h1>CWUser_plural</h1><div class="section"><a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div><div class="section"><a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div></p>\n')
 
+    def test_rqltable_nocontent(self):
+        context = self.context()
+        out = rest_publish(context, """.. rql-table::""")
+        self.assertIn("System Message: ERROR", out)
+        self.assertIn("Content block expected for the &quot;rql-table&quot; "
+                      "directive; none found" , out)
+
+    def test_rqltable_norset(self):
+        context = self.context()
+        rql = "Any X WHERE X is CWUser, X firstname 'franky'"
+        out = rest_publish(
+            context, """\
+.. rql-table::
+
+            %(rql)s""" % {'rql': rql})
+        self.assertIn("System Message: WARNING", out)
+        self.assertIn("empty result set", out)
+
+    def test_rqltable_nooptions(self):
+        rql = """Any S,F,L WHERE X is CWUser, X surname S,
+                                 X firstname F, X login L"""
+        out = rest_publish(
+            self.context(), """\
+.. rql-table::
+
+   %(rql)s
+            """ % {'rql': rql})
+        req = self.request()
+        view = self.vreg['views'].select('table', req, rset=req.execute(rql))
+        self.assertEqual(view.render(w=None)[49:], out[49:])
+
+    def test_rqltable_vid(self):
+        rql = """Any S,F,L WHERE X is CWUser, X surname S,
+                                 X firstname F, X login L"""
+        vid = 'mytable'
+        out = rest_publish(
+            self.context(), """\
+.. rql-table::
+   :vid: %(vid)s
+
+   %(rql)s
+            """ % {'rql': rql, 'vid': vid})
+        req = self.request()
+        view = self.vreg['views'].select(vid, req, rset=req.execute(rql))
+        self.assertEqual(view.render(w=None)[49:], out[49:])
+        self.assertIn(vid, out[:49])
+
+    def test_rqltable_badvid(self):
+        rql = """Any S,F,L WHERE X is CWUser, X surname S,
+                                 X firstname F, X login L"""
+        vid = 'mytabel'
+        out = rest_publish(
+            self.context(), """\
+.. rql-table::
+   :vid: %(vid)s
+
+   %(rql)s
+            """ % {'rql': rql, 'vid': vid})
+        self.assertIn("fail to select '%s' view" % vid, out)
+
+    def test_rqltable_headers(self):
+        rql = """Any S,F,L WHERE X is CWUser, X surname S,
+                                 X firstname F, X login L"""
+        headers = ["nom", "prenom", "identifiant"]
+        out = rest_publish(
+            self.context(), """\
+.. rql-table::
+   :headers: %(headers)s
+
+   %(rql)s
+            """ % {'rql': rql, 'headers': ', '.join(headers)})
+        req = self.request()
+        view = self.vreg['views'].select('table', req, rset=req.execute(rql))
+        view.headers = headers
+        self.assertEqual(view.render(w=None)[49:], out[49:])
+
+    def test_rqltable_headers_missing(self):
+        rql = """Any S,F,L WHERE X is CWUser, X surname S,
+                                 X firstname F, X login L"""
+        headers = ["nom", "", "identifiant"]
+        out = rest_publish(
+            self.context(), """\
+.. rql-table::
+   :headers: %(headers)s
+
+   %(rql)s
+            """ % {'rql': rql, 'headers': ', '.join(headers)})
+        req = self.request()
+        view = self.vreg['views'].select('table', req, rset=req.execute(rql))
+        view.headers = [headers[0], None, headers[2]]
+        self.assertEqual(view.render(w=None)[49:], out[49:])
+
+    def test_rqltable_headers_missing_edges(self):
+        rql = """Any S,F,L WHERE X is CWUser, X surname S,
+                                 X firstname F, X login L"""
+        headers = [" ", "prenom", ""]
+        out = rest_publish(
+            self.context(), """\
+.. rql-table::
+   :headers: %(headers)s
+
+   %(rql)s
+            """ % {'rql': rql, 'headers': ', '.join(headers)})
+        req = self.request()
+        view = self.vreg['views'].select('table', req, rset=req.execute(rql))
+        view.headers = [None, headers[1], None]
+        self.assertEqual(view.render(w=None)[49:], out[49:])
+
+    def test_rqltable_colvids(self):
+        rql = """Any X,S,F,L WHERE X is CWUser, X surname S,
+                                   X firstname F, X login L"""
+        colvids = {0: "oneline"}
+        out = rest_publish(
+            self.context(), """\
+.. rql-table::
+   :colvids: %(colvids)s
+
+   %(rql)s
+            """ % {'rql': rql,
+                   'colvids': ', '.join(["%d=%s" % (k, v)
+                                         for k, v in colvids.iteritems()])
+                  })
+        req = self.request()
+        view = self.vreg['views'].select('table', req, rset=req.execute(rql))
+        view.cellvids = colvids
+        self.assertEqual(view.render(w=None)[49:], out[49:])
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/hooks/metadata.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/hooks/metadata.py	Mon Dec 09 16:13:10 2013 +0100
@@ -149,7 +149,7 @@
 
 # entity source handling #######################################################
 
-class ChangeEntityUpdateCaches(hook.Operation):
+class ChangeEntitySourceUpdateCaches(hook.Operation):
     oldsource = newsource = entity = None # make pylint happy
 
     def postcommit_event(self):
@@ -221,6 +221,6 @@
                      'mtime': datetime.now()}
             self._cw.system_sql(syssource.sqlgen.insert('entities', attrs), attrs)
             # register an operation to update repository/sources caches
-            ChangeEntityUpdateCaches(self._cw, entity=entity,
-                                     oldsource=oldsource.repo_source,
-                                     newsource=syssource)
+            ChangeEntitySourceUpdateCaches(self._cw, entity=entity,
+                                           oldsource=oldsource.repo_source,
+                                           newsource=syssource)
--- a/hooks/security.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/hooks/security.py	Mon Dec 09 16:13:10 2013 +0100
@@ -111,17 +111,11 @@
     events = ('after_update_entity',)
 
     def __call__(self):
-        try:
-            # check user has permission right now, if not retry at commit time
-            self.entity.cw_check_perm('update')
-            check_entity_attributes(self._cw, self.entity)
-        except Unauthorized:
-            self.entity._cw_clear_local_perm_cache('update')
-            # save back editedattrs in case the entity is reedited later in the
-            # same transaction, which will lead to cw_edited being
-            # overwritten
-            CheckEntityPermissionOp.get_instance(self._cw).add_data(
-                (self.entity.eid, 'update', self.entity.cw_edited) )
+        # save back editedattrs in case the entity is reedited later in the
+        # same transaction, which will lead to cw_edited being
+        # overwritten
+        CheckEntityPermissionOp.get_instance(self._cw).add_data(
+            (self.entity.eid, 'update', self.entity.cw_edited) )
 
 
 class BeforeDelEntitySecurityHook(SecurityHook):
--- a/hooks/syncschema.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/hooks/syncschema.py	Mon Dec 09 16:13:10 2013 +0100
@@ -28,7 +28,7 @@
 
 from copy import copy
 from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema
-from yams import buildobjs as ybo, schema2sql as y2sql
+from yams import buildobjs as ybo, schema2sql as y2sql, convert_default_value
 
 from logilab.common.decorators import clear_cache
 
@@ -39,21 +39,6 @@
 from cubicweb.server import hook, schemaserial as ss
 from cubicweb.server.sqlutils import SQL_PREFIX
 
-
-TYPE_CONVERTER = { # XXX
-    'Boolean': bool,
-    'Int': int,
-    'BigInt': int,
-    'Float': float,
-    'Password': str,
-    'String': unicode,
-    'Date' : unicode,
-    'Datetime' : unicode,
-    'Time' : unicode,
-    'TZDatetime' : unicode,
-    'TZTime' : unicode,
-    }
-
 # core entity and relation types which can't be removed
 CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set(
     ('CWUser', 'CWGroup','login', 'upassword', 'name', 'in_group'))
@@ -437,11 +422,11 @@
     def precommit_event(self):
         session = self.session
         entity = self.entity
-        # entity.defaultval is a string or None, but we need a correctly typed
+        # entity.defaultval is a Binary or None, but we need a correctly typed
         # value
         default = entity.defaultval
         if default is not None:
-            default = TYPE_CONVERTER[entity.otype.name](default)
+            default = default.unzpickle()
         props = {'default': default,
                  'indexed': entity.indexed,
                  'fulltextindexed': entity.fulltextindexed,
@@ -493,20 +478,11 @@
         # attribute is still set to False, so we've to ensure it's False
         rschema.final = True
         insert_rdef_on_subclasses(session, eschema, rschema, rdefdef, props)
-        # set default value, using sql for performance and to avoid
-        # modification_date update
-        if default:
-            if rdefdef.object in ('Date', 'Datetime', 'TZDatetime'):
-                # XXX may may want to use creation_date
-                if default == 'TODAY':
-                    default = syssource.dbhelper.sql_current_date()
-                elif default == 'NOW':
-                    default = syssource.dbhelper.sql_current_timestamp()
-                session.system_sql('UPDATE %s SET %s=%s'
-                                   % (table, column, default))
-            else:
-                session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
-                                   {'default': default})
+        # update existing entities with the default value of newly added attribute
+        if default is not None:
+            default = convert_default_value(self.rdefdef, default)
+            session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
+                               {'default': default})
 
     def revertprecommit_event(self):
         # revert changes on in memory schema
--- a/hooks/test/unittest_syncschema.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/hooks/test/unittest_syncschema.py	Mon Dec 09 16:13:10 2013 +0100
@@ -19,7 +19,7 @@
 
 from logilab.common.testlib import TestCase, unittest_main
 
-from cubicweb import ValidationError
+from cubicweb import ValidationError, Binary
 from cubicweb.schema import META_RTYPES
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.sqlutils import SQL_PREFIX
@@ -74,9 +74,10 @@
         self.commit()
         self.assertTrue(schema.has_entity('Societe2'))
         self.assertTrue(schema.has_relation('concerne2'))
-        attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval "noname", '
+        attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval %(default)s, '
                                '   X indexed TRUE, X relation_type RT, X from_entity E, X to_entity F '
-                               'WHERE RT name "name", E name "Societe2", F name "String"')[0][0]
+                               'WHERE RT name "name", E name "Societe2", F name "String"',
+                               {'default': Binary.zpickle('noname')})[0][0]
         self._set_attr_perms(attreid)
         concerne2_rdef_eid = self.execute(
             'INSERT CWRelation X: X cardinality "**", X relation_type RT, X from_entity E, X to_entity E '
@@ -290,8 +291,10 @@
 
 
     def test_add_attribute_to_base_class(self):
-        attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval "noname", X indexed TRUE, X relation_type RT, X from_entity E, X to_entity F '
-                               'WHERE RT name "messageid", E name "BaseTransition", F name "String"')[0][0]
+        attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval %(default)s, '
+                               'X indexed TRUE, X relation_type RT, X from_entity E, X to_entity F '
+                               'WHERE RT name "messageid", E name "BaseTransition", F name "String"',
+                               {'default': Binary.zpickle('noname')})[0][0]
         assert self.execute('SET X read_permission Y WHERE X eid %(x)s, Y name "managers"',
                      {'x': attreid})
         self.commit()
--- a/interfaces.py	Fri Dec 06 17:20:59 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,214 +0,0 @@
-# copyright 2003-2010 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/>.
-"""Standard interfaces. Deprecated in favor of adapters.
-
-.. note::
-
-  The `implements` selector used to match not only entity classes but also their
-  interfaces. This will disappear in a future version. You should define an
-  adapter for that interface and use `adaptable('MyIFace')` selector on appobjects
-  that require that interface.
-
-"""
-__docformat__ = "restructuredtext en"
-
-from logilab.common.interface import Interface
-
-
-# XXX deprecates in favor of IProgressAdapter
-class IProgress(Interface):
-    """something that has a cost, a state and a progression"""
-
-    @property
-    def cost(self):
-        """the total cost"""
-
-    @property
-    def done(self):
-        """what is already done"""
-
-    @property
-    def todo(self):
-        """what remains to be done"""
-
-    def progress_info(self):
-        """returns a dictionary describing progress/estimated cost of the
-        version.
-
-        - mandatory keys are (''estimated', 'done', 'todo')
-
-        - optional keys are ('notestimated', 'notestimatedcorrected',
-          'estimatedcorrected')
-
-        'noestimated' and 'notestimatedcorrected' should default to 0
-        'estimatedcorrected' should default to 'estimated'
-        """
-
-    def finished(self):
-        """returns True if status is finished"""
-
-    def in_progress(self):
-        """returns True if status is not finished"""
-
-    def progress(self):
-        """returns the % progress of the task item"""
-
-# XXX deprecates in favor of IMileStoneAdapter
-class IMileStone(IProgress):
-    """represents an ITask's item"""
-
-    parent_type = None # specify main task's type
-
-    def get_main_task(self):
-        """returns the main ITask entity"""
-
-    def initial_prevision_date(self):
-        """returns the initial expected end of the milestone"""
-
-    def eta_date(self):
-        """returns expected date of completion based on what remains
-        to be done
-        """
-
-    def completion_date(self):
-        """returns date on which the subtask has been completed"""
-
-    def contractors(self):
-        """returns the list of persons supposed to work on this task"""
-
-# XXX deprecates in favor of IEmbedableAdapter
-class IEmbedable(Interface):
-    """interface for embedable entities"""
-
-    def embeded_url(self):
-        """embed action interface"""
-
-# XXX deprecates in favor of ICalendarViewsAdapter
-class ICalendarViews(Interface):
-    """calendar views interface"""
-    def matching_dates(self, begin, end):
-        """
-        :param begin: day considered as begin of the range (`DateTime`)
-        :param end: day considered as end of the range (`DateTime`)
-
-        :return:
-          a list of dates (`DateTime`) in the range [`begin`, `end`] on which
-          this entity apply
-        """
-
-# XXX deprecates in favor of ICalendarableAdapter
-class ICalendarable(Interface):
-    """interface for items that do have a begin date 'start' and an end date 'stop'
-    """
-
-    @property
-    def start(self):
-        """return start date"""
-
-    @property
-    def stop(self):
-        """return stop state"""
-
-# XXX deprecates in favor of ICalendarableAdapter
-class ITimetableViews(Interface):
-    """timetable views interface"""
-    def timetable_date(self):
-        """XXX explain
-
-        :return: date (`DateTime`)
-        """
-
-# XXX deprecates in favor of IGeocodableAdapter
-class IGeocodable(Interface):
-    """interface required by geocoding views such as gmap-view"""
-
-    @property
-    def latitude(self):
-        """returns the latitude of the entity"""
-
-    @property
-    def longitude(self):
-        """returns the longitude of the entity"""
-
-    def marker_icon(self):
-        """returns the icon that should be used as the marker"""
-
-
-# XXX deprecates in favor of IEmailableAdapter
-class IFeed(Interface):
-    """interface for entities with rss flux"""
-
-    def rss_feed_url(self):
-        """"""
-
-# XXX deprecates in favor of IDownloadableAdapter
-class IDownloadable(Interface):
-    """interface for downloadable entities"""
-
-    def download_url(self): # XXX not really part of this interface
-        """return an url to download entity's content"""
-    def download_content_type(self):
-        """return MIME type of the downloadable content"""
-    def download_encoding(self):
-        """return encoding of the downloadable content"""
-    def download_file_name(self):
-        """return file name of the downloadable content"""
-    def download_data(self):
-        """return actual data of the downloadable content"""
-
-# XXX deprecates in favor of IPrevNextAdapter
-class IPrevNext(Interface):
-    """interface for entities which can be linked to a previous and/or next
-    entity
-    """
-
-    def next_entity(self):
-        """return the 'next' entity"""
-    def previous_entity(self):
-        """return the 'previous' entity"""
-
-# XXX deprecates in favor of IBreadCrumbsAdapter
-class IBreadCrumbs(Interface):
-
-    def breadcrumbs(self, view, recurs=False):
-        pass
-
-# XXX deprecates in favor of ITreeAdapter
-class ITree(Interface):
-
-    def parent(self):
-        """returns the parent entity"""
-
-    def children(self):
-        """returns the item's children"""
-
-    def children_rql(self):
-        """XXX returns RQL to get children"""
-
-    def iterchildren(self):
-        """iterates over the item's children"""
-
-    def is_leaf(self):
-        """returns true if this node as no child"""
-
-    def is_root(self):
-        """returns true if this node has no parent"""
-
-    def root(self):
-        """returns the root object"""
-
--- a/migration.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/migration.py	Mon Dec 09 16:13:10 2013 +0100
@@ -257,7 +257,7 @@
             home_key = 'HOME'
             if sys.platform == 'win32':
                 home_key = 'USERPROFILE'
-            histfile = os.path.join(os.environ[home_key], ".eshellhist")
+            histfile = os.path.join(os.environ[home_key], ".cwshell_history")
             try:
                 readline.read_history_file(histfile)
             except IOError:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.18.0_Any.py	Mon Dec 09 16:13:10 2013 +0100
@@ -0,0 +1,83 @@
+sync_schema_props_perms('defaultval')
+
+def convert_defaultval(cwattr, default):
+    from decimal import Decimal
+    import yams
+    from cubicweb import Binary
+    if default is None:
+        return
+    atype = cwattr.to_entity[0].name
+    if atype == 'Boolean':
+        assert default in ('True', 'False'), default
+        default = default == 'True'
+    elif atype in ('Int', 'BigInt'):
+        default = int(default)
+    elif atype == 'Float':
+        default = float(default)
+    elif atype == 'Decimal':
+        default = Decimal(default)
+    elif atype in ('Date', 'Datetime', 'TZDatetime', 'Time'):
+        try:
+            # handle NOW and TODAY, keep them stored as strings
+            yams.KEYWORD_MAP[atype][default.upper()]
+            default = default.upper()
+        except KeyError:
+            # otherwise get an actual date or datetime
+            default = yams.DATE_FACTORY_MAP[atype](default)
+    else:
+        assert atype == 'String', atype
+        default = unicode(default)
+    return Binary.zpickle(default)
+
+dbh = repo.system_source.dbhelper
+driver = config.sources()['system']['db-driver']
+
+if driver == 'postgres' or driver.startswith('sqlserver'):
+
+    sql('ALTER TABLE cw_cwattribute ADD new_defaultval %s' % dbh.TYPE_MAPPING['Bytes'])
+
+    for cwattr in rql('CWAttribute X').entities():
+        olddefault = cwattr.defaultval
+        if olddefault is not None:
+            req = "UPDATE cw_cwattribute SET new_defaultval = %(val)s WHERE cw_eid = %(eid)s"
+            args = {'val': dbh.binary_value(convert_defaultval(cwattr, olddefault).getvalue()), 'eid': cwattr.eid}
+            sql(req, args, ask_confirm=False)
+
+    sql('ALTER TABLE cw_cwattribute DROP COLUMN cw_defaultval')
+    if config.sources()['system']['db-driver'] == 'postgres':
+        sql('ALTER TABLE cw_cwattribute RENAME COLUMN new_defaultval TO cw_defaultval')
+    else:
+        sql("sp_rename 'cw_cwattribute.new_defaultval', 'cw_defaultval', 'COLUMN'")
+
+elif driver == 'sqlite':
+
+    import re
+    create = sql("SELECT sql FROM sqlite_master WHERE name = 'cw_CWAttribute'")[0][0]
+    create = re.sub('cw_defaultval varchar[^,]*,', 'cw_defaultval bytea,', create, re.I)
+    create = re.sub('cw_CWAttribute', 'tmp_cw_CWAttribute', create, re.I)
+    sql(create)
+    sql("INSERT INTO tmp_cw_CWAttribute SELECT * FROM cw_CWAttribute")
+    for cwattr in rql('CWAttribute X').entities():
+        olddefault = cwattr.defaultval
+        if olddefault is None:
+            continue
+        req = "UPDATE tmp_cw_CWAttribute SET cw_defaultval = %(val)s WHERE cw_eid = %(eid)s"
+        args = {'val': dbh.binary_value(convert_defaultval(cwattr, olddefault).getvalue()),
+                'eid': cwattr.eid}
+        sql(req, args, ask_confirm=False)
+
+    sql('DROP TABLE cw_CWAttribute')
+    sql('ALTER TABLE tmp_cw_CWAttribute RENAME TO cw_CWAttribute')
+
+else:
+    assert False, 'upgrade not supported on this database backend'
+
+# Set object type to "Bytes" for CWAttribute's "defaultval" attribute
+rql('SET X to_entity B WHERE X is CWAttribute, X from_entity Y, Y name "CWAttribute", '
+    'X relation_type Z, Z name "defaultval", B name "Bytes"')
+
+from yams import buildobjs as ybo
+schema.add_relation_def(ybo.RelationDefinition('CWAttribute', 'defaultval', 'Bytes'))
+schema.del_relation_def('CWAttribute', 'defaultval', 'String')
+
+commit()
--- a/mixins.py	Fri Dec 06 17:20:59 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,308 +0,0 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""mixins of entity/views organized somewhat in a graph or tree structure"""
-__docformat__ = "restructuredtext en"
-
-from itertools import chain
-
-from logilab.common.decorators import cached
-from logilab.common.deprecation import deprecated, class_deprecated
-
-from cubicweb.predicates import implements
-from cubicweb.interfaces import ITree
-
-
-class TreeMixIn(object):
-    """base tree-mixin implementing the tree interface
-
-    This mixin has to be inherited explicitly and configured using the
-    tree_attribute, parent_target and children_target class attribute to
-    benefit from this default implementation
-    """
-    __metaclass__ = class_deprecated
-    __deprecation_warning__ = '[3.9] TreeMixIn is deprecated, use/override ITreeAdapter instead (%(cls)s)'
-
-    tree_attribute = None
-    # XXX misnamed
-    parent_target = 'subject'
-    children_target = 'object'
-
-    def different_type_children(self, entities=True):
-        """return children entities of different type as this entity.
-
-        according to the `entities` parameter, return entity objects or the
-        equivalent result set
-        """
-        res = self.related(self.tree_attribute, self.children_target,
-                           entities=entities)
-        if entities:
-            return [e for e in res if e.e_schema != self.e_schema]
-        return res.filtered_rset(lambda x: x.e_schema != self.e_schema, self.cw_col)
-
-    def same_type_children(self, entities=True):
-        """return children entities of the same type as this entity.
-
-        according to the `entities` parameter, return entity objects or the
-        equivalent result set
-        """
-        res = self.related(self.tree_attribute, self.children_target,
-                           entities=entities)
-        if entities:
-            return [e for e in res if e.e_schema == self.e_schema]
-        return res.filtered_rset(lambda x: x.e_schema is self.e_schema, self.cw_col)
-
-    def iterchildren(self, _done=None):
-        if _done is None:
-            _done = set()
-        for child in self.children():
-            if child.eid in _done:
-                self.error('loop in %s tree: %s', self.__regid__.lower(), child)
-                continue
-            yield child
-            _done.add(child.eid)
-
-    def prefixiter(self, _done=None):
-        if _done is None:
-            _done = set()
-        if self.eid in _done:
-            return
-        _done.add(self.eid)
-        yield self
-        for child in self.same_type_children():
-            for entity in child.prefixiter(_done):
-                yield entity
-
-    @cached
-    def path(self):
-        """returns the list of eids from the root object to this object"""
-        path = []
-        parent = self
-        while parent:
-            if parent.eid in path:
-                self.error('loop in %s tree: %s', self.__regid__.lower(), parent)
-                break
-            path.append(parent.eid)
-            try:
-                # check we are not leaving the tree
-                if (parent.tree_attribute != self.tree_attribute or
-                    parent.parent_target != self.parent_target):
-                    break
-                parent = parent.parent()
-            except AttributeError:
-                break
-
-        path.reverse()
-        return path
-
-    def iterparents(self, strict=True):
-        def _uptoroot(self):
-            curr = self
-            while True:
-                curr = curr.parent()
-                if curr is None:
-                    break
-                yield curr
-        if not strict:
-            return chain([self], _uptoroot(self))
-        return _uptoroot(self)
-
-    ## ITree interface ########################################################
-    def parent(self):
-        """return the parent entity if any, else None (e.g. if we are on the
-        root
-        """
-        try:
-            return self.related(self.tree_attribute, self.parent_target,
-                                entities=True)[0]
-        except (KeyError, IndexError):
-            return None
-
-    def children(self, entities=True, sametype=False):
-        """return children entities
-
-        according to the `entities` parameter, return entity objects or the
-        equivalent result set
-        """
-        if sametype:
-            return self.same_type_children(entities)
-        else:
-            return self.related(self.tree_attribute, self.children_target,
-                                entities=entities)
-
-    def children_rql(self):
-        return self.cw_related_rql(self.tree_attribute, self.children_target)
-
-    def is_leaf(self):
-        return len(self.children()) == 0
-
-    def is_root(self):
-        return self.parent() is None
-
-    def root(self):
-        """return the root object"""
-        return self._cw.entity_from_eid(self.path()[0])
-
-
-class EmailableMixIn(object):
-    """base mixin providing the default get_email() method used by
-    the massmailing view
-
-    NOTE: The default implementation is based on the
-    primary_email / use_email scheme
-    """
-    @deprecated("[3.9] use entity.cw_adapt_to('IEmailable').get_email()")
-    def get_email(self):
-        if getattr(self, 'primary_email', None):
-            return self.primary_email[0].address
-        if getattr(self, 'use_email', None):
-            return self.use_email[0].address
-        return None
-
-
-"""pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity
-classes which have the relation described by the dict's key.
-
-NOTE: pluggable mixins can't override any method of the 'explicit' user classes tree
-(eg without plugged classes). This includes bases Entity and AnyEntity classes.
-"""
-MI_REL_TRIGGERS = {
-    ('primary_email',   'subject'): EmailableMixIn,
-    ('use_email',   'subject'): EmailableMixIn,
-    }
-
-
-# XXX move to cubicweb.web.views.treeview once we delete usage from this file
-def _done_init(done, view, row, col):
-    """handle an infinite recursion safety belt"""
-    if done is None:
-        done = set()
-    entity = view.cw_rset.get_entity(row, col)
-    if entity.eid in done:
-        msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
-            'rel': entity.cw_adapt_to('ITree').tree_relation,
-            'eid': entity.eid
-            }
-        return None, msg
-    done.add(entity.eid)
-    return done, entity
-
-
-class TreeViewMixIn(object):
-    """a recursive tree view"""
-    __metaclass__ = class_deprecated
-    __deprecation_warning__ = '[3.9] TreeViewMixIn is deprecated, use/override BaseTreeView instead (%(cls)s)'
-
-    __regid__ = 'tree'
-    __select__ = implements(ITree, warn=False)
-    item_vid = 'treeitem'
-
-    def call(self, done=None, **kwargs):
-        if done is None:
-            done = set()
-        super(TreeViewMixIn, self).call(done=done, **kwargs)
-
-    def cell_call(self, row, col=0, vid=None, done=None, maxlevel=None, **kwargs):
-        assert maxlevel is None or maxlevel > 0
-        done, entity = _done_init(done, self, row, col)
-        if done is None:
-            # entity is actually an error message
-            self.w(u'<li class="badcontent">%s</li>' % entity)
-            return
-        self.open_item(entity)
-        entity.view(vid or self.item_vid, w=self.w, **kwargs)
-        if maxlevel is not None:
-            maxlevel -= 1
-            if maxlevel == 0:
-                self.close_item(entity)
-                return
-        relatedrset = entity.children(entities=False)
-        self.wview(self.__regid__, relatedrset, 'null', done=done,
-                   maxlevel=maxlevel, **kwargs)
-        self.close_item(entity)
-
-    def open_item(self, entity):
-        self.w(u'<li class="%s">\n' % entity.cw_etype.lower())
-    def close_item(self, entity):
-        self.w(u'</li>\n')
-
-
-class TreePathMixIn(object):
-    """a recursive path view"""
-    __metaclass__ = class_deprecated
-    __deprecation_warning__ = '[3.9] TreePathMixIn is deprecated, use/override TreePathView instead (%(cls)s)'
-    __regid__ = 'path'
-    item_vid = 'oneline'
-    separator = u'&#160;&gt;&#160;'
-
-    def call(self, **kwargs):
-        self.w(u'<div class="pathbar">')
-        super(TreePathMixIn, self).call(**kwargs)
-        self.w(u'</div>')
-
-    def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
-        done, entity = _done_init(done, self, row, col)
-        if done is None:
-            # entity is actually an error message
-            self.w(u'<span class="badcontent">%s</span>' % entity)
-            return
-        parent = entity.parent()
-        if parent:
-            parent.view(self.__regid__, w=self.w, done=done)
-            self.w(self.separator)
-        entity.view(vid or self.item_vid, w=self.w)
-
-
-class ProgressMixIn(object):
-    """provide a default implementations for IProgress interface methods"""
-    __metaclass__ = class_deprecated
-    __deprecation_warning__ = '[3.9] ProgressMixIn is deprecated, use/override IProgressAdapter instead (%(cls)s)'
-
-    @property
-    def cost(self):
-        return self.progress_info()['estimated']
-
-    @property
-    def revised_cost(self):
-        return self.progress_info().get('estimatedcorrected', self.cost)
-
-    @property
-    def done(self):
-        return self.progress_info()['done']
-
-    @property
-    def todo(self):
-        return self.progress_info()['todo']
-
-    @cached
-    def progress_info(self):
-        raise NotImplementedError()
-
-    def finished(self):
-        return not self.in_progress()
-
-    def in_progress(self):
-        raise NotImplementedError()
-
-    def progress(self):
-        try:
-            return 100. * self.done / self.revised_cost
-        except ZeroDivisionError:
-            # total cost is 0 : if everything was estimated, task is completed
-            if self.progress_info().get('notestimated'):
-                return 0.
-            return 100
--- a/predicates.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/predicates.py	Mon Dec 09 16:13:10 2013 +0100
@@ -204,27 +204,6 @@
 # remember, these imports are there for bw compat only
 __BACKWARD_COMPAT_IMPORTS = (yes,)
 
-def score_interface(etypesreg, eclass, iface):
-    """Return XXX if the give object (maybe an instance or class) implements
-    the interface.
-    """
-    if getattr(iface, '__registry__', None) == 'etypes':
-        # adjust score if the interface is an entity class
-        parents, any = etypesreg.parent_classes(eclass.__regid__)
-        if iface is eclass:
-            return len(parents) + 4
-        if iface is any: # Any
-            return 1
-        for index, basecls in enumerate(reversed(parents)):
-            if iface is basecls:
-                return index + 3
-        return 0
-    # XXX iface in implements deprecated in 3.9
-    if implements_iface(eclass, iface):
-        # implementing an interface takes precedence other special Any interface
-        return 2
-    return 0
-
 
 # abstract predicates / mixin helpers ###########################################
 
@@ -745,53 +724,6 @@
         return 1 # necessarily true if we're there
 
 
-class implements(EClassPredicate):
-    """Return non-zero score for entity that are of the given type(s) or
-    implements at least one of the given interface(s). If multiple arguments are
-    given, matching one of them is enough.
-
-    Entity types should be given as string, the corresponding class will be
-    fetched from the entity types registry at selection time.
-
-    See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
-    class lookup / score rules according to the input context.
-
-    .. note::
-
-       when interface is an entity class, the score will reflect class
-       proximity so the most specific object will be selected.
-
-    .. note::
-
-       deprecated in cubicweb >= 3.9, use either
-       :class:`~cubicweb.predicates.is_instance` or
-       :class:`~cubicweb.predicates.adaptable`.
-    """
-
-    def __init__(self, *expected_ifaces, **kwargs):
-        emit_warn = kwargs.pop('warn', True)
-        super(implements, self).__init__(**kwargs)
-        self.expected_ifaces = expected_ifaces
-        if emit_warn:
-            warn('[3.9] implements predicate is deprecated, use either '
-                 'is_instance or adaptable', DeprecationWarning, stacklevel=2)
-
-    def __str__(self):
-        return '%s(%s)' % (self.__class__.__name__,
-                           ','.join(str(s) for s in self.expected_ifaces))
-
-    def score_class(self, eclass, req):
-        score = 0
-        etypesreg = req.vreg['etypes']
-        for iface in self.expected_ifaces:
-            if isinstance(iface, basestring):
-                # entity type
-                try:
-                    iface = etypesreg.etype_class(iface)
-                except KeyError:
-                    continue # entity type not in the schema
-            score += score_interface(etypesreg, eclass, iface)
-        return score
 
 def _reset_is_instance_cache(vreg):
     vreg._is_instance_predicate_cache = {}
--- a/schema.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/schema.py	Mon Dec 09 16:13:10 2013 +0100
@@ -44,6 +44,15 @@
 import cubicweb
 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
 
+try:
+    from cubicweb import server
+except ImportError:
+    # We need to lookup DEBUG from there,
+    # however a pure dbapi client may not have it.
+    class server(object): pass
+    server.DEBUG = False
+
+
 PURE_VIRTUAL_RTYPES = set(('identity', 'has_text',))
 VIRTUAL_RTYPES = set(('eid', 'identity', 'has_text',))
 
@@ -268,13 +277,25 @@
         return False
 PermissionMixIn.has_perm = has_perm
 
+
 def check_perm(self, _cw, action, **kwargs):
     # NB: _cw may be a server transaction or a request object.
     #
     # check user is in an allowed group, if so that's enough internal
     # transactions should always stop there
+    DBG = False
+    if server.DEBUG & server.DBG_SEC:
+        if action in server._SECURITY_CAPS:
+            _self_str = str(self)
+            if server._SECURITY_ITEMS:
+                if any(item in _self_str for item in server._SECURITY_ITEMS):
+                    DBG = True
+            else:
+                DBG = True
     groups = self.get_groups(action)
     if _cw.user.matching_groups(groups):
+        if DBG:
+            print 'check_perm: %r %r: user matches %s' % (action, _self_str, groups)
         return
     # if 'owners' in allowed groups, check if the user actually owns this
     # object, if so that's enough
@@ -284,8 +305,15 @@
     if 'owners' in groups and (
           kwargs.get('creating')
           or ('eid' in kwargs and _cw.user.owns(kwargs['eid']))):
+        if DBG:
+            print ('check_perm: %r %r: user is owner or creation time' %
+                   (action, _self_str))
         return
     # else if there is some rql expressions, check them
+    if DBG:
+        print ('check_perm: %r %r %s' %
+               (action, _self_str, [(rqlexpr, kwargs, rqlexpr.check(_cw, **kwargs))
+                                    for rqlexpr in self.get_rqlexprs(action)]))
     if any(rqlexpr.check(_cw, **kwargs)
            for rqlexpr in self.get_rqlexprs(action)):
         return
@@ -933,14 +961,11 @@
     distinct_query = None
 
     def serialize(self):
-        # start with a comma for bw compat,see below
+        # start with a semicolon for bw compat, see below
         return ';' + ','.join(sorted(self.mainvars)) + ';' + self.expression
 
     @classmethod
     def deserialize(cls, value):
-        # XXX < 3.5.10 bw compat
-        if not value.startswith(';'):
-            return cls(value)
         _, mainvars, expression = value.split(';', 2)
         return cls(expression, mainvars)
 
@@ -995,9 +1020,6 @@
                                self.msg or '')
 
     def deserialize(cls, value):
-        # XXX < 3.5.10 bw compat
-        if not value.startswith(';'):
-            return cls(value)
         value, msg = value.split('\n', 1)
         _, mainvars, expression = value.split(';', 2)
         return cls(expression, mainvars, msg)
--- a/schemas/bootstrap.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/schemas/bootstrap.py	Mon Dec 09 16:13:10 2013 +0100
@@ -83,7 +83,7 @@
     indexed = Boolean(description=_('create an index for quick search on this attribute'))
     fulltextindexed = Boolean(description=_('index this attribute\'s value in the plain text index'))
     internationalizable = Boolean(description=_('is this attribute\'s value translatable'))
-    defaultval = String(maxsize=256)
+    defaultval = Bytes(description=_('default value as gziped pickled python object'))
     extra_props = Bytes(description=_('additional type specific properties'))
 
     description = RichString(internationalizable=True,
--- a/server/__init__.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/__init__.py	Mon Dec 09 16:13:10 2013 +0100
@@ -26,6 +26,7 @@
 import sys
 from os.path import join, exists
 from glob import glob
+from contextlib import contextmanager
 
 from logilab.common.modutils import LazyObject
 from logilab.common.textutils import splitstrip
@@ -80,14 +81,57 @@
 DBG_HOOKS = 16
 #: operations
 DBG_OPS = 32
+#: security
+DBG_SEC = 64
 #: more verbosity
-DBG_MORE = 64
+DBG_MORE = 128
 #: all level enabled
-DBG_ALL  = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_HOOKS + DBG_OPS + DBG_MORE
+DBG_ALL  = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_HOOKS + DBG_OPS + DBG_SEC + DBG_MORE
+
+_SECURITY_ITEMS = []
+_SECURITY_CAPS = ['read', 'add', 'update', 'delete']
 
 #: current debug mode
 DEBUG = 0
 
+@contextmanager
+def tunesecurity(items=(), capabilities=()):
+    """Context manager to use in conjunction with DBG_SEC.
+
+    This allows some tuning of:
+    * the monitored capabilities ('read', 'add', ....)
+    * the object being checked by the security checkers
+
+    When no item is given, all of them will be watched.
+    By default all capabilities are monitored, unless specified.
+
+    Example use::
+
+      from cubicweb.server import debugged, DBG_SEC, tunesecurity
+      with debugged(DBG_SEC):
+          with tunesecurity(items=('Elephant', 'trumps'),
+                            capabilities=('update', 'delete')):
+              babar.cw_set(trumps=celeste)
+              flore.cw_delete()
+
+      ==>
+
+      check_perm: 'update' 'relation Elephant.trumps.Elephant'
+       [(ERQLExpression(Any X WHERE U has_update_permission X, X eid %(x)s, U eid %(u)s),
+       {'eid': 2167}, True)]
+      check_perm: 'delete' 'Elephant'
+       [(ERQLExpression(Any X WHERE U has_delete_permission X, X eid %(x)s, U eid %(u)s),
+       {'eid': 2168}, True)]
+
+    """
+    olditems = _SECURITY_ITEMS[:]
+    _SECURITY_ITEMS.extend(list(items))
+    oldactions = _SECURITY_CAPS[:]
+    _SECURITY_CAPS[:] = capabilities
+    yield
+    _SECURITY_ITEMS[:] = olditems
+    _SECURITY_CAPS[:] = oldactions
+
 def set_debug(debugmode):
     """change the repository debugging mode"""
     global DEBUG
@@ -309,7 +353,6 @@
 SOURCE_TYPES = {'native': LazyObject('cubicweb.server.sources.native', 'NativeSQLSource'),
                 'datafeed': LazyObject('cubicweb.server.sources.datafeed', 'DataFeedSource'),
                 'ldapfeed': LazyObject('cubicweb.server.sources.ldapfeed', 'LDAPFeedSource'),
-                'ldapuser': LazyObject('cubicweb.server.sources.ldapuser', 'LDAPUserSource'),
                 'pyrorql': LazyObject('cubicweb.server.sources.pyrorql', 'PyroRQLSource'),
                 'zmqrql': LazyObject('cubicweb.server.sources.zmqrql', 'ZMQRQLSource'),
                 }
--- a/server/hook.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/hook.py	Mon Dec 09 16:13:10 2013 +0100
@@ -678,16 +678,6 @@
                         {'x': self.eidfrom, 'p': self.eidto})
 
 
-PropagateSubjectRelationHook = class_renamed(
-    'PropagateSubjectRelationHook', PropagateRelationHook,
-    '[3.9] PropagateSubjectRelationHook has been renamed to PropagateRelationHook')
-PropagateSubjectRelationAddHook = class_renamed(
-    'PropagateSubjectRelationAddHook', PropagateRelationAddHook,
-    '[3.9] PropagateSubjectRelationAddHook has been renamed to PropagateRelationAddHook')
-PropagateSubjectRelationDelHook = class_renamed(
-    'PropagateSubjectRelationDelHook', PropagateRelationDelHook,
-    '[3.9] PropagateSubjectRelationDelHook has been renamed to PropagateRelationDelHook')
-
 
 # abstract classes for operation ###############################################
 
--- a/server/migractions.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/migractions.py	Mon Dec 09 16:13:10 2013 +0100
@@ -1461,12 +1461,9 @@
                 # no result to fetch
                 return
 
-    def rqlexec(self, rql, kwargs=None, cachekey=None, build_descr=True,
+    def rqlexec(self, rql, kwargs=None, build_descr=True,
                 ask_confirm=False):
         """rql action"""
-        if cachekey is not None:
-            warn('[3.8] cachekey is deprecated, you can safely remove this argument',
-                 DeprecationWarning, stacklevel=2)
         if not isinstance(rql, (tuple, list)):
             rql = ( (rql, kwargs), )
         res = None
--- a/server/msplanner.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/msplanner.py	Mon Dec 09 16:13:10 2013 +0100
@@ -92,6 +92,7 @@
 
 from logilab.common.compat import any
 from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated
 
 from rql import BadRQLQuery
 from rql.stmts import Union, Select
@@ -1262,6 +1263,7 @@
         inputmap.update(step.outputmap)
 
 
+@deprecated('[3.18] old multi-source system will go away in the next version')
 class MSPlanner(SSPlanner):
     """MultiSourcesPlanner: build execution plan for rql queries
 
--- a/server/querier.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/querier.py	Mon Dec 09 16:13:10 2013 +0100
@@ -85,6 +85,7 @@
     # use `term_etype` since we've to deal with rewritten constants here,
     # when used as an external source by another repository.
     # XXX what about local read security w/ those rewritten constants...
+    DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS
     schema = session.repo.schema
     if rqlst.where is not None:
         for rel in rqlst.where.iget_nodes(Relation):
@@ -102,8 +103,14 @@
                                     term_etype(session, rel.children[1].children[0],
                                                solution, args))
             if not session.user.matching_groups(rdef.get_groups('read')):
+                if DBG:
+                    print ('check_read_access: %s %s does not match %s' %
+                           (rdef, session.user.groups, rdef.get_groups('read')))
                 # XXX rqlexpr not allowed
                 raise Unauthorized('read', rel.r_type)
+            if DBG:
+                print ('check_read_access: %s %s matches %s' %
+                       (rdef, session.user.groups, rdef.get_groups('read')))
     localchecks = {}
     # iterate on defined_vars and not on solutions to ignore column aliases
     for varname in rqlst.defined_vars:
@@ -115,6 +122,9 @@
             if not erqlexprs:
                 ex = Unauthorized('read', solution[varname])
                 ex.var = varname
+                if DBG:
+                    print ('check_read_access: %s %s %s %s' %
+                           (varname, eschema, session.user.groups, eschema.get_groups('read')))
                 raise ex
             # don't insert security on variable only referenced by 'NOT X relation Y' or
             # 'NOT EXISTS(X relation Y)'
--- a/server/repository.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/repository.py	Mon Dec 09 16:13:10 2013 +0100
@@ -36,6 +36,7 @@
 from os.path import join
 from datetime import datetime
 from time import time, localtime, strftime
+from warnings import warn
 
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.compat import any
@@ -299,6 +300,8 @@
             # initialized)
             source.init(True, sourceent)
             if not source.copy_based_source:
+                warn('[3.18] old multi-source system will go away in the next version',
+                     DeprecationWarning)
                 self.sources.append(source)
                 self.querier.set_planner()
                 if add_to_cnxsets:
@@ -1665,18 +1668,24 @@
 
     @cached
     def rel_type_sources(self, rtype):
+        warn('[3.18] old multi-source system will go away in the next version',
+             DeprecationWarning)
         return tuple([source for source in self.sources
                       if source.support_relation(rtype)
                       or rtype in source.dont_cross_relations])
 
     @cached
     def can_cross_relation(self, rtype):
+        warn('[3.18] old multi-source system will go away in the next version',
+             DeprecationWarning)
         return tuple([source for source in self.sources
                       if source.support_relation(rtype)
                       and rtype in source.cross_relations])
 
     @cached
     def is_multi_sources_relation(self, rtype):
+        warn('[3.18] old multi-source system will go away in the next version',
+             DeprecationWarning)
         return any(source for source in self.sources
                    if not source is self.system_source
                    and source.support_relation(rtype))
--- a/server/schemaserial.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/schemaserial.py	Mon Dec 09 16:13:10 2013 +0100
@@ -27,7 +27,7 @@
 
 from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo
 
-from cubicweb import CW_SOFTWARE_ROOT, Binary
+from cubicweb import CW_SOFTWARE_ROOT, Binary, typed_eid
 from cubicweb.schema import (KNOWN_RPROPERTIES, CONSTRAINTS, ETYPE_NAME_MAP,
                              VIRTUAL_RTYPES, PURE_VIRTUAL_RTYPES)
 from cubicweb.server import sqlutils
@@ -77,8 +77,6 @@
 def cstrtype_mapping(cursor):
     """cached constraint types mapping"""
     map = dict(cursor.execute('Any T, X WHERE X is CWConstraintType, X name T'))
-    if not 'BoundConstraint' in map:
-        map['BoundConstraint'] = map['BoundaryConstraint']
     return map
 
 # schema / perms deserialization ##############################################
@@ -214,6 +212,11 @@
         rdefeid, seid, reid, oeid, card, ord, desc, idx, ftidx, i18n, default = values
         typeparams = extra_props.get(rdefeid)
         typeparams = json.load(typeparams) if typeparams else {}
+        if default is not None:
+            if isinstance(default, Binary):
+                # while migrating from 3.17 to 3.18, we still have to
+                # handle String defaults
+                default = default.unzpickle()
         _add_rdef(rdefeid, seid, reid, oeid,
                   cardinality=card, description=desc, order=ord,
                   indexed=idx, fulltextindexed=ftidx, internationalizable=i18n,
@@ -234,28 +237,24 @@
         if rdefs is not None:
             set_perms(rdefs, permsidx)
     unique_togethers = {}
-    try:
-        rset = session.execute(
-        'Any X,E,R WHERE '
-        'X is CWUniqueTogetherConstraint, '
-        'X constraint_of E, X relations R', build_descr=False)
-    except Exception:
-        session.rollback() # first migration introducing CWUniqueTogetherConstraint cw 3.9.6
-    else:
-        for values in rset:
-            uniquecstreid, eeid, releid = values
-            eschema = schema.schema_by_eid(eeid)
-            relations = unique_togethers.setdefault(uniquecstreid, (eschema, []))
-            rel = ertidx[releid]
-            if isinstance(rel, schemamod.RelationDefinitionSchema):
-                # not yet migrated 3.9 database ('relations' target type changed
-                # to CWRType in 3.10)
-                rtype = rel.rtype.type
-            else:
-                rtype = str(rel)
-            relations[1].append(rtype)
-        for eschema, unique_together in unique_togethers.itervalues():
-            eschema._unique_together.append(tuple(sorted(unique_together)))
+    rset = session.execute(
+    'Any X,E,R WHERE '
+    'X is CWUniqueTogetherConstraint, '
+    'X constraint_of E, X relations R', build_descr=False)
+    for values in rset:
+        uniquecstreid, eeid, releid = values
+        eschema = schema.schema_by_eid(eeid)
+        relations = unique_togethers.setdefault(uniquecstreid, (eschema, []))
+        rel = ertidx[releid]
+        if isinstance(rel, schemamod.RelationDefinitionSchema):
+            # not yet migrated 3.9 database ('relations' target type changed
+            # to CWRType in 3.10)
+            rtype = rel.rtype.type
+        else:
+            rtype = str(rel)
+        relations[1].append(rtype)
+    for eschema, unique_together in unique_togethers.itervalues():
+        eschema._unique_together.append(tuple(sorted(unique_together)))
     schema.infer_specialization_rules()
     session.commit()
     schema.reading_from_database = False
@@ -344,13 +343,10 @@
     cstrtypemap = {}
     rql = 'INSERT CWConstraintType X: X name %(ct)s'
     for cstrtype in CONSTRAINTS:
-        if cstrtype == 'BoundConstraint':
-            continue # XXX deprecated in yams 0.29 / cw 3.8.1
         cstrtypemap[cstrtype] = execute(rql, {'ct': unicode(cstrtype)},
                                         build_descr=False)[0][0]
         if pb is not None:
             pb.update()
-    cstrtypemap['BoundConstraint'] = cstrtypemap['BoundaryConstraint']
     # serialize relations
     for rschema in schema.relations():
         # skip virtual relations such as eid, has_text and identity
@@ -536,10 +532,7 @@
         elif isinstance(value, str):
             value = unicode(value)
         if value is not None and prop == 'default':
-            if value is False:
-                value = u''
-            if not isinstance(value, unicode):
-                value = unicode(value)
+            value = Binary.zpickle(value)
         values[amap.get(prop, prop)] = value
     if extra:
         values['extra_props'] = Binary(json.dumps(extra))
--- a/server/serverctl.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/serverctl.py	Mon Dec 09 16:13:10 2013 +0100
@@ -1065,6 +1065,25 @@
             if val:
                 print key, ':', val
 
+
+
+def permissionshandler(relation, perms):
+    from yams.schema import RelationDefinitionSchema
+    from yams.buildobjs import DEFAULT_ATTRPERMS
+    from cubicweb.schema import (PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS,
+                                 PUB_SYSTEM_ATTR_PERMS, RO_REL_PERMS, RO_ATTR_PERMS)
+    defaultrelperms = (DEFAULT_ATTRPERMS, PUB_SYSTEM_REL_PERMS,
+                       PUB_SYSTEM_ATTR_PERMS, RO_REL_PERMS, RO_ATTR_PERMS)
+    defaulteperms = (PUB_SYSTEM_ENTITY_PERMS,)
+    # canonicalize vs str/unicode
+    for p in ('read', 'add', 'update', 'delete'):
+        rule = perms.get(p)
+        if rule:
+            perms[p] = tuple(str(x) if isinstance(x, basestring) else x
+                             for x in rule)
+    return perms, perms in defaultrelperms or perms in defaulteperms
+
+
 class SchemaDiffCommand(Command):
     """Generate a diff between schema and fsschema description.
 
@@ -1085,7 +1104,7 @@
         repo, cnx = repo_cnx(config)
         session = repo._get_session(cnx.sessionid, setcnxset=True)
         fsschema = config.load_schema(expand_cubes=True)
-        schema_diff(repo.schema, fsschema, diff_tool)
+        schema_diff(fsschema, repo.schema, permissionshandler, diff_tool, ignore=('eid',))
 
 
 for cmdclass in (CreateInstanceDBCommand, InitInstanceCommand,
--- a/server/session.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/session.py	Mon Dec 09 16:13:10 2013 +0100
@@ -1173,14 +1173,11 @@
     source_from_eid = tx_meth('source_from_eid')
 
 
-    def execute(self, rql, kwargs=None, eid_key=None, build_descr=True):
+    def execute(self, rql, kwargs=None, build_descr=True):
         """db-api like method directly linked to the querier execute method.
 
         See :meth:`cubicweb.dbapi.Cursor.execute` documentation.
         """
-        if eid_key is not None:
-            warn('[3.8] eid_key is deprecated, you can safely remove this argument',
-                 DeprecationWarning, stacklevel=2)
         self.timestamp = time() # update timestamp
         rset = self._execute(self, rql, kwargs, build_descr)
         rset.req = self
--- a/server/sources/__init__.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/sources/__init__.py	Mon Dec 09 16:13:10 2013 +0100
@@ -183,7 +183,7 @@
             elif value is not None:
                 # type check
                 try:
-                    value = configuration.convert(value, optdict, optname)
+                    value = configuration._validate(value, optdict, optname)
                 except Exception as ex:
                     msg = unicode(ex) # XXX internationalization
                     raise ValidationError(eid, {role_name('config', 'subject'): msg})
--- a/server/sources/ldapuser.py	Fri Dec 06 17:20:59 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,551 +0,0 @@
-# copyright 2003-2013 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/>.
-"""cubicweb ldap user source
-
-this source is for now limited to a read-only CWUser source
-"""
-from __future__ import division, with_statement
-from base64 import b64decode
-
-import ldap
-from ldap.filter import escape_filter_chars
-
-from rql.nodes import Relation, VariableRef, Constant, Function
-
-import warnings
-from cubicweb import UnknownEid, RepositoryError
-from cubicweb.server import ldaputils
-from cubicweb.server.utils import cartesian_product
-from cubicweb.server.sources import (AbstractSource, TrFunc, GlobTrFunc,
-                                     TimedCache)
-
-# search scopes
-BASE = ldap.SCOPE_BASE
-ONELEVEL = ldap.SCOPE_ONELEVEL
-SUBTREE = ldap.SCOPE_SUBTREE
-
-# map ldap protocol to their standard port
-PROTO_PORT = {'ldap': 389,
-              'ldaps': 636,
-              'ldapi': None,
-              }
-
-
-# module is lazily imported
-warnings.warn('Imminent drop of ldapuser. Switch to ldapfeed now!',
-              DeprecationWarning)
-
-
-class LDAPUserSource(ldaputils.LDAPSourceMixIn, AbstractSource):
-    """LDAP read-only CWUser source"""
-    support_entities = {'CWUser': False}
-
-    options = ldaputils.LDAPSourceMixIn.options + (
-
-        ('synchronization-interval',
-         {'type' : 'time',
-          'default': '1d',
-          'help': 'interval between synchronization with the ldap \
-directory (default to once a day).',
-          'group': 'ldap-source', 'level': 3,
-          }),
-        ('cache-life-time',
-         {'type' : 'time',
-          'default': '2h',
-          'help': 'life time of query cache (default to two hours).',
-          'group': 'ldap-source', 'level': 3,
-          }),
-
-    )
-
-    def update_config(self, source_entity, typedconfig):
-        """update configuration from source entity. `typedconfig` is config
-        properly typed with defaults set
-        """
-        super(LDAPUserSource, self).update_config(source_entity, typedconfig)
-        self._interval = typedconfig['synchronization-interval']
-        self._cache_ttl = max(71, typedconfig['cache-life-time'])
-        self.reset_caches()
-        # XXX copy from datafeed source
-        if source_entity is not None:
-            self._entity_update(source_entity)
-        self.config = typedconfig
-        # /end XXX
-
-    def reset_caches(self):
-        """method called during test to reset potential source caches"""
-        self._cache = {}
-        self._query_cache = TimedCache(self._cache_ttl)
-
-    def init(self, activated, source_entity):
-        """method called by the repository once ready to handle request"""
-        super(LDAPUserSource, self).init(activated, source_entity)
-        if activated:
-            self.info('ldap init')
-            # set minimum period of 5min 1s (the additional second is to
-            # minimize resonnance effet)
-            if self.user_rev_attrs['email']:
-                self.repo.looping_task(max(301, self._interval), self.synchronize)
-            self.repo.looping_task(self._cache_ttl // 10,
-                                   self._query_cache.clear_expired)
-
-    def synchronize(self):
-        with self.repo.internal_session() as session:
-            self.pull_data(session)
-
-    def pull_data(self, session, force=False, raise_on_error=False):
-        """synchronize content known by this repository with content in the
-        external repository
-        """
-        self.info('synchronizing ldap source %s', self.uri)
-        ldap_emailattr = self.user_rev_attrs['email']
-        assert ldap_emailattr
-        execute = session.execute
-        cursor = session.system_sql("SELECT eid, extid FROM entities WHERE "
-                                    "source='%s'" % self.uri)
-        for eid, b64extid in cursor.fetchall():
-            extid = b64decode(b64extid)
-            self.debug('ldap eid %s', eid)
-            # if no result found, _search automatically delete entity information
-            res = self._search(session, extid, BASE)
-            self.debug('ldap search %s', res)
-            if res:
-                ldapemailaddr = res[0].get(ldap_emailattr)
-                if ldapemailaddr:
-                    if isinstance(ldapemailaddr, list):
-                        ldapemailaddr = ldapemailaddr[0] # XXX consider only the first email in the list
-                    rset = execute('Any X,A WHERE '
-                                   'X address A, U use_email X, U eid %(u)s',
-                                   {'u': eid})
-                    ldapemailaddr = unicode(ldapemailaddr)
-                    for emaileid, emailaddr, in rset:
-                        if emailaddr == ldapemailaddr:
-                            break
-                    else:
-                        self.debug('updating email address of user %s to %s',
-                                  extid, ldapemailaddr)
-                        emailrset = execute('EmailAddress A WHERE A address %(addr)s',
-                                            {'addr': ldapemailaddr})
-                        if emailrset:
-                            execute('SET U use_email X WHERE '
-                                    'X eid %(x)s, U eid %(u)s',
-                                    {'x': emailrset[0][0], 'u': eid})
-                        elif rset:
-                            if not execute('SET X address %(addr)s WHERE '
-                                           'U primary_email X, U eid %(u)s',
-                                           {'addr': ldapemailaddr, 'u': eid}):
-                                execute('SET X address %(addr)s WHERE '
-                                        'X eid %(x)s',
-                                        {'addr': ldapemailaddr, 'x': rset[0][0]})
-                        else:
-                            # no email found, create it
-                            _insert_email(session, ldapemailaddr, eid)
-        session.commit()
-
-    def ldap_name(self, var):
-        if var.stinfo['relations']:
-            relname = iter(var.stinfo['relations']).next().r_type
-            return self.user_rev_attrs.get(relname)
-        return None
-
-    def prepare_columns(self, mainvars, rqlst):
-        """return two list describing how to build the final results
-        from the result of an ldap search (ie a list of dictionary)
-        """
-        columns = []
-        global_transforms = []
-        for i, term in enumerate(rqlst.selection):
-            if isinstance(term, Constant):
-                columns.append(term)
-                continue
-            if isinstance(term, Function): # LOWER, UPPER, COUNT...
-                var = term.get_nodes(VariableRef)[0]
-                var = var.variable
-                try:
-                    mainvar = var.stinfo['attrvar'].name
-                except AttributeError: # no attrvar set
-                    mainvar = var.name
-                assert mainvar in mainvars
-                trname = term.name
-                ldapname = self.ldap_name(var)
-                if trname in ('COUNT', 'MIN', 'MAX', 'SUM'):
-                    global_transforms.append(GlobTrFunc(trname, i, ldapname))
-                    columns.append((mainvar, ldapname))
-                    continue
-                if trname in ('LOWER', 'UPPER'):
-                    columns.append((mainvar, TrFunc(trname, i, ldapname)))
-                    continue
-                raise NotImplementedError('no support for %s function' % trname)
-            if term.name in mainvars:
-                columns.append((term.name, 'dn'))
-                continue
-            var = term.variable
-            mainvar = var.stinfo['attrvar'].name
-            columns.append((mainvar, self.ldap_name(var)))
-            #else:
-            #    # probably a bug in rql splitting if we arrive here
-            #    raise NotImplementedError
-        return columns, global_transforms
-
-    def syntax_tree_search(self, session, union,
-                           args=None, cachekey=None, varmap=None, debug=0):
-        """return result from this source for a rql query (actually from a rql
-        syntax tree and a solution dictionary mapping each used variable to a
-        possible type). If cachekey is given, the query necessary to fetch the
-        results (but not the results themselves) may be cached using this key.
-        """
-        self.debug('ldap syntax tree search')
-        # XXX not handled : transform/aggregat function, join on multiple users...
-        assert len(union.children) == 1, 'union not supported'
-        rqlst = union.children[0]
-        assert not rqlst.with_, 'subquery not supported'
-        rqlkey = rqlst.as_string(kwargs=args)
-        try:
-            results = self._query_cache[rqlkey]
-        except KeyError:
-            try:
-                results = self.rqlst_search(session, rqlst, args)
-                self._query_cache[rqlkey] = results
-            except ldap.SERVER_DOWN:
-                # cant connect to server
-                msg = session._("can't connect to source %s, some data may be missing")
-                session.set_shared_data('sources_error', msg % self.uri, txdata=True)
-                return []
-        return results
-
-    def rqlst_search(self, session, rqlst, args):
-        mainvars = []
-        for varname in rqlst.defined_vars:
-            for sol in rqlst.solutions:
-                if sol[varname] == 'CWUser':
-                    mainvars.append(varname)
-                    break
-        assert mainvars, rqlst
-        columns, globtransforms = self.prepare_columns(mainvars, rqlst)
-        eidfilters = [lambda x: x > 0]
-        allresults = []
-        generator = RQL2LDAPFilter(self, session, args, mainvars)
-        for mainvar in mainvars:
-            # handle restriction
-            try:
-                eidfilters_, ldapfilter = generator.generate(rqlst, mainvar)
-            except GotDN as ex:
-                assert ex.dn, 'no dn!'
-                try:
-                    res = [self._cache[ex.dn]]
-                except KeyError:
-                    res = self._search(session, ex.dn, BASE)
-            except UnknownEid as ex:
-                # raised when we are looking for the dn of an eid which is not
-                # coming from this source
-                res = []
-            else:
-                eidfilters += eidfilters_
-                res = self._search(session, self.user_base_dn,
-                                   self.user_base_scope, ldapfilter)
-            allresults.append(res)
-        # 1. get eid for each dn and filter according to that eid if necessary
-        for i, res in enumerate(allresults):
-            filteredres = []
-            for resdict in res:
-                # get sure the entity exists in the system table
-                eid = self.repo.extid2eid(self, resdict['dn'], 'CWUser', session)
-                for eidfilter in eidfilters:
-                    if not eidfilter(eid):
-                        break
-                else:
-                    resdict['eid'] = eid
-                    filteredres.append(resdict)
-            allresults[i] = filteredres
-        # 2. merge result for each "mainvar": cartesian product
-        allresults = cartesian_product(allresults)
-        # 3. build final result according to column definition
-        result = []
-        for rawline in allresults:
-            rawline = dict(zip(mainvars, rawline))
-            line = []
-            for varname, ldapname in columns:
-                if ldapname is None:
-                    value = None # no mapping available
-                elif ldapname == 'dn':
-                    value = rawline[varname]['eid']
-                elif isinstance(ldapname, Constant):
-                    if ldapname.type == 'Substitute':
-                        value = args[ldapname.value]
-                    else:
-                        value = ldapname.value
-                elif isinstance(ldapname, TrFunc):
-                    value = ldapname.apply(rawline[varname])
-                else:
-                    value = rawline[varname].get(ldapname)
-                line.append(value)
-            result.append(line)
-        for trfunc in globtransforms:
-            result = trfunc.apply(result)
-        #print '--> ldap result', result
-        return result
-
-    def _process_ldap_item(self, dn, iterator):
-        itemdict = super(LDAPUserSource, self)._process_ldap_item(dn, iterator)
-        self._cache[dn] = itemdict
-        return itemdict
-
-    def _process_no_such_object(self, session, dn):
-        eid = self.repo.extid2eid(self, dn, 'CWUser', session, insert=False)
-        if eid:
-            self.warning('deleting ldap user with eid %s and dn %s', eid, dn)
-            entity = session.entity_from_eid(eid, 'CWUser')
-            self.repo.delete_info(session, entity, self.uri)
-            self.reset_caches()
-
-    def before_entity_insertion(self, session, lid, etype, eid, sourceparams):
-        """called by the repository when an eid has been attributed for an
-        entity stored here but the entity has not been inserted in the system
-        table yet.
-
-        This method must return the an Entity instance representation of this
-        entity.
-        """
-        self.debug('ldap before entity insertion')
-        entity = super(LDAPUserSource, self).before_entity_insertion(
-            session, lid, etype, eid, sourceparams)
-        res = self._search(session, lid, BASE)[0]
-        for attr in entity.e_schema.indexable_attributes():
-            entity.cw_edited[attr] = res[self.user_rev_attrs[attr]]
-        return entity
-
-    def after_entity_insertion(self, session, lid, entity, sourceparams):
-        """called by the repository after an entity stored here has been
-        inserted in the system table.
-        """
-        self.debug('ldap after entity insertion')
-        super(LDAPUserSource, self).after_entity_insertion(
-            session, lid, entity, sourceparams)
-        for group in self.user_default_groups:
-            session.execute('SET X in_group G WHERE X eid %(x)s, G name %(group)s',
-                            {'x': entity.eid, 'group': group})
-        # search for existant email first
-        try:
-            # lid = dn
-            emailaddr = self._cache[lid][self.user_rev_attrs['email']]
-        except KeyError:
-            return
-        if isinstance(emailaddr, list):
-            emailaddr = emailaddr[0] # XXX consider only the first email in the list
-        rset = session.execute('EmailAddress X WHERE X address %(addr)s',
-                               {'addr': emailaddr})
-        if rset:
-            session.execute('SET U primary_email X WHERE U eid %(u)s, X eid %(x)s',
-                            {'x': rset[0][0], 'u': entity.eid})
-        else:
-            # not found, create it
-            _insert_email(session, emailaddr, entity.eid)
-
-    def update_entity(self, session, entity):
-        """replace an entity in the source"""
-        raise RepositoryError('this source is read only')
-
-    def delete_entity(self, session, entity):
-        """delete an entity from the source"""
-        raise RepositoryError('this source is read only')
-
-
-def _insert_email(session, emailaddr, ueid):
-    session.execute('INSERT EmailAddress X: X address %(addr)s, U primary_email X '
-                    'WHERE U eid %(x)s', {'addr': emailaddr, 'x': ueid})
-
-class GotDN(Exception):
-    """exception used when a dn localizing the searched user has been found"""
-    def __init__(self, dn):
-        self.dn = dn
-
-
-class RQL2LDAPFilter(object):
-    """generate an LDAP filter for a rql query"""
-    def __init__(self, source, session, args=None, mainvars=()):
-        self.source = source
-        self.repo = source.repo
-        self._ldap_attrs = source.user_rev_attrs
-        self._base_filters = source.base_filters
-        self._session = session
-        if args is None:
-            args = {}
-        self._args = args
-        self.mainvars = mainvars
-
-    def generate(self, selection, mainvarname):
-        self._filters = res = self._base_filters[:]
-        self._mainvarname = mainvarname
-        self._eidfilters = []
-        self._done_not = set()
-        restriction = selection.where
-        if isinstance(restriction, Relation):
-            # only a single relation, need to append result here (no AND/OR)
-            filter = restriction.accept(self)
-            if filter is not None:
-                res.append(filter)
-        elif restriction:
-            restriction.accept(self)
-        if len(res) > 1:
-            return self._eidfilters, '(&%s)' % ''.join(res)
-        return self._eidfilters, res[0]
-
-    def visit_and(self, et):
-        """generate filter for a AND subtree"""
-        for c in et.children:
-            part = c.accept(self)
-            if part:
-                self._filters.append(part)
-
-    def visit_or(self, ou):
-        """generate filter for a OR subtree"""
-        res = []
-        for c in ou.children:
-            part = c.accept(self)
-            if part:
-                res.append(part)
-        if res:
-            if len(res) > 1:
-                part = '(|%s)' % ''.join(res)
-            else:
-                part = res[0]
-            self._filters.append(part)
-
-    def visit_not(self, node):
-        """generate filter for a OR subtree"""
-        part = node.children[0].accept(self)
-        if part:
-            self._filters.append('(!(%s))'% part)
-
-    def visit_relation(self, relation):
-        """generate filter for a relation"""
-        rtype = relation.r_type
-        # don't care of type constraint statement (i.e. relation_type = 'is')
-        if rtype == 'is':
-            return ''
-        lhs, rhs = relation.get_parts()
-        # attribute relation
-        if self.source.schema.rschema(rtype).final:
-            # dunno what to do here, don't pretend anything else
-            if lhs.name != self._mainvarname:
-                if lhs.name in self.mainvars:
-                    # XXX check we don't have variable as rhs
-                    return
-                raise NotImplementedError
-            rhs_vars = rhs.get_nodes(VariableRef)
-            if rhs_vars:
-                if len(rhs_vars) > 1:
-                    raise NotImplementedError
-                # selected variable, nothing to do here
-                return
-            # no variables in the RHS
-            if isinstance(rhs.children[0], Function):
-                res = rhs.children[0].accept(self)
-            elif rtype != 'has_text':
-                res = self._visit_attribute_relation(relation)
-            else:
-                raise NotImplementedError(relation)
-        # regular relation XXX todo: in_group
-        else:
-            raise NotImplementedError(relation)
-        return res
-
-    def _visit_attribute_relation(self, relation):
-        """generate filter for an attribute relation"""
-        lhs, rhs = relation.get_parts()
-        lhsvar = lhs.variable
-        if relation.r_type == 'eid':
-            # XXX hack
-            # skip comparison sign
-            eid = int(rhs.children[0].accept(self))
-            if relation.neged(strict=True):
-                self._done_not.add(relation.parent)
-                self._eidfilters.append(lambda x: not x == eid)
-                return
-            if rhs.operator != '=':
-                filter = {'>': lambda x: x > eid,
-                          '>=': lambda x: x >= eid,
-                          '<': lambda x: x < eid,
-                          '<=': lambda x: x <= eid,
-                          }[rhs.operator]
-                self._eidfilters.append(filter)
-                return
-            dn = self.repo.eid2extid(self.source, eid, self._session)
-            raise GotDN(dn)
-        try:
-            filter = '(%s%s)' % (self._ldap_attrs[relation.r_type],
-                                 rhs.accept(self))
-        except KeyError:
-            # unsupported attribute
-            self.source.warning('%s source can\'t handle relation %s, no '
-                                'results will be returned from this source',
-                                self.source.uri, relation)
-            raise UnknownEid # trick to return no result
-        return filter
-
-    def visit_comparison(self, cmp):
-        """generate filter for a comparaison"""
-        return '%s%s'% (cmp.operator, cmp.children[0].accept(self))
-
-    def visit_mathexpression(self, mexpr):
-        """generate filter for a mathematic expression"""
-        raise NotImplementedError
-
-    def visit_function(self, function):
-        """generate filter name for a function"""
-        if function.name == 'IN':
-            return self.visit_in(function)
-        raise NotImplementedError
-
-    def visit_in(self, function):
-        grandpapa = function.parent.parent
-        ldapattr = self._ldap_attrs[grandpapa.r_type]
-        res = []
-        for c in function.children:
-            part = c.accept(self)
-            if part:
-                res.append(part)
-        if res:
-            if len(res) > 1:
-                part = '(|%s)' % ''.join('(%s=%s)' % (ldapattr, v) for v in res)
-            else:
-                part = '(%s=%s)' % (ldapattr, res[0])
-        return part
-
-    def visit_constant(self, constant):
-        """generate filter name for a constant"""
-        value = constant.value
-        if constant.type is None:
-            raise NotImplementedError
-        if constant.type == 'Date':
-            raise NotImplementedError
-            #value = self.keyword_map[value]()
-        elif constant.type == 'Substitute':
-            value = self._args[constant.value]
-        else:
-            value = constant.value
-        if isinstance(value, unicode):
-            value = value.encode('utf8')
-        else:
-            value = str(value)
-        return escape_filter_chars(value)
-
-    def visit_variableref(self, variableref):
-        """get the sql name for a variable reference"""
-        pass
-
--- a/server/sqlutils.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/sqlutils.py	Mon Dec 09 16:13:10 2013 +0100
@@ -41,11 +41,6 @@
 SQL_PREFIX = 'cw_'
 
 def _run_command(cmd):
-    """backup/restore command are string w/ lgc < 0.47, lists with earlier versions
-    """
-    if isinstance(cmd, basestring):
-        print '->', cmd
-        return subprocess.call(cmd, shell=True)
     print ' '.join(cmd)
     return subprocess.call(cmd)
 
--- a/server/test/data/migratedapp/schema.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/test/data/migratedapp/schema.py	Mon Dec 09 16:13:10 2013 +0100
@@ -16,6 +16,7 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """cw.server.migraction test"""
+import datetime as dt
 from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
                             SubjectRelation, Bytes,
                             RichString, String, Int, Boolean, Datetime, Date)
@@ -62,11 +63,14 @@
                                              'PE require_permission P, P name "add_note", '
                                              'P require_group G'),)}
 
-    whatever = Int(default=2)  # keep it before `date` for unittest_migraction.test_add_attribute_int
+    whatever = Int(default=0)  # keep it before `date` for unittest_migraction.test_add_attribute_int
+    yesno = Boolean(default=False)
     date = Datetime()
     type = String(maxsize=1)
     unique_id = String(maxsize=1, required=True, unique=True)
     mydate = Date(default='TODAY')
+    oldstyledefaultdate = Date(default='2013/01/01')
+    newstyledefaultdate = Date(default=dt.date(2013, 1, 1))
     shortpara = String(maxsize=64, default='hop')
     ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
     attachment = SubjectRelation('File')
--- a/server/test/unittest_ldapsource.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/test/unittest_ldapsource.py	Mon Dec 09 16:13:10 2013 +0100
@@ -33,7 +33,6 @@
 from cubicweb.devtools.httptest import get_available_port
 from cubicweb.devtools import get_test_db_handler
 
-from cubicweb.server.sources.ldapuser import GlobTrFunc, UnknownEid, RQL2LDAPFilter
 
 CONFIG_LDAPFEED = u'''
 user-base-dn=ou=People,dc=cubicweb,dc=test
@@ -453,386 +452,6 @@
             self.setUpClass()
 
 
-class LDAPUserSourceTC(LDAPFeedTestBase):
-    test_db_id = 'ldap-user'
-    tags = CubicWebTC.tags | Tags(('ldap'))
-
-    @classmethod
-    def pre_setup_database(cls, session, config):
-        session.create_entity('CWSource', name=u'ldap', type=u'ldapuser',
-                              url=URL, config=CONFIG_LDAPUSER)
-        session.commit()
-        # XXX keep it there
-        session.execute('CWUser U')
-
-    def setup_database(self):
-        # XXX a traceback may appear in the logs of the test due to
-        # the _init_repo method that may fail to connect to the ldap
-        # source if its URI has changed (from what is stored in the
-        # database). This TB is NOT a failure or so.
-        with self.session.repo.internal_session(safe=True) as session:
-            session.execute('SET S url %(url)s, S config %(conf)s '
-                            'WHERE S is CWSource, S name "ldap"',
-                            {"conf": CONFIG_LDAPUSER, 'url': URL} )
-            session.commit()
-        self.pull()
-
-    def assertMetadata(self, entity):
-        self.assertEqual(entity.creation_date, None)
-        self.assertEqual(entity.modification_date, None)
-
-    def test_synchronize(self):
-        source = self.repo.sources_by_uri['ldap']
-        source.synchronize()
-
-    def test_base(self):
-        # check a known one
-        rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
-        e = rset.get_entity(0, 0)
-        self.assertEqual(e.login, 'syt')
-        e.complete()
-        self.assertMetadata(e)
-        self.assertEqual(e.firstname, None)
-        self.assertEqual(e.surname, None)
-        self.assertEqual(e.in_group[0].name, 'users')
-        self.assertEqual(e.owned_by[0].login, 'syt')
-        self.assertEqual(e.created_by, ())
-        addresses = [pe.address for pe in e.use_email]
-        addresses.sort()
-        # should habe two element but ldapuser seems buggy. It's going to be dropped anyway.
-        self.assertEqual(['sylvain.thenault@logilab.fr',], # 'syt@logilab.fr'],
-                         addresses)
-        self.assertIn(e.primary_email[0].address,
-                      ['sylvain.thenault@logilab.fr', 'syt@logilab.fr'])
-        # email content should be indexed on the user
-        rset = self.sexecute('CWUser X WHERE X has_text "thenault"')
-        self.assertEqual(rset.rows, [[e.eid]])
-
-    def test_not(self):
-        eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
-        rset = self.sexecute('CWUser X WHERE NOT X eid %s' % eid)
-        self.assert_(rset)
-        self.assert_(not eid in (r[0] for r in rset))
-
-    def test_multiple(self):
-        seid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
-        aeid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'adim'})[0][0]
-        rset = self.sexecute('CWUser X, Y WHERE X login %(syt)s, Y login %(adim)s',
-                            {'syt': 'syt', 'adim': 'adim'})
-        self.assertEqual(rset.rows, [[seid, aeid]])
-        rset = self.sexecute('Any X,Y,L WHERE X login L, X login %(syt)s, Y login %(adim)s',
-                            {'syt': 'syt', 'adim': 'adim'})
-        self.assertEqual(rset.rows, [[seid, aeid, 'syt']])
-
-    def test_in(self):
-        seid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
-        aeid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'adim'})[0][0]
-        rset = self.sexecute('Any X,L ORDERBY L WHERE X login IN("%s", "%s"), X login L' % ('syt', 'adim'))
-        self.assertEqual(rset.rows, [[aeid, 'adim'], [seid, 'syt']])
-
-    def test_relations(self):
-        eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
-        rset = self.sexecute('Any X,E WHERE X is CWUser, X login L, X primary_email E')
-        self.assert_(eid in (r[0] for r in rset))
-        rset = self.sexecute('Any X,L,E WHERE X is CWUser, X login L, X primary_email E')
-        self.assert_('syt' in (r[1] for r in rset))
-
-    def test_count(self):
-        nbusers = self.sexecute('Any COUNT(X) WHERE X is CWUser')[0][0]
-        # just check this is a possible number
-        self.assert_(nbusers > 1, nbusers)
-        self.assert_(nbusers < 30, nbusers)
-
-    def test_upper(self):
-        eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
-        rset = self.sexecute('Any UPPER(L) WHERE X eid %s, X login L' % eid)
-        self.assertEqual(rset[0][0], 'syt'.upper())
-
-    def test_unknown_attr(self):
-        eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
-        rset = self.sexecute('Any L,C,M WHERE X eid %s, X login L, '
-                            'X creation_date C, X modification_date M' % eid)
-        self.assertEqual(rset[0][0], 'syt')
-        self.assertEqual(rset[0][1], None)
-        self.assertEqual(rset[0][2], None)
-
-    def test_sort(self):
-        logins = [l for l, in self.sexecute('Any L ORDERBY L WHERE X login L')]
-        self.assertEqual(logins, sorted(logins))
-
-    def test_lower_sort(self):
-        logins = [l for l, in self.sexecute('Any L ORDERBY lower(L) WHERE X login L')]
-        self.assertEqual(logins, sorted(logins))
-
-    def test_or(self):
-        rset = self.sexecute('DISTINCT Any X WHERE X login %(login)s OR (X in_group G, G name "managers")',
-                            {'login': 'syt'})
-        self.assertEqual(len(rset), 2, rset.rows) # syt + admin
-
-    def test_nonregr_set_owned_by(self):
-        # test that when a user coming from ldap is triggering a transition
-        # the related TrInfo has correct owner information
-        self.sexecute('SET X in_group G WHERE X login %(syt)s, G name "managers"', {'syt': 'syt'})
-        self.commit()
-        syt = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'}).get_entity(0, 0)
-        self.assertEqual([g.name for g in syt.in_group], ['managers', 'users'])
-        cnx = self.login('syt', password='syt')
-        cu = cnx.cursor()
-        adim = cu.execute('CWUser X WHERE X login %(login)s', {'login': 'adim'}).get_entity(0, 0)
-        iworkflowable = adim.cw_adapt_to('IWorkflowable')
-        iworkflowable.fire_transition('deactivate')
-        try:
-            cnx.commit()
-            adim.cw_clear_all_caches()
-            self.assertEqual(adim.in_state[0].name, 'deactivated')
-            trinfo = iworkflowable.latest_trinfo()
-            self.assertEqual(trinfo.owned_by[0].login, 'syt')
-            # select from_state to skip the user's creation TrInfo
-            rset = self.sexecute('Any U ORDERBY D DESC WHERE WF wf_info_for X,'
-                                'WF creation_date D, WF from_state FS,'
-                                'WF owned_by U?, X eid %(x)s',
-                                {'x': adim.eid})
-            self.assertEqual(rset.rows, [[syt.eid]])
-        finally:
-            # restore db state
-            self.restore_connection()
-            adim = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'adim'}).get_entity(0, 0)
-            adim.cw_adapt_to('IWorkflowable').fire_transition('activate')
-            self.sexecute('DELETE X in_group G WHERE X login %(syt)s, G name "managers"', {'syt': 'syt'})
-
-    def test_same_column_names(self):
-        self.sexecute('Any X, Y WHERE X copain Y, X login "comme", Y login "cochon"')
-
-    def test_multiple_entities_from_different_sources(self):
-        req = self.request()
-        self.create_user(req, 'cochon')
-        self.assertTrue(self.sexecute('Any X,Y WHERE X login %(syt)s, Y login "cochon"', {'syt': 'syt'}))
-
-    def test_exists1(self):
-        self.session.set_cnxset()
-        self.session.create_entity('CWGroup', name=u'bougloup1')
-        self.session.create_entity('CWGroup', name=u'bougloup2')
-        self.sexecute('SET U in_group G WHERE G name ~= "bougloup%", U login "admin"')
-        self.sexecute('SET U in_group G WHERE G name = "bougloup1", U login %(syt)s', {'syt': 'syt'})
-        rset = self.sexecute('Any L,SN ORDERBY L WHERE X in_state S, '
-                             'S name SN, X login L, EXISTS(X in_group G, G name ~= "bougloup%")')
-        self.assertEqual(rset.rows, [['admin', 'activated'], ['syt', 'activated']])
-
-    def test_exists2(self):
-        req = self.request()
-        self.create_user(req, 'comme')
-        self.create_user(req, 'cochon')
-        self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
-        rset = self.sexecute('Any GN ORDERBY GN WHERE X in_group G, G name GN, '
-                             '(G name "managers" OR EXISTS(X copain T, T login in ("comme", "cochon")))')
-        self.assertEqual(rset.rows, [['managers'], ['users']])
-
-    def test_exists3(self):
-        req = self.request()
-        self.create_user(req, 'comme')
-        self.create_user(req, 'cochon')
-        self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
-        self.assertTrue(self.sexecute('Any X, Y WHERE X copain Y, X login "comme", Y login "cochon"'))
-        self.sexecute('SET X copain Y WHERE X login %(syt)s, Y login "cochon"', {'syt': 'syt'})
-        self.assertTrue(self.sexecute('Any X, Y WHERE X copain Y, X login %(syt)s, Y login "cochon"', {'syt': 'syt'}))
-        rset = self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, G name "managers" '
-                             'OR EXISTS(X copain T, T login in ("comme", "cochon"))')
-        self.assertEqual(sorted(rset.rows), [['managers', 'admin'], ['users', 'comme'], ['users', 'syt']])
-
-    def test_exists4(self):
-        req = self.request()
-        self.create_user(req, 'comme')
-        self.create_user(req, 'cochon', groups=('users', 'guests'))
-        self.create_user(req, 'billy')
-        self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
-        self.sexecute('SET X copain Y WHERE X login "cochon", Y login "cochon"')
-        self.sexecute('SET X copain Y WHERE X login "comme", Y login "billy"')
-        self.sexecute('SET X copain Y WHERE X login %(syt)s, Y login "billy"', {'syt': 'syt'})
-        # search for group name, login where
-        #   CWUser copain with "comme" or "cochon" AND same login as the copain
-        # OR
-        #   CWUser in_state activated AND not copain with billy
-        #
-        # SO we expect everybody but "comme" and "syt"
-        rset= self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, '
-                           'EXISTS(X copain T, T login L, T login in ("comme", "cochon")) OR '
-                           'EXISTS(X in_state S, S name "activated", NOT X copain T2, T2 login "billy")')
-        all = self.sexecute('Any GN, L WHERE X in_group G, X login L, G name GN')
-        all.rows.remove(['users', 'comme'])
-        all.rows.remove(['users', 'syt'])
-        self.assertEqual(sorted(rset.rows), sorted(all.rows))
-
-    def test_exists5(self):
-        req = self.request()
-        self.create_user(req, 'comme')
-        self.create_user(req, 'cochon', groups=('users', 'guests'))
-        self.create_user(req, 'billy')
-        self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
-        self.sexecute('SET X copain Y WHERE X login "cochon", Y login "cochon"')
-        self.sexecute('SET X copain Y WHERE X login "comme", Y login "billy"')
-        self.sexecute('SET X copain Y WHERE X login %(syt)s, Y login "cochon"', {'syt': 'syt'})
-        rset= self.sexecute('Any L WHERE X login L, '
-                           'EXISTS(X copain T, T login in ("comme", "cochon")) AND '
-                           'NOT EXISTS(X copain T2, T2 login "billy")')
-        self.assertEqual(sorted(rset.rows), [['cochon'], ['syt']])
-        rset= self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, '
-                           'EXISTS(X copain T, T login in ("comme", "cochon")) AND '
-                           'NOT EXISTS(X copain T2, T2 login "billy")')
-        self.assertEqual(sorted(rset.rows), [['guests', 'cochon'],
-                                              ['users', 'cochon'],
-                                              ['users', 'syt']])
-
-    def test_cd_restriction(self):
-        rset = self.sexecute('CWUser X WHERE X creation_date > "2009-02-01"')
-        # admin/anon but no ldap user since it doesn't support creation_date
-        self.assertEqual(sorted(e.login for e in rset.entities()),
-                          ['admin', 'anon'])
-
-    def test_union(self):
-        afeids = self.sexecute('State X')
-        ueids = self.sexecute('CWUser X')
-        rset = self.sexecute('(Any X WHERE X is State) UNION (Any X WHERE X is CWUser)')
-        self.assertEqual(sorted(r[0] for r in rset.rows),
-                          sorted(r[0] for r in afeids + ueids))
-
-    def _init_security_test(self):
-        req = self.request()
-        self.create_user(req, 'iaminguestsgrouponly', groups=('guests',))
-        cnx = self.login('iaminguestsgrouponly')
-        return cnx.cursor()
-
-    def test_security1(self):
-        cu = self._init_security_test()
-        rset = cu.execute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
-        self.assertEqual(rset.rows, [])
-        rset = cu.execute('Any X WHERE X login "iaminguestsgrouponly"')
-        self.assertEqual(len(rset.rows), 1)
-
-    def test_security2(self):
-        cu = self._init_security_test()
-        rset = cu.execute('Any X WHERE X has_text %(syt)s', {'syt': 'syt'})
-        self.assertEqual(rset.rows, [])
-        rset = cu.execute('Any X WHERE X has_text "iaminguestsgrouponly"')
-        self.assertEqual(len(rset.rows), 1)
-
-    def test_security3(self):
-        cu = self._init_security_test()
-        rset = cu.execute('Any F WHERE X has_text %(syt)s, X firstname F', {'syt': 'syt'})
-        self.assertEqual(rset.rows, [])
-        rset = cu.execute('Any F WHERE X has_text "iaminguestsgrouponly", X firstname F')
-        self.assertEqual(rset.rows, [[None]])
-
-    def test_nonregr1(self):
-        self.sexecute('Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, E owned_by X, '
-                     'X modification_date AA',
-                     {'x': self.session.user.eid})
-
-    def test_nonregr2(self):
-        self.sexecute('Any X,L,AA WHERE E eid %(x)s, E owned_by X, '
-                     'X login L, X modification_date AA',
-                     {'x': self.session.user.eid})
-
-    def test_nonregr3(self):
-        self.sexecute('Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, '
-                     'X modification_date AA',
-                     {'x': self.session.user.eid})
-
-    def test_nonregr4(self):
-        emaileid = self.sexecute('INSERT EmailAddress X: X address "toto@logilab.org"')[0][0]
-        self.sexecute('Any X,AA WHERE X use_email Y, Y eid %(x)s, X modification_date AA',
-                     {'x': emaileid})
-
-    def test_nonregr5(self):
-        # original jpl query:
-        # Any X, NOW - CD, P WHERE P is Project, U interested_in P, U is CWUser,
-        # U login "sthenault", X concerns P, X creation_date CD ORDERBY CD DESC LIMIT 5
-        rql = ('Any X, NOW - CD, P ORDERBY CD DESC LIMIT 5 WHERE P bookmarked_by U, '
-               'U login "%s", P is X, X creation_date CD') % self.session.user.login
-        self.sexecute(rql, )#{'x': })
-
-    def test_nonregr6(self):
-        self.sexecute('Any B,U,UL GROUPBY B,U,UL WHERE B created_by U?, B is File '
-                     'WITH U,UL BEING (Any U,UL WHERE ME eid %(x)s, (EXISTS(U identity ME) '
-                     'OR (EXISTS(U in_group G, G name IN("managers", "staff")))) '
-                     'OR (EXISTS(U in_group H, ME in_group H, NOT H name "users")), U login UL, U is CWUser)',
-                     {'x': self.session.user.eid})
-
-class GlobTrFuncTC(TestCase):
-
-    def test_count(self):
-        trfunc = GlobTrFunc('count', 0)
-        res = trfunc.apply([[1], [2], [3], [4]])
-        self.assertEqual(res, [[4]])
-        trfunc = GlobTrFunc('count', 1)
-        res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]])
-        self.assertEqual(res, [[1, 2], [2, 1], [3, 1]])
-
-    def test_sum(self):
-        trfunc = GlobTrFunc('sum', 0)
-        res = trfunc.apply([[1], [2], [3], [4]])
-        self.assertEqual(res, [[10]])
-        trfunc = GlobTrFunc('sum', 1)
-        res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]])
-        self.assertEqual(res, [[1, 7], [2, 4], [3, 6]])
-
-    def test_min(self):
-        trfunc = GlobTrFunc('min', 0)
-        res = trfunc.apply([[1], [2], [3], [4]])
-        self.assertEqual(res, [[1]])
-        trfunc = GlobTrFunc('min', 1)
-        res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]])
-        self.assertEqual(res, [[1, 2], [2, 4], [3, 6]])
-
-    def test_max(self):
-        trfunc = GlobTrFunc('max', 0)
-        res = trfunc.apply([[1], [2], [3], [4]])
-        self.assertEqual(res, [[4]])
-        trfunc = GlobTrFunc('max', 1)
-        res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]])
-        self.assertEqual(res, [[1, 5], [2, 4], [3, 6]])
-
-
-class RQL2LDAPFilterTC(RQLGeneratorTC):
-
-    tags = RQLGeneratorTC.tags | Tags(('ldap'))
-
-    @property
-    def schema(self):
-        """return the application schema"""
-        return self._schema
-
-    def setUp(self):
-        self.handler = get_test_db_handler(LDAPUserSourceTC.config)
-        self.handler.build_db_cache('ldap-user', LDAPUserSourceTC.pre_setup_database)
-        self.handler.restore_database('ldap-user')
-        self._repo = repo = self.handler.get_repo()
-        self._schema = repo.schema
-        super(RQL2LDAPFilterTC, self).setUp()
-        ldapsource = repo.sources[-1]
-        self.cnxset = repo._get_cnxset()
-        session = mock_object(cnxset=self.cnxset)
-        self.o = RQL2LDAPFilter(ldapsource, session)
-        self.ldapclasses = ''.join(ldapsource.base_filters)
-
-    def tearDown(self):
-        self._repo.turn_repo_off()
-        super(RQL2LDAPFilterTC, self).tearDown()
-
-    def test_base(self):
-        rqlst = self._prepare('CWUser X WHERE X login "toto"').children[0]
-        self.assertEqual(self.o.generate(rqlst, 'X')[1],
-                          '(&%s(uid=toto))' % self.ldapclasses)
-
-    def test_kwargs(self):
-        rqlst = self._prepare('CWUser X WHERE X login %(x)s').children[0]
-        self.o._args = {'x': "toto"}
-        self.assertEqual(self.o.generate(rqlst, 'X')[1],
-                          '(&%s(uid=toto))' % self.ldapclasses)
-
-    def test_get_attr(self):
-        rqlst = self._prepare('Any X WHERE E firstname X, E eid 12').children[0]
-        self.assertRaises(UnknownEid, self.o.generate, rqlst, 'E')
-
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_migractions.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/test/unittest_migractions.py	Mon Dec 09 16:13:10 2013 +0100
@@ -72,6 +72,22 @@
         CubicWebTC.tearDown(self)
         self.repo.vreg['etypes'].clear_caches()
 
+    def test_add_attribute_bool(self):
+        self.assertFalse('yesno' in self.schema)
+        self.session.create_entity('Note')
+        self.commit()
+        self.mh.cmd_add_attribute('Note', 'yesno')
+        self.assertTrue('yesno' in self.schema)
+        self.assertEqual(self.schema['yesno'].subjects(), ('Note',))
+        self.assertEqual(self.schema['yesno'].objects(), ('Boolean',))
+        self.assertEqual(self.schema['Note'].default('yesno'), False)
+        # test default value set on existing entities
+        note = self.session.execute('Note X').get_entity(0, 0)
+        self.assertEqual(note.yesno, False)
+        # test default value set for next entities
+        self.assertEqual(self.session.create_entity('Note').yesno, False)
+        self.mh.rollback()
+
     def test_add_attribute_int(self):
         self.assertFalse('whatever' in self.schema)
         self.session.create_entity('Note')
@@ -82,12 +98,13 @@
         self.assertTrue('whatever' in self.schema)
         self.assertEqual(self.schema['whatever'].subjects(), ('Note',))
         self.assertEqual(self.schema['whatever'].objects(), ('Int',))
-        self.assertEqual(self.schema['Note'].default('whatever'), 2)
+        self.assertEqual(self.schema['Note'].default('whatever'), 0)
         # test default value set on existing entities
         note = self.session.execute('Note X').get_entity(0, 0)
-        self.assertEqual(note.whatever, 2)
+        self.assertIsInstance(note.whatever, int)
+        self.assertEqual(note.whatever, 0)
         # test default value set for next entities
-        self.assertEqual(self.session.create_entity('Note').whatever, 2)
+        self.assertEqual(self.session.create_entity('Note').whatever, 0)
         # test attribute order
         orderdict2 = dict(self.mh.rqlexec('Any RTN, O WHERE X name "Note", RDEF from_entity X, '
                                           'RDEF relation_type RT, RDEF ordernum O, RT name RTN'))
@@ -127,9 +144,14 @@
 
     def test_add_datetime_with_default_value_attribute(self):
         self.assertFalse('mydate' in self.schema)
-        self.assertFalse('shortpara' in self.schema)
+        self.assertFalse('oldstyledefaultdate' in self.schema)
+        self.assertFalse('newstyledefaultdate' in self.schema)
         self.mh.cmd_add_attribute('Note', 'mydate')
+        self.mh.cmd_add_attribute('Note', 'oldstyledefaultdate')
+        self.mh.cmd_add_attribute('Note', 'newstyledefaultdate')
         self.assertTrue('mydate' in self.schema)
+        self.assertTrue('oldstyledefaultdate' in self.schema)
+        self.assertTrue('newstyledefaultdate' in self.schema)
         self.assertEqual(self.schema['mydate'].subjects(), ('Note', ))
         self.assertEqual(self.schema['mydate'].objects(), ('Date', ))
         testdate = date(2005, 12, 13)
@@ -137,8 +159,13 @@
         eid2 = self.mh.rqlexec('INSERT Note N: N mydate %(mydate)s', {'mydate' : testdate})[0][0]
         d1 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X mydate D', {'x': eid1})[0][0]
         d2 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X mydate D', {'x': eid2})[0][0]
+        d3 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X oldstyledefaultdate D', {'x': eid1})[0][0]
+        d4 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X newstyledefaultdate D', {'x': eid1})[0][0]
         self.assertEqual(d1, date.today())
         self.assertEqual(d2, testdate)
+        myfavoritedate = date(2013, 1, 1)
+        self.assertEqual(d3, myfavoritedate)
+        self.assertEqual(d4, myfavoritedate)
         self.mh.rollback()
 
     def test_drop_chosen_constraints_ctxmanager(self):
--- a/server/test/unittest_schemaserial.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/server/test/unittest_schemaserial.py	Mon Dec 09 16:13:10 2013 +0100
@@ -22,6 +22,7 @@
 
 from logilab.common.testlib import TestCase, unittest_main
 
+from cubicweb import Binary
 from cubicweb.schema import CubicWebSchemaLoader
 from cubicweb.devtools import TestServerConfiguration
 
@@ -188,7 +189,6 @@
         self.assertIn('extra_props', got[1][1])
         # this extr
         extra_props = got[1][1]['extra_props']
-        from cubicweb import Binary
         self.assertIsInstance(extra_props, Binary)
         got[1][1]['extra_props'] = got[1][1]['extra_props'].getvalue()
         self.assertListEqual(expected, got)
@@ -197,7 +197,8 @@
         self.assertListEqual([
             ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,X description %(description)s,X fulltextindexed %(fulltextindexed)s,X indexed %(indexed)s,X internationalizable %(internationalizable)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
              {'se': None, 'rt': None, 'oe': None,
-              'description': u'', 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 3, 'defaultval': u'text/plain', 'indexed': False, 'cardinality': u'?1'}),
+              'description': u'', 'internationalizable': True, 'fulltextindexed': False,
+              'ordernum': 3, 'defaultval': Binary('text/plain'), 'indexed': False, 'cardinality': u'?1'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
              {'x': None, 'value': u'None', 'ct': 'FormatConstraint_eid'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
--- a/test/data/migration/0.1.0_web.py	Fri Dec 06 17:20:59 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-# copyright 2003-2010 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/>.
-"""web only
-
-"""
--- a/test/data/schema.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/test/data/schema.py	Mon Dec 09 16:13:10 2013 +0100
@@ -41,7 +41,7 @@
                                   constraints=[RQLConstraint('NOT EXISTS(O contrat_exclusif S)')])
     dirige = SubjectRelation('Societe', cardinality='??',
                              constraints=[RQLConstraint('S actionnaire O')])
-    associe = SubjectRelation('Personne', cardinality='1*',
+    associe = SubjectRelation('Personne', cardinality='?*',
                               constraints=[RQLConstraint('S actionnaire SOC, O actionnaire SOC')])
 
 class Ami(EntityType):
--- a/test/unittest_entity.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/test/unittest_entity.py	Mon Dec 09 16:13:10 2013 +0100
@@ -133,6 +133,12 @@
         self.assertEqual(sorted(user._cw_related_cache), ['in_group_subject', 'primary_email_subject'])
         for group in groups:
             self.assertFalse('in_group_subject' in group._cw_related_cache, list(group._cw_related_cache))
+        user.cw_clear_all_caches()
+        user.related('in_group', entities=True)
+        self.assertIn('in_group_subject', user._cw_related_cache)
+        user.cw_clear_all_caches()
+        user.related('in_group', targettypes=('CWGroup',), entities=True)
+        self.assertNotIn('in_group_subject', user._cw_related_cache)
 
     def test_related_limit(self):
         req = self.request()
@@ -146,6 +152,18 @@
         self.assertEqual(len(p.related('tags', 'object', entities=True, limit=2)), 2)
         self.assertEqual(len(p.related('tags', 'object', entities=True)), 4)
 
+    def test_related_targettypes(self):
+        req = self.request()
+        p = req.create_entity('Personne', nom=u'Loxodonta', prenom=u'Babar')
+        n = req.create_entity('Note', type=u'scratch', ecrit_par=p)
+        t = req.create_entity('Tag', name=u'a tag', tags=(p, n))
+        self.commit()
+        req = self.request()
+        t = req.entity_from_eid(t.eid)
+        self.assertEqual(2, t.related('tags').rowcount)
+        self.assertEqual(1, t.related('tags', targettypes=('Personne',)).rowcount)
+        self.assertEqual(1, t.related('tags', targettypes=('Note',)).rowcount)
+
     def test_cw_instantiate_relation(self):
         req = self.request()
         p1 = req.create_entity('Personne', nom=u'di')
@@ -742,7 +760,7 @@
         self.assertEqual(card.absolute_url(),
                           'http://testing.fr/cubicweb/%s' % card.eid)
 
-    def test_create_entity(self):
+    def test_create_and_compare_entity(self):
         req = self.request()
         p1 = req.create_entity('Personne', nom=u'fayolle', prenom=u'alexandre')
         p2 = req.create_entity('Personne', nom=u'campeas', prenom=u'aurelien')
@@ -756,6 +774,15 @@
         self.assertEqual(sorted([c.nom for c in p.evaluee]), ['campeas', 'fayolle'])
         self.assertEqual([c.type for c in p.reverse_ecrit_par], ['z'])
 
+        req = self.request()
+        auc = req.execute('Personne P WHERE P prenom "aurelien"').get_entity(0,0)
+        persons = set()
+        persons.add(p1)
+        persons.add(p2)
+        persons.add(auc)
+        self.assertEqual(2, len(persons))
+        self.assertNotEqual(p1, p2)
+        self.assertEqual(p2, auc)
 
 
 if __name__ == '__main__':
--- a/test/unittest_mail.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/test/unittest_mail.py	Mon Dec 09 16:13:10 2013 +0100
@@ -31,7 +31,7 @@
 
 
 def getlogin():
-    """avoid usinng os.getlogin() because of strange tty / stdin problems
+    """avoid using os.getlogin() because of strange tty / stdin problems
     (man 3 getlogin)
     Another solution would be to use $LOGNAME, $USER or $USERNAME
     """
--- a/test/unittest_migration.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/test/unittest_migration.py	Mon Dec 09 16:13:10 2013 +0100
@@ -79,10 +79,6 @@
         self.assert_(not isinstance(config.migration_handler(), ServerMigrationHelper))
         self.assertIsInstance(config.migration_handler(), MigrationHelper)
         config = self.config
-        config.__class__.name = 'twisted'
-        self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)),
-                              [((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'),
-                               ((0, 1 ,0), TMIGRDIR+'0.1.0_web.py')])
         config.__class__.name = 'repository'
         self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)),
                               [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'),
@@ -92,8 +88,7 @@
         self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)),
                               [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'),
                                ((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'),
-                               ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py'),
-                               ((0, 1 ,0), TMIGRDIR+'0.1.0_web.py')])
+                               ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py')])
         config.__class__.name = 'repository'
 
 
--- a/view.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/view.py	Mon Dec 09 16:13:10 2013 +0100
@@ -558,34 +558,6 @@
     __registry__ = 'adapters'
 
 
-def implements_adapter_compat(iface):
-    def _pre39_compat(func):
-        def decorated(self, *args, **kwargs):
-            entity = self.entity
-            if hasattr(entity, func.__name__):
-                warn('[3.9] %s method is deprecated, define it on a custom '
-                     '%s for %s instead' % (func.__name__, iface,
-                                            entity.__class__),
-                     DeprecationWarning)
-                member = getattr(entity, func.__name__)
-                if callable(member):
-                    return member(*args, **kwargs)
-                return member
-            return func(self, *args, **kwargs)
-        decorated.decorated = func
-        return decorated
-    return _pre39_compat
-
-
-def unwrap_adapter_compat(cls):
-    parent = cls.__bases__[0]
-    for member_name in dir(parent):
-        member = getattr(parent, member_name)
-        if isinstance(member, types.MethodType) and hasattr(member.im_func, 'decorated') and not member_name in cls.__dict__:
-            method = new.instancemethod(member.im_func.decorated, None, cls)
-            setattr(cls, member_name, method)
-
-
 class auto_unwrap_bw_compat(type):
     def __new__(mcs, name, bases, classdict):
         cls = type.__new__(mcs, name, bases, classdict)
@@ -596,7 +568,6 @@
 
 class EntityAdapter(Adapter):
     """base class for entity adapters (eg adapt an entity to an interface)"""
-    __metaclass__ = auto_unwrap_bw_compat
     def __init__(self, _cw, **kwargs):
         try:
             self.entity = kwargs.pop('entity')
--- a/web/__init__.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/__init__.py	Mon Dec 09 16:13:10 2013 +0100
@@ -31,8 +31,6 @@
 from cubicweb.uilib import eid_param
 
 assert json_dumps is not None, 'no json module installed'
-dumps = deprecated('[3.9] use cubicweb.utils.json_dumps instead of dumps')(
-    json_dumps)
 
 INTERNAL_FIELD_VALUE = '__cubicweb_internal_field__'
 
--- a/web/data/cubicweb.ajax.js	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/data/cubicweb.ajax.js	Mon Dec 09 16:13:10 2013 +0100
@@ -339,11 +339,6 @@
         cw.log('loadxhtml called without an element');
     }
     var callback = null;
-    if (form && form.callback) {
-        cw.log('[3.9] callback given through form.callback is deprecated, add ' + 'callback on the defered');
-        callback = form.callback;
-        delete form.callback;
-    }
     var node = this.get(0); // only consider the first element
     if (cursor) {
         setProgressCursor();
@@ -734,13 +729,7 @@
 
 /* DEPRECATED *****************************************************************/
 
-preprocessAjaxLoad = cw.utils.deprecatedFunction(
-    '[3.9] preprocessAjaxLoad() is deprecated, use loadAjaxHtmlHead instead',
-    function(node, newdomnode) {
-        return loadAjaxHtmlHead(newdomnode);
-    }
-);
-
+// still used in cwo and keyword cubes at least
 reloadComponent = cw.utils.deprecatedFunction(
     '[3.9] reloadComponent() is deprecated, use loadxhtml instead',
     function(compid, rql, registry, nodeid, extraargs) {
@@ -754,52 +743,6 @@
     }
 );
 
-reloadBox = cw.utils.deprecatedFunction(
-    '[3.9] reloadBox() is deprecated, use loadxhtml instead',
-    function(boxid, rql) {
-        return reloadComponent(boxid, rql, 'ctxcomponents', boxid);
-    }
-);
-
-replacePageChunk = cw.utils.deprecatedFunction(
-    '[3.9] replacePageChunk() is deprecated, use loadxhtml instead',
-    function(nodeId, rql, vid, extraparams, /* ... */ swap, callback) {
-        var params = null;
-        if (callback) {
-            params = {
-                callback: callback
-            };
-        }
-        var node = jQuery('#' + nodeId)[0];
-        var props = {};
-        if (node) {
-            props['rql'] = rql;
-            props['fname'] = 'view';
-            props['pageid'] = pageid;
-            if (vid) {
-                props['vid'] = vid;
-            }
-            if (extraparams) {
-                jQuery.extend(props, extraparams);
-            }
-            // FIXME we need to do asURL(props) manually instead of
-            // passing `props` directly to loadxml because replacePageChunk
-            // is sometimes called (abusively) with some extra parameters in `vid`
-            var mode = swap ? 'swap': 'replace';
-            var url = AJAX_BASE_URL + asURL(props);
-            jQuery(node).loadxhtml(url, params, 'get', mode);
-        } else {
-            cw.log('Node', nodeId, 'not found');
-        }
-    }
-);
-
-loadxhtml = cw.utils.deprecatedFunction(
-    '[3.9] loadxhtml() function is deprecated, use loadxhtml method instead',
-    function(nodeid, url, /* ... */ replacemode) {
-        jQuery('#' + nodeid).loadxhtml(url, null, 'post', replacemode);
-    }
-);
 
 function remoteExec(fname /* ... */) {
     setProgressCursor();
--- a/web/data/cubicweb.compat.js	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/data/cubicweb.compat.js	Mon Dec 09 16:13:10 2013 +0100
@@ -1,34 +1,3 @@
-cw.utils.movedToNamespace(['log', 'jqNode', 'getNode', 'evalJSON', 'urlEncode',
-                           'swapDOM'], cw);
-cw.utils.movedToNamespace(['nodeWalkDepthFirst', 'formContents', 'isArray',
-                           'isString', 'isArrayLike', 'sliceList',
-                           'toISOTimestamp'], cw.utils);
-
-
-if ($.noop === undefined) {
-    function noop() {}
-} else {
-    noop = cw.utils.deprecatedFunction(
-        '[3.9] noop() is deprecated, use $.noop() instead (XXX requires jQuery 1.4)',
-        $.noop);
-}
-
-// ========== ARRAY EXTENSIONS ========== ///
-Array.prototype.contains = cw.utils.deprecatedFunction(
-    '[3.9] array.contains(elt) is deprecated, use $.inArray(elt, array)!=-1 instead',
-    function(element) {
-        return jQuery.inArray(element, this) != - 1;
-    }
-);
-
-// ========== END OF ARRAY EXTENSIONS ========== ///
-forEach = cw.utils.deprecatedFunction(
-    '[3.9] forEach() is deprecated, use $.each() instead',
-    function(array, func) {
-        return $.each(array, func);
-    }
-);
-
 /**
  * .. function:: cw.utils.deprecatedFunction(msg, function)
  *
@@ -41,64 +10,20 @@
  *   [ ["a", "b", "c"], ["d", "e"] ]
  */
  // XXX why not the same argument order as $.map and forEach ?
-map = cw.utils.deprecatedFunction(
-    '[3.9] map() is deprecated, use $.map instead',
-    function(func, array) {
-        var result = [];
-        for (var i = 0, length = array.length; i < length; i++) {
-            result.push(func(array[i]));
-        }
-        return result;
-    }
-);
 
-findValue = cw.utils.deprecatedFunction(
-    '[3.9] findValue(array, elt) is deprecated, use $.inArray(elt, array) instead',
-    function(array, element) {
-        return jQuery.inArray(element, array);
-    }
-);
-
-filter = cw.utils.deprecatedFunction(
-    '[3.9] filter(func, array) is deprecated, use $.grep(array, f) instead',
-    function(func, array) {
-        return $.grep(array, func);
+function map(func, array) {
+    var result = [];
+    for (var i = 0, length = array.length; i < length; i++) {
+        result.push(func(array[i]));
     }
-);
-
-addElementClass = cw.utils.deprecatedFunction(
-    '[3.9] addElementClass(node, cls) is deprecated, use $(node).addClass(cls) instead',
-    function(node, klass) {
-        $(node).addClass(klass);
-    }
-);
+    return result;
+}
 
-removeElementClass = cw.utils.deprecatedFunction(
-    '[3.9] removeElementClass(node, cls) is deprecated, use $(node).removeClass(cls) instead',
-    function(node, klass) {
-        $(node).removeClass(klass);
-    }
-);
 
-hasElementClass = cw.utils.deprecatedFunction(
-    '[3.9] hasElementClass(node, cls) is deprecated, use $(node).hasClass(cls)',
-    function(node, klass) {
-        return $(node).hasClass(klass);
-    }
-);
-
+// skm cube still uses this
 getNodeAttribute = cw.utils.deprecatedFunction(
     '[3.9] getNodeAttribute(node, attr) is deprecated, use $(node).attr(attr)',
     function(node, attribute) {
         return $(node).attr(attribute);
     }
 );
-
-/**
- * The only known usage of KEYS is in the tag cube. Once cubicweb-tag 1.7.0 is out,
- * this current definition can be removed.
- */
-var KEYS = {
-    KEY_ESC: 27,
-    KEY_ENTER: 13
-};
--- a/web/data/cubicweb.css	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/data/cubicweb.css	Mon Dec 09 16:13:10 2013 +0100
@@ -17,9 +17,6 @@
 }
 h1, h2, h3 { margin-top:0; margin-bottom:0; }
 
-/* got rhythm ? beat of 12*1.25 = 15 px */
-.rhythm_bg { background: url("%(baseRhythmBg)s") repeat ! important; }
-
 h1,
 .vtitle {
   font-size: %(h1FontSize)s;
@@ -297,7 +294,6 @@
 
 div#pageContent {
   clear: both;
-  /* margin-top:-1px; *//* enable when testing rhythm */
   background: %(pageContentBgColor)s;
   border: 1px solid %(pageContentBorderColor)s;
   padding: 0 %(pageContentPadding)s %(pageContentPadding)s;
--- a/web/data/cubicweb.htmlhelpers.js	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/data/cubicweb.htmlhelpers.js	Mon Dec 09 16:13:10 2013 +0100
@@ -78,10 +78,10 @@
         // generate a list of couple key=value if key is multivalued
         if (cw.utils.isArrayLike(value)) {
             for (var i = 0; i < value.length; i++) {
-                chunks.push(key + '=' + urlEncode(value[i]));
+                chunks.push(key + '=' + cw.urlEncode(value[i]));
             }
         } else {
-            chunks.push(key + '=' + urlEncode(value));
+            chunks.push(key + '=' + cw.urlEncode(value));
         }
     }
     return chunks.join('&');
--- a/web/data/cubicweb.js	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/data/cubicweb.js	Mon Dec 09 16:13:10 2013 +0100
@@ -45,6 +45,15 @@
         return null;
     },
 
+    // escapes string selectors (e.g. "foo.[subject]:42" -> "foo\.\[subject\]\:42"
+    escape: function(selector) {
+        if (typeof(selector) == 'string') {
+            return  selector.replace( /(:|\.|\[|\])/g, "\\$1" );
+        }
+        // cw.log('non string selector', selector);
+        return '';
+    },
+
     getNode: function (node) {
         if (typeof(node) == 'string') {
             return document.getElementById(node);
@@ -105,15 +114,6 @@
         };
     },
 
-    movedToNamespace: function (funcnames, namespace) {
-        for (var i = 0; i < funcnames.length; i++) {
-            var funcname = funcnames[i];
-            var msg = ('[3.9] ' + funcname + ' is deprecated, use ' +
-		       namespace.__name__ + '.' + funcname + ' instead');
-            window[funcname] = cw.utils.deprecatedFunction(msg, namespace[funcname]);
-        }
-    },
-
     createDomFunction: function (tag) {
         function builddom(params, children) {
             var node = document.createElement(tag);
@@ -388,14 +388,6 @@
 
 });
 
-String.prototype.startsWith = cw.utils.deprecatedFunction('[3.9] str.startsWith() is deprecated, use str.startswith() instead', function (prefix) {
-    return this.startswith(prefix);
-});
-
-String.prototype.endsWith = cw.utils.deprecatedFunction('[3.9] str.endsWith() is deprecated, use str.endswith() instead', function (suffix) {
-    return this.endswith(prefix);
-});
-
 /** DOM factories ************************************************************/
 A = cw.utils.createDomFunction('a');
 BUTTON = cw.utils.createDomFunction('button');
@@ -472,7 +464,8 @@
     return node;
 }
 
-// XXX avoid crashes / backward compat
+// cubes: tag, keyword and apycot seem to use this, including require/provide
+// backward compat
 CubicWeb = cw;
 
 jQuery.extend(cw, {
--- a/web/data/cubicweb.rhythm.js	Fri Dec 06 17:20:59 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-$(document).ready(function() {
-    $('a.rhythm').click(function (event){
-        $('div#pageContent').toggleClass('rhythm_bg');
-        $('div#page').toggleClass('rhythm_bg');
-        event.preventDefault();
-    });
-});
Binary file web/data/rhythm15.png has changed
Binary file web/data/rhythm18.png has changed
Binary file web/data/rhythm20.png has changed
Binary file web/data/rhythm22.png has changed
Binary file web/data/rhythm24.png has changed
Binary file web/data/rhythm26.png has changed
--- a/web/data/uiprops.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/data/uiprops.py	Mon Dec 09 16:13:10 2013 +0100
@@ -66,7 +66,6 @@
 defaultSize = '12px'
 defaultLineHeight = '1.5'
 defaultLineHeightEm = lazystr('%(defaultLineHeight)sem')
-baseRhythmBg = 'rhythm18.png'
 
 inputHeight = '1.3em'
 inputPadding = 'O.2em'
--- a/web/formfields.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/formfields.py	Mon Dec 09 16:13:10 2013 +0100
@@ -404,17 +404,6 @@
                      for label, value in vocab]
         if self.sort:
             vocab = vocab_sort(vocab)
-        # XXX pre 3.9 bw compat
-        for i, option in enumerate(vocab):
-            # option may be a 2 or 3-uple (see Select widget _render method for
-            # explanation)
-            value = option[1]
-            if value is not None and not isinstance(value, basestring):
-                warn('[3.9] %s: vocabulary value should be an unicode string'
-                     % self, DeprecationWarning)
-                option = list(option)
-                option[1] = unicode(value)
-                vocab[i] = option
         return vocab
 
     # support field as argument to avoid warning when used as format field value
@@ -760,8 +749,13 @@
             # raise UnmodifiedField instead of returning None, since the later
             # will try to remove already attached file if any
             raise UnmodifiedField()
-        # value is a 2-uple (filename, stream)
+        # value is a 2-uple (filename, stream) or a list of such
+        # tuples (multiple files)
         try:
+            if isinstance(value, list):
+                value = value[0]
+                form.warning('mutiple files provided, however '
+                             'only the first will be picked')
             filename, stream = value
         except ValueError:
             raise UnmodifiedField()
--- a/web/formwidgets.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/formwidgets.py	Mon Dec 09 16:13:10 2013 +0100
@@ -588,13 +588,7 @@
     def _render(self, form, field, renderer):
         curvalues, attrs = self.values_and_attributes(form, field)
         domid = attrs.pop('id', None)
-        # XXX turn this as initializer argument
-        try:
-            sep = attrs.pop('separator')
-            warn('[3.8] separator should be specified using initializer argument',
-                 DeprecationWarning)
-        except KeyError:
-            sep = self.separator
+        sep = self.separator
         options = []
         for i, option in enumerate(field.vocabulary(form)):
             try:
--- a/web/request.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/request.py	Mon Dec 09 16:13:10 2013 +0100
@@ -684,12 +684,6 @@
                 cssfile = self.data_url(cssfile)
             add_css(cssfile, media, *extraargs)
 
-    @deprecated('[3.9] use ajax_replace_url() instead, naming rql and vid arguments')
-    def build_ajax_replace_url(self, nodeid, rql, vid, replacemode='replace',
-                               **extraparams):
-        return self.ajax_replace_url(nodeid, replacemode, rql=rql, vid=vid,
-                                     **extraparams)
-
     def ajax_replace_url(self, nodeid, replacemode='replace', **extraparams):
         """builds an ajax url that will replace nodeid's content
 
@@ -992,20 +986,6 @@
     def html_content_type(self):
         return 'text/html'
 
-    @deprecated('[3.9] use req.uiprops[rid]')
-    def external_resource(self, rid, default=_MARKER):
-        """return a path to an external resource, using its identifier
-
-        raise `KeyError` if the resource is not defined
-        """
-        try:
-            return self.uiprops[rid]
-        except KeyError:
-            if default is _MARKER:
-                raise
-            return default
-
-
 
 ## HTTP-accept parsers / utilies ##############################################
 def _mimetype_sort_key(accept_info):
--- a/web/test/data/views.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/test/data/views.py	Mon Dec 09 16:13:10 2013 +0100
@@ -18,6 +18,7 @@
 
 from cubicweb.web import Redirect
 from cubicweb.web.application import CubicWebPublisher
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
 
 # proof of concept : monkey patch handle method so that if we are in an
 # anonymous session and __fblogin is found is req.form, the user with the
@@ -40,5 +41,35 @@
         assert req.user.login == login
     return orig_handle(self, req, path)
 
+
+def _recursive_replace_stream_by_content(tree):
+    """ Search for streams (i.e. object that have a 'read' method) in a tree
+    (which branches are lists or tuples), and substitute them by their content,
+    leaving other leafs identical. A copy of the tree with only lists as
+    branches is returned.
+    """
+    if not isinstance(tree, (list, tuple)):
+        if hasattr(tree, 'read'):
+            return tree.read()
+        return tree
+    else:
+        return [_recursive_replace_stream_by_content(value)
+                for value in tree]            
+
+
+@ajaxfunc(output_type='json')
+def fileupload(self):
+    """ Return a json copy of the web request formin which uploaded files
+    are read and their content substitute the received streams.
+    """
+    try:
+        result_dict = {}
+        for key, value in self._cw.form.iteritems():
+            result_dict[key] = _recursive_replace_stream_by_content(value)
+        return result_dict
+    except Exception, ex:
+        import traceback as tb
+        tb.print_exc(ex)
+
 orig_handle = CubicWebPublisher.main_handle_request
 CubicWebPublisher.main_handle_request = auto_login_handle_request
--- a/web/test/jstests/test_utils.js	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/test/jstests/test_utils.js	Mon Dec 09 16:13:10 2013 +0100
@@ -47,11 +47,11 @@
   module("sliceList");
   test("test slicelist", function() {
       var list = ['a', 'b', 'c', 'd', 'e', 'f'];
-      same(sliceList(list, 2),  ['c', 'd', 'e', 'f']);
-      same(sliceList(list, 2, -2), ['c', 'd']);
-      same(sliceList(list, -3), ['d', 'e', 'f']);
-      same(sliceList(list, 0, -2), ['a', 'b', 'c', 'd']);
-      same(sliceList(list),  list);
+      same(cw.utils.sliceList(list, 2),  ['c', 'd', 'e', 'f']);
+      same(cw.utils.sliceList(list, 2, -2), ['c', 'd']);
+      same(cw.utils.sliceList(list, -3), ['d', 'e', 'f']);
+      same(cw.utils.sliceList(list, 0, -2), ['a', 'b', 'c', 'd']);
+      same(cw.utils.sliceList(list),  list);
   });
 
   module("formContents", {
@@ -83,7 +83,7 @@
 			     'value="one" />');
       $('#test-form').append('<input name="unchecked-choice" type="radio" ' +
 			     'value="two"/>');
-      same(formContents($('#test-form')[0]), [
+      same(cw.utils.formContents($('#test-form')[0]), [
 	['input-text', 'mytextarea', 'choice', 'check', 'theselect'],
 	['toto', 'Hello World!', 'no', 'no', 'foo']
       ]);
--- a/web/test/unittest_web.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/test/unittest_web.py	Mon Dec 09 16:13:10 2013 +0100
@@ -16,7 +16,17 @@
 # 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 json import loads
+from os.path import join
+
+try:
+    import requests
+    assert [int(n) for n in requests.__version__.split('.', 2)][:2] >= [1, 2]
+except (ImportError, AssertionError):
+    requests = None
+
 from logilab.common.testlib import TestCase, unittest_main
+from cubicweb.devtools.httptest import CubicWebServerTC
 from cubicweb.devtools.fake import FakeRequest
 
 class AjaxReplaceUrlTC(TestCase):
@@ -43,5 +53,45 @@
             (cbname, qs, req.pageid),
             req.html_headers.post_inlined_scripts[0])
 
+
+class FileUploadTC(CubicWebServerTC):
+
+    def setUp(self):
+        "Skip whole test class if a suitable requests module is not available"
+        if requests is None:
+            self.skipTest('Python ``requests`` module is not available')
+        super(FileUploadTC, self).setUp()
+
+    @property
+    def _post_url(self):
+        return self.request().build_url('ajax', fname='fileupload')
+
+    def _fobject(self, fname):
+        return open(join(self.datadir, fname), 'rb')
+
+    def _fcontent(self, fname):
+        return self._fobject(fname).read()
+
+    def test_single_file_upload(self):
+        files = {'file': ('schema.py', self._fobject('schema.py'))}
+        webreq = requests.post(self._post_url, files=files)
+        # check backward compat : a single uploaded file leads to a single
+        # 2-uple in the request form
+        expect = {'fname': u'fileupload',
+                  'file': ['schema.py', self._fcontent('schema.py')]}
+        self.assertEqual(webreq.status_code, 200)
+        self.assertDictEqual(expect, loads(webreq.content))
+
+    def test_multiple_file_upload(self):
+        files = [('files', ('schema.py', self._fobject('schema.py'))),
+                 ('files', ('views.py',  self._fobject('views.py')))]
+        webreq = requests.post(self._post_url, files=files,)
+        expect = {'fname': u'fileupload',
+                  'files': [['schema.py', self._fcontent('schema.py')],
+                            ['views.py', self._fcontent('views.py')]],}
+        self.assertEqual(webreq.status_code, 200)
+        self.assertDictEqual(expect, loads(webreq.content))
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/web/views/actions.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/actions.py	Mon Dec 09 16:13:10 2013 +0100
@@ -419,22 +419,6 @@
     def url(self):
         return 'http://www.cubicweb.org'
 
-class GotRhythmAction(action.Action):
-    __regid__ = 'rhythm'
-    __select__ = debug_mode()
-
-    category = 'footer'
-    order = 3
-    title = _('Got rhythm?')
-
-    def url(self):
-        return xml_escape(self._cw.url()+'#')
-
-    def html_class(self):
-        self._cw.add_js('cubicweb.rhythm.js')
-        return 'rhythm'
-
-
 ## default actions ui configuration ###########################################
 
 addmenu = uicfg.actionbox_appearsin_addmenu
--- a/web/views/autoform.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/autoform.py	Mon Dec 09 16:13:10 2013 +0100
@@ -726,16 +726,17 @@
     # action on the form tag
     _default_form_action_path = 'validateform'
 
-    # pre 3.8.3 compat
+    @deprecated('[3.18] you should override form_action()')
     def set_action(self, action):
         self._action = action
+
+    @deprecated('[3.18] use form_action()')
     def get_action(self):
         try:
             return self._action
         except AttributeError:
             return self._cw.build_url(self._default_form_action_path)
-    action = property(deprecated('[3.9] use form.form_action()')(get_action),
-                      set_action)
+
 
     @iclassmethod
     def field_by_name(cls_or_self, name, role=None, eschema=None):
--- a/web/views/baseviews.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/baseviews.py	Mon Dec 09 16:13:10 2013 +0100
@@ -424,11 +424,7 @@
     redirect_vid = 'incontext'
 
     def call(self, subvid=None, **kwargs):
-        if subvid is None and 'vid' in kwargs:
-            warn("[3.9] should give a 'subvid' argument instead of 'vid'",
-                 DeprecationWarning, stacklevel=2)
-        else:
-            kwargs['vid'] = subvid
+        kwargs['vid'] = subvid
         rset = self.cw_rset
         for i in xrange(len(rset)):
             self.cell_call(i, 0, **kwargs)
@@ -643,14 +639,3 @@
 RssView = class_moved(xmlrss.RSSView)
 RssItemView = class_moved(xmlrss.RSSItemView)
 TableView = class_moved(tableview.TableView)
-
-
-class SecondaryView(EntityView):
-    __metaclass__ = class_deprecated
-    __deprecation_warning__ = '[3.9] the secondary view is deprecated, use one of oneline/incontext/outofcontext'
-    __regid__ = 'secondary'
-
-    def cell_call(self, row, col, **kwargs):
-        entity = self.cw_rset.get_entity(row, col)
-        self.w(u'&#160;')
-        self.wview('oneline', self.cw_rset, row=row, col=col)
--- a/web/views/calendar.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/calendar.py	Mon Dec 09 16:13:10 2013 +0100
@@ -27,9 +27,8 @@
 from logilab.common.date import todatetime
 
 from cubicweb.utils import json_dumps, make_uid
-from cubicweb.interfaces import ICalendarable
-from cubicweb.predicates import implements, adaptable
-from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+from cubicweb.predicates import adaptable
+from cubicweb.view import EntityView, EntityAdapter
 
 # useful constants & functions ################################################
 
@@ -46,16 +45,14 @@
 class ICalendarableAdapter(EntityAdapter):
     __needs_bw_compat__ = True
     __regid__ = 'ICalendarable'
-    __select__ = implements(ICalendarable, warn=False) # XXX for bw compat, should be abstract
+    __abstract__ = True
 
     @property
-    @implements_adapter_compat('ICalendarable')
     def start(self):
         """return start date"""
         raise NotImplementedError
 
     @property
-    @implements_adapter_compat('ICalendarable')
     def stop(self):
         """return stop date"""
         raise NotImplementedError
--- a/web/views/cwuser.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/cwuser.py	Mon Dec 09 16:13:10 2013 +0100
@@ -189,8 +189,8 @@
     __select__ = StartupView.__select__ & match_user_groups('managers')
     cache_max_age = 0 # disable caching
     # XXX one could wish to display for instance only user's firstname/surname
-    # for non managers but filtering out NULL cause crash with an ldapuser
-    # source.
+    # for non managers but filtering out NULL caused crash with an ldapuser
+    # source. The ldapuser source has been dropped and this code can be updated.
     rql = ('Any U,US,F,S,U,UAA,UDS, L,UAA,USN,UDSN ORDERBY L WHERE U is CWUser, '
            'U login L, U firstname F, U surname S, '
            'U in_state US, US name USN, '
--- a/web/views/editcontroller.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/editcontroller.py	Mon Dec 09 16:13:10 2013 +0100
@@ -28,7 +28,7 @@
 from rql.utils import rqlvar_maker
 
 from cubicweb import Binary, ValidationError
-from cubicweb.view import EntityAdapter, implements_adapter_compat
+from cubicweb.view import EntityAdapter
 from cubicweb.predicates import is_instance
 from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit,
                           ProcessFormError)
@@ -36,7 +36,6 @@
 
 
 class IEditControlAdapter(EntityAdapter):
-    __needs_bw_compat__ = True
     __regid__ = 'IEditControl'
     __select__ = is_instance('Any')
 
@@ -47,7 +46,6 @@
                  DeprecationWarning)
         super(IEditControlAdapter, self).__init__(_cw, **kwargs)
 
-    @implements_adapter_compat('IEditControl')
     def after_deletion_path(self):
         """return (path, parameters) which should be used as redirect
         information when this entity is being deleted
@@ -57,7 +55,6 @@
             return parent.rest_path(), {}
         return str(self.entity.e_schema).lower(), {}
 
-    @implements_adapter_compat('IEditControl')
     def pre_web_edit(self):
         """callback called by the web editcontroller when an entity will be
         created/modified, to let a chance to do some entity specific stuff.
--- a/web/views/ibreadcrumbs.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/ibreadcrumbs.py	Mon Dec 09 16:13:10 2013 +0100
@@ -24,7 +24,6 @@
 
 from logilab.mtconverter import xml_escape
 
-#from cubicweb.interfaces import IBreadCrumbs
 from cubicweb import tags, uilib
 from cubicweb.entity import Entity
 from cubicweb.predicates import (is_instance, one_line_rset, adaptable,
@@ -35,15 +34,6 @@
 # don't use AnyEntity since this may cause bug with isinstance() due to reloading
 
 
-# ease bw compat
-def ibreadcrumb_adapter(entity):
-    if hasattr(entity, 'breadcrumbs'):
-        warn('[3.9] breadcrumbs() method is deprecated, define a custom '
-             'IBreadCrumbsAdapter for %s instead' % entity.__class__,
-             DeprecationWarning)
-        return entity
-    return entity.cw_adapt_to('IBreadCrumbs')
-
 
 class IBreadCrumbsAdapter(EntityAdapter):
     """adapters for entities which can be"located" on some path to display in
@@ -53,11 +43,6 @@
     __select__ = is_instance('Any', accept_none=False)
 
     def parent_entity(self):
-        if hasattr(self.entity, 'parent') and callable(self.entity.parent):
-            warn('[3.9] parent() method is deprecated, define a '
-                 'custom IBreadCrumbsAdapter/ITreeAdapter for %s instead'
-                 % self.entity.__class__, DeprecationWarning)
-            return self.entity.parent()
         itree = self.entity.cw_adapt_to('ITree')
         if itree is not None:
             return itree.parent()
@@ -94,7 +79,7 @@
                 self.error('cycle in breadcrumbs for entity %s' % self.entity)
                 return []
             _recurs.add(parent.eid)
-            adapter = ibreadcrumb_adapter(parent)
+            adapter = parent.cw_adapt_to('IBreadCrumbs')
             path = adapter.breadcrumbs(view, _recurs) + [self.entity]
         else:
             path = [self.entity]
@@ -125,7 +110,7 @@
             entity = self.cw_extra_kwargs['entity']
         except KeyError:
             entity = self.cw_rset.get_entity(0, 0)
-        adapter = ibreadcrumb_adapter(entity)
+        adapter = entity.cw_adapt_to('IBreadCrumbs')
         view = self.cw_extra_kwargs.get('view')
         path = adapter.breadcrumbs(view)
         if path:
--- a/web/views/navigation.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/navigation.py	Mon Dec 09 16:13:10 2013 +0100
@@ -55,10 +55,9 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.deprecation import deprecated
 
-from cubicweb.predicates import (paginated_rset, sorted_rset,
-                                adaptable, implements)
+from cubicweb.predicates import paginated_rset, sorted_rset, adaptable
 from cubicweb.uilib import cut
-from cubicweb.view import EntityAdapter, implements_adapter_compat
+from cubicweb.view import EntityAdapter
 from cubicweb.web.component import EmptyComponent, EntityCtxComponent, NavigationComponent
 
 
@@ -324,7 +323,6 @@
 View.handle_pagination = False
 
 
-from cubicweb.interfaces import IPrevNext
 
 class IPrevNextAdapter(EntityAdapter):
     """Interface for entities which can be linked to a previous and/or next
@@ -335,14 +333,12 @@
     """
     __needs_bw_compat__ = True
     __regid__ = 'IPrevNext'
-    __select__ = implements(IPrevNext, warn=False) # XXX for bw compat, else should be abstract
+    __abstract__ = True
 
-    @implements_adapter_compat('IPrevNext')
     def next_entity(self):
         """return the 'next' entity"""
         raise NotImplementedError
 
-    @implements_adapter_compat('IPrevNext')
     def previous_entity(self):
         """return the 'previous' entity"""
         raise NotImplementedError
--- a/web/views/primary.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/primary.py	Mon Dec 09 16:13:10 2013 +0100
@@ -189,10 +189,6 @@
     def render_entity_toolbox(self, entity):
         self.content_navigation_components('ctxtoolbar')
 
-    @deprecated('[3.8] render_entity_metadata method is deprecated')
-    def render_entity_metadata(self, entity):
-        entity.view('metadata', w=self.w)
-
     def summary(self, entity):
         """default implementation return an empty string"""
         return u''
@@ -217,16 +213,8 @@
         if display_attributes:
             self.w(u'<table>')
             for rschema, role, dispctrl, value in display_attributes:
-                # pylint: disable=E1101
-                if not hasattr(self, '_render_attribute'):
-                    label = self._rel_label(entity, rschema, role, dispctrl)
-                    self.render_attribute(label, value, table=True)
-                else:
-                    warn('[3.9] _render_attribute prototype has changed and '
-                         'renamed to render_attribute, please update %s'
-                         % self.__class__, DeprecationWarning)
-                    self._render_attribute(dispctrl, rschema, value, role=role,
-                                           table=True)
+                label = self._rel_label(entity, rschema, role, dispctrl)
+                self.render_attribute(label, value, table=True)
             self.w(u'</table>')
 
     def render_attribute(self, label, value, table=False):
@@ -253,13 +241,6 @@
                 rset = self._relation_rset(entity, rschema, role, dispctrl, limit=limit)
                 if not rset:
                     continue
-                if hasattr(self, '_render_relation'):
-                    # pylint: disable=E1101
-                    self._render_relation(dispctrl, rset, 'autolimited')
-                    warn('[3.9] _render_relation prototype has changed and has '
-                         'been renamed to render_relation, please update %s'
-                         % self.__class__, DeprecationWarning)
-                    continue
                 try:
                     rview = self._cw.vreg['views'].select(
                         vid, self._cw, rset=rset, dispctrl=dispctrl)
--- a/web/views/reledit.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/reledit.py	Mon Dec 09 16:13:10 2013 +0100
@@ -91,9 +91,6 @@
         rschema = self._cw.vreg.schema[rtype]
         rctrl = self._cw.vreg['uicfg'].select('reledit', self._cw, entity=entity)
         self._rules = rctrl.etype_get(self.entity.e_schema.type, rschema.type, role, '*')
-        if rvid is not None or default_value is not None:
-            warn('[3.9] specifying rvid/default_value on select is deprecated, '
-                 'reledit_ctrl rtag to control this' % self, DeprecationWarning)
         reload = self._compute_reload(rschema, role, reload)
         divid = self._build_divid(rtype, role, self.entity.eid)
         if rschema.final:
@@ -322,19 +319,11 @@
         rdef = entity.e_schema.rdef(rschema)
         return rdef.has_perm(self._cw, 'update', eid=entity.eid)
 
-    should_edit_attributes = deprecated('[3.9] should_edit_attributes is deprecated,'
-                                        ' use _should_edit_attribute instead',
-                                        _should_edit_attribute)
-
     def _should_edit_relation(self, rschema, role):
         eeid = self.entity.eid
         perm_args = {'fromeid': eeid} if role == 'subject' else {'toeid': eeid}
         return rschema.has_perm(self._cw, 'add', **perm_args)
 
-    should_edit_relations = deprecated('[3.9] should_edit_relations is deprecated,'
-                                       ' use _should_edit_relation instead',
-                                       _should_edit_relation)
-
     def _open_form_wrapper(self, divid, value, form, renderer,
                            _edit_related, _add_related, _delete_related):
         w = self.w
--- a/web/views/schema.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/schema.py	Mon Dec 09 16:13:10 2013 +0100
@@ -225,6 +225,7 @@
             {'x': entity.eid})
         self.wview('table', rset, 'null',
                    cellvids={0: 'rdef-name-cell',
+                             2: 'etype-attr-defaultval-cell',
                              3: 'etype-attr-cardinality-cell',
                              4: 'rdef-constraints-cell',
                              6: 'rdef-options-cell'},
@@ -271,6 +272,14 @@
             self.w(self._cw._(u'no'))
 
 
+class CWETypeAttributeDefaultValCell(baseviews.FinalView):
+    __regid__ = 'etype-attr-defaultval-cell'
+
+    def cell_call(self, row, col):
+        defaultval = self.cw_rset.rows[row][col]
+        if defaultval is not None:
+            self.w(unicode(self.cw_rset.rows[row][col].unzpickle()))
+
 class CWETypeRelationCardinalityCell(baseviews.FinalView):
     __regid__ = 'etype-rel-cardinality-cell'
 
--- a/web/views/sessions.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/sessions.py	Mon Dec 09 16:13:10 2013 +0100
@@ -100,8 +100,6 @@
     def _update_last_login_time(self, req):
         # XXX should properly detect missing permission / non writeable source
         # and avoid "except (RepositoryError, Unauthorized)" below
-        if req.user.cw_metainformation()['source']['type'] == 'ldapuser':
-            return
         try:
             req.execute('SET X last_login_time NOW WHERE X eid %(x)s',
                         {'x' : req.user.eid})
--- a/web/views/treeview.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/treeview.py	Mon Dec 09 16:13:10 2013 +0100
@@ -29,13 +29,26 @@
 from cubicweb.utils import make_uid, json
 from cubicweb.predicates import adaptable
 from cubicweb.view import EntityView
-from cubicweb.mixins import _done_init
 from cubicweb.web.views import baseviews
 from cubicweb.web.views.ajaxcontroller import ajaxfunc
 
 def treecookiename(treeid):
     return str('%s-treestate' % treeid)
 
+def _done_init(done, view, row, col):
+    """handle an infinite recursion safety belt"""
+    if done is None:
+        done = set()
+    entity = view.cw_rset.get_entity(row, col)
+    if entity.eid in done:
+        msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
+            'rel': entity.cw_adapt_to('ITree').tree_relation,
+            'eid': entity.eid
+            }
+        return None, msg
+    done.add(entity.eid)
+    return done, entity
+
 
 class BaseTreeView(baseviews.ListView):
     """base tree view"""
--- a/web/views/urlrewrite.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/urlrewrite.py	Mon Dec 09 16:13:10 2013 +0100
@@ -140,7 +140,7 @@
         return None, None
 
 
-def build_rset(rql, rgxgroups=None, cachekey=None, setuser=False,
+def build_rset(rql, rgxgroups=None, setuser=False,
                vid=None, vtitle=None, form={}, **kwargs):
 
     def do_build_rset(inputurl, uri, req, schema, kwargs=kwargs):
@@ -156,7 +156,7 @@
             req.form['vid'] = vid
         if vtitle:
             req.form['vtitle'] = req._(vtitle) % kwargs
-        return None, req.execute(rql, kwargs, cachekey)
+        return None, req.execute(rql, kwargs)
     return do_build_rset
 
 def update_form(**kwargs):
--- a/web/views/xmlrss.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/views/xmlrss.py	Mon Dec 09 16:13:10 2013 +0100
@@ -28,7 +28,6 @@
 from cubicweb.predicates import (is_instance, non_final_entity, one_line_rset,
                                 appobject_selectable, adaptable)
 from cubicweb.view import EntityView, EntityAdapter, AnyRsetView, Component
-from cubicweb.view import implements_adapter_compat
 from cubicweb.uilib import simple_sgml_tag
 from cubicweb.web import httpcache, component
 
@@ -185,7 +184,6 @@
     __regid__ = 'IFeed'
     __select__ = is_instance('Any')
 
-    @implements_adapter_compat('IFeed')
     def rss_feed_url(self):
         """return an url to the rss feed for this entity"""
         return self.entity.absolute_url(vid='rss')
--- a/web/webconfig.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/web/webconfig.py	Mon Dec 09 16:13:10 2013 +0100
@@ -403,7 +403,7 @@
             self._load_ui_properties_file(uiprops, path)
         self._load_ui_properties_file(uiprops, self.apphome)
         datadir_url = uiprops.context['datadir_url']
-        # XXX pre 3.9 css compat
+        # pre 3.9 css compat, however the old css still rules
         if self['use-old-css']:
             if (datadir_url+'/cubicweb.css') in uiprops['STYLESHEETS']:
                 idx = uiprops['STYLESHEETS'].index(datadir_url+'/cubicweb.css')
@@ -415,21 +415,6 @@
             uiprops['JAVASCRIPTS'].insert(0, cubicweb_js_url)
 
     def _load_ui_properties_file(self, uiprops, path):
-        resourcesfile = join(path, 'data', 'external_resources')
-        if exists(resourcesfile):
-            warn('[3.9] %s file is deprecated, use an uiprops.py file'
-                 % resourcesfile, DeprecationWarning)
-            datadir_url = uiprops.context['datadir_url']
-            for rid, val in read_config(resourcesfile).iteritems():
-                if rid in ('STYLESHEETS', 'STYLESHEETS_PRINT',
-                           'IE_STYLESHEETS', 'JAVASCRIPTS'):
-                    val = [w.strip().replace('DATADIR', datadir_url)
-                           for w in val.split(',') if w.strip()]
-                    if rid == 'IE_STYLESHEETS':
-                        rid = 'STYLESHEETS_IE'
-                else:
-                    val = val.strip().replace('DATADIR', datadir_url)
-                uiprops[rid] = val
         uipropsfile = join(path, 'uiprops.py')
         if exists(uipropsfile):
             self.debug('loading %s', uipropsfile)
--- a/wsgi/handler.py	Fri Dec 06 17:20:59 2013 +0100
+++ b/wsgi/handler.py	Mon Dec 09 16:13:10 2013 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2013 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/>.
 """WSGI request handler for cubicweb"""
 
-
-
 __docformat__ = "restructuredtext en"
 
 from itertools import chain, repeat, izip
@@ -92,29 +90,21 @@
         return iter(self.body)
 
 
-
 class CubicWebWSGIApplication(object):
     """This is the wsgi application which will be called by the
     wsgi server with the WSGI ``environ`` and ``start_response``
     parameters.
-
-    XXX: missing looping tasks and proper repository shutdown when
-    the application is stopped.
-    NOTE: no pyro
     """
 
-    def __init__(self, config, vreg=None):
-        self.appli = CubicWebPublisher(config, vreg=vreg)
+    def __init__(self, config):
+        self.appli = CubicWebPublisher(config)
         self.config = config
         self.base_url = self.config['base-url']
-        self.https_url = self.config['https-url']
         self.url_rewriter = self.appli.vreg['components'].select_or_none('urlrewriter')
 
     def _render(self, req):
         """this function performs the actual rendering
         """
-        if self.base_url is None:
-            self.base_url = self.config._base_url = req.base_url()
         try:
             path = req.path
             result = self.appli.handle_request(req, path)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wsgi/server.py	Mon Dec 09 16:13:10 2013 +0100
@@ -0,0 +1,46 @@
+# copyright 2003-2013 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/>.
+"""dummy wsgi server for CubicWeb web instances"""
+
+__docformat__ = "restructuredtext en"
+
+from cubicweb.wsgi.handler import CubicWebWSGIApplication
+from cubicweb import ConfigurationError
+from wsgiref import simple_server
+
+from logging import getLogger
+LOGGER = getLogger('cubicweb')
+
+
+def run(config):
+    config.check_writeable_uid_directory(config.appdatahome)
+
+    port = config['port'] or 8080
+    interface = config['interface']
+
+    app = CubicWebWSGIApplication(config)
+    handler_cls = simple_server.WSGIRequestHandler
+    httpd = simple_server.WSGIServer((interface, port), handler_cls)
+    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:
+        repo.shutdown()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wsgi/wz.py	Mon Dec 09 16:13:10 2013 +0100
@@ -0,0 +1,47 @@
+# copyright 2003-2013 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/>.
+"""dummy wsgi server for CubicWeb web instances"""
+
+__docformat__ = "restructuredtext en"
+
+import socket
+
+from cubicweb.wsgi.handler import CubicWebWSGIApplication
+from cubicweb import ConfigurationError
+from werkzeug.serving import run_simple
+
+from logging import getLogger
+LOGGER = getLogger('cubicweb')
+
+
+def run(config):
+    config.check_writeable_uid_directory(config.appdatahome)
+
+    port = config['port'] or 8080
+    interface = config['interface']
+
+    app = CubicWebWSGIApplication(config)
+    repo = app.appli.repo
+    try:
+        repo.start_looping_tasks()
+        run_simple(interface, port, app,
+                   threaded=True,
+                   use_debugger=True,
+                   processes=1) # more processes yield weird errors
+    finally:
+        repo.shutdown()